From b51156664d97e5def3eaaf90346190353d176583 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 14 Feb 2026 23:37:19 +0000 Subject: [PATCH] Cleanup run-store entries when chat streams finish Co-authored-by: Eric Allam --- packages/ai/src/chatTransport.test.ts | 70 +++++++++++++++++++++++++++ packages/ai/src/chatTransport.ts | 2 + 2 files changed, 72 insertions(+) diff --git a/packages/ai/src/chatTransport.test.ts b/packages/ai/src/chatTransport.test.ts index e707e00d6..4c258c58e 100644 --- a/packages/ai/src/chatTransport.test.ts +++ b/packages/ai/src/chatTransport.test.ts @@ -338,6 +338,67 @@ describe("TriggerChatTransport", function () { expect(observedRunId).toBe("run_factory"); }); + it("cleans run store state when stream completes", async function () { + const trackedRunStore = new TrackedRunStore(); + + const server = await startServer(function (req, res) { + if (req.method === "POST" && req.url === "/api/v1/tasks/chat-task/trigger") { + res.writeHead(200, { + "content-type": "application/json", + "x-trigger-jwt": "pk_run_cleanup", + }); + res.end(JSON.stringify({ id: "run_cleanup" })); + return; + } + + if (req.method === "GET" && req.url === "/realtime/v1/streams/run_cleanup/chat-stream") { + res.writeHead(200, { + "content-type": "text/event-stream", + }); + writeSSE( + res, + "1-0", + JSON.stringify({ type: "text-start", id: "cleanup_1" }) + ); + writeSSE( + res, + "2-0", + JSON.stringify({ type: "text-end", id: "cleanup_1" }) + ); + res.end(); + return; + } + + res.writeHead(404); + res.end(); + }); + + const transport = new TriggerChatTransport({ + task: "chat-task", + stream: "chat-stream", + accessToken: "pk_trigger", + baseURL: server.url, + runStore: trackedRunStore, + }); + + const stream = await transport.sendMessages({ + trigger: "submit-message", + chatId: "chat-cleanup", + messageId: undefined, + messages: [], + abortSignal: undefined, + }); + + const chunks = await readChunks(stream); + expect(chunks).toHaveLength(2); + + await waitForCondition(function () { + return trackedRunStore.deleteCalls.includes("chat-cleanup"); + }); + + expect(trackedRunStore.get("chat-cleanup")).toBeUndefined(); + }); + it("reconnects active streams using tracked lastEventId", async function () { let reconnectLastEventId: string | undefined; let firstStreamResponse: ServerResponse | undefined; @@ -528,3 +589,12 @@ async function waitForCondition(condition: () => boolean, timeoutInMs = 5000) { throw new Error(`Condition was not met within ${timeoutInMs}ms`); } + +class TrackedRunStore extends InMemoryTriggerChatRunStore { + public readonly deleteCalls: string[] = []; + + public delete(chatId: string): void { + this.deleteCalls.push(chatId); + super.delete(chatId); + } +} diff --git a/packages/ai/src/chatTransport.ts b/packages/ai/src/chatTransport.ts index 34099edf4..bc7a4fc74 100644 --- a/packages/ai/src/chatTransport.ts +++ b/packages/ai/src/chatTransport.ts @@ -269,12 +269,14 @@ export class TriggerChatTransport< if (runState) { runState.isActive = false; await this.runStore.set(runState); + await this.runStore.delete(chatId); } } catch { const runState = await this.runStore.get(chatId); if (runState) { runState.isActive = false; await this.runStore.set(runState); + await this.runStore.delete(chatId); } } }