diff --git a/src/gateway/chat-abort.test.ts b/src/gateway/chat-abort.test.ts index 5a1d464022e..f7e262c5fd8 100644 --- a/src/gateway/chat-abort.test.ts +++ b/src/gateway/chat-abort.test.ts @@ -360,6 +360,7 @@ describe("resolveInFlightRunSnapshot", () => { aborted?: boolean; projectSessionActive?: boolean; startedAtMs?: number; + kind?: ChatAbortControllerEntry["kind"]; }, ): ChatAbortControllerEntry => { const now = Date.now(); @@ -376,6 +377,7 @@ describe("resolveInFlightRunSnapshot", () => { startedAtMs, expiresAtMs: startedAtMs + 10_000, projectSessionActive: opts?.projectSessionActive ?? true, + kind: opts?.kind, }; }; @@ -446,6 +448,18 @@ describe("resolveInFlightRunSnapshot", () => { } }); + it("ignores hidden agent runs that are not visible chat sends", () => { + expect( + snap({ + chatAbortControllers: new Map([ + ["run-agent", inFlightEntry("agent:main:s", { kind: "agent" })], + ]), + chatRunBuffers: new Map([["run-agent", "hidden partial"]]), + sessionKey: "agent:main:s", + }), + ).toBeUndefined(); + }); + it("treats an entry with undefined projectSessionActive as active (sessions.list contract)", () => { const entry = inFlightEntry("agent:main:s"); delete (entry as { projectSessionActive?: boolean }).projectSessionActive; diff --git a/src/gateway/chat-abort.ts b/src/gateway/chat-abort.ts index 7573480e0ca..d470eea332d 100644 --- a/src/gateway/chat-abort.ts +++ b/src/gateway/chat-abort.ts @@ -175,9 +175,10 @@ function normalizeActiveAgentId(agentId: string | undefined): string | undefined * key, so accept a match on EITHER `requestedSessionKey` or `canonicalSessionKey`, * scoping the shared "global" session by agent. Only runs still projected active * (`projectSessionActive !== false`, matching sessions.list; the terminal lifecycle - * flips it to false) and not aborted are returned, so a finalized run — already in - * persisted history — is not duplicated and a stale run cannot leave the client - * stuck in `streaming`. + * flips it to false), not aborted, and visible chat-send runs are returned, so a + * finalized run — already in persisted history — is not duplicated and hidden + * agent runs cannot be adopted by chat clients that will not receive their final + * events. */ export function resolveInFlightRunSnapshot(params: { chatAbortControllers: Map; @@ -218,7 +219,11 @@ export function resolveInFlightRunSnapshot(params: { // Active unless explicitly projected inactive — mirrors sessions.list's // collectTrackedActiveSessionRuns (`projectSessionActive !== false`), so a run // that indicator shows active is never silently dropped here. - if (entry.projectSessionActive === false || entry.controller.signal.aborted) { + if ( + entry.projectSessionActive === false || + entry.controller.signal.aborted || + entry.kind === "agent" + ) { continue; } if (