fix(terminal): bound hidden-worktree retention with a force-park budget (C1, slice B)

Un-parkable worktrees (remote-runtime ptys, uncoverable tabs, SSH with the
slice-A switch off) had unlimited retention: the parking cap/TTL only ever
saw eligibility-passing worktrees, so one bad tab pinned a whole worktree's
panes forever. Retention is now memory-bounded, not eligibility-bounded.

- terminal-hidden-worktree-retention.ts: retention budget (12 hidden / 45min
  TTL, sized from the measured 2.5-19MB per-pane V8 cost, DESIGN.md §2) over
  hidden worktrees ordinary parking can never evict; reuses the hot-retain
  ranking so last-active exemption, deterministic ties, and deadline-driven
  rechecks hold. Fail-open/foreign-pty tabs are eviction-exempt (a remount
  would fresh-spawn and orphan the live shell).
- Terminal.tsx: force-parked ids join the parked set AFTER the coverage veto
  (darkness for uncoverable tabs is the accepted cost); buffers captured via
  the sleep-flow registry before the unmount render; retention TTL added to
  the recheck deadlines for budget candidates only.
- Verdict stays out of its own effect deps; policy test asserts idempotence
  and time-monotone membership (flip-loop dwell regression).
- Kill switch: settings.terminalHiddenWorktreeRetentionBudget (default on).

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 6362a5c442
commit e7703a772b
8 changed files with 366 additions and 8 deletions
+93 -1
View File
@@ -79,10 +79,18 @@ import { buildDuplicatedBrowserTabOptions } from '@/lib/duplicate-browser-tab-op
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
import { setForegroundTerminalTabIds } from '@/lib/foreground-terminal-tabs'
import {
canParkTerminalWorktreeRenderers,
getTerminalWorktreeColdParkRecheckDelayMs,
selectColdParkedTerminalWorktrees,
type TerminalWorktreeColdParkCandidate
} from './terminal-pane/terminal-hidden-view-parking'
import {
TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS,
isEvictionExemptTerminalTab,
selectRetentionForceParkedTerminalWorktrees,
type TerminalWorktreeRetentionCandidate
} from './terminal-pane/terminal-hidden-worktree-retention'
import { shutdownBufferCaptures } from './terminal-pane/shutdown-buffer-captures'
import { getTerminalParkingPolicyOverrides } from './terminal-pane/terminal-parking-e2e-overrides'
import { useColdParkedTerminalPresentation } from './terminal-pane/use-cold-parked-terminal-presentation'
import {
@@ -273,6 +281,9 @@ function Terminal(): React.JSX.Element | null {
const pendingStartupByTabId = useAppStore((s) => s.pendingStartupByTabId)
const terminalParkingEnabled = useAppStore((s) => s.settings?.terminalHiddenViewParking !== false)
const terminalSshParkingEnabled = useAppStore((s) => s.settings?.terminalSshViewParking !== false)
const terminalRetentionBudgetEnabled = useAppStore(
(s) => s.settings?.terminalHiddenWorktreeRetentionBudget !== false
)
const terminalTitleSnapshotAuthorityEnabled = useAppStore((s) =>
isMainTerminalSideEffectAuthorityForPty({
settings: s.settings,
@@ -752,6 +763,8 @@ function Terminal(): React.JSX.Element | null {
combineTerminalWorktreeParkIds(parkedTerminalWorktreeIds, manuallyParkedTerminalWorktreeIds),
[manuallyParkedTerminalWorktreeIds, parkedTerminalWorktreeIds]
)
// Why a ref: eviction captures buffers exactly once per force-park episode, before the unmount render.
const forceParkedCaptureDoneRef = useRef(new Set<string>())
// Tab restriction for targeted background mounts (wake/resume); a worktree absent from this map mounts all its tabs.
const backgroundMountTabIdsByWorktreeRef = useRef(new Map<string, ReadonlySet<string>>())
// Why: only cold-activation deferral (not targeted mounts, which share the map above) creates watcher coverage for every unmounted tab.
@@ -894,11 +907,83 @@ function Terminal(): React.JSX.Element | null {
nextParkedTerminalWorktreeIds.delete(worktreeId)
}
}
// C1 retention budget: worktrees ordinary parking can never evict (SSH off,
// remote-runtime, uncoverable tabs) force-park beyond a count/TTL bound —
// added AFTER the coverage veto because darkness for their uncoverable tabs
// is the accepted cost of bounding retention.
const restorePolicy = { sshParkingEnabled: terminalSshParkingEnabled }
const retentionBudgetCandidates: TerminalWorktreeRetentionCandidate[] = retentionCandidates.map(
(candidate) => {
const tabs = tabsByWorktree[candidate.worktreeId] ?? []
const parkEligible = canParkTerminalWorktreeRenderers({
...candidate,
pendingStartupByTabId,
parkingEnabled: terminalParkingEnabled,
nowMs,
restorePolicy,
...(overrides.coldParkDelayMs !== undefined
? { coldParkDelayMs: overrides.coldParkDelayMs }
: {})
})
return {
worktreeId: candidate.worktreeId,
hiddenSinceMs: candidate.hiddenSinceMs,
isVisible: candidate.isVisible,
shouldMeasureHiddenWorktree: candidate.shouldMeasureHiddenWorktree,
hasActivityTerminalPortal: candidate.hasActivityTerminalPortal,
ordinaryParkingCovers:
parkEligible &&
tabs.every((tab) => canWatcherCoverParkedTerminalTab(candidate.worktreeId, tab)),
hasEvictionExemptTab: tabs.some((tab) =>
isEvictionExemptTerminalTab(tab, candidate.worktreeId)
),
hasPendingSpawnWork: tabs.some(
(tab) =>
pendingStartupByTabId[tab.id] !== undefined ||
tab.pendingActivationSpawn === true ||
(typeof tab.pendingActivationSpawn === 'number' && tab.pendingActivationSpawn > 0)
)
}
}
)
const forceParkedWorktreeIds = selectRetentionForceParkedTerminalWorktrees({
worktrees: retentionBudgetCandidates,
parkingEnabled: terminalParkingEnabled,
retentionBudgetEnabled: terminalRetentionBudgetEnabled,
nowMs,
...overrides
})
const capturedForceParked = forceParkedCaptureDoneRef.current
for (const id of Array.from(capturedForceParked)) {
if (!forceParkedWorktreeIds.has(id)) {
capturedForceParked.delete(id)
}
}
for (const worktreeId of forceParkedWorktreeIds) {
if (!capturedForceParked.has(worktreeId)) {
capturedForceParked.add(worktreeId)
// Why: serialize buffers while the panes still exist (same registry sleep uses), so classes with no snapshot source still reveal last-known content.
for (const tab of tabsByWorktree[worktreeId] ?? []) {
shutdownBufferCaptures.get(tab.id)?.()
}
}
nextParkedTerminalWorktreeIds.add(worktreeId)
}
setParkedTerminalWorktreeIds((current) =>
haveSameWorktreeIds(current, nextParkedTerminalWorktreeIds)
? current
: nextParkedTerminalWorktreeIds
)
const retentionTtlEligibleIds = new Set(
retentionBudgetCandidates
.filter(
(candidate) =>
!candidate.ordinaryParkingCovers &&
!candidate.hasEvictionExemptTab &&
!candidate.hasPendingSpawnWork
)
.map((candidate) => candidate.worktreeId)
)
for (const candidate of retentionCandidates) {
if (
@@ -913,7 +998,13 @@ function Terminal(): React.JSX.Element | null {
parkingEnabled: terminalParkingEnabled,
hiddenSinceMs: candidate.hiddenSinceMs,
nowMs,
...overrides
...overrides,
// Why: only retention candidates wake at the eviction TTL; everyone else keeps the ordinary deadlines.
...(terminalRetentionBudgetEnabled && retentionTtlEligibleIds.has(candidate.worktreeId)
? {
retentionTtlMs: overrides.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS
}
: {})
})
if (delayMs !== null && delayMs > 0) {
const worktreeId = candidate.worktreeId
@@ -933,6 +1024,7 @@ function Terminal(): React.JSX.Element | null {
tabsByWorktree,
terminalParkingEnabled,
terminalParkingRevision,
terminalRetentionBudgetEnabled,
terminalSshParkingEnabled,
workspaceSurfaces
])
@@ -592,6 +592,28 @@ describe('getTerminalWorktreeColdParkRecheckDelayMs', () => {
})
).toBeNull()
})
it('wakes at the retention TTL for budget candidates past the ordinary deadlines', () => {
expect(
getTerminalWorktreeColdParkRecheckDelayMs({
parkingEnabled: true,
hiddenSinceMs: 1_000,
nowMs: 2_500,
coldParkDelayMs: 100,
hotRetainMs: 1_000,
retentionTtlMs: 5_000
})
).toBe(3_500)
expect(
getTerminalWorktreeColdParkRecheckDelayMs({
parkingEnabled: true,
hiddenSinceMs: 1_000,
nowMs: 2_500,
coldParkDelayMs: 100,
hotRetainMs: 1_000
})
).toBeNull()
})
})
describe('getTerminalTabColdParkRecheckDelayMs', () => {
@@ -24,6 +24,8 @@ export type TerminalColdParkPolicyOverrides = {
coldParkDelayMs?: number
hotRetainMs?: number
hotRetainLimit?: number
retentionTtlMs?: number
retentionLimit?: number
}
export type ColdParkableTerminalTab = Pick<TerminalTab, 'id' | 'ptyId' | 'pendingActivationSpawn'>
@@ -158,7 +160,7 @@ export function canParkTerminalTabRenderer(args: {
return isParkRestorableTerminalPty(tab.ptyId, args.worktreeId, args.restorePolicy)
}
type ColdParkRetainCandidate = { id: string; hiddenSinceMs: number }
export type ColdParkRetainCandidate = { id: string; hiddenSinceMs: number }
// Why: the single most-recently-hidden candidate is the view the user just
// switched away from; keeping it warm regardless of the TTL or cap means
@@ -183,7 +185,7 @@ function selectLastActiveRetainedId(candidates: ColdParkRetainCandidate[]): stri
// ids hidden past hotRetainMs or beyond the limit cold-park. The last-active
// id is exempt from both so returning to it never pays a remount. Ties sort by
// id so the selection is deterministic.
function selectIdsBeyondHotRetain(
export function selectIdsBeyondHotRetain(
candidates: ColdParkRetainCandidate[],
args: { nowMs: number; hotRetainMs: number; hotRetainLimit: number }
): Set<string> {
@@ -298,13 +300,15 @@ function nextColdParkDeadlineDelayMs(args: {
nowMs: number
coldParkDelayMs: number
hotRetainMs: number
retentionTtlMs?: number
}): number | null {
if (!args.parkingEnabled || args.hiddenSinceMs === null) {
return null
}
const pendingDeadlines = [
args.hiddenSinceMs + args.coldParkDelayMs,
args.hiddenSinceMs + args.hotRetainMs
args.hiddenSinceMs + args.hotRetainMs,
...(args.retentionTtlMs !== undefined ? [args.hiddenSinceMs + args.retentionTtlMs] : [])
].filter((deadlineMs) => deadlineMs > args.nowMs)
return pendingDeadlines.length === 0 ? null : Math.min(...pendingDeadlines) - args.nowMs
}
@@ -315,13 +319,16 @@ export function getTerminalWorktreeColdParkRecheckDelayMs(args: {
nowMs: number
coldParkDelayMs?: number
hotRetainMs?: number
/** Provided only for retention-budget candidates so their TTL wakes the verdict effect. */
retentionTtlMs?: number
}): number | null {
return nextColdParkDeadlineDelayMs({
parkingEnabled: args.parkingEnabled,
hiddenSinceMs: args.hiddenSinceMs,
nowMs: args.nowMs,
coldParkDelayMs: args.coldParkDelayMs ?? TERMINAL_WORKTREE_COLD_PARK_DELAY_MS,
hotRetainMs: args.hotRetainMs ?? TERMINAL_WORKTREE_HOT_RETAIN_MS
hotRetainMs: args.hotRetainMs ?? TERMINAL_WORKTREE_HOT_RETAIN_MS,
...(args.retentionTtlMs !== undefined ? { retentionTtlMs: args.retentionTtlMs } : {})
})
}
@@ -0,0 +1,141 @@
import { describe, expect, it } from 'vitest'
import { TERMINAL_WORKTREE_PARK_DELAY_MS } from './terminal-hidden-view-parking'
import {
TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS,
isEvictionExemptTerminalTab,
selectRetentionForceParkedTerminalWorktrees,
type TerminalWorktreeRetentionCandidate
} from './terminal-hidden-worktree-retention'
describe('isEvictionExemptTerminalTab', () => {
const worktreeId = 'repo::/worktree'
it('exempts only live local ptys a remount could not reattach', () => {
expect(isEvictionExemptTerminalTab({ ptyId: 'pty-local-detached' }, worktreeId)).toBe(true)
expect(isEvictionExemptTerminalTab({ ptyId: 'other::wt@@session-1' }, worktreeId)).toBe(true)
})
it('never exempts snapshot-backed, SSH, remote-runtime, or unbound tabs', () => {
expect(isEvictionExemptTerminalTab({ ptyId: `${worktreeId}@@session-1` }, worktreeId)).toBe(
false
)
expect(isEvictionExemptTerminalTab({ ptyId: 'ssh:conn-1@@pty-1' }, worktreeId)).toBe(false)
expect(isEvictionExemptTerminalTab({ ptyId: 'remote:env-1@@t-1' }, worktreeId)).toBe(false)
expect(isEvictionExemptTerminalTab({ ptyId: null }, worktreeId)).toBe(false)
})
})
describe('selectRetentionForceParkedTerminalWorktrees', () => {
const nowMs = 5_000_000
function retentionCandidate(
worktreeId: string,
hiddenSinceMs: number | null,
partial: Partial<TerminalWorktreeRetentionCandidate> = {}
): TerminalWorktreeRetentionCandidate {
return {
worktreeId,
hiddenSinceMs,
isVisible: false,
shouldMeasureHiddenWorktree: false,
hasActivityTerminalPortal: false,
ordinaryParkingCovers: false,
hasEvictionExemptTab: false,
hasPendingSpawnWork: false,
...partial
}
}
const base = {
parkingEnabled: true,
retentionBudgetEnabled: true,
nowMs
}
it('returns empty when either kill switch is off', () => {
const worktrees = [
retentionCandidate('wt-1', nowMs - TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS)
]
expect(
selectRetentionForceParkedTerminalWorktrees({ ...base, worktrees, parkingEnabled: false })
).toEqual(new Set())
expect(
selectRetentionForceParkedTerminalWorktrees({
...base,
worktrees,
retentionBudgetEnabled: false
})
).toEqual(new Set())
})
it('force-parks the least-recently-hidden candidates beyond the retention limit', () => {
const worktrees = [
retentionCandidate('wt-1', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 3),
retentionCandidate('wt-2', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 2),
retentionCandidate('wt-3', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 1),
retentionCandidate('wt-4', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS)
]
// Why limit 2: wt-4 is last-active exempt, wt-3 fills the remaining slot; the two oldest evict.
expect(
selectRetentionForceParkedTerminalWorktrees({ ...base, worktrees, retentionLimit: 2 })
).toEqual(new Set(['wt-1', 'wt-2']))
})
it('force-parks past the TTL even under the limit, but never the last-active candidate', () => {
const worktrees = [
retentionCandidate('wt-old', nowMs - TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS),
retentionCandidate('wt-recent', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS)
]
expect(selectRetentionForceParkedTerminalWorktrees({ ...base, worktrees })).toEqual(
new Set(['wt-old'])
)
})
it('never force-parks visible, measuring, portaled, covered, exempt, pending, or fresh candidates', () => {
const aged = nowMs - TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS
const worktrees = [
retentionCandidate('wt-visible', aged, { isVisible: true }),
retentionCandidate('wt-measure', aged, { shouldMeasureHiddenWorktree: true }),
retentionCandidate('wt-portal', aged, { hasActivityTerminalPortal: true }),
retentionCandidate('wt-covered', aged, { ordinaryParkingCovers: true }),
retentionCandidate('wt-exempt', aged, { hasEvictionExemptTab: true }),
retentionCandidate('wt-pending', aged, { hasPendingSpawnWork: true }),
retentionCandidate('wt-fresh', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS + 1),
retentionCandidate('wt-unhidden', null)
]
expect(selectRetentionForceParkedTerminalWorktrees({ ...base, worktrees })).toEqual(new Set())
})
it('is idempotent and only grows as time advances (flip-loop dwell)', () => {
const worktrees = [
retentionCandidate('wt-1', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 3),
retentionCandidate('wt-2', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 2),
retentionCandidate('wt-3', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS - 1),
retentionCandidate('wt-4', nowMs - TERMINAL_WORKTREE_PARK_DELAY_MS)
]
const first = selectRetentionForceParkedTerminalWorktrees({
...base,
worktrees,
retentionLimit: 2
})
const second = selectRetentionForceParkedTerminalWorktrees({
...base,
worktrees,
retentionLimit: 2
})
expect(second).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 = selectRetentionForceParkedTerminalWorktrees({
...base,
worktrees,
retentionLimit: 2,
nowMs: laterMs
})
for (const id of first) {
expect(later.has(id)).toBe(true)
}
}
})
})
@@ -0,0 +1,92 @@
import { isRemoteRuntimePtyId } from '@/runtime/runtime-terminal-inspection'
import { parseAppSshPtyId } from '../../../../shared/ssh-pty-id'
import type { TerminalTab } from '../../../../shared/types'
import {
TERMINAL_WORKTREE_COLD_PARK_DELAY_MS,
isSnapshotBackedTerminalPty,
selectIdsBeyondHotRetain,
type ColdParkRetainCandidate,
type TerminalColdParkPolicyOverrides
} from './terminal-hidden-view-parking'
// Why these sizes (C1, DESIGN.md §2): a retained hidden pane costs a measured
// ~2.5MB of V8 heap at the 5k-row default scrollback and ~19MB at 50k (plus
// per-pane queues), not the ~4-5MB per WORKTREE the warm cap assumed — so
// un-parkable worktrees (pty classes parking can't restore) get a hard
// retention budget: at most 12 stay mounted while hidden, and none past 45
// minutes, evicted least-recently-hidden first via force-park.
export const TERMINAL_HIDDEN_WORKTREE_RETENTION_LIMIT = 12
export const TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS = 45 * 60_000
// Why: an eviction-exempt tab holds a live local pty a remount could not
// reattach (daemon-fail-open separator-less ids, ptys minted under another
// worktree) — a fresh spawn would orphan the live shell, so these worktrees
// never force-park and rely on scrollback demotion instead.
export function isEvictionExemptTerminalTab(
tab: Pick<TerminalTab, 'ptyId'>,
worktreeId: string
): boolean {
const ptyId = tab.ptyId
if (!ptyId || isRemoteRuntimePtyId(ptyId) || parseAppSshPtyId(ptyId)) {
return false
}
return !isSnapshotBackedTerminalPty(ptyId, worktreeId)
}
export type TerminalWorktreeRetentionCandidate = {
worktreeId: string
hiddenSinceMs: number | null
isVisible: boolean
shouldMeasureHiddenWorktree: boolean
hasActivityTerminalPortal: boolean
/** Ordinary cold parking can evict this worktree (park-eligible AND watcher-coverable) — the warm cap bounds it already. */
ordinaryParkingCovers: boolean
/** See isEvictionExemptTerminalTab. */
hasEvictionExemptTab: boolean
/** Pending startup or activation spawn — a mount is imminent; never evict. */
hasPendingSpawnWork: boolean
}
/**
* Retention budget over the worktrees ordinary parking can never evict: any
* hidden un-parkable worktree beyond the retention limit or TTL force-parks —
* panes unmount, watchers cover the tabs whose transport exists, and reveal
* restores per pty class (the app-restart experience). Ranking reuses the
* hot-retain machinery, so the last-active exemption and deterministic ties
* hold here too, and the verdict changes only at deadlines or on real state
* transitions (no new flip-loop inputs).
*/
export function selectRetentionForceParkedTerminalWorktrees(
args: {
worktrees: readonly TerminalWorktreeRetentionCandidate[]
parkingEnabled: boolean
retentionBudgetEnabled: boolean
nowMs: number
} & TerminalColdParkPolicyOverrides
): Set<string> {
if (!args.parkingEnabled || !args.retentionBudgetEnabled) {
return new Set()
}
const coldParkDelayMs = args.coldParkDelayMs ?? TERMINAL_WORKTREE_COLD_PARK_DELAY_MS
const candidates: ColdParkRetainCandidate[] = []
for (const worktree of args.worktrees) {
if (
worktree.hiddenSinceMs === null ||
worktree.isVisible ||
worktree.shouldMeasureHiddenWorktree ||
worktree.hasActivityTerminalPortal ||
worktree.ordinaryParkingCovers ||
worktree.hasEvictionExemptTab ||
worktree.hasPendingSpawnWork ||
args.nowMs - worktree.hiddenSinceMs < coldParkDelayMs
) {
continue
}
candidates.push({ id: worktree.worktreeId, hiddenSinceMs: worktree.hiddenSinceMs })
}
return selectIdsBeyondHotRetain(candidates, {
nowMs: args.nowMs,
hotRetainMs: args.retentionTtlMs ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_TTL_MS,
hotRetainLimit: args.retentionLimit ?? TERMINAL_HIDDEN_WORKTREE_RETENTION_LIMIT
})
}
@@ -46,12 +46,13 @@ describe('getTerminalParkingPolicyOverrides', () => {
expect(getTerminalParkingPolicyOverrides()).toEqual({})
})
it('maps the e2e delay to BOTH coldParkDelayMs and hotRetainMs', async () => {
it('maps the e2e delay to coldParkDelayMs, hotRetainMs, and retentionTtlMs', async () => {
mockE2EConfig = { exposeStore: true, terminalParkingDelayMs: 500 }
const { getTerminalParkingPolicyOverrides } = await importOverridesModule()
expect(getTerminalParkingPolicyOverrides()).toEqual({
coldParkDelayMs: 500,
hotRetainMs: 500
hotRetainMs: 500,
retentionTtlMs: 500
})
})
@@ -12,7 +12,8 @@ import { getParkedTerminalWatcherTabIds } from './terminal-parked-tab-watchers'
export function getTerminalParkingPolicyOverrides(): TerminalColdParkPolicyOverrides {
const delayMs = e2eConfig.exposeStore ? e2eConfig.terminalParkingDelayMs : null
return typeof delayMs === 'number' && Number.isFinite(delayMs) && delayMs > 0
? { coldParkDelayMs: delayMs, hotRetainMs: delayMs }
? // Why retentionTtlMs too: the retention-budget e2e needs its 45min TTL shrunk the same way.
{ coldParkDelayMs: delayMs, hotRetainMs: delayMs, retentionTtlMs: delayMs }
: {}
}
+2
View File
@@ -2850,6 +2850,8 @@ export type GlobalSettings = {
terminalHiddenViewParking?: boolean
/** Kill switch for SSH terminal parking (C1): SSH panes park like local ones; reveal restores from main's headless model, falling back to relay replay. */
terminalSshViewParking?: boolean
/** Kill switch for the hidden-worktree retention budget (C1): force-parks the least-recently-hidden un-parkable worktrees beyond a count budget or TTL. */
terminalHiddenWorktreeRetentionBudget?: boolean
/** Kill switch for main-process PTY side-effect authority; on (default) = title/bell/agent facts via pty:sideEffect channel, not renderer byte parsing. */
terminalMainSideEffectAuthority?: boolean
/** Kill switch for main's hidden-delivery gate (Phase 4): drops PTY bytes to hidden views after model ingestion; requires terminalMainSideEffectAuthority. */