* Post-pass for buildConversationChain: recover sibling assistant blocks and * tool_results that the single-parent walk orphaned. * * Streaming (claude.ts:~2024) emits one AssistantMessage per content_block_stop * — N parallel tool_uses → N messages, distinct uuid, same message.id. Each * tool_
( messages: Map<UUID, TranscriptMessage>, chain: TranscriptMessage[], seen: Set<UUID>, )
| 2116 | * this recovery pass handles them. |
| 2117 | */ |
| 2118 | function recoverOrphanedParallelToolResults( |
| 2119 | messages: Map<UUID, TranscriptMessage>, |
| 2120 | chain: TranscriptMessage[], |
| 2121 | seen: Set<UUID>, |
| 2122 | ): TranscriptMessage[] { |
| 2123 | type ChainAssistant = Extract<TranscriptMessage, { type: 'assistant' }> |
| 2124 | const chainAssistants = chain.filter( |
| 2125 | (m): m is ChainAssistant => m.type === 'assistant', |
| 2126 | ) |
| 2127 | if (chainAssistants.length === 0) return chain |
| 2128 | |
| 2129 | // Anchor = last on-chain member of each sibling group. chainAssistants is |
| 2130 | // already in chain order, so later iterations overwrite → last wins. |
| 2131 | const anchorByMsgId = new Map<string, ChainAssistant>() |
| 2132 | for (const a of chainAssistants) { |
| 2133 | if (a.message.id) anchorByMsgId.set(a.message.id, a) |
| 2134 | } |
| 2135 | |
| 2136 | // O(n) precompute: sibling groups and TR index. |
| 2137 | // TRs indexed by parentUuid — insertMessageChain:~894 already wrote that |
| 2138 | // as the srcUUID, and --fork-session strips srcUUID but keeps parentUuid. |
| 2139 | const siblingsByMsgId = new Map<string, TranscriptMessage[]>() |
| 2140 | const toolResultsByAsst = new Map<UUID, TranscriptMessage[]>() |
| 2141 | for (const m of messages.values()) { |
| 2142 | if (m.type === 'assistant' && m.message.id) { |
| 2143 | const group = siblingsByMsgId.get(m.message.id) |
| 2144 | if (group) group.push(m) |
| 2145 | else siblingsByMsgId.set(m.message.id, [m]) |
| 2146 | } else if ( |
| 2147 | m.type === 'user' && |
| 2148 | m.parentUuid && |
| 2149 | Array.isArray(m.message.content) && |
| 2150 | m.message.content.some(b => b.type === 'tool_result') |
| 2151 | ) { |
| 2152 | const group = toolResultsByAsst.get(m.parentUuid) |
| 2153 | if (group) group.push(m) |
| 2154 | else toolResultsByAsst.set(m.parentUuid, [m]) |
| 2155 | } |
| 2156 | } |
| 2157 | |
| 2158 | // For each message.id group touching the chain: collect off-chain siblings, |
| 2159 | // then off-chain TRs for ALL members. Splice right after the last on-chain |
| 2160 | // member so the group stays contiguous for normalizeMessagesForAPI's merge |
| 2161 | // and every TR lands after its tool_use. |
| 2162 | const processedGroups = new Set<string>() |
| 2163 | const inserts = new Map<UUID, TranscriptMessage[]>() |
| 2164 | let recoveredCount = 0 |
| 2165 | for (const asst of chainAssistants) { |
| 2166 | const msgId = asst.message.id |
| 2167 | if (!msgId || processedGroups.has(msgId)) continue |
| 2168 | processedGroups.add(msgId) |
| 2169 | |
| 2170 | const group = siblingsByMsgId.get(msgId) ?? [asst] |
| 2171 | const orphanedSiblings = group.filter(s => !seen.has(s.uuid)) |
| 2172 | const orphanedTRs: TranscriptMessage[] = [] |
| 2173 | for (const member of group) { |
| 2174 | const trs = toolResultsByAsst.get(member.uuid) |
| 2175 | if (!trs) continue |