Gate inbox delivery on worker-side steer ACK

Replace the fire-and-forget steer with a correlated ACK round-trip so main
only marks an inbox row delivered once the worker confirms the message
landed on the live Agent. Steer against a run that is still in the
`preparing` window or has already started `closing` now falls back to the
wake + inbox projection path instead of being silently dropped.

Worker publishes `run-agent-state` transitions at Agent construction and
teardown; main tracks these on `ActiveRunContext.state` and refuses to
steer runs that are not `agent_active`. A rejected or timed-out ACK
triggers `wakeAgentIfPossible` so the next `prepareRun` replays the row.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-19 14:38:04 -04:00
1 parent ad9205fcc9
commit e917c614c9
12 files changed
+264 -57

No files matched your search

+15 -11
View File
@@ -28,8 +28,10 @@ interface RunnerDeps {
* - Builds an `Agent` via `createPiAgent` with the full Pi contract.
* - Streams every pi `AgentEvent` into the worker → main pipe.
* - Persists each `message_end` message through `persistMessage` RPC.
* - Exposes `steer` / `followUp` to the worker entry so mid-run
* injections from main land on the in-process Agent instance.
* - Exposes `steer` to the worker entry so mid-run injections from
* main land on the in-process Agent instance.
* - Publishes `run-agent-state` transitions so main can tell whether
* an active run is actually ready to accept steer messages.
*/
export class WorkerRunner {
private readonly abortControllers = new Map<string, AbortController>();
@@ -43,7 +45,15 @@ export class WorkerRunner {
if (controller) controller.abort();
}
/** Enqueue a steering message onto the active Agent for this run. */
/**
* Enqueue a steering message onto the active Agent for this run. Returns
* true iff the Agent instance exists in `activeAgents` and the message
* was appended to its steering queue. A false return means the run has
* either not finished construction (pre-`agent_active`) or has already
* torn down (post-`closing`); callers must treat the steer as rejected
* so main can hold the inbox `delivered_at` write and fall back to the
* inbox + wake path instead.
*/
steer(runId: string, message: AgentMessage): boolean {
const agent = this.activeAgents.get(runId);
if (!agent) return false;
@@ -51,14 +61,6 @@ export class WorkerRunner {
return true;
}
/** Enqueue a follow-up message onto the active Agent for this run. */
followUp(runId: string, message: AgentMessage): boolean {
const agent = this.activeAgents.get(runId);
if (!agent) return false;
agent.followUp(message);
return true;
}
/**
* Aborts every in-flight run and returns a promise that resolves once
* each `run()` has settled (success, failure, or abort). Callers (the
@@ -150,6 +152,7 @@ export class WorkerRunner {
rpc,
});
this.activeAgents.set(job.runId, agent);
post({ type: "run-agent-state", runId: job.runId, state: "agent_active" });
const unsubscribe = agent.subscribe(async (event) => {
const timestamp = new Date().toISOString();
@@ -192,6 +195,7 @@ export class WorkerRunner {
abortController.signal.addEventListener("abort", () => agent.abort(), { once: true });
await runPromise;
} finally {
post({ type: "run-agent-state", runId: job.runId, state: "closing" });
unsubscribe();
this.activeAgents.delete(job.runId);
}