From 387cf89afdf17a792e4dbc648970918291e50d0d Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 31 Aug 2026 16:03:59 -0700 Subject: [PATCH] fix(pty): preserve owner close without incarnation --- src/main/ipc/pty/ipc/kill-sessions.test.ts | 18 ++++++++++++++++++ src/main/ipc/pty/ipc/kill-sessions.ts | 5 ++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/pty/ipc/kill-sessions.test.ts b/src/main/ipc/pty/ipc/kill-sessions.test.ts index 30974302f70..200ee336344 100644 --- a/src/main/ipc/pty/ipc/kill-sessions.test.ts +++ b/src/main/ipc/pty/ipc/kill-sessions.test.ts @@ -69,6 +69,24 @@ describe('killPtySessions input bounds', () => { ]) }) + it('allows explicit owner-close when a capable host omits incarnation evidence', async () => { + const provider = { + listProcesses: vi.fn(async () => []), + supportsIncarnationFence: vi.fn(() => true) + } + const shutdown = vi.fn(async () => undefined) + const results = await killPtySessions([{ id: 'session-1' }], 'owner-close', { + listProviders: () => [{ provider: provider as never }], + providerForSession: () => provider as never, + shutdown, + supportsIncarnationFence: (target, id) => + target.supportsIncarnationFence?.({ sessionId: id }) ?? false + }) + + expect(shutdown).toHaveBeenCalledTimes(1) + expect(results).toEqual([{ id: 'session-1', verdict: 'exited' }]) + }) + it('resolves incarnation-fence capability per session route', async () => { const provider = { listProcesses: vi.fn(async () => []), diff --git a/src/main/ipc/pty/ipc/kill-sessions.ts b/src/main/ipc/pty/ipc/kill-sessions.ts index 70a8df24d2f..98e69393be9 100644 --- a/src/main/ipc/pty/ipc/kill-sessions.ts +++ b/src/main/ipc/pty/ipc/kill-sessions.ts @@ -81,7 +81,10 @@ export async function killPtySessions( } } fenceCapabilities.set(ref.id, fenceCapable) - if (fenceCapable && !ref.incarnationId) { + // Orphan cleanup must prove the exact incarnation before acting. Explicit + // owner-close is already user-authorized and retains legacy id-only behavior + // when the listing omits an incarnation. + if (intent === 'orphan-cleanup' && fenceCapable && !ref.incarnationId) { return { ...ref, verdict: 'refused', reason: 'missing incarnation fence' } } try {