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
@@ -13,6 +13,7 @@ import {
|
||||
handleAfterToolCall,
|
||||
handleTransformContext,
|
||||
type ActiveRunContext,
|
||||
type ActiveRunLifecycleState,
|
||||
} from "./hook-handlers";
|
||||
import type {
|
||||
AfterToolCallRpcPayload,
|
||||
@@ -79,14 +80,34 @@ export class RuntimeBridge {
|
||||
/** Returns the active run context for a given agent, if any. Used by
|
||||
* main-side services (inbox dispatcher) to decide whether a new inbox
|
||||
* row should route to `agent.steer()` instead of waiting for the next
|
||||
* `prepareRun` cycle. */
|
||||
findActiveRunForAgent(agentId: string): ActiveRunContext | null {
|
||||
* `prepareRun` cycle. When `requireSteerReady` is set, only runs whose
|
||||
* worker-side Agent is actually subscribed (`agent_active`) are
|
||||
* returned — this avoids steering into `preparing` or `closing` runs
|
||||
* where the steer would be silently dropped. */
|
||||
findActiveRunForAgent(
|
||||
agentId: string,
|
||||
options?: { requireSteerReady?: boolean },
|
||||
): ActiveRunContext | null {
|
||||
for (const ctx of this.activeRuns.values()) {
|
||||
if (ctx.agentId === agentId) return ctx;
|
||||
if (ctx.agentId !== agentId) continue;
|
||||
if (options?.requireSteerReady && ctx.state !== "agent_active") continue;
|
||||
return ctx;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition an active run between lifecycle phases as the worker
|
||||
* publishes `run-agent-state`. Unknown run ids are ignored: the worker
|
||||
* may publish state for a run that main has already evicted when a
|
||||
* `run-finished` races ahead of the trailing state message.
|
||||
*/
|
||||
setRunAgentState(runId: string, state: ActiveRunLifecycleState): void {
|
||||
const ctx = this.activeRuns.get(runId);
|
||||
if (!ctx) return;
|
||||
ctx.state = state;
|
||||
}
|
||||
|
||||
private getOrCreateStore(taskSessionId: string): SqliteSessionStore {
|
||||
const existing = this.sessionStores.get(taskSessionId);
|
||||
if (existing) return existing;
|
||||
@@ -268,6 +289,7 @@ export class RuntimeBridge {
|
||||
modelId: payload.modelId,
|
||||
taskSessionId: payload.taskSessionId,
|
||||
workspacePath: payload.workspacePath,
|
||||
state: "preparing",
|
||||
});
|
||||
const definition = getConnectorDefinition(payload.connectorId);
|
||||
const resolved = await this.deps.credentialResolver.resolve(payload.connectorId);
|
||||
|
||||
Reference in new issue
Block a user