Skip to content

Catch interrupts in the agent turn loop#81

Merged
TroyHernandez merged 2 commits into
mainfrom
interrupt-key
May 14, 2026
Merged

Catch interrupts in the agent turn loop#81
TroyHernandez merged 2 commits into
mainfrom
interrupt-key

Conversation

@TroyHernandez
Copy link
Copy Markdown
Contributor

Summary

  • Interrupts during an in-flight turn() are now caught in both corteza::chat() and the ~/bin/corteza CLI, returning control to the prompt instead of dropping out of the session.
  • The CLI inspects the callr worker's state on interrupt: busy means a tool was running, so it sends SIGINT and drains; idle means the LLM round-trip was in flight and the worker is left alone (preserves background-process registry etc.). Worker is recycled only as a fallback.
  • An [Interrupted by user before completing.] marker is injected into history (and transcript) so the next turn's LLM sees the prior exchange was aborted.
  • Separate fix: load_saber_briefing() now wraps saber::briefing() in suppressMessages(). The briefing was leaking via message() every time a subagent called session_setup, which had been masquerading as a session restart whenever archival fired after a turn.

Terminal Esc remains a no-op — terminals send raw \033 for Esc, not a signal. RStudio Esc works because the IDE translates it to an R interrupt. Documented in NEWS.

Test plan

  • In RStudio: tinypkgr::reload("corteza"); corteza::chat(), kick off a multi-step request, press Esc mid-loop. Expect: yellow "Interrupted." and the prompt comes back.
  • Same as above with Ctrl+C instead of Esc.
  • After the interrupt, send a follow-up like "what were you about to do?". The model should reference the aborted exchange (sees the marker in history).
  • CLI: corteza::install_cli(), exit R, run corteza, give it a long bash call (sleep 30), press Ctrl+C. Expect: "Interrupted.", worker recovers silently, next prompt works without restarting.
  • CLI: confirm the spurious # Briefing: <project> text no longer prints after a turn completes / archival fires.

Notes

Codex review of an earlier draft caught two real issues that this PR fixes: the worker state check used the wrong string ("ready" instead of "idle"), which would have recycled the worker on every interrupt and dropped its state; and the prior turn was invisible to the next call because we returned NULL without persisting any marker. Both are addressed here.

The agent loop in both corteza::chat() and the ~/bin/corteza CLI wrapped
turn() in a tryCatch that only caught errors, so an R-level interrupt
(Esc in RStudio, Ctrl+C in either) escaped the REPL and dropped the
user out of the session mid-tool-call. There was no way to abort a
multi-step loop without killing R.

Catch the interrupt condition at the turn() boundary. In the CLI,
inspect the callr worker's state: if it's "busy" a tool call was in
flight, so send SIGINT to the child and drain the queued result;
recycle the worker only if it doesn't return to "idle". When the
interrupt arrived during the LLM round-trip the worker is already idle
and we leave it alone so we don't drop worker-local state like the
background-process registry.

Inject a synthetic [Interrupted by user before completing.] marker into
history (turn_session$history in chat(), the persistent session in the
CLI) so the next turn's LLM sees the prior exchange was aborted instead
of silently losing the prompt.

Terminal Esc remains unsupported — terminals send raw \\033 for Esc, not
a signal — and is documented as such in NEWS.
load_saber_briefing() wrapped saber::briefing() in capture.output(),
which only grabs stdout. saber::briefing() emits its full text via
message(), so the briefing leaked to the user's terminal every time
session_setup() ran — including the subagents that archival spawns
post-turn. That made interrupts look like they were restarting the
session when they were just landing right before a normal archival
cycle.

Wrap the briefing call in suppressMessages() so message() is silenced
while capture.output() continues to catch any stdout side effects.
@TroyHernandez TroyHernandez merged commit c061862 into main May 14, 2026
4 checks passed
@TroyHernandez TroyHernandez deleted the interrupt-key branch May 14, 2026 18:27
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