From b39b2a057a63387256abf5e1fe45ccdbb0820596 Mon Sep 17 00:00:00 2001 From: m4air Date: Sat, 19 Sep 2026 15:45:58 -0700 Subject: [PATCH] fix(memory): bound host mirror gap verdicts --- .../lib/host-mirror-handle-gap-verdict-union.test.ts | 12 ++++++++++++ src/renderer/src/lib/host-mirror-handle-gap-wait.ts | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts b/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts index 61fae8cbe3d..d1cb35be2fa 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-verdict-union.test.ts @@ -244,4 +244,16 @@ describe('handle-gap verdict map, all rules on one tree', () => { expect(countHostMirrorHandleGapVerdictsForTests()).toBe(0) expect(vi.getTimerCount()).toBe(0) }) + + it('bounds permanently orphaned verdicts across distinct environments', () => { + for (let round = 0; round < 600; round += 1) { + const environmentId = `env-orphan-${round}` + setRuntimeEnvironmentConnectionGenerationForTests(environmentId, 1) + const tabId = `orphan-${round}` + setLiveTabs([tabId], { [tabId]: `remote:${environmentId}@@term_${round}` }) + parkAndExpire(environmentId, tabId) + } + + expect(countHostMirrorHandleGapVerdictsForTests()).toBeLessThanOrEqual(512) + }) }) diff --git a/src/renderer/src/lib/host-mirror-handle-gap-wait.ts b/src/renderer/src/lib/host-mirror-handle-gap-wait.ts index e2d3a58367d..626c241f13d 100644 --- a/src/renderer/src/lib/host-mirror-handle-gap-wait.ts +++ b/src/renderer/src/lib/host-mirror-handle-gap-wait.ts @@ -106,6 +106,7 @@ type ExpiredHandleGapVerdict = { /** Sorted environment-minted PTY ids the tab's leaves held AT PARK TIME; '' when none. */ paneBinding: string } +const MAX_EXPIRED_HANDLE_GAP_VERDICTS = 512 const expiredGenerationByPane = new Map() let unsubscribeStore: (() => void) | null = null @@ -199,6 +200,16 @@ function recordExpiredWait(environmentId: string, key: string): void { // gate stops recording anything at all rather than admitting ''. It pins a different property // (reconnect-void, host-mirror-handle-gap-resume.test.ts). Both are load-bearing, for different // reasons — do not collapse them as redundant. + // Eviction is conservative: a missing verdict makes the pane wait once more, never resume early. + if (!expiredGenerationByPane.has(key)) { + while (expiredGenerationByPane.size >= MAX_EXPIRED_HANDLE_GAP_VERDICTS) { + const oldest = expiredGenerationByPane.keys().next() + if (oldest.done) { + break + } + expiredGenerationByPane.delete(oldest.value) + } + } expiredGenerationByPane.set(key, { generation, paneBinding: waitersByPane.get(key)?.paneBinding ?? ''