diff --git a/src/renderer/src/components/Terminal.tsx b/src/renderer/src/components/Terminal.tsx index caebd0f7d4a..e06119c34fc 100644 --- a/src/renderer/src/components/Terminal.tsx +++ b/src/renderer/src/components/Terminal.tsx @@ -993,13 +993,7 @@ function Terminal(): React.JSX.Element | null { ) const retentionTtlEligibleIds = new Set( retentionBudgetCandidates - .filter( - (candidate) => - !candidate.ordinaryParkingCovers && - !candidate.hasPendingSpawnWork && - // Why: exempt worktrees still need the TTL wakeup — it fires their scrollback demotion. - (!candidate.hasEvictionExemptTab || terminalScrollbackDemotionEnabled) - ) + .filter((candidate) => !candidate.ordinaryParkingCovers && !candidate.hasPendingSpawnWork) .map((candidate) => candidate.worktreeId) ) @@ -1017,8 +1011,10 @@ function Terminal(): React.JSX.Element | null { hiddenSinceMs: candidate.hiddenSinceMs, nowMs, ...overrides, - // Why: only retention candidates wake at the eviction TTL; everyone else keeps the ordinary deadlines. - ...(terminalRetentionBudgetEnabled && retentionTtlEligibleIds.has(candidate.worktreeId) + // Why: only retention candidates wake at the eviction TTL; everyone else keeps the ordinary + // deadlines. Demotion (slice C) needs the wakeup too — it no longer requires slice B's switch. + ...((terminalRetentionBudgetEnabled || terminalScrollbackDemotionEnabled) && + retentionTtlEligibleIds.has(candidate.worktreeId) ? { retentionTtlMs: overrides.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS } diff --git a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts index ed6df790c44..18b3e9874dc 100644 --- a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.test.ts @@ -185,21 +185,25 @@ describe('selectScrollbackDemotedTerminalWorktrees', () => { expect(selected).toEqual(new Set(['wt-aged'])) }) - it('returns empty when any kill switch is off', () => { + it('returns empty when the master or demotion switch is off', () => { const worktrees = [exemptCandidate('wt-aged', aged)] expect( selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, parkingEnabled: false }) ).toEqual(new Set()) + expect( + selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, demotionEnabled: false }) + ).toEqual(new Set()) + }) + + it('demotes independently of the slice-B retention-budget switch', () => { + const worktrees = [exemptCandidate('wt-aged', aged)] expect( selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, retentionBudgetEnabled: false }) - ).toEqual(new Set()) - expect( - selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, demotionEnabled: false }) - ).toEqual(new Set()) + ).toEqual(new Set(['wt-aged'])) }) it('honors the retentionTtlMs override and stays time-monotone', () => { diff --git a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts index 68e8ac2bcc9..6a2ba57c480 100644 --- a/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts +++ b/src/renderer/src/components/terminal-pane/terminal-hidden-worktree-retention.ts @@ -106,7 +106,10 @@ export function selectScrollbackDemotedTerminalWorktrees( nowMs: number } & TerminalColdParkPolicyOverrides ): Set { - if (!args.parkingEnabled || !args.retentionBudgetEnabled || !args.demotionEnabled) { + // Why parkingEnabled still gates: it is the master kill switch — parking + // globally off means nothing parks OR demotes. Slice B's budget switch does + // not gate slice C; each slice reverts independently (approved contract). + if (!args.parkingEnabled || !args.demotionEnabled) { return new Set() } const ttlMs = args.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS