Skip to content

perf(runtime): stop re-sending the worktree list to mobile pollers on every terminal chunk - #28062

Merged
Jinwoo-H merged 8 commits into
mainfrom
mobile-worktree-ps-quiet-activity
Oct 13, 2026
Merged

Jinwoo-H merged 8 commits into
mainfrom
mobile-worktree-ps-quiet-activity

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​138 $\color{#cf222e}{\Huge{\mathbf{−}}}$​21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​117
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​34 $\color{#cf222e}{\Huge{\mathbf{−}}}$​4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​30

What

The mobile workspace list polls worktree.ps every 3 s while it's open, with afterSnapshotId. The host replies "unchanged" when a content hash of the catalog matches. But each row's preview and lastOutputAt change 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

  • Host: when the caller is a snapshot poller (afterSnapshotId present, including the first poll's null), worktree.ps blanks preview and floors lastOutputAt to 30 s before computing the snapshot id. The id then holds steady while only fine-grained activity moves. Callers without afterSnapshotId (the desktop renderer, orca worktree ps, the relay host catalog) get exactly what they get today, and a test compares the serialized output.
  • Mobile: preview is 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

Measured

A scratch script ran today's unchanged mobile WorktreeCatalogSnapshotClient against 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.

Agents printing Main host: full replies, KB/poll This branch: full replies, KB/poll
1 200, 143.7 21, 15.1
10 200, 143.7 40, 28.6
idle 1, 0.8 1, 0.8

Scope

  • A terminal that keeps printing still causes one resend per 30 s bucket: about 10× fewer than today, not zero.
  • Agent rows' updatedAt / toolInput / lastAssistantMessage change on hook events, so an agent reporting tool calls still changes the snapshot id on each one.
  • Home and agent history fetch without a snapshot id, so they still get the full catalog, preview included.

Conflicts trivially with #28058 (both edit worktree-list-snapshot.ts and the row type); whichever lands second gets a one-line merge.

…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
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9189a1c1-9cd2-43d4-b6a3-3c3629771376
📥 Commits

Reviewing files that changed from the base of the PR and between 4bab494 and bd24a74.

📒 Files selected for processing (1)
  • mobile/src/components/WorktreeListRow.test.ts
💤 Files with no reviewable changes (1)
  • mobile/src/components/WorktreeListRow.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.

📝 Walkthrough

Walkthrough

Snapshot polling now coarsens worktree activity: it clears previews, rounds non-null output timestamps down to 30-second intervals, and sorts rows. Requests without an afterSnapshotId still return the original result. The mobile Worktree type no longer declares preview, and related test fixtures and a preview-only live-update case were removed.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to bd24a

No actionable issue is established for the selected change; it is mergeable after normal checks.

Pre-merge checks · Passed 3 · Failed 2

Failed Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check Warning The description explains the problem, implementation, scope, safety rationale, measurements, and conflicts. However, it omits or leaves incomplete several required template sections, including the lin… Add the required template sections. Provide an actual linked issue after “Fixes #”, state “N/A” with a reason for visual proof if no UI change exists, document testing and platforms, complete the applicable checkboxes, address AI disclosure…
Docstring Coverage Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 10 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (3 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the main performance change: reducing repeated worktree-list responses to mobile pollers during terminal output.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the problem, implementation, scope, safety rationale, measurements, and conflicts. However, it omits or leaves incomplete several required template sections, including the linked issue, visual proof or an explicit N/A, testing checklist, AI disclosure, review, agent skill boundary, notes, and final checklist.

Resolution

Add the required template sections. Provide an actual linked issue after “Fixes #”, state “N/A” with a reason for visual proof if no UI change exists, document testing and platforms, complete the applicable checkboxes, address AI disclosure requirements, and include review, agent skill boundary, notes, and checklist information.

✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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
@Jinwoo-H Jinwoo-H changed the title perf(mobile): stop re-sending the worktree list on every terminal chunk perf(runtime): stop re-sending the worktree list to mobile pollers on every terminal chunk Oct 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9881bb29-c148-4a3b-af88-716555c1b0b2
📥 Commits

Reviewing files that changed from the base of the PR and between fa299d7 and 16c0ad9.

📒 Files selected for processing (14)
  • mobile/src/agent-history/agent-history-resume-target.test.ts
  • mobile/src/agent-history/agent-history-session-worktree.test.ts
  • mobile/src/components/WorktreeListRow.test.ts
  • mobile/src/components/WorktreeListRow.tsx
  • mobile/src/host-screen/host-ssh-health-badge-wiring.test.ts
  • mobile/src/worktree/mobile-workspace-lineage.test.ts
  • mobile/src/worktree/workspace-list-pinned-lineage.test.ts
  • mobile/src/worktree/workspace-list-sections.test.ts
  • mobile/src/worktree/workspace-list-types.ts
  • mobile/src/worktree/worktree-host-context-labels.test.ts
  • mobile/src/worktree/worktree-list-snapshot.ts
  • src/main/runtime/rpc/methods/worktree-catalog-methods.ts
  • src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts
  • src/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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 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 win

Add a deterministic tie-break for coarsened rows.

getWorktreePs first orders rows by raw lastOutputAt. Supported folder-workspace siblings can share a path but have distinct worktreeId values. If their activity changes within one 30-second bucket, their arrival order can change. Coarsening makes the compared fields equal, so compareWorktreePs preserves 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
📥 Commits

Reviewing files that changed from the base of the PR and between 16c0ad9 and 4bab494.

📒 Files selected for processing (2)
  • src/main/runtime/rpc/methods/worktree-catalog-snapshot-method.test.ts
  • src/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.

@Jinwoo-H
Jinwoo-H merged commit 686c385 into main Oct 13, 2026
32 checks passed
@Jinwoo-H
Jinwoo-H deleted the mobile-worktree-ps-quiet-activity branch October 13, 2026 02:57
karlorz added a commit to karlorz/orca that referenced this pull request Oct 13, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant