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.
This commit is contained in:
Mohamed Boudra
2026-11-10 13:47:49 +01:00
parent 457d8d6ec5
commit 10c443a32b
3 changed files with 8 additions and 1 deletions
@@ -559,6 +559,7 @@ test.each(["mode", "thinking", "rewind"])(
expect.objectContaining({ message: "continue" }), expect.objectContaining({ message: "continue" }),
]), ]),
); );
runtime.latestSession().beginTurn();
runtime.latestSession().finishTurn(); runtime.latestSession().finishTurn();
await manager.waitForAgentEvent(agentId); await manager.waitForAgentEvent(agentId);
await manager.archiveAgent(agentId); await manager.archiveAgent(agentId);
@@ -4265,6 +4265,12 @@ export class AgentManager {
agentId: string, agentId: string,
options?: WaitForAgentOptions, options?: WaitForAgentOptions,
): Promise<WaitForAgentResult> { ): Promise<WaitForAgentResult> {
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); const snapshot = this.getAgent(agentId);
if (!snapshot) { if (!snapshot) {
throw new Error(`Agent ${agentId} not found`); throw new Error(`Agent ${agentId} not found`);
@@ -74,7 +74,7 @@ export class AgentRunState {
*/ */
async waitForForegroundStart(agentId: string): Promise<void> { async waitForForegroundStart(agentId: string): Promise<void> {
const run = this.getPendingRun(agentId); const run = this.getPendingRun(agentId);
if (run?.start.status !== "pending") { if (!run || run.start.status === "started") {
return; return;
} }
await Promise.race([run.turnStartedPromise, run.settledPromise]); await Promise.race([run.turnStartedPromise, run.settledPromise]);