Repository navigation
perf(runtime): stop re-sending the worktree list to mobile pollers on every terminal chunk - #28062
Conversation
…churn worktree.ps carries `preview` and an exact `lastOutputAt` on every row, both of which move on each PTY chunk, so the content-addressed snapshot id changed on every poll while any agent printed and the whole catalog was resent. Add an optional `coarseActivity: true` param. When set, the host drops `preview` and floors `lastOutputAt` to 30 s before hashing, so the id holds while only chunk-level activity moves. Callers without the param get the byte-identical response they get today; older hosts strip the unknown key. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
The host-screen snapshot poll now asks worktree.ps for `coarseActivity`, so the catalog's snapshot id survives terminal output and the 3 s poll gets a tiny `unchanged` reply instead of the whole catalog. `preview` is optional on mobile rows because a coarse host leaves it out; old hosts ignore the param and keep sending it. Re-records the three worktree.catalog-snapshot goldens; the only semantic change is the new request param. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to No actionable issue is established for the selected change; it is mergeable after normal checks. Pre-merge checks ·
|
Drop the `coarseActivity` param: the only caller that sends `afterSnapshotId` is the mobile snapshot poll, which never renders `preview` and uses `lastOutputAt` only to rank rows. Snapshot polls now always get a blank `preview` and a 30 s-floored `lastOutputAt`, so already-shipped apps stop re-downloading the catalog on every terminal chunk with only a desktop update. Callers without `afterSnapshotId` still get the byte-identical response. Reverts the mobile param, its scenario steps and the three re-recorded goldens, and removes the unread `preview` field from mobile row types. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
9881bb29-c148-4a3b-af88-716555c1b0b2
📒 Files selected for processing (14)
mobile/src/agent-history/agent-history-resume-target.test.tsmobile/src/agent-history/agent-history-session-worktree.test.tsmobile/src/components/WorktreeListRow.test.tsmobile/src/components/WorktreeListRow.tsxmobile/src/host-screen/host-ssh-health-badge-wiring.test.tsmobile/src/worktree/mobile-workspace-lineage.test.tsmobile/src/worktree/workspace-list-pinned-lineage.test.tsmobile/src/worktree/workspace-list-sections.test.tsmobile/src/worktree/workspace-list-types.tsmobile/src/worktree/worktree-host-context-labels.test.tsmobile/src/worktree/worktree-list-snapshot.tssrc/main/runtime/rpc/methods/worktree-catalog-methods.tssrc/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.tssrc/main/runtime/rpc/worktree-ps-coarse-activity.ts
💤 Files with no reviewable changes (11)
- mobile/src/worktree/worktree-host-context-labels.test.ts
- mobile/src/agent-history/agent-history-resume-target.test.ts
- mobile/src/worktree/worktree-list-snapshot.ts
- mobile/src/worktree/workspace-list-sections.test.ts
- mobile/src/worktree/mobile-workspace-lineage.test.ts
- mobile/src/agent-history/agent-history-session-worktree.test.ts
- mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts
- mobile/src/components/WorktreeListRow.test.ts
- mobile/src/worktree/workspace-list-pinned-lineage.test.ts
- mobile/src/components/WorktreeListRow.tsx
- mobile/src/worktree/workspace-list-types.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
… the snapshot id Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 Minor · Add a deterministic tie-break for coarsened rows. · worktree-ps-coarse-activity.ts:20-23
src/main/runtime/rpc/worktree-ps-coarse-activity.ts:20-23
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAdd a deterministic tie-break for coarsened rows.
getWorktreePsfirst orders rows by rawlastOutputAt. Supported folder-workspace siblings can share a path but have distinctworktreeIdvalues. If their activity changes within one 30-second bucket, their arrival order can change. Coarsening makes the compared fields equal, socompareWorktreePspreserves that order and the JSON-derived snapshot ID can change for the same catalog.Suggested fix
- .sort(compareWorktreePs) + .sort((left, right) => { + const comparison = compareWorktreePs(left, right) + if (comparison !== 0) { + return comparison + } + return left.worktreeId < right.worktreeId ? -1 : 1 + })
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
04a7ecc5-574e-4f21-a8b5-5fef3cbba650
📒 Files selected for processing (2)
src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.tssrc/main/runtime/rpc/worktree-ps-coarse-activity.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/runtime/rpc/worktree-ps-coarse-activity.ts
- src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.
…iet-activity # Conflicts: # mobile/src/components/WorktreeListRow.tsx # mobile/src/worktree/worktree-list-snapshot.ts
* Update README downloads badge * fix(i18n): drop chat-mode keys removed from en.json in stablyai#28113 (stablyai#28131) * Speed up Mobile Checks by overlapping typechecks and tests (stablyai#28127) Overlap mobile typechecks with tests and avoid repeated filesystem probes and unused device substitutes in recording replays. Keep state isolated between loaders, give exhaustive snapshot cases an explicit execution deadline, and retire obsolete locale keys that blocked repository validation. * fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time (stablyai#28125) * fix(orcad): identify instances by kernel start ticks, not drifting wall-clock start time On Linux the start time is derived from /proc/stat btime, which drifts (WSL2, NTP), so a live orcad was read as a reused PID and its managed stop reported exited without ever writing the request. The lock and stop request now carry optional linuxStartTicks + bootId; readers prefer them, and a wall-clock mismatch on Linux for records without them is unverifiable, never proof of exit. Co-Authored-By: Claude <noreply@anthropic.com> * fix(orcad): match stop receipts without the incarnation an older client drops A request recovered by an older client loses linuxStartTicks/bootId, which made the newer host reject its own receipts and decision records as mismatched. Also exercise the incarnation reader against a real Linux /proc. --------- Co-authored-by: Claude <noreply@anthropic.com> * docs(readme): point the Android APK links at 0.0.55 (stablyai#28134) Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(runtime): stop re-sending the worktree list to mobile pollers on every terminal chunk (stablyai#28062) * perf(runtime): let worktree.ps pollers opt out of per-chunk activity churn worktree.ps carries `preview` and an exact `lastOutputAt` on every row, both of which move on each PTY chunk, so the content-addressed snapshot id changed on every poll while any agent printed and the whole catalog was resent. Add an optional `coarseActivity: true` param. When set, the host drops `preview` and floors `lastOutputAt` to 30 s before hashing, so the id holds while only chunk-level activity moves. Callers without the param get the byte-identical response they get today; older hosts strip the unknown key. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(mobile): stop re-sending the worktree list on every terminal chunk The host-screen snapshot poll now asks worktree.ps for `coarseActivity`, so the catalog's snapshot id survives terminal output and the 3 s poll gets a tiny `unchanged` reply instead of the whole catalog. `preview` is optional on mobile rows because a coarse host leaves it out; old hosts ignore the param and keep sending it. Re-records the three worktree.catalog-snapshot goldens; the only semantic change is the new request param. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * perf(runtime): coarsen worktree.ps activity for every snapshot poller Drop the `coarseActivity` param: the only caller that sends `afterSnapshotId` is the mobile snapshot poll, which never renders `preview` and uses `lastOutputAt` only to rank rows. Snapshot polls now always get a blank `preview` and a 30 s-floored `lastOutputAt`, so already-shipped apps stop re-downloading the catalog on every terminal chunk with only a desktop update. Callers without `afterSnapshotId` still get the byte-identical response. Reverts the mobile param, its scenario steps and the three re-recorded goldens, and removes the unread `preview` field from mobile row types. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(runtime): rank coarsened worktree.ps rows so in-bucket swaps keep the snapshot id Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test: drop preview from row fixtures and keep the runtime cast on one line Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): load markdown tabs from any desktop split group; center load-error Retry (stablyai#28132) * fix(mobile): load markdown tabs from any desktop split group and center load-error Retry The desktop answered markdown.readTab only for tabs in its focused split group, so a phone opening a markdown tab from another group got tab_not_found and showed "Couldn't load markdown". Resolve the tab across all of the worktree's groups. Load-error states now share one component that centers the message and its Retry button; the markdown reader's button was pinned to the left. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(renderer): mobile markdown read picks the requested tab across groups Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(renderer): resolve mobile markdown tab to one file id Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(wsl): run WSL git with Windows Git line-ending settings on checkouts Windows Git wrote (stablyai#27778) * fix(wsl): run WSL git with Windows Git settings on Windows-authored drive checkouts * fix(wsl): detect Windows Git checkouts from host autocrlf plus a whole-tree sample * fix(wsl): keep WSL-created worktrees on Linux Git defaults and never commit CRLF on an unproven checkout - worktree add / clone write a new checkout whose .git points at /mnt/..., which Windows Git cannot open; injecting autocrlf=true there produced CRLF files later read unflagged. - Writes on a host-autocrlf=true checkout whose sample is unproven run with autocrlf=input, so staging normalizes CRLF without ever writing CRLF to disk. - Writes wait for a stale host answer and re-check checkout identity after joining a sample. * fix(wsl): compare a joined sample with the checkout identity read after it settles * fix(runtime): stop a project's terminals before the host forgets the project (stablyai#28024) * fix(runtime): stop a project's terminals before the host forgets the project Removing a project on a runtime host (an SSH-managed or paired server) sent repo.rm first; the host resolves terminal.stop through the project's workspace rows, which repo.rm deletes, so the stop failed and the shells kept running on the host. Terminals stop first now. The removal toast also drops the IPC wrapper from its message. * fix(runtime): only stop host terminals when the host will accept the project removal Probe with repo.show (same resolver as repo.rm) so a removal the host rejects, such as an ambiguous project id, no longer kills the shells first. Also poll through terminal_not_writable in the paired-client e2e. * test(e2e): read the host shell pid through sh so non-POSIX login shells work * fix: preserve relay pane claims across managed tab mirrors * fix(mobile): cancel bridge-post retries when a terminal document stops Mobile Checks verify failed on an unhandled ReferenceError: window is not defined inside postToReactNativeWebView's scheduled send() retry while viewport-frame.test.ts ran. Retries lived outside the document lifecycle, so a stop left timers that later read a torn-down window. Bind the default postToHost per scope, clear its timers on stop, no-op posts after stop, and guard window before reading ReactNativeWebView. Preserve bounded retry and deliver-when-bridge-appears behavior. * fix(mobile): use supported own-property check for host overrides * fix(mobile): align bridge retry teardown with document lifecycle contract * style(mobile): format bridge retry lifecycle regression test --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com>
What
The mobile workspace list polls
worktree.psevery 3 s while it's open, withafterSnapshotId. The host replies "unchanged" when a content hash of the catalog matches. But each row'spreviewandlastOutputAtchange on every chunk of terminal output, so while any terminal on the host is printing, every poll gets the full catalog. That's about 145 KB per poll at 200 workspaces, sent over the relay, then decrypted and parsed in JS on the phone.Change
afterSnapshotIdpresent, including the first poll'snull),worktree.psblankspreviewand floorslastOutputAtto 30 s before computing the snapshot id. The id then holds steady while only fine-grained activity moves. Callers withoutafterSnapshotId(the desktop renderer,orca worktree ps, the relay host catalog) get exactly what they get today, and a test compares the serialized output.previewis deleted from the worktree row types and from the list's equality check, since nothing on mobile reads it.No new params, no wire-format change, no golden re-record.
Why this is safe for clients already in the field
afterSnapshotIdis mobile'sworktree-catalog-snapshot-client.ts. It shipped in perf(mobile): avoid unchanged worktree catalog payloads #11735 (2026-07-31), and mobile stopped rendering the worktree preview in feat(mobile): pickers, workspace parity, active-workspace focus, tap-to-open, source-control parity, artifact viewing #5330 (2026-06-15). So no client that polls snapshots has ever displayedpreview; to them it's only an equality input.lastOutputAtonly feeds mobile's "recent" sort and the Home resume fallback. Flooring can only reorder rows whose activity is within 30 s of each other. With exact timestamps those rows already swap on every 3 s poll, and ties fall back to the display-name sort.Measured
A scratch script ran today's unchanged mobile
WorktreeCatalogSnapshotClientagainst two real dispatchers: one from main and one from this branch. 200 rows, polls every 3 s, 10 minutes. Every poll delivered all 200 rows.Scope
updatedAt/toolInput/lastAssistantMessagechange on hook events, so an agent reporting tool calls still changes the snapshot id on each one.Conflicts trivially with #28058 (both edit
worktree-list-snapshot.tsand the row type); whichever lands second gets a one-line merge.