Review finding round 15: inventory ids trim at parse but lifecycle/tool
agent_id stayed raw, so a padded stop missed the trimmed roster key and
the row could never drain. All id reads now canonicalize identically.
Review findings round 14: (1) the clearing emission after a wait owner's
death carried the cached tool/interactive card, leaving it actionable on
a resolved pane — the cache resets like the answered-question clear.
(2) Whitespace-padded inventory ids were stored raw yet published
trimmed, so their own stops could never drain them — canonicalize at
parse time.
Review findings round 13 share one root: stop-time inventories are
mutually unordered, so any deletion they drive can be a stale claim
against newer live state. Fold only running valid entries (no deletions;
listed-non-running cleanup stays with turn-serialized lead Stops), reap
only pre-restart restoredFromSnapshot rows on an empty inventory, and
drop the untrackable raw-scan working mint (truncated stays silent).
Review finding round 12: reaping the last inventory-derived rows emitted
nothing, stranding the previously published working until the sweeper.
The reap is same-source-correlated removal evidence — the authority that
minted those rows now reports empty — so it rides the existing done veto
chain.
Review finding round 11: the complete-empty fold cleared live-tracked
rows, so a delayed stale stop's empty list could retire a newer live
start — the uncorrelated-retirement race again. The empty inventory now
reaps only restored/inventory-derived claims not backed by live
lifecycle events.
Review findings round 9: (1) a delayed prior-turn stop for a reused
teammate id could park the newer start's working row and ride the wait-
clear into done — the clearing done now fires only when the owner has no
roster row left; a parked owner stays silent (sweeper-bounded card).
(2) Over-length ids no longer consume inventory parser cap slots, so
junk cannot fake truncation and veto a legitimate drain. (3) A waiting-
inducing child event with an untrackable id mints no card at all.
Review findings round 8: (1) over-length running inventory ids could
allocate rosters or claim liveness despite being untrackable — the fold
guard and raw liveness scan now require an id the upsert would accept.
(2) Deleting a fresh pane's no-stash wait record left the stale amber
waiting card fresh for the stale sweeper; the wait's own agent id
parking/stopping is an identity-correlated resolution and now emits a
clearing done.
Review findings round 7: (1) a child permission wait that is a fresh
pane's first lead record fabricated 'working' from the no-stash clear
fallback when the child died — drop the record instead and let evidence
rules decide. (2) A complete empty stop inventory now clears restored
phantom rows (it is complete by construction; the non-empty additive fold
is unchanged). (3) An uncapped raw scan sees running subagents hidden
past the 32-entry parser cap, so a capped inventory with live work emits
truthful working instead of retaining stale state.
Review findings round 6: (1) veto-only left the vetoing child untracked,
so its own later drain was start-less-suppressed and the pane stuck on
working — fold running non-teammate inventory entries additively
(inventoryComplete: false; no reap, so a just-parked unconfirmed teammate
row survives). (2) The agent-task parser caps at 32 entries; a running
child past the cap is invisible, so a truncated inventory now claims
neither done nor working. The raw-array shell/cron veto stays uncapped.
Review finding round 5: after a restart the listener can miss a child's
start; another child's stop still lists it running in background_tasks.
A running non-teammate task or live shell/cron now vetoes the no-lead
removal->done into working. Veto only — folding the inventory at stop
time would reap a just-parked teammate row before its TeammateIdle
confirmation arrives.
Review finding round 4: teammate ids persist across turns, so a delayed
stale SubagentStop parks a live teammate's row and the drain->done
inference retired newer work — PR #11353's uncorrelated-completion race.
With no lead record, only a live working child (working) or a one-shot
row removal (done; duplicate-proof since the row is gone) may speak;
teammate parks and idle-only refreshes stay silent.
Review finding round 3: rosterChanged let a known-child drain bypass the
no-evidence guard into the unconditional 'working' default — sticky until
the stale sweeper when no lead hook ever arrives. With no lead record the
pane state now follows tracked children: live child -> working, identity-
matched drain/park -> done. Duplicate stale stops stay suppressed.
Review finding round 2: upsert silently rejects over-length agent ids after
the caller already allocated, stranding one empty roster per invented pane
key with zero emitted payloads. Export the id validity check and gate both
wire-reachable allocation sites.
Review finding: the early null return left getOrCreateClaudeSubagentRoster's
empty allocation behind, one per untrusted pane key with no lifecycle bound.
Only SubagentStart may allocate; stop/idle now read the existing roster.
Claude compact emits PreCompact -> SubagentStop (never started) ->
SessionStart(compact) -> PostCompact. On a freshly resumed pane with no
cached lead state, the child-driven default minted a sticky empty-prompt
'working' that no Stop ever clears (STA-2915). Emit child-driven status
only on positive evidence: a lead record, a live working child, or an
actual roster change.
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>