Skip to content

fix(session): abort tools on deadline and keep truncated output when its file fails - #206

Merged
filipeforattini merged 1 commit into
mainfrom
tool-deadline-abort
Sep 11, 2026
Merged

fix(session): abort tools on deadline and keep truncated output when its file fails#206
filipeforattini merged 1 commit into
mainfrom
tool-deadline-abort

Conversation

@filipeforattini

@filipeforattini filipeforattini commented Sep 11, 2026

Copy link
Copy Markdown

Problem

Tool deadline could not cancel the tool. ToolDeadline.guard raced an Effect.promise around the tool call. Interrupting an Effect.promise does not abort the underlying promise, and EffectBridge.run.promise runs each tool in its own root fiber. On expiry the model was told the tool failed while the edit, webfetch, or LSP request kept running and could land later. Only shell, task, skill, and code-mode honour ctx.abort, and none of them ever saw the deadline fire.

Managed output file failure killed the tool. Truncate.output piped ensureDir/writeFileString through Effect.orDie, so an unwritable directory or a full disk turned a tool call that had already succeeded into a defect. CONTEXT.md ("Managed Tool Output File") requires an explicitly lossy bounded output without a path instead.

Fix

  • ToolDeadline.guard now takes the call as (abort: AbortSignal) => Effect plus an optional parent abort. It creates a per-call AbortController, joins it to the parent with AbortSignal.any (so a stopped turn still stops the tool, with no listener to clean up), and calls own.abort(reason) in the expiry path before recording the guard trip and dying.
  • SessionTools passes the guard's signal through ToolExecutionOptions.abortSignal, which is what context() exposes as ctx.abort. Tools that honour it now actually end on expiry. Unbounded tools are unchanged.
  • Truncate.output catches the write failure via a typed inner retain, logs a warning with the directory and error, and returns the truncated preview with no outputPath. The notice now says the full output could not be saved and suggests narrowing, rather than pointing at a file. Result.outputPath is optional; the five metadata sites that mirrored it (tool.ts, registry.ts, four in session/tools.ts) only emit the key when a path exists. Truncate.write keeps its die-on-failure contract for the shell tool.

Not done: wiring ctx.abort into webfetch. It uses Effect's HttpClient rather than fetch, so honouring the signal would mean racing the effect against it, which is not the trivial change the task allowed for.

Test

Regression tests, both run before and after the fix:

  • packages/redcode/test/session/tool-deadline.test.ts: a fake tool that settles only when its ctx.abort fires; asserts the signal is aborted on expiry, the tool observes it, and the parent (turn) signal is untouched. Also: the tool sees the turn being aborted through the joined signal, and a call that finishes in time is never aborted. Before: 5 fail (the two existing guard tests adjusted to the new signature plus the three new ones). After: all pass.
  • packages/redcode/test/tool/truncation.test.ts: replaces the truncation directory with a regular file, asserts the truncated preview is returned with no outputPath and a notice that does not promise a file. Before: dies with AlreadyExists: FileSystem.makeDirectory. After: passes.

Suites: test/tool 348 pass, 0 fail. test/session 514 pass, 1 fail — prompt.test.ts "finalizes assistant when cancelled before processor creation completes" times out at its own 3000ms limit identically on unmodified main, so it is pre-existing and unrelated. bun run typecheck clean. Prettier clean on changed files.

https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS


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

…its file fails

Two tool-execution robustness bugs.

The tool deadline raced an `Effect.promise` around the tool call, but
interrupting an `Effect.promise` does not stop the promise, and each tool
runs in its own root fiber via `EffectBridge.run.promise`. On expiry the
model was told the tool failed while the edit, fetch, or LSP request kept
running and could land later. `ToolDeadline.guard` now hands the call a
per-call `AbortSignal`, joined with `AbortSignal.any` to the turn's own
signal so a stopped turn still stops the tool, and aborts it on expiry.
`SessionTools` passes that signal through `ToolExecutionOptions`, so it is
what tools see as `ctx.abort`; the ones that honour it now actually end.

`Truncate.output` died when the managed output file could not be written
(unwritable directory, full disk), turning a tool call that had already
succeeded into a defect. CONTEXT.md requires an explicitly lossy bounded
output without a path instead. The write failure is now caught and logged
as a warning, the truncation notice says the full output could not be
saved rather than pointing at a file, and `outputPath` is omitted from the
result and from the tool metadata that mirrors it. `Truncate.write` keeps
its die-on-failure contract for the shell tool, which manages its own file.

Claude-Session: https://claude.ai/code/session_01KAcoL6wpgVEs2ebUrUGpHS
@filipeforattini
filipeforattini merged commit 657200b into main Sep 11, 2026
12 of 15 checks passed
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