feat(host): add workflows.list and teams.* sidebar routes

- workflows.list folds tool-workflow/* session events (the same four types
  the official workflow-run panel folds) across the whole session tree,
  merging the stored log with a live session/event mirror deduped by seq;
  absence of events yields an empty list, never an error.
- teams.view / teams.taskCreate / teams.taskUpdate ride the experimental
  agentTeams service's Remote vocabulary with structural degradation:
  layer absent -> {available:false}; root leads no team -> {team:null};
  CAS conflicts pass through the service's own result union.
- wire gains the team-error code; context-types gains structural mirrors
  of the team view/request vocabulary (no experimental package import).
- 29 unit tests across fold, route merging/degradation, and CAS shaping.
This commit is contained in:
Menghuan1918
2026-09-27 22:18:14 +08:00
parent ad6b0b1486
commit c445495f8b
9 changed files with 1016 additions and 0 deletions
+71
View File
@@ -275,6 +275,77 @@ export interface SidebarAgentPresetsService {
mount(agentCtx: unknown, presetId: string): Promise<void>
}
/**
* The experimental Agent Teams service face (`ctx.agentTeams`, mounted only
* when the deployment loads `dsh-experimental-agent-team-profile`; absent →
* `ctx.get` returns undefined and the Teams block hides). Only the
* browser-facing Remote vocabulary the Tasks page needs is mirrored —
* structurally, so the plugin never imports the experimental package.
*/
export interface SidebarAgentTeamsService {
/** The agent's team membership, or undefined for a non-team/stale agent. */
tryMembership(agent: unknown): unknown
/** The point-in-time team snapshot the official panel renders. */
remoteView(agent: unknown): Promise<{ members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] }>
/** Create one shared task (CAS-free; ids are server-issued). */
remoteCreateTask(agent: unknown, req: SidebarCreateTeamTaskRequest): Promise<SidebarTeamTaskMutationResult>
/** Compare-and-set mutation of one shared task (stale revision → conflict). */
remoteUpdateTask(agent: unknown, req: SidebarUpdateTeamTaskRequest): Promise<SidebarTeamTaskMutationResult>
}
/** One team member as the runtime-enriched view reports it. */
export interface SidebarTeamMemberView {
/** The member's session id (the teammate's child session under the lead). */
id: string
name: string
role: 'lead' | 'teammate'
status: 'running' | 'idle' | 'inactive' | 'provisioning' | 'failed'
description?: string
provider?: string
context?: 'fresh' | 'fork'
model?: string
diagnostics: string[]
}
/** One shared task-board row (durable fields plus derived readiness). */
export interface SidebarTeamTaskView {
id: string
revision: number
subject: string
description: string
status: 'pending' | 'in_progress' | 'completed' | 'deleted'
ownerName?: string
blockedBy: string[]
writeScopes: string[]
ready: boolean
writeScopeWarnings: string[]
}
/** Input for creating one shared task. */
export interface SidebarCreateTeamTaskRequest {
subject: string
description: string
blockedBy?: readonly string[]
writeScopes?: readonly string[]
}
/** Input for one CAS task mutation. */
export interface SidebarUpdateTeamTaskRequest {
taskId: string
expectedRevision: number
action: 'claim' | 'release' | 'edit' | 'set_dependencies' | 'complete' | 'reopen' | 'reassign' | 'delete'
subject?: string
description?: string
blockedBy?: readonly string[]
writeScopes?: readonly string[]
owner?: string
}
/** Browser task-mutation result (stale revisions kept distinct, passthrough). */
export type SidebarTeamTaskMutationResult =
| { ok: true; value: SidebarTeamTaskView }
| { ok: false; error: { code: 'team-task-conflict' | 'team-rejected'; message: string } }
/** The host session-title service face (mirror of the sessionTitle service). */
export interface SidebarSessionTitleService {
/** Rename one live session's title (pins it against auto-regeneration). */
+21
View File
@@ -43,6 +43,8 @@ import { SettingsConflictError } from '@deepseek-ai/dsh-settings'
import { AgentOpenRegistry, registerOpenTool, type AgentOpenRequest } from './agent-opens.ts'
import { buildJobsApi, type SidebarJobsRoutes } from './jobs-routes.ts'
import { buildSubagentLiveApi, type SidebarSubagentLiveRoutes } from './subagent-live-route.ts'
import { buildTeamsApi, type SidebarTeamsRoutes } from './team-routes.ts'
import { buildWorkflowsApi, type SidebarWorkflowRoutes } from './workflow-routes.ts'
import { buildSidechatApi } from './sidechat-routes.ts'
import { createAssistantLiveBuffer, type AssistantLiveBuffer } from './assistant-live.ts'
import { readJsonBody, requireString, SidebarError, writeError, writeJson, writeOk } from './wire.ts'
@@ -272,6 +274,17 @@ function buildApi(
// `subagents.history` calls. The route degrades to a 503 when the host
// subagent runtime is absent (the page has no topology to show anyway).
const subagentLiveApi: SidebarSubagentLiveRoutes = buildSubagentLiveApi(ctx)
// Workflow runs: no service registry exists in DSH (runs are
// holder-owned), so the route folds the `tool-workflow/*` session events
// the tool's recorder appends — the same four types the official panel
// folds in the browser. A deployment that never runs workflows simply
// returns an empty list (absence is normal, never an error).
const workflowsApi: SidebarWorkflowRoutes = buildWorkflowsApi(ctx)
// Agent Teams (experimental layer): the routes degrade structurally —
// service absent → `{available:false}` and the client hides the block;
// the tree root leading no team → `{team:null}`. Mutations ride the
// service's own CAS result union (conflicts stay distinct).
const teamsApi: SidebarTeamsRoutes = buildTeamsApi(ctx)
return {
'session.cwd': async (payload) => {
const { sessionId, cwd } = await cwdOf(payload)
@@ -484,6 +497,14 @@ function buildApi(
// Subagent live previews: one batch request per refresh; the route folds
// the newest text/tool activity of every running child in the tree.
'subagents.live': (payload) => subagentLiveApi.live(payload),
// Workflow runs of the whole tree (folded from `tool-workflow/*`
// session events; empty list when the tree never ran one).
'workflows.list': (payload) => workflowsApi.list(payload),
// Agent Teams (experimental): the root-led team's roster + task board,
// and the task board's create/CAS-update mutations.
'teams.view': (payload) => teamsApi.view(payload),
'teams.taskCreate': (payload) => teamsApi.taskCreate(payload),
'teams.taskUpdate': (payload) => teamsApi.taskUpdate(payload),
// The side card preferences. The settings service is optional in the
// composition; while absent the routes report undefined and the client
// keeps the schema defaults. Writes are revision-guarded: a stale editor
+167
View File
@@ -0,0 +1,167 @@
/**
* The Agent Teams routes of the /sidebar JSON API ('teams.view' /
* 'teams.taskCreate' / 'teams.taskUpdate'). The experimental
* `dsh-experimental-agent-team` service (`ctx.agentTeams`) exists only when
* the deployment opts in, so the routes degrade structurally:
*
* - service absent (`ctx.get('agentTeams')` undefined) → `{available:false}`
* — the client hides the whole Teams block silently (absence is a
* deployment fact, never an error banner);
* - the tree's root agent is not a team member (`tryMembership` miss) →
* `{available:true, team:null}` — the page shows the plain agent graph;
* - hit → the service's own Remote vocabulary (`remoteView` /
* `remoteCreateTask` / `remoteUpdateTask`), whose mutation result union
* already keeps CAS conflicts (`team-task-conflict`) distinct from other
* rejections — the route passes it through untouched so the client's
* edit popover can render "someone else changed this task, refreshing".
*
* Teams are implicit (TeamId ≡ the lead's session id) and every service
* method demands the live member Agent as its authority token, so the
* routes re-derive the caller per request (`ctx.agents.get(rootSessionId)`)
* exactly like the jobs.kill fence. Zero DSH source changes; the plugin
* never imports the experimental package (structural mirrors only).
*/
import type {
Context,
SidebarAgentTeamsService,
SidebarAgentsService,
SidebarCreateTeamTaskRequest,
SidebarTeamMemberView,
SidebarTeamTaskMutationResult,
SidebarTeamTaskView,
SidebarUpdateTeamTaskRequest,
} from './context-types.ts'
import { requireString, SidebarError } from './wire.ts'
/** The Agent Teams routes of the sidebar API. */
export interface SidebarTeamsRoutes {
/**
* The team led by the tree's root agent, if any.
* @param payload - `{ rootSessionId }`.
* @returns `{ available: false }` when the deployment lacks the
* experimental layer; `{ available: true, team: null }` when the root
* leads no team; otherwise the members + task board.
*/
view(payload: unknown): Promise<
| { available: false }
| { available: true; team: { members: SidebarTeamMemberView[]; tasks: SidebarTeamTaskView[] } | null }
>
/** Create one shared task (payload = `{ rootSessionId, ...request }`). */
taskCreate(payload: unknown): Promise<SidebarTeamTaskMutationResult>
/** CAS-mutate one shared task (payload = `{ rootSessionId, ...request }`). */
taskUpdate(payload: unknown): Promise<SidebarTeamTaskMutationResult>
}
/** The CAS actions the wire accepts (mirrors the domain union). */
const TASK_ACTIONS = new Set([
'claim', 'release', 'edit', 'set_dependencies', 'complete', 'reopen', 'reassign', 'delete',
])
/** Narrow an unknown payload value to a string array (else undefined). */
function stringArrayOf(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined
const out = value.filter((item): item is string => typeof item === 'string')
return out.length === value.length ? out : undefined
}
/**
* Build the Agent Teams routes bound to the plugin context.
* @param ctx - host plugin context.
*/
export function buildTeamsApi(ctx: Context): SidebarTeamsRoutes {
const teams = ctx.get('agentTeams') as SidebarAgentTeamsService | undefined
const agents = ctx.get('agents') as SidebarAgentsService | undefined
/**
* The live lead agent of the requested tree plus its membership check.
* Throws 503 team-error when the experimental layer is absent (a mutation
* can never be accepted there); returns undefined when the root simply
* leads no team (a normal page state).
*/
const membershipOf = async (
payload: unknown,
): Promise<{ svc: SidebarAgentTeamsService; agent: unknown } | undefined> => {
if (teams === undefined) {
throw new SidebarError('team-error', 'the agent-teams layer is not mounted in this deployment', 503)
}
const rootSessionId = requireString(payload, 'rootSessionId')
const agent = agents?.get(rootSessionId)
if (agent === undefined) {
// The root is not live in this process (cold session / other harness):
// teams are live-led by definition, so there is nothing to show.
return undefined
}
try {
if (await teams.tryMembership(agent) === undefined) return undefined
} catch {
return undefined
}
return { svc: teams, agent }
}
/** Service rejections become a 400 team-error; CAS conflicts ride the union. */
const serviceError = (error: unknown): SidebarError =>
new SidebarError('team-error', error instanceof Error ? error.message : String(error), 400)
return {
async view(payload) {
if (teams === undefined) return { available: false }
const membership = await membershipOf(payload)
if (membership === undefined) return { available: true, team: null }
try {
const team = await membership.svc.remoteView(membership.agent)
return { available: true, team }
} catch (error) {
throw serviceError(error)
}
},
async taskCreate(payload) {
const membership = await membershipOf(payload)
if (membership === undefined) {
throw new SidebarError('team-error', 'the tree root leads no team', 404)
}
const record = payload as Record<string, unknown>
const req: SidebarCreateTeamTaskRequest = {
subject: requireString(payload, 'subject'),
description: typeof record.description === 'string' ? record.description : '',
...(stringArrayOf(record.blockedBy) !== undefined ? { blockedBy: stringArrayOf(record.blockedBy) } : {}),
...(stringArrayOf(record.writeScopes) !== undefined ? { writeScopes: stringArrayOf(record.writeScopes) } : {}),
}
try {
return await membership.svc.remoteCreateTask(membership.agent, req)
} catch (error) {
throw serviceError(error)
}
},
async taskUpdate(payload) {
const membership = await membershipOf(payload)
if (membership === undefined) {
throw new SidebarError('team-error', 'the tree root leads no team', 404)
}
const record = payload as Record<string, unknown>
const expectedRevision = record.expectedRevision
if (typeof expectedRevision !== 'number' || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0) {
throw new SidebarError('bad-request', 'missing or invalid "expectedRevision"')
}
const action = record.action
if (typeof action !== 'string' || !TASK_ACTIONS.has(action)) {
throw new SidebarError('bad-request', 'missing or invalid "action"')
}
const req: SidebarUpdateTeamTaskRequest = {
taskId: requireString(payload, 'taskId'),
expectedRevision,
action: action as SidebarUpdateTeamTaskRequest['action'],
...(typeof record.subject === 'string' ? { subject: record.subject } : {}),
...(typeof record.description === 'string' ? { description: record.description } : {}),
...(stringArrayOf(record.blockedBy) !== undefined ? { blockedBy: stringArrayOf(record.blockedBy) } : {}),
...(stringArrayOf(record.writeScopes) !== undefined ? { writeScopes: stringArrayOf(record.writeScopes) } : {}),
...(typeof record.owner === 'string' ? { owner: record.owner } : {}),
}
try {
return await membership.svc.remoteUpdateTask(membership.agent, req)
} catch (error) {
throw serviceError(error)
}
},
}
}
+1
View File
@@ -16,6 +16,7 @@ export type SidebarErrorCode =
| 'fs-error'
| 'git-error'
| 'job-error'
| 'team-error'
| 'sidechat-error'
| 'subagents-unavailable'
| 'settings-rejected'
+121
View File
@@ -0,0 +1,121 @@
/**
* The workflow-runs route of the /sidebar JSON API ('workflows.list'). DSH
* keeps workflow runs holder-owned and process-local — there is NO service
* registry, NO HTTP route, and NO persistence for them (`dsh-workflow`
* README: "Runs are holder-owned, not service-tracked"). The one durable
* observation surface is the session event log: `dsh-tool-workflow`'s
* recorder appends four `tool-workflow/*` event types to the calling agent's
* session (top-level runs only), which is exactly what the official
* `dsh-client-ui-workflow-run` panel folds in the browser.
*
* This route folds the SAME events server-side for every session of the
* current tree (the root plus its subagent descendants), so the Tasks page
* can hang run nodes under the agent that started them. Like the job-output
* replay it merges the store's event log with a live `session/event` mirror
* (deduped by seq), because the store session can lag the live append feed
* after a host restart. Zero DSH writes; the runs of a deployment that never
* uses the workflow tool simply never produce events and the route returns
* an empty list (no error — absence is the normal case, not a failure).
*/
import type { Context, SidebarSessionEvent, SidebarSubagentsService } from './context-types.ts'
import { foldWorkflowRuns, type WorkflowRunView } from './workflow-runs.ts'
import { requireString } from './wire.ts'
/** The workflow-runs route of the sidebar API. */
export interface SidebarWorkflowRoutes {
/**
* Fold the workflow runs of every session in one tree.
* @param payload - `{ rootSessionId }`.
* @returns `{ runs: WorkflowRunView[] }`, oldest run first; empty when the
* tree never ran a workflow (or the DSH version predates the recorder).
*/
list(payload: unknown): Promise<{ runs: WorkflowRunView[] }>
}
/** Per-session cap of mirrored live workflow events (a bounded, lossy ring). */
const MIRROR_MAX_ENTRIES = 500
/**
* The live workflow-event mirror: subscribes to the session append feed and
* caches `tool-workflow/*` rows the session store's own log can lag behind
* (the same rehydration-boundary gap the job-output mirror exists for).
*/
function createWorkflowEventMirror(ctx: Context): { entries(sessionId: string): readonly SidebarSessionEvent[] } {
const perSession = new Map<string, SidebarSessionEvent[]>()
if (typeof ctx.on !== 'function') {
// Test doubles without the event API degrade to seed-only folding.
return { entries: () => [] }
}
const dispose = ctx.on('session/event', (session, event) => {
const sessionId = (session as { id?: unknown } | null)?.id
if (typeof sessionId !== 'string' || !event.type.startsWith('tool-workflow/')) return
let list = perSession.get(sessionId)
if (list === undefined) perSession.set(sessionId, list = [])
// The vendored cordis `on` overload wins over the plugin's string-keyed
// one and types the event as the rich SessionEvent union; the mirror
// only ever needs the minimal structural slice, so restate it here.
const minimal: SidebarSessionEvent = {
type: event.type,
seq: event.seq,
time: event.time,
data: event.data as Record<string, unknown>,
}
list.push(minimal)
if (list.length > MIRROR_MAX_ENTRIES) {
list.splice(0, list.length - MIRROR_MAX_ENTRIES)
}
})
ctx.effect(() => dispose, 'dsh-better-sidebar: workflow event mirror')
return { entries: (sessionId) => perSession.get(sessionId) ?? [] }
}
/**
* Build the workflow-runs route bound to the plugin context. The tree walk
* reuses the host subagent runtime (`ctx.get('subagents')`); without it the
* route still folds the root session alone (a degraded but honest tree of
* one), matching the Subagent page's own fallback lineage.
* @param ctx - host plugin context.
*/
export function buildWorkflowsApi(ctx: Context): SidebarWorkflowRoutes {
const mirror = createWorkflowEventMirror(ctx)
/** The tree's session ids: the root plus every descendant the catalog knows. */
const treeSessionIds = async (rootSessionId: string): Promise<string[]> => {
const subagents = ctx.get('subagents') as SidebarSubagentsService | undefined
if (subagents === undefined || typeof subagents.listDescendants !== 'function') {
return [rootSessionId]
}
try {
const descendants = await subagents.listDescendants(rootSessionId)
return [rootSessionId, ...descendants.map(entry => entry.id)]
} catch {
// The catalog read failed (stale root, deployment seam missing): the
// root session alone still yields its own runs.
return [rootSessionId]
}
}
return {
async list(payload) {
const rootSessionId = requireString(payload, 'rootSessionId')
const runs: WorkflowRunView[] = []
for (const sessionId of await treeSessionIds(rootSessionId)) {
try {
// Merge the store's event log with the live mirror, deduped by
// seq — an event mirrored live AND already stored never
// double-folds.
const bySeq = new Map<number, SidebarSessionEvent>()
for (const event of ctx.sessions.get(sessionId)?.snapshotEvents() ?? []) {
if (event.type.startsWith('tool-workflow/')) bySeq.set(event.seq, event)
}
for (const event of mirror.entries(sessionId)) bySeq.set(event.seq, event)
if (bySeq.size === 0) continue
const ordered = [...bySeq.values()].sort((left, right) => left.seq - right.seq)
runs.push(...foldWorkflowRuns(ordered, sessionId))
} catch {
// One session's log is not readable/foldable: skip only it.
}
}
runs.sort((left, right) => left.startedSeq - right.startedSeq)
return { runs }
},
}
}
+181
View File
@@ -0,0 +1,181 @@
/**
* Pure derivation of the workflow-run views shown on the Tasks page: fold one
* session's event log into the `tool-workflow/*` runs that originated there.
*
* The recorder inside DSH's `dsh-tool-workflow` appends exactly four event
* types to the CALLING agent's session (top-level runs only — nested runs
* inside workflow agents are never recorded):
*
* - `tool-workflow/run-start` `{ runId, name }`
* - `tool-workflow/agent-start` `{ runId, seq, label, phase?, childId }`
* - `tool-workflow/agent-end` `{ runId, seq, outcome }`
* - `tool-workflow/run-end` `{ runId, stopReason }`
*
* (Evidence: `dsh-tool-workflow/lib/index.js:37-88`; the official
* `dsh-client-ui-workflow-run` panel folds the identical four types in the
* browser.) This module mirrors that fold framework-free so it is
* unit-testable in the node environment; the host route merges the durable
* log with the live mirror and the client receives ready-made views.
*/
import type { SidebarSessionEvent } from './context-types.ts'
/** Outcome of one workflow member agent (the domain's closed union). */
export type WorkflowAgentOutcome = 'completed' | 'failed' | 'cancelled'
/** Settled reason of one run (`error` maps to the `failed` display status). */
export type WorkflowStopReason = 'completed' | 'cancelled' | 'error'
/** One member agent of a workflow run (`agent-start` + optional `agent-end`). */
export interface WorkflowRunMemberView {
/** Worker-side `agent()` call sequence number (pairs start/end). */
seq: number
/** Model-supplied member label. */
label: string
/** Phase grouping title supplied at start (absent = unphased). */
phase?: string
/** The member's child session id — the jump target of the row. */
childId: string
/** Set once `agent-end` lands; absent while the member runs. */
outcome?: WorkflowAgentOutcome
}
/** Members of one phase, in first-seen order (title undefined = unphased). */
export interface WorkflowRunPhaseView {
title?: string
members: WorkflowRunMemberView[]
}
/** Display status of one run (the official panel's vocabulary). */
export type WorkflowRunStatus = 'running' | 'completed' | 'cancelled' | 'failed'
/** One folded workflow run as the Tasks page renders it. */
export interface WorkflowRunView {
runId: string
name: string
/** The session the run was started from (the events' home session). */
originSessionId: string
status: WorkflowRunStatus
/** Settled reason once `run-end` landed (`error` ⇔ status `failed`). */
stopReason?: WorkflowStopReason
/** Phase-grouped members, phases in first-seen order. */
phases: WorkflowRunPhaseView[]
/** Seq of the run-start event (stable ordering across runs). */
startedSeq: number
/** Epoch ms of the run-start event (durations and sorting). */
startedAt: number
/** Epoch ms of the run-end event; absent while running. */
finishedAt?: number
}
/** The four recorded event types (prefix match keeps the scan cheap). */
const EVENT_PREFIX = 'tool-workflow/'
/** The mutable fold state of one run (member rows indexed by seq). */
interface RunFold {
view: WorkflowRunView
memberBySeq: Map<number, WorkflowRunMemberView>
}
/**
* Fold one session's event log into its workflow runs, oldest first. Rows
* referencing an unknown run (a log window that starts mid-run, or a corrupt
* tail) are skipped — the official panel asserts a leading run-start instead,
* but the route merges two sources where ordering is guaranteed anyway.
* @param events - the session's append-only event log (oldest → newest).
* @param originSessionId - the session the log belongs to.
* @returns the session's runs in run-start order.
*/
export function foldWorkflowRuns(
events: readonly SidebarSessionEvent[],
originSessionId: string,
): WorkflowRunView[] {
const runs: RunFold[] = []
const byRunId = new Map<string, RunFold>()
for (const event of events) {
if (!event.type.startsWith(EVENT_PREFIX)) continue
const data = event.data
switch (event.type) {
case 'tool-workflow/run-start': {
const runId = typeof data.runId === 'string' ? data.runId : undefined
if (runId === undefined || byRunId.has(runId)) break
const fold: RunFold = {
view: {
runId,
name: typeof data.name === 'string' && data.name !== '' ? data.name : runId,
originSessionId,
status: 'running',
phases: [],
startedSeq: event.seq,
startedAt: event.time,
},
memberBySeq: new Map(),
}
byRunId.set(runId, fold)
runs.push(fold)
break
}
case 'tool-workflow/agent-start': {
const fold = runOf(data, byRunId)
const seq = seqOf(data)
if (fold === undefined || seq === undefined || fold.memberBySeq.has(seq)) break
const member: WorkflowRunMemberView = {
seq,
label: typeof data.label === 'string' && data.label !== '' ? data.label : `#${seq}`,
...(typeof data.phase === 'string' ? { phase: data.phase } : {}),
childId: typeof data.childId === 'string' ? data.childId : '',
}
fold.memberBySeq.set(seq, member)
// Phase groups in first-seen order; unphased members share one group.
let phase = fold.view.phases.find(candidate => candidate.title === member.phase)
if (phase === undefined) {
phase = member.phase === undefined ? { members: [] } : { title: member.phase, members: [] }
fold.view.phases.push(phase)
}
phase.members.push(member)
break
}
case 'tool-workflow/agent-end': {
const fold = runOf(data, byRunId)
const seq = seqOf(data)
const member = fold?.memberBySeq.get(seq ?? -1)
if (member === undefined) break
const outcome = data.outcome
if (outcome === 'completed' || outcome === 'failed' || outcome === 'cancelled') {
member.outcome = outcome
}
break
}
case 'tool-workflow/run-end': {
const fold = runOf(data, byRunId)
if (fold === undefined) break
const stopReason = data.stopReason
if (stopReason === 'completed' || stopReason === 'cancelled' || stopReason === 'error') {
fold.view.stopReason = stopReason
fold.view.status = stopReason === 'error' ? 'failed' : stopReason
} else {
fold.view.status = 'failed'
}
fold.view.finishedAt = event.time
break
}
default:
break
}
}
return runs.map(fold => fold.view)
}
/** The run a workflow event belongs to (undefined = unknown/orphan row). */
function runOf(
data: Record<string, unknown>,
byRunId: ReadonlyMap<string, RunFold>,
): RunFold | undefined {
const runId = data.runId
return typeof runId === 'string' ? byRunId.get(runId) : undefined
}
/** The member sequence number of one agent-start/agent-end row. */
function seqOf(data: Record<string, unknown>): number | undefined {
const seq = data.seq
return typeof seq === 'number' && Number.isSafeInteger(seq) && seq >= 0 ? seq : undefined
}