feat(session): give every tool a deadline - #104
Merged
Conversation
A tool that never returns held the whole turn: no output, no error, and the turn watchdog could not intervene because a tool in flight counts as work. Bound the one expression that wraps every tool call, so a timeout arrives at the model as an ordinary tool failure rather than a wedge. Exempt the tools whose whole point is to take as long as they take, and subtract permission wait time — a dialog left open is not a hung tool. Claude-Session: https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
PR #103 ended turns that stopped producing output, but deliberately treats a tool in flight as
work — otherwise a long
bashor a subagent would be killed mid-job. That leaves the oppositehole open: a tool that never returns keeps the turn alive indefinitely, with no output and no
error. A
readon a dead network mount, an LSP request to a server that stopped answering, anMCP call to a process that went away — all of them look exactly like progress.
What this does
withOperationHooks(session/tools.ts) wraps every tool — registry, MCP, structured output —and the call is one expression. A deadline there lands in
executed.causeand flows through theexisting failure branch, so the model reads it as an ordinary
output-errortool result and cansay so or try something else. Nothing crashes and the turn continues.
Two things it deliberately does not do:
shell/bashcarry their own deadline and let themodel choose it,
questionexists to wait for a person, andtaskis a whole child turn withits own watchdog. Bounding those would cut real work.
hung tool, so
ctx.askrecords how long it waited and the guard subtracts it. That is why thisis a race with a re-checking loop rather than a plain
Effect.timeoutOrElse.Default 600000 ms — a backstop against wedging, not a performance budget.
experimental.tool_timeoutoverrides it;
falsedisables.Verification
prompt.test.tsseen failing first (the turn never finished, 5 s timeout)and passing after: a hung
readnow fails as a tool error, the assistant message carries noabort error, and the turn completes on its own with no cancel.
trip the deadline.
bun typecheckacross all 31 packages;packages/core1143 pass.packages/redcodesession/tool/effect: two failures, both reproduced on unmodifiedmainon thismachine and unrelated to this change —
tool.writepermissions (local umask 0002) and the feat(session): end a turn that has stopped producing anything #103watchdog test timing out under load. CI is the arbiter for the latter.
https://claude.ai/code/session_01U29Yk1UscZJ5ZVBXV1Sn8b
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.