Skip to content

perf: opening a thread loads 85% less data - #24899

Merged
t3dotgg merged 1 commit into
mainfrom
perf/provider-snapshots-on-demand
Oct 25, 2026
Merged

t3dotgg merged 1 commit into
mainfrom
perf/provider-snapshots-on-demand

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Oct 25, 2026

Copy link
Copy Markdown
Member

Problem

Opening a thread loaded 0.3 to 3 MB, even when the thread itself was only 16 to 75 KB. Mobile felt it most.

The real cost was provider config. Each provider keeps per-folder "workspace snapshots" (slash commands and skills, 8 to 27 KB each, up to 16 per provider). Every thread open asked the server to scan the thread's folder. The reply held every provider with every snapshot, and the server also pushed that same list to every connected client. A deleted worktree never finished scanning, so its composer retried every 10 seconds, forever.

Images above the visible area also loaded at full size, because the timeline mounts nearby rows.

Fix

  • Snapshots on demand. Clients send workspaceSnapshotsOnDemand. The config stream then leaves snapshots out, so a rescan sends nothing. A folder refresh returns only that provider's snapshot for that folder. The client keeps what it fetched and merges it back into config, so web and mobile readers do not change.
  • Compatible both ways. Old clients send no flag and get the same payload as before. New clients merge fetched snapshots only when the server advertises providerWorkspaceSnapshotsOnDemand. Against older servers they use the config as is.
  • No retry loop. Claude reports a deleted folder as scanned, not pending. Web and mobile composers stop after 3 failed tries per folder. The 3-try cap is what limits other providers.
  • Shared scans. A second request for a folder that is being scanned waits for that scan instead of returning empty. The scan keeps running if the client that started it disconnects.
  • Fresh data. A fetched snapshot is dropped when the provider-wide commands, skills, or enabled state change. Examples: Antigravity's first live session, a late /usage-limits, a sign-out. The composer then fetches the snapshot again at once.
  • Lazy images. Web timeline images and attachment thumbnails use loading="lazy". Mobile's list already draws only rows near the screen.

Result

Real data copy (325 threads), dev server restarted and client caches cleared for each side, mobile viewport, same 7 thread opens. Bytes are WebSocket JSON plus HTTP, without dev-mode JS.

Load main this PR cut
App load (new thread) 521 KB 393 KB 25%
Open thread 1 1423 KB 203 KB 86%
Open thread 2 277 KB 94 KB 66%
Open thread 3 708 KB 40 KB 94%
Open thread 4 802 KB 88 KB 89%
Open thread 5 808 KB 48 KB 94%
Open thread 6 943 KB 137 KB 85%
Open thread 7 (71 child threads) 3046 KB 293 KB 90%
Total 8528 KB 1296 KB 85%

App load includes 228 KB of dev-only brand icons. Production icons are 114 KB and cached. Without them, app load goes from 298 KB to 170 KB. App load plus a typical thread open is now about 260 KB. JS bundle size is a separate track and is not counted here.

Limits

  • Another device's scan, or a provider rebuilt after a settings change, shows on this client's next refresh, at most one 5 minute TTL later.
  • Fetched snapshots stay in memory. After a cold start, the composer shows provider-wide skills until its first refresh returns.
  • Still large on some threads: getThreadFamily sends about 2.5 KB per child thread (176 KB for 71 children). That is a follow-up.

Proof

  • New tests, each fails without its fix:
    • ws.test.ts: the stream drops snapshots, a snapshot-only rescan sends nothing, and a scoped refresh returns one folder.
    • ProviderRegistry.test.ts: a second request waits for the running scan, still gets the result after the first request leaves, and only one scan runs.
    • server.test.ts: the merge, its capability gate, identity reuse, and dropping a snapshot when provider-wide commands change.
    • ClaudeCapabilitiesProbe.test.ts: a deleted folder is not pending.
  • vp run check:changed passes.
  • Real app on the branch: the A/B above shows one refresh per thread open and no config pushes. The new-thread / menu still lists repo-scoped skills from the folder snapshot, and /usage-limits.

