From 4c47a42b6e6c4740210eb0ed8b6effdede33e10f Mon Sep 17 00:00:00 2001 From: Prateek Date: Fri, 13 Feb 2026 21:09:49 +0530 Subject: [PATCH] fix: use literal mode for tmux send-keys, prune stale lifecycle trackers - Add -l flag to tmux send-keys for short messages so key-like payloads (e.g. "Enter", "Escape") are sent as text, not interpreted as keypresses - Prune states and reactionTrackers maps in pollAll() for sessions that no longer appear in the session list, preventing unbounded memory growth and stale retry counters from applying to reused session IDs Co-Authored-By: Claude Opus 4.6 --- packages/core/src/__tests__/tmux.test.ts | 2 +- packages/core/src/lifecycle-manager.ts | 15 +++++++++++++++ packages/core/src/tmux.ts | 4 +++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/core/src/__tests__/tmux.test.ts b/packages/core/src/__tests__/tmux.test.ts index bafb50b01..f81dc2dac 100644 --- a/packages/core/src/__tests__/tmux.test.ts +++ b/packages/core/src/__tests__/tmux.test.ts @@ -190,7 +190,7 @@ describe("sendKeys", () => { expect(escapeArgs).toEqual(["send-keys", "-t", "app-1", "Escape"]); // Call 1: text const textArgs = mockExecFile.mock.calls[1][1] as string[]; - expect(textArgs).toEqual(["send-keys", "-t", "app-1", "hello world"]); + expect(textArgs).toEqual(["send-keys", "-t", "app-1", "-l", "hello world"]); // Call 2: Enter const enterArgs = mockExecFile.mock.calls[2][1] as string[]; expect(enterArgs).toEqual(["send-keys", "-t", "app-1", "Enter"]); diff --git a/packages/core/src/lifecycle-manager.ts b/packages/core/src/lifecycle-manager.ts index e4b597b63..ef9f512b5 100644 --- a/packages/core/src/lifecycle-manager.ts +++ b/packages/core/src/lifecycle-manager.ts @@ -453,6 +453,21 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan // Poll all sessions concurrently await Promise.allSettled(sessionsToCheck.map((s) => checkSession(s))); + // Prune stale entries from states and reactionTrackers for sessions + // that no longer appear in the session list (e.g., after kill/cleanup) + const currentSessionIds = new Set(sessions.map((s) => s.id)); + for (const trackedId of states.keys()) { + if (!currentSessionIds.has(trackedId)) { + states.delete(trackedId); + } + } + for (const trackerKey of reactionTrackers.keys()) { + const sessionId = trackerKey.split(":")[0]; + if (sessionId && !currentSessionIds.has(sessionId)) { + reactionTrackers.delete(trackerKey); + } + } + // Check if all sessions are complete (emit only once) const activeSessions = sessions.filter((s) => s.status !== "merged" && s.status !== "killed"); if (sessions.length > 0 && activeSessions.length === 0 && !allCompleteEmitted) { diff --git a/packages/core/src/tmux.ts b/packages/core/src/tmux.ts index 6d2de1ae1..098a9e875 100644 --- a/packages/core/src/tmux.ts +++ b/packages/core/src/tmux.ts @@ -152,7 +152,9 @@ export async function sendKeys( } } } else { - await tmux("send-keys", "-t", sessionName, text); + // Use -l (literal) to prevent tmux from interpreting text as key names + // (e.g. "Enter", "Escape", "C-c" would be treated as keypresses without -l) + await tmux("send-keys", "-t", sessionName, "-l", text); } if (pressEnter) {