fix(terminal): decouple scrollback demotion (slice C) from the retention-budget switch (C1 review #2)

Per the approved contract each slice reverts behind its own switch: slice C
now requires only the master terminalHiddenViewParking plus its own
terminalHiddenScrollbackDemotion flag. The TTL wakeup timer fires for
demotion candidates even with the budget switch off. No DEFAULT_SETTINGS
entries exist for sibling flags (defaults are the '!== false' optional
pattern), so no explicit defaults are added.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing
2026-07-28 13:21:53 -07:00
co-authored by Orca
parent 844fb035b5
commit 6bb3a7ea90
3 changed files with 18 additions and 15 deletions
+5 -9
View File
@@ -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
}
@@ -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', () => {
@@ -106,7 +106,10 @@ export function selectScrollbackDemotedTerminalWorktrees(
nowMs: number
} & TerminalColdParkPolicyOverrides
): Set<string> {
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