[Bug]: terminal_park_verdict_churn reproduces in the field (12 flips/30s for 13.5 min), followed by a renderer reload loop
- Dominant language
- TypeScript
- Stars
- 71.3k
- Forks
- 4.7k
- Avg merge
- 17h 8m
- Merged PRs (30d)
- 475
Description
### Operating system
macOS (darwin 25.2.0)
### Orca version
1.4.167 (`appVersion` from `orca status --json`)
### Details
**Short summary:** `terminal_park_verdict_churn` — the park-flip loop that
`terminal-park-verdict-flip-telemetry.ts` says has never been reproduced — fires in the
field for 13.5 minutes straight on an SSH-backed tab, and is followed by the renderer
reloading 3× in 35 seconds.
#### Why this is worth a look
`src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.ts` ships
observation-only and states its premise explicitly:
> no park-flip loop has been reproduced, and the capture-coverage oscillator that looked
> like the cause provably self-terminates […] **Deliberately no damping**: guarding a loop
> nobody has shown can run would add a park delay and a retention path for no demonstrated
> benefit.
This report is that reproduction, so the condition the comment sets for adding damping is
now met.
#### What happened
From `main.trace.ndjson` (`renderer.breadcrumb` spans), single session:
| breadcrumb | count | window |
|---|---|---|
| `terminal_park_verdict_churn` | 14 | 08:02:55 → 08:16:26 (13.5 min) |
| `terminal_replay_guard_wedged_release` | 67 | ends 08:16:31 |
| `terminal_safe_fit_retry_exhausted` | 59 | ends 08:26:25 |
| `renderer_bootstrap_started` | 3 clustered | 08:17:46, 08:18:10, 08:18:21 |
Churn payloads, near-identical across all 14 events:
```json
{"tabId":"27354892-9cdd-491a-ba0b-6e3858e7dde8","flips":12,"elapsedMs":30232,"windowMs":60000}
{"tabId":"27354892-9cdd-491a-ba0b-6e3858e7dde8","flips":12,"elapsedMs":29143,"windowMs":60000}
{"tabId":"27354892-9cdd-491a-ba0b-6e3858e7dde8","flips":12,"elapsedMs":29530,"windowMs":60000}
```
`elapsedMs` clusters at 29.1–30.5 s in 12 of 14 events (outliers: 51569, 44933). 13 of 14
events name the same tab; 1 names a second tab.
- 12 flips / ~30 s ⇒ **one park-verdict flip every ~2.5 s, sustained for 13.5 minutes**.
- The affected tab is SSH-backed (`ptyId = ssh:@@pty-4`), with
`terminalSshViewParking` enabled, so `isParkRestorableTerminalPty` returns true for it.
- The three `renderer_bootstrap_started` events are 35 s apart and start 80 s after the
churn window ends — the renderer reloaded 3×. Every other bootstrap in the log is
one-per-app-launch. This is what the user perceives as the app "constantly reloading".
- Renderer heap across the churn window: `usedHeapMB` 60 → 97, with `blinkAllocatedMB`
spiking to 179 right after the reloads.
#### The period rules out the previously-fixed shape
A ~2.5 s period is timer-driven, not a same-tick render cascade, so this is not the React
#185 shape that `17617b1a fix(terminal-pane): resolve React #185 setState loop via state
identity (#10632)` (2026-07-25) addressed. 1.4.167 sits between `v1.4.165-rc.0` and
`v1.4.168-rc.1`, both of which contain `17617b1a`, so the churn is present after that fix
with a different driver.
#### Root-cause leads (not confirmed — no dev-build reproduction attempted)
1. **The measure-lease gate.** `Terminal.tsx:2446` and `:2496` compute
`shouldMeasureHiddenWorktree = !isVisible && measurableBackgroundWorktreeIdsRef.current.has(id)`
during render, from a **ref**. `useTerminalTabColdParking`'s `parkedTerminalTabIds` memo
uses `shouldMeasureHiddenWorktree` as a hard gate, so a flipping measure lease flips the
*rendered* verdict directly. The surrounding comments describe this window as "~3s
(automation lease, mobile mount, agent wake)", which matches the observed ~2.5 s. The
existing `measureParkCooldownUntilRef` / `terminalWorktreeParkCooldownUntilRef`
cool-downs gate the *cold-park selection*, not this render-time gate.
2. **Watcher-coverage flapping.** `canWatcherCoverParkedTerminalTab` removes tabs from the
cold set. The telemetry docstring dismisses this as self-terminating ("the unmount
capture is never cleared on remount, so coverage pins rather than alternates") — worth
re-checking specifically for the SSH pty path, since the affected tab is SSH-backed.
#### Suggested mitigation, independent of root cause
Promote the existing telemetry into a damping circuit breaker in
`terminal-park-verdict-flip-telemetry.ts` + `use-terminal-tab-cold-parking.ts`:
- when a tab's rendered park verdict reaches `TERMINAL_TAB_PARK_FLIP_NOTICE_LIMIT` flips
inside `TERMINAL_TAB_PARK_FLIP_WINDOW_MS`, **pin it unparked** (pane stays mounted) until
the window elapses with no further flips, then release;
- pin *unparked* rather than parked, so the pty stays attached and pane-owned
bells/titles/completions keep working — pinning parked risks the going-silent failure the
docstring says sank the first parking attempt;
- keep the breadcrumb and add the breaker state to its payload, so the field signal still
separates "churned" from "churned and damped";
- apply it where `parkedTerminalTabIds` is produced via a ref + revision counter, so
engaging the breaker cannot itself schedule a setState in the same effect and become a new
flip source.
Stated honestly: a tab caught by the breaker keeps its renderer resident, which is exactly
the retention the current comment declines to pay. The bound is one window per tab, and it
only engages after 12 flips — a rate no legitimate park policy should produce.
Worth covering with tests: the breaker engages exactly at the notice limit and not before, a
tab under the breaker is excluded from the parked set, it releases after a quiet window, and
an ordinary single park/unpark never engages it.
#### Environment
- 6 SSH worktrees on one `ssh-config` target, 8 remote PTY leases
- `terminalHiddenViewParking` default (on), `terminalSshViewParking: true`
- Remote relay build `0.1.0+8de1d39fd7c1`
I am not sure whether the right fix is the damping guard above or one of the two root-cause
leads, so I have filed this as a report rather than a PR. Happy to attempt either if a
maintainer indicates a preference.
Contributor guide
Research direction
Start in src/renderer/src/components/terminal-pane/terminal-park-verdict-flip-telemetry.ts and use-terminal-tab-cold-parking.ts, then inspect the Terminal.tsx gates at lines 2446 and 2496. Reproduce with an SSH-backed tab if possible and compare the two root-cause leads; completion should cover the notice-limit boundary, unparked exclusion, quiet-window release, and ordinary park/unpark behavior while preserving churn telemetry.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- desktop, frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100