Skip to content

perf(web): saving the thread list cache no longer re-encodes every thread - #19915

Merged
juliusmarminge merged 8 commits into
pingdotgg:mainfrom
bompus:perf/web-shell-cache-small
Oct 16, 2026
Merged

juliusmarminge merged 8 commits into
pingdotgg:mainfrom
bompus:perf/web-shell-cache-small

Conversation

@bompus

@bompus bompus commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Saving the thread list cache in the web client now encodes only the threads that changed. With 1,417 real threads (4.6 MB), a save after one thread changes drops from 791 ms to 11 ms of UI-thread work in Chrome, and the saved bytes are identical.

Saving the thread list cache in the web client, Chrome 154, 1,417 threads from a copy of real data, median of 5 saves. After one thread changed: 790.8 ms on main, 10.6 ms with this PR. First save after load: 790.8 ms on main, 82.9 ms with this PR.

Problem

Every saveShell in the web client ran a full Schema.encodeEffect over every thread row, even when one row had changed. The shell cache saves every 10 seconds while threads update, so a large thread list kept the UI thread busy for most of a second each time. encodeShellSnapshotForCache had kept this cheap before the V2 orchestrator (#2829) removed it.

Change

  • Mobile's row-reuse encoder (makeStoredShellSnapshotEncoder, perf(mobile): reuse encoded rows in shell cache saves #16129) moves from apps/mobile/src/connection/shell-cache-encoding.ts to packages/client-runtime/src/platform/shellCacheEncoding.ts, with its tests. Shell rows are immutable and kept by reference, so the encoder remembers each row's encoding and only runs the row codec for new rows.
  • Web saveShell uses it and yields between chunks with a MessageChannel task. A zero-delay timer is clamped to 4 ms after a few nested calls and throttled to about once a second in background tabs, and the shell flush also runs on disconnect.
  • Mobile imports the encoder from client-runtime and keeps its default setTimeout(0) yield. Its behavior is unchanged.
  • The output stays byte-for-byte equal to Schema.fromJsonString(StoredOrchestrationShellSnapshot). The web client now keeps one encoded copy of each live row, as mobile already does: about 15 MB of heap for 1,600 rows with 10 pull request links each.

Scope and approval

Fixes #18926, which was triaged as an accepted regression. The triage comment asked for exactly this scope: move the encoder into client-runtime, call it from saveShell, keep byte equality with a test like mobile's, keep mobile's yield, use a browser-friendly yield on web, and leave payload size for #18907.

Verification

  • Main's 9 encoder tests moved verbatim into packages/client-runtime/src/platform/shellCacheEncoding.test.ts: byte equality, row reuse, chunked yields, invalid rows, concurrent saves, interruption and failed encodes. 9/9 pass. The mobile store tests that stay behind pass (8/8), as do web storage.test.ts (10/10).
  • Measured above in the T3 browser panel (Chrome 154) on an isolated dev server seeded from a copy of real data. The page loaded both encoders and timed them on the same 1,417-thread snapshot the app had just saved. The new encoder's output matched main's byte for byte on every save.
  • vp run typecheck in client-runtime, web and mobile, plus targeted lint and format: clean.
  • Two independent review rounds (Claude Opus 5.5 and GPT-6.1 Sol) found no issues.
  • Not measured in Safari or the packaged desktop app.

Original work by @bompus, taken over and finished by maintainers.

Claude Opus 5.5 and Claude Fable 5.1 in Claude Code, with review by GPT-6.1 Sol in Codex.

🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 15, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR not approvable — This production-path change replaces web shell-cache encoding with shared memoization and browser task yielding, while relocating the existing mobile encoder and adding focused tests. It also adds a file-level directive suppressing two static-analysis diagnostics, so the change requires human review.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 88a582e9-cc8e-4d39-a862-c49f40ab46a5
📥 Commits

Reviewing files that changed from the base of the PR and between 1e02b3d and 212601b.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Path: .coderabbit.config.ts
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c78907b4-6651-424c-80a3-9d3ea24cdc1b
📥 Commits

Reviewing files that changed from the base of the PR and between da89192 and 1e02b3d.

📒 Files selected for processing: 4
  • apps/mobile/src/connection/environment-cache-store.test.ts
  • apps/web/src/connection/storage.ts
  • packages/client-runtime/src/platform/shellCacheEncoding.test.ts
  • packages/client-runtime/src/platform/shellCacheEncoding.ts
💤 Files with no reviewable changes: 1
  • apps/mobile/src/connection/environment-cache-store.test.ts

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

📝 Walkthrough

The shared shell snapshot encoder now accepts a caller-supplied yield effect. The web client supplies a MessageChannel-based browser-task yield, and the mobile client imports the encoder from the client-runtime platform package.

Changes: Shell snapshot encoding

Layer / File(s) Summary
Configurable cached encoder
packages/client-runtime/src/platform/shellCacheEncoding.ts, packages/client-runtime/src/platform/shellCacheEncoding.test.ts, packages/client-runtime/src/platform/index.ts
The encoder accepts an optional yield effect and defaults to a zero-delay timer. Tests cover schema-equivalent output, chunk yields, interruption, validation failures, concurrent saves, row reuse, and recovery after failure. The platform index re-exports the encoder.
Web and mobile integration
apps/web/src/connection/storage.ts, apps/mobile/src/connection/environment-cache-store.ts, apps/mobile/src/connection/environment-cache-store.test.ts
The web client supplies a MessageChannel-based yield to the encoder and closes the receiving port on interruption. The mobile client imports the encoder from the platform package. Its removed encoder tests are covered by the new client-runtime tests.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant saveShell
  participant makeStoredShellSnapshotEncoder
  participant yieldToBrowserTask
  participant MessageChannel
  saveShell->>makeStoredShellSnapshotEncoder: encode stored shell snapshot
  makeStoredShellSnapshotEncoder->>yieldToBrowserTask: yield during encoding
  yieldToBrowserTask->>MessageChannel: wait for browser task message
  MessageChannel-->>yieldToBrowserTask: deliver message
  yieldToBrowserTask-->>makeStoredShellSnapshotEncoder: resume encoding
Loading

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to 1e02b

The reviewed snapshot-encoding change is mergeable after normal checks; no unresolved behavior issue was established.

Security architecture risk: ⚪ Minimal · up to 1e02b

The shared encoder retains schema validation and environment-specific storage without adding permissions or a remotely callable operation. The inspected persistence flow preserves save ordering and writes only after encoding succeeds. No material security risk introduced or worsened by this change was identified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed behavior affects shell-cache serialization across environments handled by a client storage instance. The compared code adds no credential, network, filesystem, or server-side authority; the MessageChannel carries only a local scheduling notification.

Trust Boundaries and Controls

  • observed — The injected yield effect comes from application configuration, not snapshot data. Web retains environment-keyed storage and checks the decoded envelope’s environment ID before returning a cached snapshot. The shared row cache does not cache environment envelopes.

Resilience and Maintainability Implications

  • inferred — Failure or interruption before persistence can leave only successfully validated rows cached in memory, not a partially written snapshot. Repeating an encode safely reuses those rows under the immutable-row invariant. Timer cancellation and browser receiving-port cleanup are present in source; invalid persisted caches retain their discard path.
Pre-merge checks: Passed 4
Check name Status Explanation
Linked Issues check Passed Issue [#18926] requires shared row-reuse encoding, web integration, byte-equal output, the existing mobile yield, and a browser-appropriate web yield. The PR moves makeStoredShellSnapshotEncoder to …
Out of Scope Changes check Passed The changes stay within [#18926]. The shared export, mobile import update, test relocation, row-reuse implementation, and browser task yield directly support the issue. The PR does not change payload …
Title check Passed The title clearly and concisely identifies the main web performance change: saving the thread list cache no longer re-encodes unchanged threads.
Description check Passed The description includes the required Problem, Change, Scope and approval, and Verification sections. It explains the regression, identifies issue #18926, documents implementation details, reports foc…
✨ Finishing Touches: 💡 1
🧪 Generate unit tests (beta)
  • 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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juliusmarminge juliusmarminge self-assigned this Oct 15, 2026
@juliusmarminge

Copy link
Copy Markdown
Member

Note

Claude Opus 5.5 responding on behalf of Julius.

Taking over this one. I will push follow-up commits to this branch on top of yours and merge main in rather than rebasing.

juliusmarminge and others added 5 commits October 15, 2026 13:38
…equality and row reuse

The encoder moved from mobile into client-runtime, so its tests move with it.
Three cases remain: output matches the schema codec byte for byte across a
warm save with changed, added and archived rows; an unchanged row's encoding
is reused rather than re-encoded; and the default yield is a host timer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The encoder moved from apps/mobile to packages/client-runtime, but the
takeover's first pass replaced main's nine encoder tests with three new
ones. Move main's `cooperative shell cache encoding` block verbatim into
packages/client-runtime next to the encoder instead, keeping only the
fixture it needs, and leave the mobile store tests as they are on main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… cache

Keep the encoder's zero-delay timer as the default yield so mobile keeps
its behavior without passing anything, and scope the timer diagnostic
suppression to the one call instead of the whole module. Correct the
comment: Effect's own yield goes through setImmediate where the host
has it, which on React Native is a microtask shim.

On web, a MessageChannel message resumes encoding in a fresh task, so
input and rendering get a turn between chunks without the 4 ms nested
timer clamp or background-tab throttling. Closing the port on interrupt
is enough cleanup; a closed port delivers nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juliusmarminge juliusmarminge changed the title fix(web): saving the thread list cache no longer re-encodes every thread on the UI thread perf(web): saving the thread list cache no longer re-encodes every thread Oct 15, 2026
@juliusmarminge
juliusmarminge merged commit e2b929f into pingdotgg:main Oct 16, 2026
27 checks passed
Pipyakas pushed a commit to Pipyakas/t3code that referenced this pull request Oct 18, 2026
…read (pingdotgg#19915)

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e2b929f)
aorwall added a commit to aorwall/t3code that referenced this pull request Oct 19, 2026
Merges `pingdotgg/t3code` `main` at `3962ae8586` (96 commits since
`e536bfd33b`) into the fork. One merge commit; the tracker entry in
`docs/fork/upstream-merge-log.md` has the full decision list.

## Conflicts (8)

- **`ChatView.tsx`** (`converged`): upstream pingdotgg#19869 replaced
`useCallback` with plain functions plus `useStableCallback`. I took
upstream's file and re-applied each fork delta in that style: the
sandbox start for scripts, the draft-thread create and host-run path in
`runProjectScript`, Workspace script saves, the sandbox surface, file
retargeting, the announced preview and terminal effects, the
sandbox-commands banner, the PR-surface gate and the thread-fork dialog.
**This file is the one to review closely.**
- **`rightPanelStore.ts`** / **`FilePreviewPanel.tsx`**: the fork's
`retargetFile` sits beside upstream's new `navigateFolder` and
`folderTrail` (pingdotgg#15045).
- **`ci.yml`**: upstream folded the `typecheck` job into `build`
(pingdotgg#19633). The Moatless spec check now follows `build`'s final
`Typecheck` step.
- **`Sidebar.tsx`, `ProjectSettingsPanel.tsx`**: upstream's code plus
the fork's additions.
- **`index.html`**: kept the Moatless title. Added a `branding-shell`
path-policy entry for it and `branding.ts`.
- **`source-control-github/package.json`**: took upstream's new
`generate` script and kept `tsc --noEmit` (the `tsc-rs` tripwire).
- **`pnpm-lock.yaml`**: took upstream's copy, stripped `tsc-rs`, and
re-derived it with a real install.

Merge stats: 738 files landed against 736 in the upstream range. The one
upstream file that didn't land is `index.html`, whose only upstream
change was the title. The three extra landed files are fork docs. The
fork delta is 815 files.

## Contract
- Added `UnsupportedMethodError` to
`orchestration.subscribeCommandOutput` and `server.getDatabaseStorage`.
Both are new upstream methods Moatless doesn't dispatch.
- Moatless now serves 11 more `pullRequests.*` reads (moatless pingdotgg#1161,
pingdotgg#1164, pingdotgg#1169). They share `PullRequestRpcError` with methods it still
refuses, so the union keeps `UnsupportedMethodError`. The 11 are
declared as `pull-requests-*-shared-union` exceptions in
`inventory.json`, which retire together once the rest of the family is
served.

## Also in this PR
`sandboxSurfaces.test.ts` had been red on `main` since `95dcaca54f` made
the `pull-request` surface sandbox-independent without updating the
test. The test and module doc now match the code.

## Verification
Duplicate-adds, tripwires, resolution-check, unsupported-methods,
lockfile, format, lint, typecheck and the production build are all
green.

The full test pass hit its one-hour deadline, so I ran the remaining
packages one at a time. These still fail when run alone, in the same
files the 2026-10-15 merge logged as sandbox failures:
- `shared`: `AgentScope`
- `t3`: `ServerBrowserPage`, `AcpAdapterV2`
- `provider-opencode`: `OpenCodeServerLedger`
- `ssh`: `tunnel.test.ts`

`t3`'s `OrchestratorReplayFixtures` `claude_result_is_error` fails only
because this sandbox sets `CLAUDE_CONFIG_DIR`; it passes with that
unset. Every other package passes.

## Upstream features

### Usable as-is
- Folder breadcrumbs navigate to parent folders (pingdotgg#15045).
- Chat view, composer and sidebar no longer keep old renders alive
(pingdotgg#19869, pingdotgg#19852).
- Thread list cache saves without re-encoding every thread (pingdotgg#19915).
- React Compiler runs natively (pingdotgg#20114).
- Linked pull requests are virtualized (pingdotgg#20107).
- A linked PR can be dragged into the composer as a context chip
(pingdotgg#20102).
- HTML renders show a skeleton and open images in the image viewer
(pingdotgg#19654, pingdotgg#20036).
- Model picker search lists the newest models first (pingdotgg#18494).
- Wrapped split diffs stop jumping while scrolling (pingdotgg#20022).
- Text selection continues under the turn minimap (pingdotgg#16748).
- Sent queued messages say "from queue" (pingdotgg#19849).
- Settings stay compact with many connected machines (pingdotgg#19744).
- Middle-click and a setting to open PR links on GitHub (pingdotgg#19854,
pingdotgg#19719).
- Text generation no longer falls back to the flagship model (pingdotgg#19722).
- Pending context handoff is hidden on threads with no sent message
(pingdotgg#20033).
- Mobile and desktop changes don't affect the fork's web client.
- Not taken: the Beta branding (pingdotgg#20032).

### Unsupported in Moatless / needs implementation
- **Live command output** (pingdotgg#15094):
`orchestration.subscribeCommandOutput`, shown in
`CommandOutputPanel.tsx` behind `capabilities.commandOutputStreaming`.
Covered by the new union entry.
- **Database storage notice and `t3 doctor` reclaim** (pingdotgg#19436, pingdotgg#19437):
`server.getDatabaseStorage`, shown in `DatabaseStorageNotice.tsx` behind
`capabilities.databaseStorage`. Covered by the new union entry.
- **Delete a branch after its PR merges or closes** (pingdotgg#19257): a
`pullRequests.runAction` option. The action is refused here.
- **Device setup through a remote device host only** (pingdotgg#20077): behind
`FEATURES.deviceHub`.

### Backend behavior to consider reproducing in Moatless
- Resumed Claude subagents keep updating their own thread and pick up
the parent's renamed worktree branch (pingdotgg#20061, pingdotgg#20079).
- Steering no longer ends running subagents (pingdotgg#19210).
- Worktrees shared by subagents are cleaned up, and orphaned ones are
reported (pingdotgg#19748, pingdotgg#19749).
- Superseded history is compacted in the background (pingdotgg#19435).
- A run whose provider stream ends without a terminal event fails
instead of hanging (pingdotgg#19131).
- One malformed ACP session update no longer ends the session (pingdotgg#20015).
- A dropped HTTP/2 connection no longer crashes the server (pingdotgg#18378).
- PR refreshes are bounded by tracker reads (pingdotgg#19403).
- Cursor threads in Auto use Cursor's Auto-review (pingdotgg#19712).
- Grok health checks no longer start its updater (pingdotgg#18075).
- Expanding a wide ignored folder no longer classifies every child
(pingdotgg#20062).
- OTLP exports name the machine and build they came from (pingdotgg#19370).

Recorded in `docs/fork/gaps.md` under "Runtime fixes upstream made to
its own server".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---
Moatless task:
https://moatless.soaplabstest.com/tasks/d6f425f2-8234-412e-8189-1c71b309c04a
Pipyakas pushed a commit to Pipyakas/t3code that referenced this pull request Oct 19, 2026
…read (pingdotgg#19915)

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e2b929f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Saving the thread list cache in the web client blocks the UI thread for about 150 ms with 1,600 threads

2 participants