Skip to main content

Crashes and tool call outcomes

View as Markdown

A crash repeats at most the one call that was running. This page lists what each failure repeats, and how a tool Step's failure decides the call's record. Agent Frameworks explains the outcomes and how to reconcile outcome_unknown.

Crash matrix​

Each row of this table is a Step and the moment it is interrupted. The last three columns name the integration tests for three failures:

  • Worker kill: the Worker gets SIGKILL, and a new Worker starts.
  • Server restart: the Dex Server node gets SIGKILL and restarts on its data directory. The Worker keeps running.
  • Takeover: the active node gets SIGKILL, and a hot standby takes the lease. The Worker keeps running.

Each language names the file that holds the test. Not tested means that no test covers the cell yet, so its text states the design, not a checked result.

StepInterruptedWhat happensWorker killServer restartTakeover
RunModelBefore any output reaches DexThat model call runs again, up to 3 attempts within 30 minutes. Then the turn fails. If tools already ran, a note in the history tells the next turn's model not to repeat them.Python, TypeScriptPython, TypeScriptPython, TypeScript
RunModelAfter output reached DexThe same.Python, TypeScriptPython, TypeScriptPython, TypeScript
CallTool, for an idempotent writeBefore the first heartbeat reaches DexThe call runs again with the same idempotency key.Python, TypeScriptPython, TypeScriptPython, TypeScript
CallTool, for an idempotent writeWhile the tool runsThe same.Python, TypeScriptPython, TypeScriptPython, TypeScript
CallTool, for a readWhile the tool runsThe call runs again.Python, TypeScriptPython, TypeScriptPython, TypeScript
CallToolEvery attemptAfter the last attempt, a read records failed and a write records outcome_unknown.Python, TypeScriptPython, TypeScriptNot tested
CallSingleAttemptToolBefore the first heartbeat reaches DexThe call records outcome_unknown and does not run again. The model sees that the outcome is unknown.Python, TypeScriptPython, TypeScriptPython, TypeScript
CallSingleAttemptToolWhile the tool runsThe same.Python, TypeScript, GoPython, TypeScriptPython, TypeScript
AwaitToolApprovalWhile it waitsNothing runs again. The wait lives in Dex Server. A later approval runs the tool once.Python, TypeScript, GoPython, TypeScriptPython, TypeScript
AwaitQuestionAnswerWhile it waitsNothing runs again. A later answer resumes the turn.Python, TypeScriptPython, TypeScriptPython, TypeScript
AwaitTimerWhile it waitsNothing runs again. The Timer fires once. A Timer that came due during an outage fires when Dex Server is back.Python, TypeScriptPython, TypeScriptPython, TypeScript
PollToolTaskBetween two pollsPolling goes on with the same task. No second task starts.Python, TypeScriptPython, TypeScriptPython, TypeScript
PollToolTaskDuring a pollThe poll runs again. No second task starts.Python, TypeScriptPython, TypeScriptPython, TypeScript
CompactContextDuring the summary callOnly the summary call runs again, up to 3 attempts within 30 minutes. The session stores one summary. If every attempt fails, the turn fails, and the next user message compacts again.Python, TypeScriptPython, TypeScriptPython, TypeScript
A wait for a user messageWhile the session is idleNothing runs again. The next message starts a turn.Python, TypeScriptPython, TypeScriptPython, TypeScript
Any StepQueued, before Dex hands it to a WorkerThe Step stays in the queue and uses no attempt. It runs as attempt 1 when a Worker takes it.Python, TypeScriptserver-zdserver-zd
CallTool for a read, PollToolTask, CompactContextBefore the first heartbeat reaches DexBy design, the same as the rows above for that Step.Not testedNot testedNot tested
AwaitToolApproval, AwaitQuestionAnswer, AwaitTimerAfter the wait ended, while the handler that moves the turn on runsBy design, the handler runs again. The resolved approval, answer, or Timer stays recorded.Not testedNot testedNot tested
Any StepAfter the Worker sent the result, before Dex committed itBy design, Dex either commits the result or fails the attempt. A failed attempt follows the row for that Step.Not testedNot testedNot tested
A tool the framework cannot deferWhile it runsThe tool runs inside RunModel and repeats when RunModel runs again. It is at-least-once.Not testedNot testedNot tested
A Claude Agent SDK turn segmentWhile it runsDeferred custom tools run in their own CallTool or CallSingleAttemptTool Steps, as above. Model calls, inline read tools, and CLI tools such as Bash, Write, Edit, and Task run inside RunModel and repeat when it runs again. They are at-least-once.Python, TypeScriptNot testedNot tested

Each adapter package repeats the Worker kill cases for RunModel and its tool Steps in its own test suite.

A running Worker survives both server failures. It reconnects by itself, and Dex fails the attempt that it held, so the row for that Step applies. Workers and clients must reach the new active node through an address that follows readiness. The Streams of the failed node are gone. See Stream backend.

The takeover tests use a hot standby and SIGKILL. A cold standby and a planned step down are covered at the server level: see the failover tests and the drain tests.

A single-attempt tool can still run twice in one narrow case: a cancelled attempt that keeps running. See At-most-once has one exception. For deploys and the drain timeout, see Graceful stop.

A call that never ran​

When a tool Step fails, its failure route reads attempt_progress from the recovery error. See Failure and redelivery. CallTool and CallSingleAttemptTool use it the same way:

attempt_progressWhat the call records
NOT_STARTEDNo attempt reached a Worker, so the tool did not run. The call records failed. Its error starts with "the tool did not run, so it is safe to call again". The model sees a failed tool call and can call the tool again. A call whose tool already asked the user a question ran before, so it follows the next row.
NOT_COMPLETED, or UNSPECIFIED when the server reported no progressThe tool may have run. A read records failed. Any other call records outcome_unknown, and the session does not run its tool again.

To recognize such a call in get_agent_snapshot or your own code, compare the start of its error with TOOL_CALL_NOT_RUN_ERROR_PREFIX in Python, toolCallNotRunErrorPrefix in TypeScript, or ToolCallNotRunErrorPrefix in Go.

NOT_COMPLETED is conservative, so a call that never ran can still record outcome_unknown. For example, the server crashed after it recorded the attempt start but before the task reached the Worker. Treat that outcome as unknown. To check it, see Reconcile outcome_unknown.

A tool Step that waits in the queue does not fail and uses no attempt. See Keep tool Steps dispatchable.