Skip to content

Make mid-run context injection a clean, first-class operation - #6

Merged
frgmt0 merged 5 commits into
mainfrom
fix/mid-run-context-injection
Jul 20, 2026
Merged

Make mid-run context injection a clean, first-class operation#6
frgmt0 merged 5 commits into
mainfrom
fix/mid-run-context-injection

Conversation

@0xbeckett

Copy link
Copy Markdown
Collaborator

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 a nudge_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 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 on the next 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} 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

enqueue (nudge) interrupt (interrupt)
in-flight seat keeps running aborted (WIP committed)
when applied on ack, else next seat now, on a fresh re-staffed seat
guarantee never skipped, never lost past the run applied to the current node's work
run status unchanged stays running

Tests

  • 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 the nudge_ack round-trips over stdin and drains the buffer.
  • Full suite green: 19 files / 176 tests.

Left unmerged for a human to review.

🤖 Generated with Claude Code

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>
@frgmt0
frgmt0 merged commit 3a71913 into main Jul 20, 2026
1 check passed
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.

2 participants