From b98ef9b70aa28c7cf2bcd5d5cb215dddbd17b7cf Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Fri, 14 Aug 2026 13:18:36 -0700 Subject: [PATCH] Persist subagent lane lifecycle blocks from the span handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lane-scoped span events route to the span handler, which only recorded trace side effects — no subagent start block was ever persisted (verified: a seven-agent run stored 104 blocks with zero starts). Grouping then fell back to keying lane content by agent NAME, so a respawned agent of the same type merged invisibly into the first one's card until it resolved. The handler now persists the start block (spanId-keyed and deduped, carrying the display name) and stamps endedAt on close, giving every invocation its own card. --- apps/sim/lib/copilot/request/handlers/span.ts | 41 +++++++++++++++++++ apps/sim/lib/copilot/request/types.ts | 2 + 2 files changed, 43 insertions(+) diff --git a/apps/sim/lib/copilot/request/handlers/span.ts b/apps/sim/lib/copilot/request/handlers/span.ts index 2ad6dcf338..ba2fba1caa 100644 --- a/apps/sim/lib/copilot/request/handlers/span.ts +++ b/apps/sim/lib/copilot/request/handlers/span.ts @@ -3,6 +3,7 @@ import { MothershipStreamV1SpanPayloadKind, } from '@/lib/copilot/generated/mothership-stream-v1' import type { StreamHandler } from './types' +import { addContentBlock } from './types' /** * Mirror Go-emitted span lifecycle events onto the Sim-side TraceCollector. @@ -34,6 +35,46 @@ export const handleSpanEvent: StreamHandler = (event, context) => { // (e.g. two parallel `research` subagents) get distinct trace spans. Fall // back to agent:parentToolCallId for legacy events that predate span ids. const traceKey = event.scope?.spanId || `${scopeAgent}:${event.scope?.parentToolCallId || ''}` + // Persist the lane's lifecycle markers. Without a `subagent` start block, + // the transcript parser falls back to grouping lane content by agent NAME + // — so a respawned agent of the same type (a second concurrent `search`) + // silently merges into the first one's card and appears "missing" until + // that one resolves. Keyed and deduped by spanId, so every invocation — + // including same-type concurrent respawns — gets its own group. + const startData = payload.data as Record | undefined + if (evt === MothershipStreamV1SpanLifecycleEvent.start) { + context.openSubagentSpans ??= new Set() + if (!context.openSubagentSpans.has(traceKey)) { + context.openSubagentSpans.add(traceKey) + addContentBlock(context, { + type: 'subagent', + content: scopeAgent, + ...(event.scope?.parentToolCallId + ? { parentToolCallId: event.scope.parentToolCallId } + : {}), + ...(event.scope?.spanId ? { spanId: event.scope.spanId } : {}), + ...(event.scope?.parentSpanId ? { parentSpanId: event.scope.parentSpanId } : {}), + ...(typeof startData?.name === 'string' && startData.name + ? { subagentName: startData.name } + : {}), + }) + } + } else if (evt === MothershipStreamV1SpanLifecycleEvent.end) { + if (context.openSubagentSpans?.has(traceKey)) { + context.openSubagentSpans.delete(traceKey) + for (let i = context.contentBlocks.length - 1; i >= 0; i--) { + const b = context.contentBlocks[i] + if ( + b.type === 'subagent' && + b.endedAt === undefined && + (b.spanId || '') === (event.scope?.spanId || '') + ) { + b.endedAt = Date.now() + break + } + } + } + } if (evt === MothershipStreamV1SpanLifecycleEvent.start) { const span = context.trace.startSpan(`subagent:${scopeAgent}`, 'go.subagent', { agent: scopeAgent, diff --git a/apps/sim/lib/copilot/request/types.ts b/apps/sim/lib/copilot/request/types.ts index 35127f3edf..f11a9b15a4 100644 --- a/apps/sim/lib/copilot/request/types.ts +++ b/apps/sim/lib/copilot/request/types.ts @@ -148,6 +148,8 @@ export interface StreamingContext { * block. Per-lane keying keeps each subagent's reasoning intact. */ subagentThinkingBlocks: Map + /** Span ids whose lane start block has been persisted (dedupe across replays). */ + openSubagentSpans?: Set isInThinkingBlock: boolean subAgentContent: Record subAgentToolCalls: Record