From 10c443a32b11fbb44d5200115b9eccbab9b96279 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 10 Nov 2026 13:47:49 +0100 Subject: [PATCH] fix(server): wait for a starting run to settle before steering or waiting on it A run's start now awaits the timeline before the provider is called, and a failed start awaits its timeline writes before the run is cleared. Two readers raced those windows: - waitForForegroundStart returned as soon as a start failed, while the failed run was still tracked, so a steer send in that window replaced it and interrupted the session instead of starting its own turn. - waitForAgentEvent replayed the idle state from before a starting run and finished immediately, so `wait` after a dispatch could return before the turn ran. waitForForegroundStart now resolves only once the turn started or the run is cleared, as documented, and waitForAgentEvent waits for it before reading the agent's state. The OMP restored-history test previously finished only through that early return; its fake now starts the turn it later ends. --- packages/server/src/server/agent/agent-loading.test.ts | 1 + packages/server/src/server/agent/agent-manager.ts | 6 ++++++ packages/server/src/server/agent/agent-run-state.ts | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/agent/agent-loading.test.ts b/packages/server/src/server/agent/agent-loading.test.ts index d6a53fb1bc..eba03b7459 100644 --- a/packages/server/src/server/agent/agent-loading.test.ts +++ b/packages/server/src/server/agent/agent-loading.test.ts @@ -559,6 +559,7 @@ test.each(["mode", "thinking", "rewind"])( expect.objectContaining({ message: "continue" }), ]), ); + runtime.latestSession().beginTurn(); runtime.latestSession().finishTurn(); await manager.waitForAgentEvent(agentId); await manager.archiveAgent(agentId); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 88b66608e0..2fffd0b026 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -4265,6 +4265,12 @@ export class AgentManager { agentId: string, options?: WaitForAgentOptions, ): Promise { + if (!this.getAgent(agentId)) { + throw new Error(`Agent ${agentId} not found`); + } + // A starting run publishes its running state only once its turn starts. Until then the + // replayed state below is the idle state before the run, which would end the wait early. + await this.runs.waitForForegroundStart(agentId); const snapshot = this.getAgent(agentId); if (!snapshot) { throw new Error(`Agent ${agentId} not found`); diff --git a/packages/server/src/server/agent/agent-run-state.ts b/packages/server/src/server/agent/agent-run-state.ts index 0ea85f6d81..b8a03dce2a 100644 --- a/packages/server/src/server/agent/agent-run-state.ts +++ b/packages/server/src/server/agent/agent-run-state.ts @@ -74,7 +74,7 @@ export class AgentRunState { */ async waitForForegroundStart(agentId: string): Promise { const run = this.getPendingRun(agentId); - if (run?.start.status !== "pending") { + if (!run || run.start.status === "started") { return; } await Promise.race([run.turnStartedPromise, run.settledPromise]);