Tool Call ID Attribution#
Claude Code and Codex session transcripts can contain repeated tool_use_id values within the same session . The correct pairing strategy is source-order binding: match each tool_use block to the next unconsumed tool_result with the same ID in traversal order.
Implemented Behavior: One-to-One Call/Result Pairing#
src/ingest.ts pairs tool calls and results one-to-one based on occurrence order [PR #178]:
- During the block-insertion pass, each
tool_resultis appended to a queue stored inMap<tool_use_id, resultQueue[]>, preserving every result'sblockId,isError,text, andtimestamp. - In the linkage pass, each
tool_useconsumes the next unconsumed result from the queue for thattool_use_id. - A result occurrence counter (
resultTaken) tracks how many results with a given ID have been consumed, ensuring each call pairs with the corresponding result in sequence.
Handling edge cases:
- Results that precede any call — If a result's
blockIdis lower than the call'sblockId, the result is skipped and cannot answer that call. - Results after all calls are answered — Surplus results remain unlinked; they cannot shift outcomes when the same ID is reused later.
- Result tool names — Tool names for results are resolved through the
result_block_idlinkage established during pairing, not from the result message itself.
The tool_call table stores call_block_id and result_block_id as foreign keys to block(id) , so the linked block is authoritative for all derived data.
Duplicate ID Diagnostics#
linkageIssues() in src/diagnostics.ts emits three issue codes:
| Code | Condition |
|---|---|
duplicate_tool_use_id | A tool_use block's ID was already seen earlier in the session |
orphan_tool_result | A tool_result references a tool_use_id absent from the session |
duplicate_tool_result | A tool_use_id received more than one result |
Critical: these diagnostics are purely informational — they do not alter the queue-based linkage in ingest.ts. A duplicate_tool_use_id diagnostic does not mean those calls were dropped; both call blocks are stored in block and collected for the tool_call insertion pass . Raw blocks and diagnostic rows must be retained because pairing repeated IDs is an attribution policy, not evidence that the source IDs were unique; re-attributing historical links from corrected logic requires the original JSONL logs to remain available.
Impact#
The fix deployed in PR #178 corrects output volume, outcome, duration, and activity attribution for sessions with repeated tool-call IDs:
- Accurate output volume —
output_bytesfor each call now reflects its own result, not the last result with that ID. - Correct error attribution —
is_errormatches the outcome of the specific call/result pair. - Correct duration —
duration_msis computed from each call's timestamp to its own result's timestamp. - Preserved bucket totals — Tool-call counts, generation tokens, and context-window tokens are no longer inflated by result reuse.
The ingest pipeline revision was bumped from 5 to 8, and the session economics format version from 4 to 7. Cached bucket vectors and available logs will refresh on the next sync, re-attributing historical sessions that had repeated IDs.