Signal handling in Omni/Agent: SIGINT cancel, SIGTERM graceful exit

t-759.2·WorkTask·
·
·
Parent:t-759·Created1 week ago·Updated1 week ago·pipeline runs →

Description

Edit

Goal

Make Omni/Agent respond to standard Unix signals the way interactive programs do:

  • SIGINT (Ctrl-C) → cancel the current iteration. The Op loop check should raise SteeringCancel (or equivalent), unwinding the current turn cleanly. In stdin-mode, after cancellation the agent returns to reading the next prompt. In one-shot mode it exits with a cancellation status.
  • SIGTERM → finish the current iteration and then exit cleanly (no more prompts accepted). In one-shot mode it behaves the same as today.
  • SIGHUP → ignore, or optionally treat like SIGTERM. TBD by the implementer.

Why signals instead of JSON commands

Pi uses {"type":"cancel"} on stdin. We're not doing that — cancel is what SIGINT was designed for, it's what every REPL, shell, and interactive Unix program uses, and it doesn't require inventing a command vocabulary. Keeps stdin as a pure prompt channel.

Acceptance criteria

  • kill -INT <pid> during an in-flight loop iteration causes the current LLM call or tool to unwind cleanly via the existing SteeringCancel path in Op.check.
  • In stdin-mode, after SIGINT the agent returns to reading the next null-delimited prompt (does not exit).
  • kill -TERM <pid> causes the agent to finish the current iteration (don't cancel mid-turn), then exit 0 without reading another prompt.
  • Signal handlers are installed only when running in stdin-mode (or always — implementer's call, document the choice).
  • Existing agent script.md shebang case is unaffected except that SIGINT now behaves like a cancel instead of a hard kill (which is a strict improvement).

Depends on

t-759.1 (stdin prompt mode must exist before the "return to read next prompt after cancel" behavior makes sense).

Implementation notes

  • Use System.Posix.Signals (installHandler, sigINT, sigTERM).
  • Probably set an IORef or TVar flag that the Op loop's existing check step polls.
  • There's already a SteeringCancel plumbing in Op.check — reuse it, don't invent new cancellation machinery.

Git Commits

b3a7429dagent: handle SIGINT cancel and SIGTERM graceful stop
Coder Agent12 days ago1 files

Timeline (7)

🔄[human]Open → InProgress1 week ago
💬[human]1 week ago

Implemented SIGINT/SIGTERM handling in Omni/Agent.hs using System.Posix.Signals. Added process-level signal state: SIGINT sets a cancel flag consumed via existing Op.check SteeringCancel path; SIGTERM sets a graceful-stop flag (no mid-turn cancellation). In stdin \0 mode, SIGINT cancels current turn and loop continues reading next prompt; SIGTERM stops accepting new prompts after current turn completes. Also ignore SIGHUP. Verified with bild Omni/Agent.hs and bild --test Omni/Agent.hs.

🔄[human]InProgress → Review1 week ago
🔄[human]Review → Done1 week ago
💬[human]1 week ago

Reviewed b3a7429d. Approved.

Code review:

  • SignalState type with two IORef flags is clean. installSignalHandlers wires sigINT/sigTERM/sigHUP correctly.
  • consumeSignalFlag uses atomicModifyIORef' — correct for handler-set flag consumption.
  • SIGINT path: checkSteering polls and consumes the flag, returns SteeringCancel. Reuses existing Op.SteeringCancel plumbing as planned. After cancel, runNullDelimitedPromptLoop continues reading next prompt — matches acceptance criteria.
  • SIGTERM path: ssSigTermRequested is checked at top of runNullDelimitedPromptLoop and after each chunk batch via processPromptChunks. Loop exits cleanly without canceling current turn. Matches acceptance criteria.
  • The old shutdownRequested/shutdownHandled cruft inside runAgentWithState was correctly removed; checkShutdown is now a no-op since SIGTERM is handled at the loop level rather than mid-turn.
  • SIGHUP ignored as planned.
  • runAgent (legacy entry point) installs its own SignalState rather than threading one — slightly redundant if a caller already installed handlers, but harmless and matches the pattern of being self-sufficient.

Per author: bild + bild --test passed locally. Approving and marking Done.