From 87fb47ab19d6ecdb3c6552545366ce62680c4279 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 15:35:01 +0800 Subject: [PATCH 1/6] fix(daemon): preserve parentToolCallId in compaction engine for parallel subagent streams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TurnBoundaryCompactionEngine.mergeTextSlot merged all consecutive agent_thought_chunk / agent_message_chunk events into a single slot regardless of parentToolCallId, destroying per-subagent attribution. When 9+ parallel subagents streamed concurrently, the compacted replay contained garbled text with no parentToolCallId — the downstream transcript reducer (fixed in #4689) could not route blocks to the correct subagent tool call. - Add parentToolCallId-aware dual-path merging: subagent chunks use an indexed lookup (textSlotIndex) to merge by (kind, parentToolCallId) even when interleaved; top-level chunks preserve the original consecutive-only merge to maintain text segmentation around tool calls. - Evict textSlotIndex entries when a same-parent tool_call arrives, mirroring the transcript reducer's clearActiveText(parentToolCallId) so compacted replay block segmentation matches live behavior. - Defensive backfill: ensure parentToolCallId survives in the compacted event's _meta even if the last chunk's _meta lost it. - Harden seed() to clear in-flight state (slots, indexes, liveJournal). 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../acp-bridge/src/compactionEngine.test.ts | 245 ++++++++++++++++++ packages/acp-bridge/src/compactionEngine.ts | 120 +++++++-- 2 files changed, 340 insertions(+), 25 deletions(-) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index 70b865f0f04..a6f1f347e2f 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -152,6 +152,30 @@ function makeAvailableCommandsUpdate(id: number): BridgeEvent { }; } +function makeTextChunkWithParent( + id: number, + text: string, + parentToolCallId: string, +): BridgeEvent { + const event = makeTextChunk(id, text); + (event.data as { update: Record }).update._meta = { + parentToolCallId, + }; + return event; +} + +function makeThoughtChunkWithParent( + id: number, + text: string, + parentToolCallId: string, +): BridgeEvent { + const event = makeThoughtChunk(id, text); + (event.data as { update: Record }).update._meta = { + parentToolCallId, + }; + return event; +} + function extractTexts(events: BridgeEvent[]): string[] { return events .filter((e) => e.type === 'session_update') @@ -647,3 +671,224 @@ describe('EventBus + CompactionEngine integration', () => { expect(snapshot.liveJournal).toHaveLength(0); }); }); + +describe('parentToolCallId-aware text merging', () => { + type UpdatePayload = { + update: { + sessionUpdate: string; + content: { text: string }; + _meta?: Record; + }; + }; + + function getUpdate(event: BridgeEvent): UpdatePayload['update'] { + return (event.data as UpdatePayload).update; + } + + it('separates text chunks with different parentToolCallIds', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'Agent A says ', 'task-A')); + engine.ingest(makeTextChunkWithParent(2, 'Agent B says ', 'task-B')); + engine.ingest(makeTextChunkWithParent(3, 'hello', 'task-A')); + engine.ingest(makeTextChunkWithParent(4, 'world', 'task-B')); + engine.ingest(makeTurnComplete(5)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(2); + expect(getUpdate(textEvents[0]!).content.text).toBe('Agent A says hello'); + expect(getUpdate(textEvents[1]!).content.text).toBe('Agent B says world'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-B'); + }); + + it('merges interleaved thought chunks with the same parentToolCallId', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeThoughtChunkWithParent(1, 'A thinks ', 'task-A')); + engine.ingest(makeThoughtChunkWithParent(2, 'B thinks ', 'task-B')); + engine.ingest(makeThoughtChunkWithParent(3, 'more', 'task-A')); + engine.ingest(makeThoughtChunkWithParent(4, 'more', 'task-B')); + engine.ingest(makeTurnComplete(5)); + + const snap = engine.snapshot(); + const thoughtEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_thought_chunk', + ); + expect(thoughtEvents).toHaveLength(2); + expect(getUpdate(thoughtEvents[0]!).content.text).toBe('A thinks more'); + expect(getUpdate(thoughtEvents[1]!).content.text).toBe('B thinks more'); + expect(getUpdate(thoughtEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(thoughtEvents[1]!)._meta?.['parentToolCallId']).toBe('task-B'); + }); + + it('does not merge top-level text with subagent text', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunk(1, 'Top-level ')); + engine.ingest(makeTextChunkWithParent(2, 'subagent ', 'task-A')); + engine.ingest(makeTextChunk(3, 'more top')); + engine.ingest(makeTurnComplete(4)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(3); + expect(getUpdate(textEvents[0]!).content.text).toBe('Top-level '); + expect(getUpdate(textEvents[1]!).content.text).toBe('subagent '); + expect(getUpdate(textEvents[2]!).content.text).toBe('more top'); + expect(getUpdate(textEvents[0]!)._meta).toBeUndefined(); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[2]!)._meta).toBeUndefined(); + }); + + it('same subagent thought + text produce separate slots', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeThoughtChunkWithParent(1, 'thinking...', 'task-A')); + engine.ingest(makeThoughtChunkWithParent(2, ' deeply', 'task-A')); + engine.ingest(makeTextChunkWithParent(3, 'Answer: ', 'task-A')); + engine.ingest(makeTextChunkWithParent(4, 'yes', 'task-A')); + engine.ingest(makeTurnComplete(5)); + + const snap = engine.snapshot(); + const sessionUpdates = snap.compactedTurns.filter((e) => e.type === 'session_update'); + expect(sessionUpdates).toHaveLength(2); + + const thought = sessionUpdates.find((e) => getUpdate(e).sessionUpdate === 'agent_thought_chunk')!; + const text = sessionUpdates.find((e) => getUpdate(e).sessionUpdate === 'agent_message_chunk')!; + expect(getUpdate(thought).content.text).toBe('thinking... deeply'); + expect(getUpdate(text).content.text).toBe('Answer: yes'); + expect(getUpdate(thought)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(text)._meta?.['parentToolCallId']).toBe('task-A'); + }); + + it('same-parent tool call segments subagent text into separate slots', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunk(1, 'Before')); + engine.ingest(makeTextChunkWithParent(2, 'sub-A part1', 'task-A')); + // tool_call with parentToolCallId=task-A evicts task-A's text slot + engine.ingest({ + id: 3, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'tool_call', + toolCallId: 'tc1', + status: 'running', + _meta: { parentToolCallId: 'task-A' }, + }, + }, + }); + engine.ingest(makeTextChunkWithParent(4, 'sub-A part2', 'task-A')); + engine.ingest(makeTextChunk(5, 'After')); + engine.ingest(makeTurnComplete(6)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(4); + expect(getUpdate(textEvents[0]!).content.text).toBe('Before'); + expect(getUpdate(textEvents[1]!).content.text).toBe('sub-A part1'); + expect(getUpdate(textEvents[2]!).content.text).toBe('sub-A part2'); + expect(getUpdate(textEvents[3]!).content.text).toBe('After'); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[2]!)._meta?.['parentToolCallId']).toBe('task-A'); + }); + + it('non-parent tool call does not evict subagent text slots', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'sub-A', 'task-A')); + // tool_call WITHOUT parentToolCallId should not evict task-A + engine.ingest(makeToolCall(2, 'tc1', 'running')); + engine.ingest(makeTextChunkWithParent(3, ' more', 'task-A')); + engine.ingest(makeTurnComplete(4)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(1); + expect(getUpdate(textEvents[0]!).content.text).toBe('sub-A more'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + }); + + it('[subA, main, main, subA] produces two merged events', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'A-start ', 'task-A')); + engine.ingest(makeTextChunk(2, 'main-1 ')); + engine.ingest(makeTextChunk(3, 'main-2')); + engine.ingest(makeTextChunkWithParent(4, 'A-end', 'task-A')); + engine.ingest(makeTurnComplete(5)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(2); + expect(getUpdate(textEvents[0]!).content.text).toBe('A-start A-end'); + expect(getUpdate(textEvents[1]!).content.text).toBe('main-1 main-2'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[1]!)._meta).toBeUndefined(); + }); + + it('handles 9 parallel subagent thought streams without garbling', () => { + const engine = new TurnBoundaryCompactionEngine(); + const subagents = Array.from({ length: 9 }, (_, i) => `task-${i}`); + let eventId = 1; + + for (let round = 0; round < 3; round++) { + for (const taskId of subagents) { + engine.ingest( + makeThoughtChunkWithParent(eventId++, `[${taskId}:${round}]`, taskId), + ); + } + } + engine.ingest(makeTurnComplete(eventId)); + + const snap = engine.snapshot(); + const thoughtEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_thought_chunk', + ); + expect(thoughtEvents).toHaveLength(9); + for (let i = 0; i < 9; i++) { + const taskId = `task-${i}`; + const update = getUpdate(thoughtEvents[i]!); + expect(update.content.text).toBe(`[${taskId}:0][${taskId}:1][${taskId}:2]`); + expect(update._meta?.['parentToolCallId']).toBe(taskId); + } + }); + + it('defensive backfill: parentToolCallId in output even if last chunk _meta lost it', () => { + const engine = new TurnBoundaryCompactionEngine(); + // First chunk has parentToolCallId in _meta + engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); + // Second chunk has _meta with usage but WITHOUT parentToolCallId + engine.ingest({ + id: 2, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'world' }, + _meta: { usage: { inputTokens: 100 } }, + }, + }, + }); + engine.ingest(makeTurnComplete(3)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + // The chunk without parentToolCallId goes to the top-level path, + // so we get two separate events + expect(textEvents).toHaveLength(2); + expect(getUpdate(textEvents[0]!).content.text).toBe('hello '); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[1]!).content.text).toBe('world'); + }); +}); diff --git a/packages/acp-bridge/src/compactionEngine.ts b/packages/acp-bridge/src/compactionEngine.ts index 89c25f6cf35..94b487bccf7 100644 --- a/packages/acp-bridge/src/compactionEngine.ts +++ b/packages/acp-bridge/src/compactionEngine.ts @@ -38,9 +38,9 @@ const LATEST_WINS_UPDATES = new Set([ ]); type CompactedSlot = - | { kind: 'text'; chunks: string[]; lastEventId: number; lastMeta: unknown } | { - kind: 'thought'; + kind: 'text' | 'thought'; + parentToolCallId?: string; chunks: string[]; lastEventId: number; lastMeta: unknown; @@ -68,6 +68,7 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { private slots: CompactedSlot[] = []; private toolSlotIndex: Map = new Map(); + private textSlotIndex: Map = new Map(); ingest(event: BridgeEvent): void { if (this.closed) return; @@ -104,6 +105,10 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { if (this.closed) return; this.compactedTurns = snapshot.compactedTurns.slice(); this.lastEventId = snapshot.lastEventId; + this.liveJournal = []; + this.slots = []; + this.toolSlotIndex.clear(); + this.textSlotIndex.clear(); } close(): void { @@ -113,6 +118,7 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { this.liveJournal = []; this.slots = []; this.toolSlotIndex.clear(); + this.textSlotIndex.clear(); } private classifySessionUpdate(event: BridgeEvent): void { @@ -156,6 +162,18 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { event: normalizedEvent, }); } + // Evict text/thought slots for this tool's parent so subsequent + // chunks from the same subagent create new slots — mirrors the + // transcript reducer's clearActiveText(parentToolCallId) behavior. + const toolParent = + extractParentToolCallIdFromMeta(data?.update?._meta) ?? + (typeof data?.update?.['parentToolCallId'] === 'string' + ? data.update['parentToolCallId'] + : undefined); + if (toolParent) { + this.textSlotIndex.delete(`text::${toolParent}`); + this.textSlotIndex.delete(`thought::${toolParent}`); + } break; } default: { @@ -187,18 +205,55 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { data: SessionUpdateData | undefined, ): void { const text = data?.update?.content?.text ?? ''; - const lastSlot = this.slots[this.slots.length - 1]; - if (lastSlot && lastSlot.kind === kind) { - lastSlot.chunks.push(text); - if (event.id !== undefined) lastSlot.lastEventId = event.id; - lastSlot.lastMeta = data?.update?._meta ?? lastSlot.lastMeta; + const meta = data?.update?._meta; + const parentToolCallId = extractParentToolCallIdFromMeta(meta); + + if (parentToolCallId != null) { + // Subagent path: merge by (kind, parentToolCallId) regardless of + // position. Parallel subagents interleave chunks; the index lets + // us reassemble each subagent's stream without garbling. + const slotKey = `${kind}::${parentToolCallId}`; + const existingIdx = this.textSlotIndex.get(slotKey); + if (existingIdx !== undefined) { + const slot = this.slots[existingIdx] as Extract< + CompactedSlot, + { kind: 'text' | 'thought' } + >; + slot.chunks.push(text); + if (event.id !== undefined) slot.lastEventId = event.id; + slot.lastMeta = meta ?? slot.lastMeta; + } else { + this.textSlotIndex.set(slotKey, this.slots.length); + this.slots.push({ + kind, + parentToolCallId, + chunks: [text], + lastEventId: event.id ?? 0, + lastMeta: meta, + }); + } } else { - this.slots.push({ - kind, - chunks: [text], - lastEventId: event.id ?? 0, - lastMeta: data?.update?._meta, - }); + // Top-level path: merge only consecutive same-kind chunks that + // also have no parentToolCallId. Preserves text segmentation + // around tool calls (text before / text after stay separate). + const lastSlot = this.slots[this.slots.length - 1]; + if ( + lastSlot && + lastSlot.kind === kind && + lastSlot.parentToolCallId == null + ) { + lastSlot.chunks.push(text); + if (event.id !== undefined) lastSlot.lastEventId = event.id; + lastSlot.lastMeta = meta ?? lastSlot.lastMeta; + } else { + this.slots.push({ + kind, + parentToolCallId: undefined, + chunks: [text], + lastEventId: event.id ?? 0, + lastMeta: meta, + }); + } } } @@ -208,25 +263,29 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { for (const slot of this.slots) { switch (slot.kind) { case 'text': + case 'thought': { + let meta = slot.lastMeta; + if ( + slot.parentToolCallId && + extractParentToolCallIdFromMeta(meta) !== slot.parentToolCallId + ) { + meta = { + ...(typeof meta === 'object' && meta !== null ? meta : {}), + parentToolCallId: slot.parentToolCallId, + }; + } compacted.push( makeMergedSessionUpdateEvent( - 'agent_message_chunk', - slot.chunks.join(''), - slot.lastEventId, - slot.lastMeta, - ), - ); - break; - case 'thought': - compacted.push( - makeMergedSessionUpdateEvent( - 'agent_thought_chunk', + slot.kind === 'text' + ? 'agent_message_chunk' + : 'agent_thought_chunk', slot.chunks.join(''), slot.lastEventId, - slot.lastMeta, + meta, ), ); break; + } case 'tool': case 'misc': case 'latestWins': @@ -242,6 +301,7 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { this.liveJournal = []; this.slots = []; this.toolSlotIndex.clear(); + this.textSlotIndex.clear(); } } @@ -279,6 +339,16 @@ function normalizeToolCallType(event: BridgeEvent): BridgeEvent { return event; } +function extractParentToolCallIdFromMeta( + meta: unknown, +): string | undefined { + if (typeof meta === 'object' && meta !== null) { + const val = (meta as Record)['parentToolCallId']; + return typeof val === 'string' && val.length > 0 ? val : undefined; + } + return undefined; +} + function mergeToolCallEvent( existing: BridgeEvent, incoming: BridgeEvent, From f0d6041bdac4db537f61bd78cde5f20b9b7acd75 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 15:43:57 +0800 Subject: [PATCH 2/6] fix(daemon): rename misleading test to match actual behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review correctly noted the "defensive backfill" test name was inaccurate — it actually tests that chunks without parentToolCallId separate into the top-level path. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../acp-bridge/src/compactionEngine.test.ts | 152 ++++++++++++++---- 1 file changed, 121 insertions(+), 31 deletions(-) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index a6f1f347e2f..79c3ccac818 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -623,9 +623,33 @@ describe('EventBus + CompactionEngine integration', () => { const engine = new TurnBoundaryCompactionEngine(); const bus = new EventBus(100, undefined, engine); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'user_message_chunk', content: { type: 'text', text: 'hello' } } } }); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'Hi' } } } }); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: ' there' } } } }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'user_message_chunk', + content: { type: 'text', text: 'hello' }, + }, + }, + }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'Hi' }, + }, + }, + }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: ' there' }, + }, + }, + }); bus.publish({ type: 'turn_complete', data: { stopReason: 'end_turn' } }); const snapshot = bus.snapshotReplay(); @@ -650,8 +674,24 @@ describe('EventBus + CompactionEngine integration', () => { const engine = new TurnBoundaryCompactionEngine(); const bus = new EventBus(100, undefined, engine); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'streaming' } } } }); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: '...' } } } }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'streaming' }, + }, + }, + }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: '...' }, + }, + }, + }); const snapshot = bus.snapshotReplay()!; expect(snapshot.compactedTurns).toHaveLength(0); @@ -663,7 +703,15 @@ describe('EventBus + CompactionEngine integration', () => { const engine = new TurnBoundaryCompactionEngine(); const bus = new EventBus(100, undefined, engine); - bus.publish({ type: 'session_update', data: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'test' } } } }); + bus.publish({ + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'test' }, + }, + }, + }); bus.close(); const snapshot = engine.snapshot(); @@ -695,13 +743,19 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); expect(textEvents).toHaveLength(2); expect(getUpdate(textEvents[0]!).content.text).toBe('Agent A says hello'); expect(getUpdate(textEvents[1]!).content.text).toBe('Agent B says world'); - expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); - expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-B'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe( + 'task-B', + ); }); it('merges interleaved thought chunks with the same parentToolCallId', () => { @@ -714,13 +768,19 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const thoughtEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_thought_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_thought_chunk', ); expect(thoughtEvents).toHaveLength(2); expect(getUpdate(thoughtEvents[0]!).content.text).toBe('A thinks more'); expect(getUpdate(thoughtEvents[1]!).content.text).toBe('B thinks more'); - expect(getUpdate(thoughtEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); - expect(getUpdate(thoughtEvents[1]!)._meta?.['parentToolCallId']).toBe('task-B'); + expect(getUpdate(thoughtEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); + expect(getUpdate(thoughtEvents[1]!)._meta?.['parentToolCallId']).toBe( + 'task-B', + ); }); it('does not merge top-level text with subagent text', () => { @@ -732,14 +792,18 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); expect(textEvents).toHaveLength(3); expect(getUpdate(textEvents[0]!).content.text).toBe('Top-level '); expect(getUpdate(textEvents[1]!).content.text).toBe('subagent '); expect(getUpdate(textEvents[2]!).content.text).toBe('more top'); expect(getUpdate(textEvents[0]!)._meta).toBeUndefined(); - expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); expect(getUpdate(textEvents[2]!)._meta).toBeUndefined(); }); @@ -752,11 +816,17 @@ describe('parentToolCallId-aware text merging', () => { engine.ingest(makeTurnComplete(5)); const snap = engine.snapshot(); - const sessionUpdates = snap.compactedTurns.filter((e) => e.type === 'session_update'); + const sessionUpdates = snap.compactedTurns.filter( + (e) => e.type === 'session_update', + ); expect(sessionUpdates).toHaveLength(2); - const thought = sessionUpdates.find((e) => getUpdate(e).sessionUpdate === 'agent_thought_chunk')!; - const text = sessionUpdates.find((e) => getUpdate(e).sessionUpdate === 'agent_message_chunk')!; + const thought = sessionUpdates.find( + (e) => getUpdate(e).sessionUpdate === 'agent_thought_chunk', + )!; + const text = sessionUpdates.find( + (e) => getUpdate(e).sessionUpdate === 'agent_message_chunk', + )!; expect(getUpdate(thought).content.text).toBe('thinking... deeply'); expect(getUpdate(text).content.text).toBe('Answer: yes'); expect(getUpdate(thought)._meta?.['parentToolCallId']).toBe('task-A'); @@ -787,15 +857,21 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); expect(textEvents).toHaveLength(4); expect(getUpdate(textEvents[0]!).content.text).toBe('Before'); expect(getUpdate(textEvents[1]!).content.text).toBe('sub-A part1'); expect(getUpdate(textEvents[2]!).content.text).toBe('sub-A part2'); expect(getUpdate(textEvents[3]!).content.text).toBe('After'); - expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe('task-A'); - expect(getUpdate(textEvents[2]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[1]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); + expect(getUpdate(textEvents[2]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); }); it('non-parent tool call does not evict subagent text slots', () => { @@ -808,11 +884,15 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); expect(textEvents).toHaveLength(1); expect(getUpdate(textEvents[0]!).content.text).toBe('sub-A more'); - expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); }); it('[subA, main, main, subA] produces two merged events', () => { @@ -825,12 +905,16 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); expect(textEvents).toHaveLength(2); expect(getUpdate(textEvents[0]!).content.text).toBe('A-start A-end'); expect(getUpdate(textEvents[1]!).content.text).toBe('main-1 main-2'); - expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); expect(getUpdate(textEvents[1]!)._meta).toBeUndefined(); }); @@ -850,22 +934,24 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const thoughtEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_thought_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_thought_chunk', ); expect(thoughtEvents).toHaveLength(9); for (let i = 0; i < 9; i++) { const taskId = `task-${i}`; const update = getUpdate(thoughtEvents[i]!); - expect(update.content.text).toBe(`[${taskId}:0][${taskId}:1][${taskId}:2]`); + expect(update.content.text).toBe( + `[${taskId}:0][${taskId}:1][${taskId}:2]`, + ); expect(update._meta?.['parentToolCallId']).toBe(taskId); } }); - it('defensive backfill: parentToolCallId in output even if last chunk _meta lost it', () => { + it('chunk without parentToolCallId separates from subagent chunk into top-level path', () => { const engine = new TurnBoundaryCompactionEngine(); - // First chunk has parentToolCallId in _meta engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); - // Second chunk has _meta with usage but WITHOUT parentToolCallId engine.ingest({ id: 2, v: 1, @@ -882,13 +968,17 @@ describe('parentToolCallId-aware text merging', () => { const snap = engine.snapshot(); const textEvents = snap.compactedTurns.filter( - (e) => e.type === 'session_update' && getUpdate(e).sessionUpdate === 'agent_message_chunk', + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', ); // The chunk without parentToolCallId goes to the top-level path, // so we get two separate events expect(textEvents).toHaveLength(2); expect(getUpdate(textEvents[0]!).content.text).toBe('hello '); - expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe('task-A'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); expect(getUpdate(textEvents[1]!).content.text).toBe('world'); }); }); From fe46e373bfc352478d582cf13469287ff41dc92d Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 19:15:31 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(daemon):=20address=20wenshao=20review?= =?UTF-8?q?=20=E2=80=94=20bracket=20notation,=20eviction=20scope,=20backfi?= =?UTF-8?q?ll=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use bracket notation for _meta access in test helpers (TS4111 fix) - Move textSlotIndex eviction into the new-tool-only branch so tool_call_update does not over-segment subagent text - Add tests for meta backfill and tool_call_update non-eviction 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../acp-bridge/src/compactionEngine.test.ts | 104 +++++++++++++++++- packages/acp-bridge/src/compactionEngine.ts | 25 +++-- 2 files changed, 115 insertions(+), 14 deletions(-) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index 79c3ccac818..8cb167aaac2 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -158,7 +158,7 @@ function makeTextChunkWithParent( parentToolCallId: string, ): BridgeEvent { const event = makeTextChunk(id, text); - (event.data as { update: Record }).update._meta = { + (event.data as { update: Record }).update['_meta'] = { parentToolCallId, }; return event; @@ -170,7 +170,7 @@ function makeThoughtChunkWithParent( parentToolCallId: string, ): BridgeEvent { const event = makeThoughtChunk(id, text); - (event.data as { update: Record }).update._meta = { + (event.data as { update: Record }).update['_meta'] = { parentToolCallId, }; return event; @@ -981,4 +981,104 @@ describe('parentToolCallId-aware text merging', () => { ); expect(getUpdate(textEvents[1]!).content.text).toBe('world'); }); + + it('tool_call_update does not evict subagent text slots', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'part1', 'task-A')); + // First tool_call creates the tool block — evicts task-A + engine.ingest({ + id: 2, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'tool_call', + toolCallId: 'tc1', + status: 'running', + _meta: { parentToolCallId: 'task-A' }, + }, + }, + }); + engine.ingest(makeTextChunkWithParent(3, 'part2', 'task-A')); + // tool_call_update is a status update, not a new tool — should NOT evict + engine.ingest({ + id: 4, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'tool_call_update', + toolCallId: 'tc1', + status: 'completed', + _meta: { parentToolCallId: 'task-A' }, + }, + }, + }); + engine.ingest(makeTextChunkWithParent(5, ' part3', 'task-A')); + engine.ingest(makeTurnComplete(6)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + // part1 (evicted by tool_call), part2+part3 (merged, not evicted by update) + expect(textEvents).toHaveLength(2); + expect(getUpdate(textEvents[0]!).content.text).toBe('part1'); + expect(getUpdate(textEvents[1]!).content.text).toBe('part2 part3'); + }); + + it('meta backfill: parentToolCallId preserved when last chunk _meta overwritten', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); + // Second chunk has _meta with usage that overwrites lastMeta but keeps parentToolCallId + engine.ingest({ + id: 2, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'world' }, + _meta: { parentToolCallId: 'task-A', usage: { inputTokens: 100 } }, + }, + }, + }); + engine.ingest(makeTurnComplete(3)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(1); + expect(getUpdate(textEvents[0]!).content.text).toBe('hello world'); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); + }); + + it('meta backfill: parentToolCallId injected when last chunk _meta lacks it', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); + // Simulate a chunk routed to subagent path (has parentToolCallId) + // followed by the same slot receiving a chunk whose _meta lost it + // This requires manually tweaking — in practice the indexing ensures + // same-parent routing, but we test the defensive backfill by checking + // that even when lastMeta has no parentToolCallId, the output still has it + engine.ingest(makeTurnComplete(2)); + + const snap = engine.snapshot(); + const textEvents = snap.compactedTurns.filter( + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_message_chunk', + ); + expect(textEvents).toHaveLength(1); + expect(getUpdate(textEvents[0]!)._meta?.['parentToolCallId']).toBe( + 'task-A', + ); + }); }); diff --git a/packages/acp-bridge/src/compactionEngine.ts b/packages/acp-bridge/src/compactionEngine.ts index 94b487bccf7..f88869e77b7 100644 --- a/packages/acp-bridge/src/compactionEngine.ts +++ b/packages/acp-bridge/src/compactionEngine.ts @@ -161,18 +161,19 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { toolCallId, event: normalizedEvent, }); - } - // Evict text/thought slots for this tool's parent so subsequent - // chunks from the same subagent create new slots — mirrors the - // transcript reducer's clearActiveText(parentToolCallId) behavior. - const toolParent = - extractParentToolCallIdFromMeta(data?.update?._meta) ?? - (typeof data?.update?.['parentToolCallId'] === 'string' - ? data.update['parentToolCallId'] - : undefined); - if (toolParent) { - this.textSlotIndex.delete(`text::${toolParent}`); - this.textSlotIndex.delete(`thought::${toolParent}`); + // Evict text/thought slots for this tool's parent so subsequent + // chunks from the same subagent create new slots — mirrors the + // transcript reducer's clearActiveText(parentToolCallId) which + // only fires on new tool block creation, not on status updates. + const toolParent = + extractParentToolCallIdFromMeta(data?.update?._meta) ?? + (typeof data?.update?.['parentToolCallId'] === 'string' + ? data.update['parentToolCallId'] + : undefined); + if (toolParent) { + this.textSlotIndex.delete(`text::${toolParent}`); + this.textSlotIndex.delete(`thought::${toolParent}`); + } } break; } From 5be1f3d6d84513e6e93bd1f14ab1cb1fbab18723 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 21:14:41 +0800 Subject: [PATCH 4/6] test(daemon): add seed() slot cleanup coverage per wenshao review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verify that seed() clears in-flight slots, liveJournal, and index maps so stale pre-seed data does not leak into post-seed compaction. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../acp-bridge/src/compactionEngine.test.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index 8cb167aaac2..51fa9cc3fca 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -511,6 +511,30 @@ describe('TurnBoundaryCompactionEngine', () => { expect(snap.compactedTurns).toHaveLength(4); // 2 seeded + 2 new expect(snap.lastEventId).toBe(13); }); + + it('seed clears in-flight slots so stale data does not corrupt post-seed output', () => { + const engine = new TurnBoundaryCompactionEngine(); + // Populate in-flight state (no turn_complete to compact them) + engine.ingest(makeTextChunkWithParent(1, 'stale-sub', 'old-task')); + engine.ingest(makeTextChunk(2, 'stale-top')); + engine.ingest(makeToolCall(3, 'tc-stale', 'running')); + + // Seed replaces history — should also clear in-flight slots + engine.seed({ + compactedTurns: [makeTextChunk(100, 'seeded'), makeTurnComplete(101)], + lastEventId: 101, + }); + + // Ingest fresh events and complete the turn + engine.ingest(makeTextChunk(102, 'fresh')); + engine.ingest(makeTurnComplete(103)); + + const snap = engine.snapshot(); + const texts = extractTexts(snap.compactedTurns); + // Should contain only seeded + fresh, not the stale pre-seed events + expect(texts).toEqual(['seeded', 'fresh']); + expect(snap.compactedTurns).toHaveLength(4); // seeded text + seeded tc + fresh text + fresh tc + }); }); describe('close', () => { From 5f88fcd8ff3e3dad8ffa102cecdc11df024fe472 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 21:45:24 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(daemon):=20address=20wenshao=20review?= =?UTF-8?q?=20round=203=20=E2=80=94=20remove=20fallback,=20reword=20commen?= =?UTF-8?q?t,=20add=20thought=20eviction=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dead parentToolCallId fallback in tool eviction (emitters always use _meta), aligning with mergeTextSlot extraction - Reword eviction comment to be self-describing - Add thought slot eviction test coverage 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- .../acp-bridge/src/compactionEngine.test.ts | 30 +++++++++++++++++++ packages/acp-bridge/src/compactionEngine.ts | 15 ++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index 51fa9cc3fca..8d9555c306e 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -919,6 +919,36 @@ describe('parentToolCallId-aware text merging', () => { ); }); + it('same-parent tool call evicts thought slots too', () => { + const engine = new TurnBoundaryCompactionEngine(); + engine.ingest(makeThoughtChunkWithParent(1, 'thought-before', 'task-A')); + engine.ingest({ + id: 2, + v: 1, + type: 'session_update', + data: { + update: { + sessionUpdate: 'tool_call', + toolCallId: 'tc1', + status: 'running', + _meta: { parentToolCallId: 'task-A' }, + }, + }, + }); + engine.ingest(makeThoughtChunkWithParent(3, 'thought-after', 'task-A')); + engine.ingest(makeTurnComplete(4)); + + const snap = engine.snapshot(); + const thoughtEvents = snap.compactedTurns.filter( + (e) => + e.type === 'session_update' && + getUpdate(e).sessionUpdate === 'agent_thought_chunk', + ); + expect(thoughtEvents).toHaveLength(2); + expect(getUpdate(thoughtEvents[0]!).content.text).toBe('thought-before'); + expect(getUpdate(thoughtEvents[1]!).content.text).toBe('thought-after'); + }); + it('[subA, main, main, subA] produces two merged events', () => { const engine = new TurnBoundaryCompactionEngine(); engine.ingest(makeTextChunkWithParent(1, 'A-start ', 'task-A')); diff --git a/packages/acp-bridge/src/compactionEngine.ts b/packages/acp-bridge/src/compactionEngine.ts index f88869e77b7..7ef36f5e996 100644 --- a/packages/acp-bridge/src/compactionEngine.ts +++ b/packages/acp-bridge/src/compactionEngine.ts @@ -161,15 +161,12 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { toolCallId, event: normalizedEvent, }); - // Evict text/thought slots for this tool's parent so subsequent - // chunks from the same subagent create new slots — mirrors the - // transcript reducer's clearActiveText(parentToolCallId) which - // only fires on new tool block creation, not on status updates. - const toolParent = - extractParentToolCallIdFromMeta(data?.update?._meta) ?? - (typeof data?.update?.['parentToolCallId'] === 'string' - ? data.update['parentToolCallId'] - : undefined); + // Evict text/thought index entries for this tool's parent so + // subsequent chunks from the same subagent create new slots, + // preserving text segmentation around tool-call boundaries. + const toolParent = extractParentToolCallIdFromMeta( + data?.update?._meta, + ); if (toolParent) { this.textSlotIndex.delete(`text::${toolParent}`); this.textSlotIndex.delete(`thought::${toolParent}`); From fde8474837579066cc5ceb8b02b592b47cd2edf8 Mon Sep 17 00:00:00 2001 From: doudouOUC Date: Thu, 4 Jun 2026 23:19:58 +0800 Subject: [PATCH 6/6] refactor(daemon): remove unreachable meta backfill, rename tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The defensive parentToolCallId backfill in compactCurrentTurn was unreachable: the routing invariant in mergeTextSlot guarantees that any chunk reaching the subagent path has parentToolCallId in _meta, so slot.lastMeta always contains it. Remove the dead code and rename tests to describe what they actually verify. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --- packages/acp-bridge/src/compactionEngine.test.ts | 12 +++--------- packages/acp-bridge/src/compactionEngine.ts | 15 ++------------- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/packages/acp-bridge/src/compactionEngine.test.ts b/packages/acp-bridge/src/compactionEngine.test.ts index 8d9555c306e..f4a51d4978e 100644 --- a/packages/acp-bridge/src/compactionEngine.test.ts +++ b/packages/acp-bridge/src/compactionEngine.test.ts @@ -1083,10 +1083,9 @@ describe('parentToolCallId-aware text merging', () => { expect(getUpdate(textEvents[1]!).content.text).toBe('part2 part3'); }); - it('meta backfill: parentToolCallId preserved when last chunk _meta overwritten', () => { + it('parentToolCallId survives in lastMeta through multi-chunk merge', () => { const engine = new TurnBoundaryCompactionEngine(); engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); - // Second chunk has _meta with usage that overwrites lastMeta but keeps parentToolCallId engine.ingest({ id: 2, v: 1, @@ -1114,14 +1113,9 @@ describe('parentToolCallId-aware text merging', () => { ); }); - it('meta backfill: parentToolCallId injected when last chunk _meta lacks it', () => { + it('single subagent chunk preserves parentToolCallId in output', () => { const engine = new TurnBoundaryCompactionEngine(); - engine.ingest(makeTextChunkWithParent(1, 'hello ', 'task-A')); - // Simulate a chunk routed to subagent path (has parentToolCallId) - // followed by the same slot receiving a chunk whose _meta lost it - // This requires manually tweaking — in practice the indexing ensures - // same-parent routing, but we test the defensive backfill by checking - // that even when lastMeta has no parentToolCallId, the output still has it + engine.ingest(makeTextChunkWithParent(1, 'hello', 'task-A')); engine.ingest(makeTurnComplete(2)); const snap = engine.snapshot(); diff --git a/packages/acp-bridge/src/compactionEngine.ts b/packages/acp-bridge/src/compactionEngine.ts index 7ef36f5e996..f7bff4a1f67 100644 --- a/packages/acp-bridge/src/compactionEngine.ts +++ b/packages/acp-bridge/src/compactionEngine.ts @@ -261,17 +261,7 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { for (const slot of this.slots) { switch (slot.kind) { case 'text': - case 'thought': { - let meta = slot.lastMeta; - if ( - slot.parentToolCallId && - extractParentToolCallIdFromMeta(meta) !== slot.parentToolCallId - ) { - meta = { - ...(typeof meta === 'object' && meta !== null ? meta : {}), - parentToolCallId: slot.parentToolCallId, - }; - } + case 'thought': compacted.push( makeMergedSessionUpdateEvent( slot.kind === 'text' @@ -279,11 +269,10 @@ export class TurnBoundaryCompactionEngine implements CompactionEngine { : 'agent_thought_chunk', slot.chunks.join(''), slot.lastEventId, - meta, + slot.lastMeta, ), ); break; - } case 'tool': case 'misc': case 'latestWins':