diff --git a/src/main/codex/codex-pane-launch-account.ts b/src/main/codex/codex-pane-launch-account.ts index a59703752f4..33d091cef7c 100644 --- a/src/main/codex/codex-pane-launch-account.ts +++ b/src/main/codex/codex-pane-launch-account.ts @@ -66,8 +66,11 @@ function resolveCodexHomeOwnerAccountId(args: { getCodexSelectionLaneKey(getCodexSelectionTargetForAccount(account)) === laneKey && normalizeRuntimePathForComparison(account.managedHomePath) === launchHome ) - // Why: the shared runtime mirror hot-swaps to whichever account is selected, so - // an unattributable home is no evidence the pane is on a different one. A - // wrong restart notice silently drops every keystroke in that terminal. + // Why: an unowned home cannot be named, and naming the account a pane is stuck + // on is the prompt's whole job — so decline rather than guess. A wrong notice + // silently drops every keystroke in that terminal. Note the shared runtime + // mirror only hot-swaps to the current selection on the legacy flag-OFF lane; + // a pane resumed into it after the per-account rollout is genuinely stale but + // still unnameable, so that cohort stays unreported. return owner ? owner.id : undefined } diff --git a/src/main/ipc/pty.test.ts b/src/main/ipc/pty.test.ts index 10f17505981..b6499469fa4 100644 --- a/src/main/ipc/pty.test.ts +++ b/src/main/ipc/pty.test.ts @@ -13638,6 +13638,126 @@ describe('registerPtyHandlers', () => { expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-resumed') }) + // Why: the runtime controller is the CLI/relay resume path, and it repeats the + // same recording call the ipc handler makes. Without its own coverage a revert + // there is invisible. + it('records the origin account for a resumed Codex pane spawned by the runtime controller', async () => { + type RuntimeSpawnController = { + spawn(args: Record): Promise<{ id: string }> + } + setLocalPtyProvider({ + spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + shutdown: vi.fn(), + onData: vi.fn(() => vi.fn()), + onExit: vi.fn(() => vi.fn()), + listProcesses: vi.fn(async () => []), + getForegroundProcess: vi.fn(async () => null) + } as never) + const runtime = { + setPtyController: vi.fn(), + registerPty: vi.fn(), + noteTerminalSpawnCommand: vi.fn(), + onPtySpawned: vi.fn(), + onPtyExit: vi.fn(), + onPtyData: vi.fn() + } + const getSettings = vi.fn().mockReturnValue({ + activeCodexManagedAccountId: 'account-b', + codexManagedAccounts: [ + { id: 'account-a', managedHomePath: '/managed/origin/home' }, + { id: 'account-b', managedHomePath: '/managed/current/home' } + ] + }) + handlers.clear() + registerPtyHandlers( + mainWindow as never, + runtime as never, + vi.fn(() => '/managed/current/home'), + getSettings as never, + undefined, + undefined, + { prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/origin/home' }) } + ) + const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController + + await controller.spawn({ + cols: 80, + rows: 24, + worktreeId: 'wt-runtime', + command: 'codex resume session-a', + launchAgent: 'codex', + resumeProviderSession: { + key: 'session_id', + id: 'session-a', + transcriptPath: '/managed/origin/home/sessions/2026/07/20/rollout-a.jsonl' + } + }) + + expect(recordCodexPaneAccountMock.mock.calls).toEqual([ + ['pty-runtime-resumed', { selectionKey: 'host', accountId: 'account-a' }] + ]) + expect(forgetCodexPaneAccountMock).not.toHaveBeenCalled() + }) + + it('leaves a runtime-controller resumed Codex pane unattributed when no account owns its home', async () => { + type RuntimeSpawnController = { + spawn(args: Record): Promise<{ id: string }> + } + setLocalPtyProvider({ + spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })), + write: vi.fn(), + resize: vi.fn(), + kill: vi.fn(), + shutdown: vi.fn(), + onData: vi.fn(() => vi.fn()), + onExit: vi.fn(() => vi.fn()), + listProcesses: vi.fn(async () => []), + getForegroundProcess: vi.fn(async () => null) + } as never) + const runtime = { + setPtyController: vi.fn(), + registerPty: vi.fn(), + noteTerminalSpawnCommand: vi.fn(), + onPtySpawned: vi.fn(), + onPtyExit: vi.fn(), + onPtyData: vi.fn() + } + const getSettings = vi.fn().mockReturnValue({ + activeCodexManagedAccountId: 'account-b', + codexManagedAccounts: [{ id: 'account-b', managedHomePath: '/managed/current/home' }] + }) + handlers.clear() + registerPtyHandlers( + mainWindow as never, + runtime as never, + vi.fn(() => '/managed/current/home'), + getSettings as never, + undefined, + undefined, + { prepareCodexSessionResume: async () => ({ codexHomePath: '/managed/shared-mirror/home' }) } + ) + const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController + + await controller.spawn({ + cols: 80, + rows: 24, + worktreeId: 'wt-runtime', + command: 'codex resume session-a', + launchAgent: 'codex', + resumeProviderSession: { + key: 'session_id', + id: 'session-a', + transcriptPath: '/managed/shared-mirror/home/sessions/2026/07/20/rollout-a.jsonl' + } + }) + + expect(recordCodexPaneAccountMock).not.toHaveBeenCalled() + expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-runtime-resumed') + }) + it('seeds cold restore at recovered dimensions with a legacy dimensionless fallback', async () => { const oscLinks = [{ row: 0, startCol: 0, endCol: 8, uri: 'https://example.com/restored' }] const coldRestore = {