From 06fa69b63b280ca70b48ea709bf9945c5505f629 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 28 Aug 2026 03:00:18 -0700 Subject: [PATCH] fix(pty): read the incarnation through the shared guard, not truthiness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A malformed incarnation is evidence of nothing, so it must read as "unknown" rather than as a value that disagrees with every well-formed one — otherwise a non-string on the payload would discard the very exits the buffer exists to deliver. Route both the record and the comparison through the existing isPtyIncarnationId guard. --- .../terminal-pane/pty-pre-handler-buffer.test.ts | 14 ++++++++++++++ .../terminal-pane/pty-pre-handler-buffer.ts | 16 ++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts index 461a41e7faa..6219fc0105c 100644 --- a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts +++ b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.test.ts @@ -276,6 +276,20 @@ describe('pre-handler PTY buffer', () => { expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) }) + // A malformed incarnation is evidence of nothing. Treating it as a value that disagrees with + // everything would discard the very exits the buffer exists to deliver. + it('treats a malformed incarnation as unknown rather than as a disagreement', () => { + bufferPreHandlerPtyExit(RECYCLED_PTY_ID, 4, { not: 'a string' }) + discardPreHandlerPtyExitFromForeignIncarnation(RECYCLED_PTY_ID, FRESH_INCARNATION_ID) + expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) + drainPreHandlerPtyExit(RECYCLED_PTY_ID, vi.fn()) + + clearConsumedPreHandlerPtyExit(RECYCLED_PTY_ID) + bufferPreHandlerPtyExit(RECYCLED_PTY_ID, 5, PRIOR_INCARNATION_ID) + discardPreHandlerPtyExitFromForeignIncarnation(RECYCLED_PTY_ID, 42) + expect(hasPreHandlerPtyExit(RECYCLED_PTY_ID)).toBe(true) + }) + it('re-admits exits for a recycled id whose prior incarnation was consumed', () => { consumePreHandlerPtyState(RECYCLED_PTY_ID) diff --git a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts index ee0830c518d..dcf5f0af5dd 100644 --- a/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts +++ b/src/renderer/src/components/terminal-pane/pty-pre-handler-buffer.ts @@ -1,3 +1,4 @@ +import { isPtyIncarnationId } from '../../../../shared/pty-incarnation' import { clampUtf8Tail } from './pty-eager-buffer-clamp' import type { PtyDataMeta } from './pty-dispatcher' @@ -81,9 +82,11 @@ function reserveBoundedPtySlot(map: Map, ptyId: string, cap: numbe * existing id, and re-owning incarnation X is still proof that W's exit was not theirs. */ export function discardPreHandlerPtyExitFromForeignIncarnation( ptyId: string, - incarnationId: string | undefined + incarnationId: unknown ): void { - if (!incarnationId) { + // Why the shared guard and not a truthiness check: anything that is not a well-formed incarnation + // is evidence of nothing, and must read as "unknown" rather than disagree with everything. + if (!isPtyIncarnationId(incarnationId)) { return } const exit = preHandlerPtyExit.get(ptyId) @@ -164,7 +167,11 @@ export function replayPreHandlerPtyData(ptyId: string, observer: (data: string) } } -export function bufferPreHandlerPtyExit(ptyId: string, code: number, incarnationId?: string): void { +export function bufferPreHandlerPtyExit( + ptyId: string, + code: number, + incarnationId?: unknown +): void { if (consumedPreHandlerPtyExits.has(ptyId) || discardedPreHandlerPtyStates.has(ptyId)) { return } @@ -172,7 +179,8 @@ export function bufferPreHandlerPtyExit(ptyId: string, code: number, incarnation preHandlerPtyExit.set(ptyId, { code, sequence: nextPreHandlerPtySequence(), - ...(incarnationId ? { incarnationId } : {}) + // Record only a well-formed incarnation; a malformed one must not become evidence. + ...(isPtyIncarnationId(incarnationId) ? { incarnationId } : {}) }) }