Skip to content

tape-fold: don't synthesize tool results for aborted assistant messages - #239

Merged
ReganBell merged 2 commits into
mainfrom
tape-fold-aborted-dangling-calls
Aug 5, 2026
Merged

tape-fold: don't synthesize tool results for aborted assistant messages#239
ReganBell merged 2 commits into
mainfrom
tape-fold-aborted-dangling-calls

Conversation

@ReganBell

@ReganBell ReganBell commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Problem

When a user aborts a turn while the model is mid-stream, pi commits the partial assistant message with stopReason: "aborted". If the abort landed after a complete toolCall block, that message contains a tool call that never executed and has no tool result.

The tape fold treated that call as dangling: tapeNeedsInterruptHeal flagged it, an interrupt event was appended, and healDanglingCalls synthesized an INTERRUPTED_TOOL_RESULT for it. But pi's transformMessages drops aborted/errored assistant messages entirely when building the provider request, while keeping every toolResult message. The healed result therefore reached the provider as a tool_result block with no corresponding tool_use in the previous message:

Model provider API error (invalid_request_error): messages.N.content.1:
unexpected `tool_use_id` found in `tool_result` blocks: toolu_… Each
`tool_result` block must have a corresponding `tool_use` block in the
previous message.

Every subsequent step re-serves the same fold, so the session is permanently wedged: each turn fails with the same 400.

Fix

  • healDanglingCalls skips assistant messages with stopReason "aborted"/"error" when collecting dangling calls. Those messages are never replayed, so their calls need no results — and synthesizing one is what poisons the request.
  • lintFold no longer counts such messages' calls as open. A toolResult answering one now lints as a problem, so a fold that would orphan a result on the wire falls back to entry-based replay instead of being served.

The interrupted-result heal is unchanged for the case it exists for: a restart mid tool execution, where the assistant message completed normally (stopReason: "toolUse") and is replayed.

Testing

  • New test: an aborted assistant's dangling call is not healed and the fold lints clean.
  • New test: lintFold rejects a fold where a toolResult answers an aborted assistant's call.
  • node --test test/tape-fold.test.ts — 26/26 pass; tsc --noEmit and eslint clean.

Reproduced against a live session: abort a turn while the model is streaming a tool call, then send another message — before this fix every subsequent turn 400s with the error above.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

ReganBell and others added 2 commits August 5, 2026 15:46
A user abort mid-stream leaves an assistant message with stopReason
'aborted' that contains a completed toolCall block but no tool result
(the tool never ran). The interrupt heal then synthesized an
INTERRUPTED_TOOL_RESULT for that call. But pi's transformMessages drops
aborted/errored assistant messages entirely at request time while
keeping every toolResult message, so the healed result reached the
provider as a tool_result with no corresponding tool_use — a 400
(invalid_request_error) on every subsequent step, permanently wedging
the session.

Skip aborted/errored assistant messages when collecting dangling calls
(they are never replayed, so their calls need no results), and make
lintFold reject a fold where a toolResult answers such a call, so a
poisoned fold falls back to entry-based replay instead of being served.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in tests

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ReganBell
ReganBell merged commit 0f0e0ad into main Aug 5, 2026
20 checks passed
@ReganBell
ReganBell deleted the tape-fold-aborted-dangling-calls branch August 5, 2026 22:59
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