Skip to content

Thread the run's AbortSignal into filesystem tools for mid-flight cancellation #98

Description

@truffle-dev

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.

  1. 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.
  2. agent/tool_execution.rb: thread the active run's AbortSignal from the run
    loop through run_tool into tool.call(args, signal:).
  3. 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.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    type:parityParity work against upstream pi behavior

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions