diff --git a/src/renderer/src/lib/codex-session-restart.test.ts b/src/renderer/src/lib/codex-session-restart.test.ts index 93bde5ed353..481dce7c028 100644 --- a/src/renderer/src/lib/codex-session-restart.test.ts +++ b/src/renderer/src/lib/codex-session-restart.test.ts @@ -668,16 +668,90 @@ describe('markLiveCodexSessionsForRestart lane scoping', () => { vi.mocked(window.api.codexAccounts.listRecordedPaneLanes).mockResolvedValue({ 'pty-1': 'host' }) + vi.mocked(window.api.codexAccounts.listStalePanes).mockResolvedValue([ + { + ptyId: 'pty-1', + launchAccountId: 'account-a', + activeAccountId: 'account-b', + reason: 'account-change' + } + ]) await markLiveCodexSessionsForRestart({ previousAccountLabel: ACCOUNT_A, nextAccountLabel: ACCOUNT_B, + previousAccountId: 'account-a', + nextAccountId: 'account-b', target: { runtime: 'host' } }) expect(useAppStore.getState().codexRestartNoticeByPtyId['pty-1']).toEqual({ previousAccountLabel: ACCOUNT_A, - nextAccountLabel: ACCOUNT_B + nextAccountLabel: ACCOUNT_B, + previousAccountId: 'account-a', + nextAccountId: 'account-b' + }) + }) + + it('suppresses a null-to-account reauth notice when the recorded pane is already current', async () => { + seedPanes([{ ptyId: 'pty-1' }]) + vi.mocked(window.api.codexAccounts.listRecordedPaneLanes).mockResolvedValue({ + 'pty-1': 'host' + }) + + await markLiveCodexSessionsForRestart({ + previousAccountLabel: 'System default', + nextAccountLabel: ACCOUNT_A, + previousAccountId: null, + nextAccountId: 'account-a', + target: { runtime: 'host' } + }) + + expect(window.api.codexAccounts.listStalePanes).toHaveBeenCalledWith({ + ptyIds: ['pty-1'] + }) + expect(useAppStore.getState().codexRestartNoticeByPtyId).toEqual({}) + }) + + it('classifies current, stale, and unrecorded panes independently', async () => { + seedPanes([{ ptyId: 'pty-current' }, { ptyId: 'pty-stale' }, { ptyId: 'pty-unknown' }]) + vi.mocked(window.api.codexAccounts.listRecordedPaneLanes).mockResolvedValue({ + 'pty-current': 'host', + 'pty-stale': 'host' + }) + vi.mocked(window.api.codexAccounts.listStalePanes).mockResolvedValue([ + { + ptyId: 'pty-stale', + launchAccountId: 'account-a', + activeAccountId: 'account-b', + reason: 'account-change' + } + ]) + + await markLiveCodexSessionsForRestart({ + previousAccountLabel: ACCOUNT_A, + nextAccountLabel: ACCOUNT_B, + previousAccountId: 'account-a', + nextAccountId: 'account-b', + target: { runtime: 'host' } + }) + + expect(window.api.codexAccounts.listStalePanes).toHaveBeenCalledWith({ + ptyIds: ['pty-current', 'pty-stale'] + }) + expect(useAppStore.getState().codexRestartNoticeByPtyId).toEqual({ + 'pty-stale': { + previousAccountLabel: ACCOUNT_A, + nextAccountLabel: ACCOUNT_B, + previousAccountId: 'account-a', + nextAccountId: 'account-b' + }, + 'pty-unknown': { + previousAccountLabel: ACCOUNT_A, + nextAccountLabel: ACCOUNT_B, + previousAccountId: 'account-a', + nextAccountId: 'account-b' + } }) }) diff --git a/src/renderer/src/lib/codex-session-restart.ts b/src/renderer/src/lib/codex-session-restart.ts index 01d6a5fab00..3eae9e311c3 100644 --- a/src/renderer/src/lib/codex-session-restart.ts +++ b/src/renderer/src/lib/codex-session-restart.ts @@ -214,38 +214,39 @@ export async function markLiveCodexSessionsForRestart(args: { return } - const currentState = useAppStore.getState() - const restoredRouteNoticePtyIds = liveCodexSessionPtyIds.filter( - (ptyId) => currentState.codexRestartNoticeByPtyId[ptyId]?.homeRouteChanged === true - ) - const restoredRouteNoticePtyIdSet = new Set(restoredRouteNoticePtyIds) + const recordedLiveScans = scans.filter((scan) => scan.eligible && scan.laneSource === 'recorded') + // Why: a reauth can report null -> A even though a pane's immutable launch + // route was already A; main's per-PTY record is the restart authority. const authoritativeStalePanes = - restoredRouteNoticePtyIds.length === 0 + recordedLiveScans.length === 0 ? null : await window.api.codexAccounts - .listStalePanes({ ptyIds: restoredRouteNoticePtyIds }) + .listStalePanes({ ptyIds: recordedLiveScans.map((scan) => scan.ptyId) }) .catch(() => null) const authoritativeStaleByPtyId = authoritativeStalePanes ? new Map(authoritativeStalePanes.map((pane) => [pane.ptyId, pane])) : null if (authoritativeStaleByPtyId) { - for (const ptyId of restoredRouteNoticePtyIds) { - if (!authoritativeStaleByPtyId.has(ptyId)) { - useAppStore.getState().clearCodexRestartNotice(ptyId) + for (const scan of recordedLiveScans) { + if (!authoritativeStaleByPtyId.has(scan.ptyId)) { + useAppStore.getState().clearCodexRestartNotice(scan.ptyId) } } } useAppStore.getState().markCodexRestartNotices( - liveCodexSessionPtyIds.flatMap((ptyId) => { - if (authoritativeStaleByPtyId && restoredRouteNoticePtyIdSet.has(ptyId)) { - const stalePane = authoritativeStaleByPtyId.get(ptyId) + scans.flatMap((scan) => { + if (!scan.eligible) { + return [] + } + if (authoritativeStaleByPtyId && scan.laneSource === 'recorded') { + const stalePane = authoritativeStaleByPtyId.get(scan.ptyId) if (!stalePane) { return [] } return [ { - ptyId, + ptyId: scan.ptyId, previousAccountLabel: args.previousAccountLabel, nextAccountLabel: args.nextAccountLabel, previousAccountId: stalePane.launchAccountId, @@ -256,7 +257,7 @@ export async function markLiveCodexSessionsForRestart(args: { } return [ { - ptyId, + ptyId: scan.ptyId, previousAccountLabel: args.previousAccountLabel, nextAccountLabel: args.nextAccountLabel, ...(args.previousAccountId === undefined