diff --git a/src/renderer/src/store/slices/agent-status-quit-capture.test.ts b/src/renderer/src/store/slices/agent-status-quit-capture.test.ts index c125bb369e7..061daea2f21 100644 --- a/src/renderer/src/store/slices/agent-status-quit-capture.test.ts +++ b/src/renderer/src/store/slices/agent-status-quit-capture.test.ts @@ -686,42 +686,48 @@ describe('captureAllSleepingAgentSessions', () => { expect(store.getState().sleepingAgentSessionsByPaneKey['tab-1:leaf-1']).toBe(firstRecord) }) - it('clears the live checkpoint when the agent finishes', () => { - const store = createTestStore() - store.setState({ - tabsByWorktree: { - 'wt-1': [makeTab({ id: 'tab-1', worktreeId: 'wt-1' })] + // Why: a finished resumable-agent turn leaves the TUI alive at its prompt, so the persisted + // recovery anchor must survive `done` (state remapped to 'working' to stay cold-restore + // eligible) — else logout→relaunch cold-restores to a bare shell instead of `--resume` (#9454). + // Covers claude (the reported agent) and codex; previously this was Pi-only. + it.each([ + ['claude', 'Claude'], + ['codex', 'Codex'] + ] as const)( + 'retains the recovery anchor when a finished %s session stays resumable (#9454)', + (agentType, title) => { + const store = createTestStore() + store.setState({ + tabsByWorktree: { 'wt-1': [makeTab({ id: 'tab-1', worktreeId: 'wt-1' })] } + } as Partial) + const providerSession = { key: 'session_id', id: `${agentType}-session-1` } as const + + for (const [state, updatedAt] of [ + ['working', 10], + ['done', 20] + ] as const) { + store + .getState() + .setAgentStatus( + 'tab-1:leaf-1', + { state, prompt: 'finish the task', agentType }, + title, + { updatedAt, stateStartedAt: 10 }, + { tabId: 'tab-1', worktreeId: 'wt-1' }, + { providerSession } + ) } - } as Partial) - store.getState().setAgentStatus( - 'tab-1:leaf-1', - { - state: 'working', - prompt: 'finish the task', - agentType: 'codex' - }, - 'Codex', - { updatedAt: 10, stateStartedAt: 10 }, - { tabId: 'tab-1', worktreeId: 'wt-1' }, - { providerSession: { key: 'session_id', id: 'codex-session-1' } } - ) - store.getState().setAgentStatus( - 'tab-1:leaf-1', - { - state: 'done', - prompt: 'finish the task', - agentType: 'codex' - }, - 'Codex', - { updatedAt: 20, stateStartedAt: 10 }, - { tabId: 'tab-1', worktreeId: 'wt-1' }, - { providerSession: { key: 'session_id', id: 'codex-session-1' } } - ) - - expect(store.getState().sleepingAgentSessionsByPaneKey['tab-1:leaf-1']).toBeUndefined() - expect(store.getState().agentLaunchConfigByPaneKey['tab-1:leaf-1']).toBeUndefined() - }) + expect(store.getState().sleepingAgentSessionsByPaneKey['tab-1:leaf-1']).toMatchObject({ + agent: agentType, + providerSession, + origin: 'live', + state: 'working' + }) + // Launch config is still cleared on done: tokens must no longer authorize config reuse. + expect(store.getState().agentLaunchConfigByPaneKey['tab-1:leaf-1']).toBeUndefined() + } + ) it('does not reuse launch config from a completed same-pane agent', () => { const store = createTestStore() diff --git a/src/renderer/src/store/slices/agent-status.ts b/src/renderer/src/store/slices/agent-status.ts index 49150a4a54f..44ff696a26a 100644 --- a/src/renderer/src/store/slices/agent-status.ts +++ b/src/renderer/src/store/slices/agent-status.ts @@ -1801,13 +1801,17 @@ export const createAgentStatusSlice: StateCreator entry.paneKey === paneKey ) const liveRecoveryWorktreeId = - entry.state === 'done' && !retainsPiRecoveryIdentity + entry.state === 'done' && !retainsResumableRecoveryIdentity ? null : (entry.worktreeId ?? findAgentPaneWorktreeId(s, entry.paneKey)) const liveRecoveryRecord = liveRecoveryWorktreeId ? sleepingRecordFromEntry({ state: s, - // Why: a completed Pi turn leaves the TUI session alive — keep resume identity active without representing done as pending work. - entry: retainsPiRecoveryIdentity + // Why: a completed resumable-agent turn leaves the TUI session alive — keep resume identity active without representing done as pending work. + entry: retainsResumableRecoveryIdentity ? { ...entry, state: 'working', prompt: '', lastAssistantMessage: undefined } : entry, worktreeId: liveRecoveryWorktreeId,