mirror of
https://github.com/stablyai/orca.git
synced 2026-10-27 00:02:48 +00:00
fix(terminal): demote un-parkable worktrees the force-park lever spared (C1 review #4)
The last-active exemption means a single hidden un-parkable worktree never force-parks — and slice C previously only targeted exempt-tab worktrees, so its panes held full scrollback forever. Demotion now also covers un-parkable non-exempt worktrees past the retention TTL that are absent from the force-parked set (last-active spared, or slice B switched off). Membership stays time-monotone for fixed inputs; covered by new idempotence/monotone selector tests. Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -233,6 +233,61 @@ describe('selectScrollbackDemotedTerminalWorktrees', () => {
|
||||
).toEqual(new Set(['wt-a', 'wt-b']))
|
||||
})
|
||||
|
||||
it('demotes un-parkable non-exempt worktrees the force-park lever spared, past the TTL', () => {
|
||||
// wt-solo is the only candidate → last-active exempt from force-park; its
|
||||
// panes stay fully mounted forever unless demotion bounds them.
|
||||
const solo = [exemptCandidate('wt-solo', aged, { hasEvictionExemptTab: false })]
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: solo })).toEqual(
|
||||
new Set(['wt-solo'])
|
||||
)
|
||||
// With slice B off nothing force-parks; every un-parkable worktree past
|
||||
// the TTL demotes (C bounds independently).
|
||||
const pair = [
|
||||
exemptCandidate('wt-a', aged - 1, { hasEvictionExemptTab: false }),
|
||||
exemptCandidate('wt-b', aged, { hasEvictionExemptTab: false })
|
||||
]
|
||||
expect(
|
||||
selectScrollbackDemotedTerminalWorktrees({
|
||||
...base,
|
||||
worktrees: pair,
|
||||
retentionBudgetEnabled: false
|
||||
})
|
||||
).toEqual(new Set(['wt-a', 'wt-b']))
|
||||
// With slice B on, wt-a force-parks (unmounted — no demotion needed) and
|
||||
// only the spared last-active wt-b demotes.
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: pair })).toEqual(
|
||||
new Set(['wt-b'])
|
||||
)
|
||||
// Ordinary-parking-covered worktrees are bounded by the warm cap already.
|
||||
const covered = [
|
||||
exemptCandidate('wt-covered', aged, {
|
||||
hasEvictionExemptTab: false,
|
||||
ordinaryParkingCovers: true
|
||||
})
|
||||
]
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees: covered })).toEqual(
|
||||
new Set()
|
||||
)
|
||||
})
|
||||
|
||||
it('is idempotent and only grows as time advances (flip-loop dwell)', () => {
|
||||
const worktrees = [
|
||||
exemptCandidate('wt-exempt', aged),
|
||||
exemptCandidate('wt-force-parked', aged, { hasEvictionExemptTab: false }),
|
||||
exemptCandidate('wt-last-active', nowMs - 60_000, { hasEvictionExemptTab: false })
|
||||
]
|
||||
const first = selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees })
|
||||
expect(selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees })).toEqual(first)
|
||||
// Why: with unchanged inputs, a later evaluation may only ADD members —
|
||||
// a verdict that oscillates with time is the React-#185 ingredient.
|
||||
for (const laterMs of [nowMs + 1_000, nowMs + TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS]) {
|
||||
const later = selectScrollbackDemotedTerminalWorktrees({ ...base, worktrees, nowMs: laterMs })
|
||||
for (const id of first) {
|
||||
expect(later.has(id)).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('honors the retentionTtlMs override and stays time-monotone', () => {
|
||||
const worktrees = [exemptCandidate('wt-1', nowMs - 500)]
|
||||
expect(
|
||||
|
||||
@@ -96,11 +96,14 @@ export function selectRetentionForceParkedTerminalWorktrees(
|
||||
|
||||
/**
|
||||
* Scrollback demotion targets the hidden panes that stay mounted past the
|
||||
* retention deadlines: eviction-exempt tabs' worktrees (their panes survive
|
||||
* force-park via the per-tab exclusion) demote past the TTL — or immediately
|
||||
* when their worktree force-parks under the count budget, since the exempt
|
||||
* panes are then the only ones left mounted. Time-monotone for fixed inputs:
|
||||
* the force-parked set and the past-TTL set only ever grow with nowMs, so
|
||||
* retention deadlines: (a) eviction-exempt tabs' worktrees (their panes
|
||||
* survive force-park via the per-tab exclusion) demote past the TTL — or
|
||||
* immediately when their worktree force-parks under the count budget, since
|
||||
* the exempt panes are then the only ones left mounted; (b) un-parkable
|
||||
* worktrees the force-park lever spared (the last-active exemption, or slice
|
||||
* B switched off) demote past the TTL instead of holding full scrollback
|
||||
* forever. Time-monotone for fixed inputs: the force-parked and past-TTL sets
|
||||
* only ever grow with nowMs and the last-active pick is time-independent, so
|
||||
* membership only grows until a reveal resets hiddenSince — no oscillation
|
||||
* surface.
|
||||
*/
|
||||
@@ -128,12 +131,20 @@ export function selectScrollbackDemotedTerminalWorktrees(
|
||||
worktree.isVisible ||
|
||||
worktree.shouldMeasureHiddenWorktree ||
|
||||
worktree.hasActivityTerminalPortal ||
|
||||
!worktree.hasEvictionExemptTab ||
|
||||
worktree.hasPendingSpawnWork
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (args.nowMs - worktree.hiddenSinceMs >= ttlMs || forceParked.has(worktree.worktreeId)) {
|
||||
const pastTtl = args.nowMs - worktree.hiddenSinceMs >= ttlMs
|
||||
if (worktree.hasEvictionExemptTab) {
|
||||
if (pastTtl || forceParked.has(worktree.worktreeId)) {
|
||||
demoted.add(worktree.worktreeId)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Force-park candidates the lever spared stay fully mounted, so demotion
|
||||
// is their only bound; force-parked ones already unmounted.
|
||||
if (!worktree.ordinaryParkingCovers && pastTtl && !forceParked.has(worktree.worktreeId)) {
|
||||
demoted.add(worktree.worktreeId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user