Surfaces: web and desktop share the client-runtime path. Mobile uses the same path and gets the same retry cap.

Builds on main, not #24692. The council measured the thread payload at 1 to 8% of a thread open, so lazy folded rows were not worth the extra protocol.

Council review

Rounds: 4. A round 0 before any code also chose the cuts.

Delegate Round 1 Round 2 Round 3 Round 4
Claude Opus 5.5 7/10 8.5/10 9/10 9.5/10
Muse Wonder Full 8/10 9/10 9.5/10 9.5/10
Codex GPT-6.1 Sol 7/10 8/10 9/10 9/10

Created with Claude Opus 5.5 in Claude Code.

🤖 Generated with Claude Code

Provider workspace snapshots (per-folder slash commands and skills) were
sent in full on every config push and every folder refresh. Clients now opt
into snapshots on demand: the config stream leaves them out, a folder
refresh returns only the requested folder, and the client merges what it
fetched back into config. Also stops the endless rescan of deleted
worktrees, shares in-flight scans, and lazy-loads web timeline images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Oct 25, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This PR introduces a cross-cutting on-demand workspace-snapshot protocol, client-side cache/merge logic, and changed ProviderRegistry scan lifecycle behavior. It also enables the new transport mode by default, so the resulting production behavior and default configuration path merit human review.

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

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.9 KiB 20.9 KiB 0 B (0.0%) 29.3 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.2 KiB 21.2 KiB 0 B (0.0%) 29.3 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Baseline: 5b35402 · PR result: 67b8ab3 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 5b35402 and 67b8ab3.

Files selected for processing: 16
  • apps/mobile/src/features/threads/use-composer-command-menu.ts
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/provider/ClaudeCapabilitiesProbe.test.ts
  • apps/server/src/provider/ClaudeProvider.ts
  • apps/server/src/provider/ProviderRegistry.test.ts
  • apps/server/src/provider/ProviderRegistry.ts
  • apps/server/src/ws.test.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/chat/ChatMarkdownMedia.tsx
  • apps/web/src/components/chat/TimelineUserMessageRow.tsx
  • apps/web/src/components/chat/useProviderWorkspaceSnapshotRefresh.ts
  • packages/client-runtime/src/rpc/session.ts
  • packages/client-runtime/src/state/server.test.ts
  • packages/client-runtime/src/state/server.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/rpc.ts

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

Walkthrough

The change adds on-demand workspace snapshot delivery, client-side snapshot caching, and limits on failed refresh retries. It also updates workspace scan coordination and Claude probing, and enables lazy loading for chat images.

Changes: On-demand workspace snapshots

Layer / File(s) Summary
Snapshot contract and scan handling
packages/contracts/src/environment.ts, packages/contracts/src/rpc.ts, apps/server/src/environment/ServerEnvironment.ts, apps/server/src/provider/ProviderRegistry.ts, apps/server/src/provider/ProviderRegistry.test.ts, apps/server/src/provider/ClaudeProvider.ts, apps/server/src/provider/ClaudeCapabilitiesProbe.test.ts
The environment and RPC contracts add on-demand snapshot options. Workspace scan refreshes coordinate through deferreds, and a test covers a waiting refresh after interruption. Claude probing skips a nonexistent working directory, with a test for slashCommandsPending.
Server snapshot delivery
apps/server/src/ws.ts, apps/server/src/ws.test.ts
Config subscriptions can omit workspace snapshots. On-demand refreshes return only the requested instance and working-directory snapshot; provider-status updates omit snapshots when requested. WebSocket tests cover these responses and updates.
Client snapshot fetching and cache
packages/client-runtime/src/rpc/session.ts, packages/client-runtime/src/state/server.ts, packages/client-runtime/src/state/server.test.ts, apps/mobile/src/features/threads/use-composer-command-menu.ts, apps/web/src/components/chat/useProviderWorkspaceSnapshotRefresh.ts
Client subscriptions request on-demand snapshots. The client records and merges fetched snapshots when the saved provider basis matches, and tests cover cache behavior. Mobile and web refresh hooks stop retrying after three failures for a key.

