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:
1 parent
ad9205fcc9
commit
e917c614c9
12 files changed
+264
-57
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user