Skip to content

MCP tool calls and tool_use blocks missing from dashboard due to streaming write deduplication #110

@mat-blair

Description

@mat-blair

Bug

MCP server usage never appears in the dashboard breakdown, and other tool invocations
(Agent spawns, EnterPlanMode, etc.) are silently dropped from turns that involve
streaming responses.

Environment

codeburn 0.8.1
Claude Code 2.1.114
Node.js 20.20.0
OS Ubuntu 24.04.2 LTS (Linux 6.17.0-1010-aws x86_64)
Provider Claude Code sessions routed via Databricks AI Gateway (Bedrock backend)

Root cause

Claude Code writes the same message.id to the session JSONL multiple times as a
response streams in:

  1. First write (message_start) — model + ID, empty content, partial usage
  2. Intermediate writes — mid-stream updates
  3. Final write (message_stop) — complete content including tool_use blocks,
    final token counts

groupIntoTurns in src/parser.ts deduplicates by message.id keeping the
first occurrence and skipping all subsequent entries. The final entry — the only
one containing tool_use blocks — is discarded.

Example from a real session JSONL where mcp__playwright__browser_navigate only
appears in the third entry for the same message.id:

{"type":"assistant","message":{"id":"msg_01X...","model":"claude-opus-4-6","content":[],...}}
{"type":"assistant","message":{"id":"msg_01X...","model":"claude-opus-4-6","content":[],...}}
{"type":"assistant","message":{"id":"msg_01X...","model":"claude-opus-4-6","content":[{"type":"tool_use","name":"mcp__playwright__browser_navigate",...}],...}}

Only the first entry is parsed; the tool call is never counted.

Impact

  • MCP server breakdown panel never populates for affected turns
  • tool_use blocks in streaming turns are missed (Agent, EnterPlanMode, Bash, etc.)
  • Token counts may be slightly understated (final entry has the authoritative usage)

Fix

Add a within-file pre-pass in parseSessionFile that keeps the last occurrence
of each message.id before handing entries to groupIntoTurns. The existing
cross-file seenMsgIds dedup in groupIntoTurns (keep first-seen) is correct and
should remain unchanged — the two concerns need to be handled separately.

// In parseSessionFile, before groupIntoTurns:
const lastIdxById = new Map<string, number>()
for (let i = 0; i < filteredEntries.length; i++) {
  const msgId = getMessageId(filteredEntries[i])
  if (msgId) lastIdxById.set(msgId, i)
}
const dedupedEntries = filteredEntries.filter((entry, i) => {
  const msgId = getMessageId(entry)
  if (!msgId) return true
  return lastIdxById.get(msgId) === i
})
// pass dedupedEntries to groupIntoTurns instead of filteredEntries

Verified

MCP calls (mcp__playwright__browser_navigate, mcp__playwright__browser_snapshot,
mcp__github__get_file_contents) confirmed present in JSONL but absent from dashboard
before fix, correctly attributed after. Total call counts unchanged — no
double-counting introduced. Full test suite: 273 pass, 0 regressions.


Please let me know if you want me to raise a PR for this as I am happy to. Awesome tool you have here and we use it every day since we discovered it.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions