From bcb9724907431bbcb5c2b311e04b499e932088cb Mon Sep 17 00:00:00 2001 From: paseo-bot Date: Tue, 22 Sep 2026 20:13:33 +0200 Subject: [PATCH] fix(omp): drop a prompt's client correlation when its dispatch fails --- .../server/agent/providers/omp/agent.test.ts | 28 +++++++++++++++++++ .../src/server/agent/providers/omp/agent.ts | 14 ++++++++++ .../providers/omp/test-utils/fake-omp.ts | 4 +++ 3 files changed, 46 insertions(+) diff --git a/packages/server/src/server/agent/providers/omp/agent.test.ts b/packages/server/src/server/agent/providers/omp/agent.test.ts index 3f9fe77c43..208666d55d 100644 --- a/packages/server/src/server/agent/providers/omp/agent.test.ts +++ b/packages/server/src/server/agent/providers/omp/agent.test.ts @@ -754,6 +754,34 @@ describe("OMP agent client and session", () => { ]); }); + test("a prompt that never reaches OMP leaves no correlation for a later submission", async () => { + const omp = new OmpHarness(); + await omp.start(); + + const runtime = omp.runtime(); + runtime.promptError = new Error("OMP prompt dispatch failed"); + await omp.requireStartTurnFromClient("retry me", "client-a"); + await settle(); + expect(omp.eventTypes()).toContain("turn_failed"); + runtime.promptError = null; + + await omp.requireStartTurnFromClient("retry me", "client-b"); + runtime.beginTurn(); + runtime.acceptPrompt("retry me", "omp-user-b"); + runtime.streamAssistantText("done", "omp-assistant-b"); + runtime.finishTurn(); + await settle(); + + expect(omp.timeline().filter((item) => item.type === "user_message")).toEqual([ + { + type: "user_message", + text: "retry me", + messageId: "omp-user-b", + clientMessageId: "client-b", + }, + ]); + }); + test("the same prompt text on two turns correlates to its own submission", async () => { const omp = new OmpHarness(); await omp.start(); diff --git a/packages/server/src/server/agent/providers/omp/agent.ts b/packages/server/src/server/agent/providers/omp/agent.ts index 380a155be7..f0fc762328 100644 --- a/packages/server/src/server/agent/providers/omp/agent.ts +++ b/packages/server/src/server/agent/providers/omp/agent.ts @@ -999,6 +999,8 @@ export class OmpAgentSession implements AgentSession { return; } } catch (error) { + // The prompt never reached OMP, so no echo will ever claim it. + this.forgetClientCorrelation(options?.clientMessageId); if (this.activeTurnId !== turnId) { return; } @@ -1305,6 +1307,18 @@ export class OmpAgentSession implements AgentSession { } } + private forgetClientCorrelation(clientMessageId: string | undefined): void { + if (!clientMessageId) { + return; + } + const index = this.pendingClientCorrelations.findIndex( + (entry) => entry.clientMessageId === clientMessageId, + ); + if (index !== -1) { + this.pendingClientCorrelations.splice(index, 1); + } + } + private takeClientCorrelation(text: string): string | null { const index = this.pendingClientCorrelations.findIndex((entry) => entry.text === text); if (index === -1) { diff --git a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts index 0b17fe8c5b..4df5b327b5 100644 --- a/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts +++ b/packages/server/src/server/agent/providers/omp/test-utils/fake-omp.ts @@ -131,6 +131,7 @@ export class FakeOmpSession implements OmpRuntimeSession { compactError: Error | null = null; emitCompactEnd = true; getStateError: Error | null = null; + promptError: Error | null = null; promptAck: OmpPromptAck = {}; branchResponse: { text?: string; cancelled?: boolean } = { text: "" }; branchMessages: Array<{ entryId: string; text: string }> = []; @@ -180,6 +181,9 @@ export class FakeOmpSession implements OmpRuntimeSession { } this.prompts.push({ message, imageCount: images?.length ?? 0 }); this.promptWaiters.shift()?.(); + if (this.promptError) { + throw this.promptError; + } const heldPrompt = this.nextHeldPrompt; if (heldPrompt) { this.nextHeldPrompt = null;