Chat image loading

Layer / File(s) Summary
Lazy loading for chat images
apps/web/src/components/chat/ChatMarkdownMedia.tsx, apps/web/src/components/chat/TimelineUserMessageRow.tsx
Markdown images and attachment previews set image loading to lazy.

Priority: Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant ClientRuntime
  participant WebSocketServer
  participant ProviderRegistry
  participant ClientSnapshotState
  ClientRuntime->>WebSocketServer: Subscribe with workspaceSnapshotsOnDemand
  WebSocketServer-->>ClientRuntime: Config without workspace snapshots
  ClientRuntime->>WebSocketServer: Refresh requested instance and cwd
  WebSocketServer->>ProviderRegistry: Refresh workspace snapshot
  ProviderRegistry-->>WebSocketServer: Workspace snapshot
  WebSocketServer-->>ClientRuntime: Return requested snapshot
  ClientRuntime->>ClientSnapshotState: Record and merge snapshot
Loading

Possibly related issues

  • Issue 24375: The changes add per-working-directory snapshot delivery and refresh; the reported wrong-working-directory discovery objective may remain unresolved.
  • Issue 24378: The changes add per-working-directory snapshot delivery and refresh; the reported wrong-working-directory discovery objective may remain unresolved.
  • Issue 23507: The changes affect when workspace skill snapshots load, but the reported plugin-discovery gap may remain unresolved.

Suggested reviewers: juliusmarminge, exotic209093, justyannicc

Merge Risk: Minimal · up to 67b8a

The supplied evidence does not establish a merge-blocking problem. Complete normal checks before merging.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly states the primary performance improvement: opening a thread transfers substantially less data. It is concise and relevant to the changes.
Description check Passed The description is detailed and covers the problem, implementation, compatibility behavior, limitations, verification, test coverage, and measured results. The scope and approval section does not expl…
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 is detailed and covers the problem, implementation, compatibility behavior, limitations, verification, test coverage, and measured results. The scope and approval section does not explicitly link a triaged issue or maintainer approval, but the overall description is complete enough to pass.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • 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.

