From 0bddf1e75580910cac7afbf28f1137aaff71b44b Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sun, 10 May 2026 09:05:43 -0600 Subject: [PATCH 1/4] fix(cli): prevent memory leak in KiloSessionPromptQueue.cancel for sessions without active tails The cancel() function was always incrementing and storing a version in the versions map, even for sessions with no queued prompts. Since cleanup only happens in enqueue's release phase, sessions without active tails left permanent map entries. Now cancel() checks if the session has an active tail before modifying state. If no tail exists, it cleans up any existing versions/targets/latest/activeSince entries instead of creating new ones. --- packages/opencode/src/kilocode/session/prompt-queue.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/opencode/src/kilocode/session/prompt-queue.ts b/packages/opencode/src/kilocode/session/prompt-queue.ts index 405906eb9c..5550fa135f 100644 --- a/packages/opencode/src/kilocode/session/prompt-queue.ts +++ b/packages/opencode/src/kilocode/session/prompt-queue.ts @@ -36,6 +36,13 @@ export namespace KiloSessionPromptQueue { export function cancel(sessionID: SessionID) { return Effect.sync(() => { + if (!tails.has(sessionID)) { + versions.delete(sessionID) + targets.delete(sessionID) + latest.delete(sessionID) + activeSince.delete(sessionID) + return + } versions.set(sessionID, version(sessionID) + 1) }) } From 1403b660924d097c6f24859bc80b01f9e7765765 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 12 May 2026 17:43:15 -0600 Subject: [PATCH 2/4] test: add regression test for cancel memory leak Adds a test that verifies cancel() on a session with no active tail does not leave stale entries in the internal maps (versions, targets, latest, activeSince). Also adds _hasInternalState() debug helper to verify map cleanup. --- .../src/kilocode/session/prompt-queue.ts | 4 ++++ .../kilocode/session-prompt-queue.test.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/opencode/src/kilocode/session/prompt-queue.ts b/packages/opencode/src/kilocode/session/prompt-queue.ts index 5550fa135f..19fcb421f0 100644 --- a/packages/opencode/src/kilocode/session/prompt-queue.ts +++ b/packages/opencode/src/kilocode/session/prompt-queue.ts @@ -27,6 +27,10 @@ export namespace KiloSessionPromptQueue { const activeSince = new Map() let seq = 0 + export function _hasInternalState(sessionID: SessionID): boolean { + return versions.has(sessionID) || targets.has(sessionID) || latest.has(sessionID) || activeSince.has(sessionID) + } + const version = (sessionID: SessionID) => versions.get(sessionID) ?? 0 const settle = (promise: Promise) => promise.then( diff --git a/packages/opencode/test/kilocode/session-prompt-queue.test.ts b/packages/opencode/test/kilocode/session-prompt-queue.test.ts index 4d5cb905af..0f4b317f5a 100644 --- a/packages/opencode/test/kilocode/session-prompt-queue.test.ts +++ b/packages/opencode/test/kilocode/session-prompt-queue.test.ts @@ -491,6 +491,25 @@ describe("session prompt queue", () => { } }) + test("cancel on a session with no active tail is a no-op and does not leak state", async () => { + const sessionID = SessionID.make("session_cancel_noop") + + await Effect.runPromise(KiloSessionPromptQueue.cancel(sessionID)) + + expect(KiloSessionPromptQueue._hasInternalState(sessionID)).toBe(false) + + const result = await Effect.runPromise( + KiloSessionPromptQueue.enqueue( + sessionID, + MessageID.make("message_probe"), + Effect.succeed("work executed"), + Effect.succeed("cancelled returned"), + ), + ) + + expect(result).toBe("work executed") + }) + test("cancel drops queued prompts and resets internal state", async () => { const ready = Promise.withResolvers() const calls: number[] = [] From 0acf36b4c8b2e389a7676c1229d6646f83a445b0 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Tue, 12 May 2026 17:50:45 -0600 Subject: [PATCH 3/4] chore: add @internal JSDoc to test-only helper --- packages/opencode/src/kilocode/session/prompt-queue.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/kilocode/session/prompt-queue.ts b/packages/opencode/src/kilocode/session/prompt-queue.ts index 19fcb421f0..fb454011b1 100644 --- a/packages/opencode/src/kilocode/session/prompt-queue.ts +++ b/packages/opencode/src/kilocode/session/prompt-queue.ts @@ -27,6 +27,7 @@ export namespace KiloSessionPromptQueue { const activeSince = new Map() let seq = 0 + /** @internal - test-only helper */ export function _hasInternalState(sessionID: SessionID): boolean { return versions.has(sessionID) || targets.has(sessionID) || latest.has(sessionID) || activeSince.has(sessionID) } From df30123e5474cdbd2ad3b56d59c6eb5d06b89189 Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Sat, 16 May 2026 10:23:37 -0600 Subject: [PATCH 4/4] chore: add changeset for session prompt queue memory leak fix --- .changeset/session-prompt-queue-memory-leak.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/session-prompt-queue-memory-leak.md diff --git a/.changeset/session-prompt-queue-memory-leak.md b/.changeset/session-prompt-queue-memory-leak.md new file mode 100644 index 0000000000..7a035f2f78 --- /dev/null +++ b/.changeset/session-prompt-queue-memory-leak.md @@ -0,0 +1,5 @@ +--- +"@kilocode/cli": patch +--- + +Prevent memory leak in KiloSessionPromptQueue.cancel for sessions without active tails