`markRendererCrashesAutoRecovered` rules on each pending renderer-process
report against `notBeforeMs`, then sweeps burst siblings. The sweep did not
honour the first pass's decision: `isRelatedCrashEvent` ignores processType
and reaches 5s either side of any anchor, and the guard only excluded
react-render. A renderer crash the cutoff had just excluded was therefore
dismissed anyway as a "burst sibling" of a crash from the same process,
pushing effective reach to cutoff + 5s and dropping the
`rendererAutoRecovered` flag that records why it was resolved.
Collapse the guard to `status !== 'pending' || source === 'renderer'`. Every
renderer-sourced report is either an anchor the first pass already ruled on,
or a react-render boundary report the reload did not cause; neither is ever a
burst sibling, which is what the sweep exists to catch. This subsumes the
previous react-render exclusion rather than replacing it — deleting the whole
guard reds three tests, the two pre-existing boundary ones plus the new
cutoff one, so both protections still ride on it.
Scope, so a reader does not overrate this: the newest anchor normally sits
250ms before the arm (the recovery timer delay), so the naturally exploitable
band is about 250ms wide. It only widens toward the full 5s when the arm
lands late relative to its crash, or through a chain of anchors. The fix is
worth making on structural grounds regardless — the sweep should not be able
to override the cutoff the anchor pass just applied.
Adversarial round 6 found round 5 had covered two of four manual
main-window reload paths, and that the outcome check's lookback was
wider than its own arming delay.
- `app:reload` (the renderer's own retry button) never disarmed. The
round-5 wiring test could not see it: it anchors on `onBeforeReload`
and this site is spelled `onBeforeRendererReload`, so even its
"exactly two occurrences" assertion still passed.
- A replacement main window consumed a live arm. The arm is module-
global rather than keyed to a webContents, so a fresh window opened
after the old one died (dock re-activation, tray reopen, second
instance) bootstrapped and claimed the dead window's arm, dismissing
every renderer crash in the lookback as auto-recovered.
- The lookback reused RENDERER_RECOVERY_OUTCOME_WINDOW_MS for two
different jobs. That constant bounds how long an arm stays valid; it
must not also bound how far back the arm may credit crashes. The
reload is armed 250ms after its crash, so only that crash's own kill
burst is ever in scope. At 30s an unrelated crash 25s older — with a
different reason the reload never healed — was dismissed and stamped
rendererAutoRecovered. Split out a burst-sized lookback.
Rather than add a third and fourth call-site disarm, the disarm now
lives inside markExpectedRendererReload. Every caller is a manual
main-window reload, so this covers all of them at once and a future
site cannot forget it. The wiring test pins the invariant at the
definition instead of at three call-site spellings.
Known gap, deliberately not fixed here: browser-guest-ui's forceReload
shortcut reloads the host renderer directly, bypassing this path. That
site is also missing the pre-existing markExpectedRendererReload
marking, so both halves belong in one browser-scoped change.
Mutation-verified: deleting any of the three fixes turns exactly one
test red. Gates: 3152 passed | 11 skipped, tsc 0, oxlint/oxfmt clean.
Two gaps found by adversarial round 5.
The arm could be consumed by the user's own reload. A recovery reload
that stalls short of renderer_bootstrap_rendered without tripping the
breaker leaves the arm set; a Cmd+R or View > Reload within 30s then
boots, consumes it, and stamps details.rendererAutoRecovered on a crash
the USER healed - dismissing its burst siblings too. The disarm goes in
both onBeforeReload bodies (createMainWindow's shortcut and the app menu
wire them separately) and inside the existing main-window guard, since
the menu path reloads whichever window has focus.
The arm/disarm call sites had no coverage at all: deleting them left the
full src/main suite green, so the earlier regression tests pinned the
module functions and not the wiring. index.ts cannot be imported by a
test (app-level side effects at module scope), so this follows the
repo's existing answer - a bounded, code-anchored source assertion,
mutation-verified to kill each of the four call sites individually.
The arm was set in one place and cancelled in none. When the renderer
recovery circuit breaker opens, the arm from the last allowed attempt
stays live for 30s, so the user's own reload from the "Orca keeps
failing to load" dialog consumes it — and the whole crash-on-load loop
is written dismissed with rendererAutoRecovered: true.
That flag is a direct lie there: the same code path had just recorded
renderer_recovery_circuit_breaker_open. It silences the highest-signal
crash class we have, repeated launch-failed before bootstrap.
Clearing on exhaustion leaves the manual retry unarmed, so the crash
prompts on its own evidence at the next launch.
Relation matching ignores processType, so a react-render report was swept the
moment it shared reason and exitCode with the anchor. Unreachable today only
because the boundary reason literal never collides with an Electron exit
reason — an invariant held 200 lines away with nothing pinning it.
Restates the anchor pass's own exclusion rather than reusing
isReactErrorBoundaryReport(), which tests that same reason literal. Both
regression tests now use a crash-shaped reason/exitCode so they fail without
the guard instead of passing on the mismatch.
Co-authored-by: Orca <help@stably.ai>
One kill burst (kernel OOM-killer, macOS jetsam) can emit a Utility or GPU
exit alongside the renderer's. dismiss() already sweeps those siblings via
isRelatedCrashEvent; markRendererCrashesAutoRecovered did not, so the healed
crash still reached the user wearing the sibling's report.
Reuses the exact predicate dismiss() uses, anchored on a snapshot of the
resolved set so a swept sibling cannot anchor a further sweep.
Co-authored-by: Orca <help@stably.ai>
React error-boundary reports share source 'renderer' but are produced by the
renderer itself, not by a dead process, so a recovery reload must not dismiss
them.
Co-authored-by: Orca <help@stably.ai>
A renderer killed by the OS is recorded as a pending crash report, then the
recovery reload brings a new renderer up ~600ms later. That renderer's
CrashReportDialog immediately queries getLatestPending and opens the crash
prompt for a crash the app already healed (10/10 of the cluster).
Key the suppression on observed recovery, not on reason/exit code: arm an
outcome check when the recovery reload is issued, and resolve the pending
renderer report only once the new renderer emits renderer_bootstrap_rendered.
A reload that never brings React back leaves the report pending, so the
blank-window case still prompts. The record stays on disk, keeps its
breadcrumbs, stays sendable from Help > Report Crash, and is stamped
rendererAutoRecovered.
Co-authored-by: Orca <help@stably.ai>
Use async profile probes and serialize the current-time due decision plus mutations under one async owner so sync flushes and detached writers cannot double-shift or miss the recovery interval. Add syscall, timer-liveness, parity, and held-I/O interleaving coverage.
* fix(relay): chunk fs.changed notifications to fit the client frame capacity
emitRelayWatcherEvents published an entire watcher batch as one
fs.changed notification on the ordinary lane. A batch runs routinely
tens of times over the per-frame producer capacity (12,288 bytes against
a Node <=21 socket, 49,152 against Node >=22), and an over-capacity
frame there is not queued or trimmed -- notify() fails admission and
closes the client, tearing down every in-flight request on that
connection. Regressed in 5f7807497e (#11005).
The emitter now sizes chunks against the tightest capacity across
attached clients, measured in encoded bytes through the same envelope
admission measures, and preserves event order. A single event too large
for an empty envelope cannot be chunked, so the ordinary-lane close
remains its backstop: a silent drop on this lane has no resync contract.
* chore(relay): shorten watcher chunking comments
Swap the worktree-name emoji picker from emojibase-data's `github` shortcode preset to `emojibase`, which carries both `flag_kr` and `south_korea` style flag names, and drop the hand-maintained `kr` entry that patched around the gap. Filter skin-tone aliases so they neither crowd the suggestion list nor clobber base-emoji branch names.
Search now matches anywhere in the shortcode, ranked exact > prefix > word-start > substring, so `:korea` surfaces both Koreas.
Emoji-derived branch names now prefer spelled-out aliases: flags use country names (japan, germany, south-korea) and cryptic stubs are skipped (thumbsdown over no, victory over v).
Keep only the durable docs already allowlisted for tracking
(STYLEGUIDE, assets, localized readme, and reference compatibility
guides). Drop feature design notes, plans, and repro artifacts that
were force-added past the existing docs ignore rules.
Remove one-off incident docs and committed test-results noise, move
dev/repro/bench tools under tests/tools, and relocate i18next config
into config/ so the GitHub root scrolls to the description faster.
* perf(mobile): avoid unchanged worktree catalog payloads
* fix(mobile): isolate catalog snapshots by limit
* review: reassert host truth on unchanged polls; content-address snapshots
Client — the `changed` gate meant an unchanged poll skipped setWorktrees /
setLastKnownWorktrees / setCachedWorktrees, so optimistic local edits
(togglePin, handleDeleteWorktree's failure re-add) and the #8498 cache guard
were no longer repaired while the host catalog was stable. The gate bought
nothing: setCachedWorktrees is an in-memory Map write and areWorktreeListsEqual
already ran every poll, so the steady state still short-circuits on array
identity. All wire savings are unaffected. admit() now just returns the
confirmed rows and HostScreen applies them exactly as it did pre-PR.
Also on the client:
- a stale response from a superseded client/host no longer clears the token the
current client/host just established
- discriminate on `worktrees` rather than on `'unchanged' in response`, so a
future catalog field named `unchanged` can't reclassify a full response
- useRef over useMemo for the snapshot client; React may discard memoized values
- hoist WORKTREE_PS_FULL_LIMIT so the truncates-at-200 rationale travels with it
Host — replace the per-limit snapshot cache with a content-addressed id (ETag
semantics). Ownership lives in the id, so concurrent clients, differing limits,
and runtime restarts are correct by construction; this drops the LRU, the
eviction policy, the per-runtime WeakMap, and the retention of up to 8 full
catalogs. The remaining cache is a pure memo: because ids derive from content,
dropping or thrashing it costs CPU and nothing else. Keeping the memo also
keeps the measured steady-state cost — hashing every poll instead measured
2.24ms vs 0.75ms for the compare on a 310KB catalog.
Verified: mobile 2784 passed / 3 skipped, src/main/runtime/rpc 1064 passed,
node + mobile typechecks, oxlint, oxfmt, max-lines ratchet.
* fix(runtime): isolate catalog snapshot memo
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(preflight): route landing banner through the runtime-aware preflight slice
Landing called window.api.preflight.check directly, which always probes the
local client. The preflight slice is the only caller that consults
getActiveRuntimeTarget and forwards to preflight.check on the active runtime
environment, so while connected to a remote runtime the landing banner
reported the client machine's git/gh state instead of the server's.
Delegate to refreshPreflightStatus and derive the issue list from
state.preflightStatus. This also drops Landing's duplicate probe: the slice
dedupes concurrent and forced checks, so the mount/focus/poll paths now share
one in-flight request with the rest of the app.
* fix(preflight): refresh landing status across runtime sessions
* test(preflight): cover paired runtime session races
* test: make landing preflight oracle behavioral
* fix(preflight): scope runtime session invalidation
* test(preflight): cover headed runtime switching
* test(preflight): isolate runtime status toast
---------
Co-authored-by: Marty <marty@localhost>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): stop serving a pre-write host-list snapshot to loads issued after the write
removeHost/persistHost await hostListMutation, but the in-flight loadHosts()
de-dupe handed back a pass that started BEFORE the write committed, so a load
issued after removal repainted the removed host card (#8791). Every durable
write now drops the shared pass via host-list-load-sharing.ts so the next
caller reads fresh; concurrent loads with no write between them still share
one Keychain pass.
Also extracts the host action sheet into host-list-action-sheet-actions.ts to
pin closeBeforePress on Edit host + Remove (the freeze half of #8791, already
fixed by #8536).
* fix(mobile): invalidate host loads after token writes
* fix(mobile): protect host token cache from stale reads
* fix(remote): unthrottle host renderer while serving a paired client
A paired desktop host left in the background could not open or close
agent sessions for its remote/relay client: the action stalled and
eventually failed with the host-side "Timed out waiting for terminal
surface after creation" (10s) error, while an already-live terminal's
keystrokes stayed fast.
Root cause: creating/closing a session routes through the host
renderer's setTimeout-coalesced graph sync to publish the terminal
surface, but the host window runs with Electron background throttling
(the hidden-window default, reaffirmed on macOS). When the window is
backgrounded/occluded, those renderer timers are throttled to a crawl
and the surface publication misses the 10s deadline. Live keystrokes are
unaffected because PTY I/O flows through the main process, never the
renderer.
Keep the authoritative renderer unthrottled while at least one remote
client is connected and restore the throttled power-saving default once
the last one disconnects. Connect/disconnect are driven from the shared
MobileSocketWiring onReady/onClose, so both direct-WS and cloud-relay
clients are covered; headless serve has no window and is a safe no-op.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(remote): tidy renderer-throttle comment and test per review
Address automated review nits on #11581:
- Trim the module-level rationale comment to the non-obvious contract,
matching the repo's concise-comment guideline.
- Drop the dead `detachedThrottle` variable from the reapply test; the
detached-target scenario is already covered by the lazy-resolution
test, so the case now asserts only what it exercises.
No behavior change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(remote): scope paired terminal publication throttling
Keep headed paired terminal creation and close renderer-owned so host inventory, input routing, ACK recovery, and cleanup retain the established lifecycle. Hold a reference-counted background-throttle lease only while the renderer publishes a paired operation, and epoch-fence async resolution so renderer reloads reject before any request or PTY spawn. Preserve headless main ownership and prevent paired clients from falling back to a local terminal.
* test(e2e): verify minimized host terminal repaint
* fix(remote): preserve paired terminal inventory through graph gaps
---------
Co-authored-by: fanyunqian.1 <fanyunqian.1@bytedance.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(mobile): keep source-control layout steady while Create PR eligibility loads
The Create PR entry unmounted until the first hostedReview.getCreationEligibility
answer arrived, so on a cold open the changed-files list painted first and then
shifted down 54pt (createPrBlock marginTop 12 + createPrButton minHeight 42)
when the button appeared — while the user was already tapping (#8411).
- buildMobileCreatePrAction: cold loading now reserves the row with a disabled
placeholder instead of unmounting it.
- useMobileHostedReviewEligibility: a fetch-imminent idle frame renders as an
in-flight load, so the reservation is present on the first painted frame.
- New per-worktree+branch memory of the last resolved eligibility seeds cold
loads, so branches whose answer is hidden (existing review, unsupported
provider) do not get a placeholder that collapses on every reopen.
Fixes#8411
* fix(mobile): harden source-control layout reservation
* fix(mobile): keep review status row footprint fixed
* fix(mobile): derive eligibility state from keyed snapshots