@t3dotgg
t3dotgg merged commit abd9d9f into main Oct 25, 2026
29 checks passed
@t3dotgg
t3dotgg deleted the perf/provider-snapshots-on-demand branch October 25, 2026 21:01
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Oct 26, 2026
## What's Changed
* fix(server): attachments are no longer silently dropped from prompts by @maria-rcks in pingdotgg/t3code#21356
* fix(mobile): images picked with choose files reach the agent as images by @maria-rcks in pingdotgg/t3code#21352
* feat(web): attach svg files to messages by @maria-rcks in pingdotgg/t3code#21355
* feat(server): send pdf attachments to claude and antigravity as documents by @maria-rcks in pingdotgg/t3code#21358
* feat(mobile): play audio inline and open shared files in the viewer by @maria-rcks in pingdotgg/t3code#21353
* feat(web): play audio inline and open any file the agent shares in the viewer by @maria-rcks in pingdotgg/t3code#21363
* perf(pull-requests): faster pull request timeline, code tab, summary and search by @maria-rcks in pingdotgg/t3code#22697
* perf(mobile): bound streaming markdown cache and pending work by @juliusmarminge in pingdotgg/t3code#24263
* fix(web): question options show a radio and an aligned shortcut key by @maria-rcks in pingdotgg/t3code#23255
* fix(server): notify parents after delegated background runs by @juliusmarminge in pingdotgg/t3code#23542
* fix(server): running delegates tell agents to end the turn by @t3dotgg in pingdotgg/t3code#24887
* perf(ci): cache server tests individually across shards by @juliusmarminge in pingdotgg/t3code#24966
* feat(mobile): one cached native-client install command for agents and machines by @juliusmarminge in pingdotgg/t3code#24965
* perf(source-control): pull request previews read only the pull request by @maria-rcks in pingdotgg/t3code#22583
* feat(infra): native client cache panels on the task cache dashboard by @juliusmarminge in pingdotgg/t3code#24994
* feat(web): UI design overhaul, with the new look behind a Beta toggle by @t3dotgg in pingdotgg/t3code#24865
* test(server): tests no longer depend on the host machine by @t3dotgg in pingdotgg/t3code#25001
* perf: opening a thread loads 85% less data by @t3dotgg in pingdotgg/t3code#24899
* fix(server): orchestrator records command count and duration again by @yordis in pingdotgg/t3code#23966
* fix(observability): every app reports its own environment and release channel by @yordis in pingdotgg/t3code#24340
* chore(lint): React effects can't grow past each file's budget by @juliusmarminge in pingdotgg/t3code#24964
* fix(observability): every exported resource names its service instance by @yordis in pingdotgg/t3code#23973
* fix(install): stable installs no longer fail behind a page of nightlies by @t3dotgg in pingdotgg/t3code#22907
* fix(server): Grok refresh no longer restores removed models from cache by @t3dotgg in pingdotgg/t3code#22849
* fix(server): resume fallback keeps the latest turn when an older handoff is pending by @t3dotgg in pingdotgg/t3code#22902
* fix(client-runtime): "Codex (hub)" badges read CH instead of C( by @t3dotgg in pingdotgg/t3code#22918
* fix(mobile): retain requested PR environment while connecting by @juliusmarminge in pingdotgg/t3code#24262
* fix(web): usage-limit banner reset time follows the Time format setting by @t3dotgg in pingdotgg/t3code#22915
* fix(web): terminal drag selection scrolls faster the farther past the edge by @t3dotgg in pingdotgg/t3code#22872
* fix(observability): resources add OTel semconv runtime and browser keys by @yordis in pingdotgg/t3code#23972
* chore(lint): main passes lint again with effects merged after the budget by @t3dotgg in pingdotgg/t3code#25055
* fix(server): ended roots no longer leave provider turns running by @t3dotgg in pingdotgg/t3code#22846
* feat(mobile): open threads and devices in a resizable iOS inspector by @juliusmarminge in pingdotgg/t3code#24985
* chore(web): measure renders before/after with render scenarios by @juliusmarminge in pingdotgg/t3code#24980
* fix(web): asset URLs re-mint per revision and before expiry without effects by @juliusmarminge in pingdotgg/t3code#24970
* fix(web): queued-message edits survive navigation and settle once by @juliusmarminge in pingdotgg/t3code#24972
* refactor(mobile): thread visits are tracked outside React by @juliusmarminge in pingdotgg/t3code#24973
* refactor(client): cloud account transitions run in one service by @juliusmarminge in pingdotgg/t3code#24974
* refactor(client): one VCS status refresh owner per checkout by @juliusmarminge in pingdotgg/t3code#24975
* refactor(web): document appearance and title are applied from app start by @juliusmarminge in pingdotgg/t3code#25004
* refactor(web): app shell shortcuts register once through one command dispatcher by @juliusmarminge in pingdotgg/t3code#25016
* refactor(web): shared intersection and document-activity stores by @juliusmarminge in pingdotgg/t3code#25020
* refactor(web): the theme store and diff worker pool stop syncing the theme through effects by @juliusmarminge in pingdotgg/t3code#25007
* fix(server): keep history discovery off the writer and pace compaction by @JustYannicc in pingdotgg/t3code#20877
* perf(mobile): compact persisted thread snapshots by @juliusmarminge in pingdotgg/t3code#24265


**Full Changelog**: pingdotgg/t3code@v0.1.3-nightly.20261025.3682...v0.1.3-nightly.20261025.3692

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.3-nightly.20261025.3692
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant