Summary
pi threads an AbortSignal into every coding-agent tool's execute and polls
it at I/O boundaries so a run can be cancelled mid-flight. Our harness only
threads a cancellation signal into Truffle::Exec (so bash can be killed by
its watcher thread). The filesystem tools (write, edit, read, ls,
find, grep) receive no signal and have no cancellation checkpoint, so an
aborted run keeps running them to completion. This is a behavioral parity gap
against pi.
Evidence in pi
Every tool's execute takes signal?: AbortSignal as its third argument and
checks it:
write.ts (execute, lines 194-225): defines throwIfAborted() and calls it
in three places, before mkdir, after mkdir, and after writeFile. The
in-code comment explains the shape: it polls signal.aborted after each
await rather than rejecting from an abort event listener, so the file
mutation queue stays locked until the current filesystem operation has
settled. Rejecting from the listener would release the queue while an
in-flight write is still running.
ls.ts (execute, lines 106-173): rejects immediately if signal.aborted at
entry, registers an onAbort listener for the duration, and removes it when
the listing finishes.
find.ts (execute, from line 118): rejects immediately if signal.aborted
at entry before spawning the search.
edit.ts and read.ts follow the same signal?: AbortSignal execute
signature.
Current state in truffle-rb
Truffle::AbortSignal exists (lib/truffle/abort_signal.rb) and is a
faithful cooperative-cancellation port. Agent#run / #run_stream accept
signal: and the loop checks it at turn boundaries.
Truffle::Exec.command threads the signal into a watcher thread, so bash
gets cancellation.
- The tool seam does not thread it.
agent/tool_execution.rb#run_tool calls
tool.call(args) with no signal, and the Tool DSL run do |...| block has no
way to receive one. So write/edit/read/ls/find/grep cannot observe
an abort: they run to completion even after the owner aborts the run.
The model-visible output contract of each tool is already ported faithfully;
this is specifically about honoring an in-flight abort.
Proposed shape (coordinate first: Codex lane)
This spans the Tool DSL and the agent tool-execution loop, so it needs
coordination before anyone builds it.
- Tool DSL: let a
run block opt into a signal: keyword, the same way pi's
execute takes an optional third AbortSignal argument. Blocks that do not
declare it keep working unchanged.
agent/tool_execution.rb: thread the active run's AbortSignal from the run
loop through run_tool into tool.call(args, signal:).
- Filesystem tools poll
signal.aborted? at the same checkpoints pi uses and
raise "Operation aborted":
write: before mkdir_p, after mkdir_p, after File.write, all inside
the FileMutationQueue.with block so the queue stays locked until the
current op settles (pi's stated rationale).
ls / find / grep: at entry before the walk.
edit: at entry and before the write-back.
- Mutation-prove each new checkpoint (pre-abort the signal, assert the tool
raises before touching the filesystem; revert).
Scope / non-goals
- No new runtime gem dependency;
Truffle::AbortSignal is stdlib-only already.
bash is out of scope: it already cancels through Exec's watcher.
- Keeps the offline default suite; tests use a pre-aborted
AbortSignal.
Summary
pi threads an
AbortSignalinto every coding-agent tool'sexecuteand pollsit at I/O boundaries so a run can be cancelled mid-flight. Our harness only
threads a cancellation signal into
Truffle::Exec(sobashcan be killed byits watcher thread). The filesystem tools (
write,edit,read,ls,find,grep) receive no signal and have no cancellation checkpoint, so anaborted run keeps running them to completion. This is a behavioral parity gap
against pi.
Evidence in pi
Every tool's
executetakessignal?: AbortSignalas its third argument andchecks it:
write.ts(execute, lines 194-225): definesthrowIfAborted()and calls itin three places, before
mkdir, aftermkdir, and afterwriteFile. Thein-code comment explains the shape: it polls
signal.abortedafter eachawaitrather than rejecting from an abort event listener, so the filemutation queue stays locked until the current filesystem operation has
settled. Rejecting from the listener would release the queue while an
in-flight write is still running.
ls.ts(execute, lines 106-173): rejects immediately ifsignal.abortedatentry, registers an
onAbortlistener for the duration, and removes it whenthe listing finishes.
find.ts(execute, from line 118): rejects immediately ifsignal.abortedat entry before spawning the search.
edit.tsandread.tsfollow the samesignal?: AbortSignalexecutesignature.
Current state in truffle-rb
Truffle::AbortSignalexists (lib/truffle/abort_signal.rb) and is afaithful cooperative-cancellation port.
Agent#run/#run_streamacceptsignal:and the loop checks it at turn boundaries.Truffle::Exec.commandthreads the signal into a watcher thread, sobashgets cancellation.
agent/tool_execution.rb#run_toolcallstool.call(args)with no signal, and the Tool DSLrun do |...|block has noway to receive one. So
write/edit/read/ls/find/grepcannot observean abort: they run to completion even after the owner aborts the run.
The model-visible output contract of each tool is already ported faithfully;
this is specifically about honoring an in-flight abort.
Proposed shape (coordinate first: Codex lane)
This spans the Tool DSL and the agent tool-execution loop, so it needs
coordination before anyone builds it.
runblock opt into asignal:keyword, the same way pi'sexecutetakes an optional thirdAbortSignalargument. Blocks that do notdeclare it keep working unchanged.
agent/tool_execution.rb: thread the active run'sAbortSignalfrom the runloop through
run_toolintotool.call(args, signal:).signal.aborted?at the same checkpoints pi uses andraise
"Operation aborted":write: beforemkdir_p, aftermkdir_p, afterFile.write, all insidethe
FileMutationQueue.withblock so the queue stays locked until thecurrent op settles (pi's stated rationale).
ls/find/grep: at entry before the walk.edit: at entry and before the write-back.raises before touching the filesystem; revert).
Scope / non-goals
Truffle::AbortSignalis stdlib-only already.bashis out of scope: it already cancels throughExec's watcher.AbortSignal.