From cc8c95a5d121f8238acee56a02a7e35ae206723f Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg <5861166+CompleteDotTech@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:11:39 -0400 Subject: [PATCH] fix(chat): reap replaced stream buffers --- src/lib/server/chat-stream-buffer.test.ts | 24 +++++++++++++++++++++++ src/lib/server/chat-stream-buffer.ts | 6 ++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/lib/server/chat-stream-buffer.test.ts b/src/lib/server/chat-stream-buffer.test.ts index bcd832a14..6c82822a5 100644 --- a/src/lib/server/chat-stream-buffer.test.ts +++ b/src/lib/server/chat-stream-buffer.test.ts @@ -107,6 +107,30 @@ test("a follow-up turn owns the shared conversation key; unknown keys return nul resetRunBuffersForTest(); }); +test("a follow-up preserves the predecessor reap timer", (t) => { + resetRunBuffersForTest(); + t.mock.timers.enable({ apis: ["setTimeout"] }); + + const first = openRunBuffer(["run-old", "conv-shared"]); + first.record({ kind: "assistant_chunk", text: "old transcript" }); + first.finish(); + + openRunBuffer(["run-new", "conv-shared"]); + t.mock.timers.tick(2 * 60_000); + + assert.equal( + hasRunBuffer("run-old"), + false, + "the finished predecessor is reaped under its unique run id", + ); + assert.equal( + hasRunBuffer("conv-shared"), + true, + "the predecessor timer never deletes the replacement conversation mapping", + ); + resetRunBuffersForTest(); +}); + test("getRunBufferStatus returns payload-free metadata without side effects", () => { resetRunBuffersForTest(); let attachCount = 0; diff --git a/src/lib/server/chat-stream-buffer.ts b/src/lib/server/chat-stream-buffer.ts index 3e35f5092..d8db8b3c6 100644 --- a/src/lib/server/chat-stream-buffer.ts +++ b/src/lib/server/chat-stream-buffer.ts @@ -80,8 +80,10 @@ export function openRunBuffer( }; for (const key of keys) { if (!key) continue; - const stale = buffers.get(key); - if (stale && stale.reapTimer) clearTimeout(stale.reapTimer); + // A finished predecessor can still be reachable through another key + // (normally its unique run id). Keep its reap timer armed: the callback + // already checks map identity, so it will remove only predecessor + // mappings and leave this replacement untouched. buffers.set(key, buffer); buffer.keys.push(key); }