Make mid-run context injection a clean, first-class operation - #6
Merged
Conversation
Injecting / interrupting / adding context to a running agent was
fire-and-forget: nudge() wrote the steer to the live worker's stdin and
returned "delivered" with no guarantee it was ever picked up. A worker
mid-turn could barrel past the new context — steers landed late or were
silently skipped, and a delivered steer was never buffered, so a worker
that ignored it, died, or retried lost it entirely.
Steering is now deterministic, with two explicit modes:
- enqueue (nudge): the steer is durably buffered AND handed to the live
worker best-effort. Every steer carries a stable id; a worker that picks
it up echoes a nudge_ack, which drains the buffer. A worker that ignores
it, dies, or retries leaves it buffered, so it folds into the next seat's
brief. The steer is never silently skipped and never lost past the run.
- interrupt (new verb): buffer the steer, abort the in-flight seat with a
WIP checkpoint, and re-staff the same visit (no retry cap burned) with the
steer folded into a fresh brief — immediate, deterministic application to
the current node's work; the run never leaves `running`.
Mechanics:
- new run events nudge_delivered.steerId + nudge_acked; the fold buffers any
steer carrying an id (dedup by id) and drains on ack or seat spawn. The
buffer is a fold of the persisted log, so it survives an engine reboot.
- new WorkerEvent nudge_ack; the process adapter streams {kind,steerId,text}
and parses the ack back. Sentinel liveness pokes carry no id and never
fold into a brief.
- StageManager.interrupt(); tracker/server/CLI (--interrupt, mode=interrupt)
and flow-script hooks expose it.
Tests: test/steering.test.ts covers enqueue durability + ack drain, an
unacked steer surviving into the next seat, buffering on a parked run and
across a reboot, and interrupt abort+re-staff. process-adapter test asserts
the ack round-trips over stdin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Running an agent as a service, then injecting / interrupting / adding context to that already-running agent, was unreliable. The live-steering path was fire-and-forget:
nudge()wrote the steer to the live worker's stdin and returned"delivered"with no guarantee it was ever applied. A worker mid-turn could barrel past the new context — steers landed late or were silently skipped — and a delivered steer was never buffered, so a worker that ignored it, died, or retried lost it entirely.Fix — steering is now deterministic, with two explicit modes
enqueue (
nudge) — the steer is durably buffered and handed to the live worker best-effort. Every steer carries a stable id; a worker that picks it up echoes anudge_ack, which drains the buffer. A worker that ignores it, dies, or retries leaves the steer buffered, so it folds into the next seat's brief. The steer is never silently skipped and never lost past the run — it lands either on the running worker (confirmed by ack) or, at latest, on the run's next seat. The in-flight seat keeps running.interrupt (
interrupt, new verb) — the steer is buffered, the in-flight seat is aborted with a WIP checkpoint, and the same visit is re-staffed (next attempt, no retry cap burned) with the steer folded into a fresh brief. Immediate, deterministic application to the current node's work; the run never leavesrunning.Mechanics
nudge_delivered.steerId+nudge_acked. The fold buffers any steer carrying an id (dedup by id) and drains on ack or on the next seat spawn. The buffer is a fold of the persisted log, so it survives an engine reboot.WorkerEventnudge_ack; the process adapter streams{kind,steerId,text}on stdin and parses the ack back off stdout. The Sentinel's liveness pokes carry no id and never fold into a brief.StageManager.interrupt(), plumbed through the tracker, HTTP API (mode: "enqueue" | "interrupt"), CLI (nudge … --interrupt), and flow-script hooks.Interrupt-vs-enqueue, stated
nudge)interrupt)runningTests
test/steering.test.ts(new): enqueue durability + ack drain; an unacked steer surviving into the next seat's brief; buffering on a parked run and across an engine reboot; interrupt abort + same-visit re-staff carrying the steer.test/process-adapter.test.ts: asserts thenudge_ackround-trips over stdin and drains the buffer.Left unmerged for a human to review.
🤖 Generated with Claude Code