JSONL session format: distinguish tool-approval wait from active tool execution

Resolved 💬 3 comments Opened May 1, 2026 by frederick-douglas-pearce Closed Jun 1, 2026

Body

Summary

The session JSONL files at ~/.claude/projects/<slug>/ record tool calls as a tool_use content block followed (eventually) by a matching tool_result block. When a tool call requires user approval and the user is AFK, the gap between those two events can grow to hours — but the JSONL records nothing during that period to indicate the call is pending approval rather than actively executing.

This makes it impossible for downstream consumers (analytics tools, dashboards, post-hoc behavior reviews) to distinguish "the tool ran for 2 hours" from "the tool waited 1h59m for approval, then ran for 1 minute." Two very different signals collapsed into one observable.

Filing this as a feature request: add a structural marker so consumers can attribute time correctly.

Concrete observation

Building a local-first agent analytics tool against this data, we found a pm subagent invocation that registered ~113 minutes total duration. The trace contained 13 paired tool_use / tool_result events. One specific pair — an mcp__github__list_issues call — accounted for 111 of those 113 minutes:

  • tool_use timestamp: T₀
  • tool_result timestamp: T₀ + 1h51m

Between those two messages: zero records of any kind. Top-level keys on both messages are standard (agentId, message, parentUuid, sessionId, timestamp, type, uuid, version); no tool_status field, no progress event, no marker indicating the call was awaiting user approval.

Looking across 212 subagent JSONL files from this user, this pattern is consistent: of 13 traces longer than 10 minutes, 46% had a single tool_use→tool_result gap accounting for >80% of total duration. The gap-causing tools were all approval-gated:

  • mcp__github__list_issues, mcp__github__add_issue_comment (MCP tool calls awaiting approval)
  • Write (filesystem writes awaiting approval)
  • WebSearch

Domain knowledge confirms the cause: the user was AFK, the IDE prompted for approval, the tool sat pending. But the JSONL itself doesn't say so — the structural shape of "approval-pending for 1h51m, then ran for seconds" is identical to "executed for 1h51m."

Why this matters

Three downstream use cases that need to distinguish these:

  1. Per-tool latency analytics. "How long does mcp__github__list_issues typically take?" is a fundamentally different question from "how long do users typically take to approve mcp__github__list_issues?" Both are useful; today the JSONL conflates them.
  2. Outlier / "stuck" detection. Tools that surface "agents that were unusually slow" can't tell whether the slowness was the agent's fault (model picking too many tools, retries) or the user's (away from keyboard during an approval prompt). Misattribution leads to misleading recommendations.
  3. Wall-clock vs active-duration billing reasoning. Users on usage-based plans care about active token-generating time vs idle wait time. Today the only timestamps available roll those together.

Proposed minimum viable change

Add an optional tool_status field to one of the existing message types — preferably a small synthetic "transition" event emitted between the tool_use and tool_result messages whenever an approval prompt is shown. Something like:

{"type": "tool_use", "message": {...}, "timestamp": "T0"}
{"type": "tool_status", "tool_use_id": "toolu_…", "status": "pending_user_approval", "timestamp": "T0+50ms"}
{"type": "tool_status", "tool_use_id": "toolu_…", "status": "executing", "timestamp": "T0+1h51m"}
{"type": "tool_result", "message": {...}, "timestamp": "T0+1h51m+0.4s"}

Even just the transition from pending_user_approval to executing would solve most of the analytics problem — consumers can compute wait_ms = (executing_ts - tool_use_ts) and active_ms = (tool_result_ts - executing_ts). Anything else (cancellation, denial, timeout) can be added later.

Acceptable lighter alternatives

If a new message type is too heavy:

  • A field on the existing tool_result carrying the approval-wait portion: e.g., "approval_wait_ms": 6540000. Loses the streaming nicety but solves the analytics problem with a one-field schema addition.
  • A field on the existing tool_use marking that approval was required (boolean). Doesn't give the wait duration directly, but lets consumers infer from the gap alone (with a tool_use → tool_result gap > N seconds AND requires_approval: true → assume approval-wait).

Either is dramatically better than the current "structurally invisible" state.

What we shipped as a workaround

For context on the use case: we shipped a heuristic in our analytics tool that flags tool_use → tool_result gaps as "idle" when gap > max(10 × median_other_gaps_in_trace, 5 minutes). This catches 100% of the obviously-stuck traces in our test dataset but it's a workaround, not a structural fix. We'd happily replace it with a tool_status field on day one of availability.

Empirical analysis (the calibration notebook with the full per-trace gap distribution and threshold sweep) lives in this PR: https://github.com/frederick-douglas-pearce/agentfluent/pull/233

Out of scope for this issue

  • API-level changes to how Claude itself handles tool approval (that's an SDK / runtime concern).
  • Changes to MCP server protocols.
  • Per-tool-type approval policies in settings.

The ask here is narrow: make the wait observable in the persisted session record, so post-hoc analysis can attribute time correctly.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