Skip to content

fix(tools): keep the tool that was running when the interrupt landed - #23

Merged
ericleepi314 merged 1 commit into
mainfrom
interrupted-tool-row
Aug 19, 2026
Merged

fix(tools): keep the tool that was running when the interrupt landed#23
ericleepi314 merged 1 commit into
mainfrom
interrupted-tool-row

Conversation

@ericleepi314

Copy link
Copy Markdown
Collaborator

What

Pressing Esc mid-tool erased the tool. The transcript kept the calls that had already finished and the "Interrupted · …" note, but the one row that said what was cut off — the command still running — vanished with the turn.

A tool that never completes never gets a completion event, and activeTools is dropped on interrupt.

before                                          after
❯ PROBE bash {"command": "sleep 30", …}         ❯ PROBE bash {"command": "sleep 30", …}
· Interrupted · What should dsh-ccTUI do…         Ran 1 shell command

                                                (ctrl+o)
                                                ⏺ Bash(sleep 30)
                                                  ⎿  Interrupted · What should dsh-ccTUI do instead?

Claude Code 2.1.235, Esc four seconds into a long find:

⏺ Bash(find / -name '*.zzzznope' 2>/dev/null)
  ⎿  Interrupted · What should Claude do instead?

and its collapsed view folds that row into the brief (Searched for 1 pattern) like any other settled call — which is why the note attaches as an ordinary result and not an error. An error would break the row out of the brief and read as a failure it was not.

Printed once

The note had been the reply text too, so carrying it on the row would print the same sentence twice, three lines apart. The reply now keeps only the model's own partial text. With no partial text at all, the shelf hangs off a bare trail message the way a settled segment does — an assistant bubble with no text rendered as an empty row.

Also: a workflow was identified by the script it runs

before   ⏺ Workflow(script: log("hello from the workflow") return { ok: true }, met…)
after    ⏺ Workflow(gap-probe)

A script is payload like a file's content, so it joins the bulk set. And an object argument carrying its own name (a workflow's meta block) now renders as that name — as JSON the identity sits behind a brace and two quotes, and the row's 64 columns lose it to the ellipsis, which is the one thing the parens exist to prevent.

Test

  • npm run typecheck, npm test — 150 files / 1942 passing
  • node scripts/verify-boundary.mjs
  • new: the in-flight tool survives the interrupt with its note, and the note is not also the reply (createGatewayEventHandler.test.ts); a workflow renders as its meta name (toolArgs.test.ts)
  • live captures: interrupt probe + scripts/tool-gallery.py workflow

🤖 Generated with Claude Code

Pressing Esc mid-tool erased the tool. The transcript kept the calls that
had already finished and the "Interrupted · …" note, but the one row that
said WHAT was cut off — the command still running — vanished with the
turn, because a tool that never completes never gets a completion event
and `activeTools` is dropped on interrupt.

Upstream keeps it. Claude Code 2.1.235, Esc four seconds into a long find:

    ⏺ Bash(find / -name '*.zzzznope' 2>/dev/null)
      ⎿  Interrupted · What should Claude do instead?

and the collapsed view folds that row into the brief like any other
settled call, which is why the note is attached as an ordinary result and
not an error — an error would break the row out of the brief and read as a
failure it was not.

Now the same, with the note printed once. It had been the reply text too,
so carrying it on the row would have printed the same sentence twice three
lines apart; the reply keeps only the model's own partial text. With no
partial text at all the shelf hangs off a bare trail message the way a
settled segment does, rather than an assistant bubble with no text, which
rendered as an empty `⏺` row.

Separately, a workflow call was identified by the script it runs:

    before  ⏺ Workflow(script: log("hello from the workflow") return { ok: true }, met…)
    after   ⏺ Workflow(gap-probe)

A script is payload like a file's content, so it joins the bulk set — and
an object argument carrying its own `name` (a workflow's `meta` block) now
renders as that name, since as JSON the identity sits behind a brace and
two quotes and the row's 64 columns lose it to the ellipsis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericleepi314
ericleepi314 merged commit fdf986d into main Aug 19, 2026
1 check passed
@ericleepi314
ericleepi314 deleted the interrupted-tool-row branch August 19, 2026 14:51
ericleepi314 added a commit that referenced this pull request Aug 19, 2026
…e fold (#24)

The token read-outs undercounted every run that fanned out. A child's
`assistant/message` usage went onto the child's own row — the agents
overlay, and the `Done (3 tool uses · 1.2k tokens)` a delegation settles
with — and stopped there. The session odometer behind `tokens: N in /
M out`, `/status` and the context read-out never heard about it, so a
turn that delegated most of its work reported the sliver the parent did
itself:

    tokens: 1.1k in / 84 out        ← parent only
    tokens: 42.6k in / 3.1k out     ← what the session actually spent

It is one bill. The two halves of the bus are already disjoint —
`bindAgent` routes by session identity and `onChildSessionEvent` drops
anything the lifecycle never announced as a child — so folding the
child's step in counts it exactly once, and a sibling top-level session
still cannot leak in.

Also lands the test coverage for the fold that #23 carried in without
it: `statsFromUsage` and its precedence behind a CostSnapshot, the
price-free `usage` path through `message.complete`, and the rider a
resume replays out of the log.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant