Round 6 corrected my own round-5 comment. I had written that `true` is the
repair-loop signature and `false` covers a deliberate background activation. The
repair effect can emit `false` too: its closure holds the worktree from its render
while the guard runs against live state, so a worktree switch landing in between
reattributes the tab. The verdict hints at the caller; it does not prove it, and
neither value should be discarded. Comment-only.
Also take the free scan win the perf review measured: Object.entries allocates a
pair array per worktree on a path that runs per tab activation. Own keys are safe
to index by, so Object.keys plus an indexed read is behaviour-identical
(16.1us -> 5.0us at 170 worktrees x 10 tabs).
Three comments said `false` is the verdict that matters because it is the
only one showing the activation could not converge. That is backwards.
The repair effect activates a tab drawn from tabsByWorktree[active], so
the React #185 path can only ever emit `true`; `false` is what a
deliberate jump-to-agent into a background worktree emits from a fully
converged state. A reader of the next bundle would have discarded the
exact sample the breadcrumb exists to capture.
The mechanism was right, only its stated reason was wrong: the real
justification for keying on the verdict is symmetric, since coalescing
keeps only the newest payload and either verdict would erase the other.
The suite also did not pin the "at most 2 per tab id" bound - a guard
keyed on `${tabId}:${activeWorktreeId}` passed all 11 tests while
emitting once per worktree, the storm the guard exists to prevent. Adds
a count-pinning test that kills it.
Caps the never-pruned guard set at 256 distinct verdict keys (~85KB),
mirroring MAX_COALESCE_KEYS. Measured 330 B/entry; a realistic thousand
duplicated tab ids is ~0.6MB, negligible but unbounded in principle.
The two earlier commits contradicted each other. Splitting the coalesce
key existed so a `false` verdict could not be erased by a later benign
`true` — but the renderer guard was keyed on the tab id alone, so for
any one id only the first verdict was ever emitted.
A duplicated id that first resolves benignly, then stops converging when
the user switches worktrees, dropped the `false` sample at the source.
That sample is the whole reason the breadcrumb exists: it is the only
value saying the activation could not converge activeTabId.
Key the guard on id plus verdict. At most two crumbs per tab id, and
the main process still folds each verdict into its own ring entry, so
the flood bound is unchanged.
Name-only coalescing keeps only the newest payload, so a resolvedToActiveWorktree
false sample — the one value saying the activation still could not converge — was
erased by any later benign true in the same 30s window. Keys on the flag instead,
mirroring the WebGL name:kind branch; two keys still bound the burst.
Also: the previous coalescing commit had no test at all (removing the name from
both sets broke zero of 2701 tests), the resolver's activeWorktreeId truthiness
check was a hole rather than a guard for a '' active id, and the resolver test
file failed oxfmt --check.
Co-authored-by: Orca <help@stably.ai>
Its renderer guard is once-per-tab-id, so the stale worktree map it exists to
diagnose duplicates every tab id at once and could evict the whole 30-entry
ring. Also drops two keyed re-reads of tabsByWorktree that would throw for a
prototype-named worktree id, and pins the activeTabIdByWorktree guard with a
test that fails without it.
Co-authored-by: Orca <help@stably.ai>
Prefer the active worktree when resolving a terminal tab's owner. First-match
ownership left activeTabId permanently unconvergeable under a duplicated tab
id, so the active-terminal repair effect re-triggered itself into React #185.
Breadcrumb the duplicate-ownership state (once per tab id) so a crash bundle
can prove or kill the production origin of the precondition.
Co-authored-by: Orca <help@stably.ai>
setActiveTab always reallocated activeTabIdByWorktree, even when the tab was
already active for that worktree. Terminal's active-terminal repair effect
depends on that map, so when the repair cannot converge activeTabId -- which
happens when an earlier-scanned worktree reuses the tab id -- the effect
re-triggers itself every commit until React throws #185.
Crash cluster A: 12 reports, boundary terminal.workbench, 1.4.162/1.4.163.
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