Multi-Agent Interleaving
When Claude Code spawns sub-agents via the Agent tool, multiple independent content streams interleave on the same stdout. This breaks naive index-based deduplication.
The Problem
With a single lastContentIndex, interleaved events from different agents corrupt the dedup state:
Main agent: content=[thinking, text, tool_use(Agent)] → index=3
Sub-agent A: content=[text_A] → length 1 < 3 → reset, index=1
Sub-agent B: content=[text_B] → length 1 = 1 → SKIPS text_B!Sub-agent B's content array has length 1, same as the current lastContentIndex. The loop starts at index 1, but there's only 1 block (at index 0) — so B's event is silently dropped.
The Solution: Content Fingerprinting
The translator fingerprints the first content block of each assistant event. When the fingerprint changes, it knows the events are from a different agent context and resets the dedup index.
How Fingerprinting Works
| Block Type | Fingerprint |
|---|---|
tool_use | "tool_use:{id}" (unique ID is the strongest signal) |
thinking | "thinking:{first 64 chars}" |
text | "text:{first 64 chars}" |
| Other | "{type}:{tool_use_id or 'unknown'}" |
Behavior on Context Switch
Main agent: content=[thinking("plan...")] → fingerprint="thinking:plan..."
index=1
Sub-agent A: content=[text("Searching...")] → fingerprint="text:Searching..."
≠ previous → RESET, index=1
Sub-agent B: content=[text("Running tests...")] → fingerprint="text:Running tests..."
≠ previous → RESET, index=1
Sub-agent A: content=[text("Searching..."), tool_use] → fingerprint="text:Searching..."
= saved → DEDUP, process from index 1What Gets Re-Emitted
When the translator switches context (fingerprint changes), it resets the index to 0 and re-processes all blocks in the new content array. This means:
- Returning to an agent after a context switch will re-emit that agent's existing blocks
- This is a deliberate trade-off: duplicate events are safer than missing events
For a relay/dashboard, duplicates can be handled on the consumer side (e.g., by tracking tool_use IDs). Missing events would be invisible data loss.
Example: Parallel Sub-Agents
STDOUT SEQUENCE:
─────────────────────────────────────────────────────────────
1. system/init
2. assistant [thinking, text, tool_use(Agent_A), tool_use(Agent_B)] ← main agent
3. assistant [text("Agent A searching...")] ← sub-agent A
4. assistant [text("Agent A searching..."), tool_use(Grep)] ← sub-agent A (cumulative)
5. assistant [text("Agent B testing...")] ← sub-agent B (context switch!)
6. assistant [text("Agent B testing..."), tool_use(Bash)] ← sub-agent B (cumulative)
7. user [tool_result(Grep), tool_result(Bash)] ← results
8. user [tool_result(Agent_A), tool_result(Agent_B)] ← agent results
9. assistant [text("Both done.")] ← main agent resumes
10. result/success
TRANSLATOR OUTPUT:
─────────────────────────────────────────────────────────────
1 → session_meta
2 → thinking_delta, text_delta, tool_use(Agent_A), tool_use(Agent_B)
3 → text_delta("Agent A searching...") ← context switch, reset
4 → tool_use(Grep) ← dedup within A's context
5 → text_delta("Agent B testing...") ← context switch, reset
6 → tool_use(Bash) ← dedup within B's context
7 → tool_result(Grep), tool_result(Bash)
8 → tool_result(Agent_A), tool_result(Agent_B)
9 → text_delta("Both done.") ← context switch, reset
10 → turn_completeUsage
No special API is needed. The Translator handles this automatically:
const translator = new Translator()
// Works correctly even with interleaved multi-agent events
for (const line of lines) {
const event = parseLine(line)
if (!event) continue
const relays = translator.translate(event)
// relays are correctly deduplicated across agent contexts
}