[BUG] Deferred tool resume drops the supplied tool_result when a task-notification was appended after the tool_use (synthetic "[Tool result missing due to internal error]" sent instead)
Summary
When a tool call is deferred (stop_reason: tool_deferred) and a background agent's completion notice is appended to the transcript after that tool call before the process exits, resuming the session with an answer loses the answer. The CLI writes the answered tool_result to the transcript, correctly parented to the tool_use, but the request it sends to the model carries {"type":"tool_result","content":"[Tool result missing due to internal error]","is_error":true} in that slot instead. The model then believes its question failed and asks it again.
Reproduced on 2.1.259, 2.1.273 and 2.1.280 with the Python Agent SDK (ClaudeSDKClient) and a local stub provider. Not a regression as far as we can tell; every version we ran behaves the same.
Environment
- Claude Code CLI 2.1.259, 2.1.273, 2.1.280 (
cli_pathoverride), spawned by claude-agent-sdk (Python) ANTHROPIC_BASE_URLpointed at a local stub that records request bodies- PreToolUse hook on
AskUserQuestionreturningpermissionDecision: "defer"on first ask andpermissionDecision: "allow"withupdatedInput: {questions, answers}on resume
Steps to reproduce
- Query: the model (stub) calls
Agentwithrun_in_background: true, then callsAskUserQuestion. The PreToolUse hook returnsdefer. The result arrives withstop_reason: tool_deferred. - Keep the process alive. The background agent finishes a few seconds later and the CLI appends its
<task-notification>to the transcript as auserrow after theAskUserQuestiontool_userow (and, if the process is still up, runs a turn on it). - Disconnect. Start a new client with
resume=<session_id>and the hook configured toallowthe replayedAskUserQuestionwith answers. - Capture the first request the CLI sends to the model.
Expected
The AskUserQuestion tool_use is followed by a tool_result carrying the answers supplied through updatedInput, which is what the transcript file records:
{"type":"assistant", "uuid":"A", ... "content":[{"type":"tool_use","name":"AskUserQuestion","id":"toolu_question_002",...}]}
{"type":"user", "parentUuid":"A", ... "content":[{"type":"tool_result","tool_use_id":"toolu_question_002","content":"Your questions have been answered: ..."}]}
Actual
The request sent to the model:
assistant: [tool_use AskUserQuestion toolu_question_002]
user: [tool_result toolu_question_002 "[Tool result missing due to internal error]" is_error=true]
[text <system-reminder>[SYSTEM NOTIFICATION - NOT USER INPUT] ... <task-notification>...</task-notification></system-reminder>]
assistant: [text "ok"] (the turn the CLI ran on the notice, or a synthetic "No response requested." if the process died first)
user: [text "Continue from where you left off."]
The string Your questions have been answered does not appear anywhere in the request. The transcript on disk contains it exactly once, on the tool_result row whose parentUuid is the tool_use row.
The transcript has two children under the tool_use row: the notice (written before exit, and closed on resume with a synthetic No response requested. assistant row) and the answered tool_result (written on resume). The request keeps the notice branch and drops the answer. I have not traced which part of request assembly does this; the observable defect is that a persisted answer attached to the deferred call is omitted from the model request whenever a trailing notification exists. Without the late notice (process exits before the agent finishes), the same resume assembles the request correctly. A control on the real transcript that removes only the notice row also assembles it correctly.
In our production case the window was the host's own wait for a live answer before deferring (270 s), during which an already-queued completion notice was flushed into the transcript as a user row at the moment of deferral.
Impact
Any host that asks a question while background agents are running and defers it can hit this. In our case the model relaunched the lost agents and re-asked the same question on every resume, which recreated the shape each time; the user answered the same card three times.
Reproduction script
Single pytest file against a local stub provider, about a minute, no API credentials. Happy to share it or to run a candidate build against it.
Related
#97196 reports the same [Tool result missing due to internal error] substitution on a deferred-tool resume from a different angle (a later defer ignored when the resume carries a new message). The two may share the request-assembly path on resume.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