Skip to content

fix(stream): claude tool calls never emit a closing event #182

Closed

Description

@MAOLVLHL

What happened

Streaming a Claude Code run writes a tool_start record for every tool call and never writes the matching tool_end. execution.ndjson — and the cockpit and --progress views built from it — therefore show each Bash/Edit call as still running, and nothing in the log distinguishes a call that finished from one that was killed.

Two independent causes:

  1. src/core/agent-stream.js:227 (mapClaudePayload) treats every type: "user" line as output echo and returns []. In Claude Code's stream-json, that is exactly where tool_result blocks arrive, so the closing event is thrown away.
  2. Neither adapter records the CLI's own tool-call id — Codex sends item.id, Claude sends tool_use.id and tool_use_id — so even with the first cause fixed, nothing can pair a result to its call, or tell two concurrent runs apart when the CLI reuses an id.

A run killed mid-call (the default 300s agent timeout, Ctrl-C, a crashed CLI) never emits a result at all, so its calls stay open forever with no terminal status.

What you expected

Every tool_start eventually gets exactly one tool_end for the same call, carrying the CLI's own id, and a call the CLI never finished is closed with a status that says so when the sink shuts down.

Reproduction

Smallest reproduction, no real CLI needed (evalid 0.5.33, Node 24):

import { normalizeAgentEvents } from './src/core/agent-stream.js';

const lines = [
  '{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls"}}]}}',
  '{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]}}',
];

for (const line of lines) {
  console.log(JSON.stringify(normalizeAgentEvents(line, { adapter: 'claude', seq: 0 })));
}

Output today:

[{"seq":0,"event":"tool_start","event_type":"tool_use","tool":"Bash","args_preview":"{\"command\":\"ls\"}","args_digest":"4cf29611a6693486"}]
[]

The tool_start carries no id and the result maps to nothing.

xflow doctor --project-root .

reports a healthy project — this is a streaming-shape gap, not a configuration problem.

Environment

  • evalid version: 0.5.33 (405ffa5)
  • Node: 24.21.0
  • Python: 3.15.2
  • OS: Windows 11
  • Agent CLI involved: Claude Code (stream-json); Codex has the same missing-id gap but does emit its closing events

I have a fix with tests ready and will open the PR next — additive record fields only, no consumer changes.

Activity

  1. surlymochan commented on Nov 12, 2026

    @surlymochan
    Owner

    Fixed by #183 (merged 2026-11-12, squash 37a1b1a — auto-closed on merge): claude tool_result now maps to tool_end with the CLI's own tool_call_id, and sink.close() marks still-open calls abandoned. Thanks @MAOLVLHL!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions