perf: prune per-thread client state and evict idle VCS status cache - #15301
RusiruSadathana wants to merge 1 commit into
Conversation
Long-lived sessions grew without bound in three places. On the client, deleting a thread left entries behind in the right-panel, diff-panel, mini-player, and ui-state stores (the zustand removeThread helpers existed but had no callers), previewStateAtom used Atom.keepAlive so one registry node per thread ever visited lived for the process lifetime, and a released browser surface left a tombstone in byTabId per preview tab ever opened. On the server, VcsStatusBroadcaster kept its per-cwd status cache for the broadcaster's lifetime after the last poller subscriber released. Deletion now prunes every per-thread store (both the live and archived paths), preview atoms use a 5-minute idle TTL (threads with live sessions stay resident via activePreviewSessionsAtom; idle threads re-seed from the next server snapshot), surface release deletes the entry, and the final poller release drops the cwd's cache entry inside the same critical section that removes the poller so a concurrent retain cannot lose its fresh entry. The orchestration read-model half of pingdotgg#4176 is not ported: orchestration V2 loads per-thread state from indexed SQLite inside each command and holds no process-wide in-memory read model, so the O(N) scans that change removed no longer exist. Closes pingdotgg#4178. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (9)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe changes remove cached VCS status when the final remote poller releases, clear per-thread client state after successful thread deletion, and remove browser surface entries when their owner releases them. ChangesVCS status cache
Per-thread client state
Browser surface release
Priority: ⬆️ High Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Bug fix · Severity of issue fixed: High Suggested reviewers: Merge Risk: ⚪ Minimal · up to The change prunes per-thread client state and evicts idle VCS status cache, and no concrete merge-blocking risk was identified. The author did not run an integrated check in a live client, but that is normal residual uncertainty. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The cleanup generally reduces retained state and preserves thread isolation. One lifecycle question remains: discarded preview state also contains safeguards against stale updates, which could allow a previously closed preview to reappear. No new unauthorized access or privilege gain was established. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Hardening Proposals
🚥 Pre-merge checks | ✅ 3 | ❌ 1 | ❓ 1❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
Full details: Linked Issues checkExplanation For [
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Problem
Long-lived sessions accumulated state that was never released: deleting a thread left entries behind in the right-panel, diff-panel, mini-player, and ui-state stores (the zustand
removeThreadhelpers existed with no callers, and ui-state had no removal at all — these persist to localStorage, so the growth survives restarts),previewStateAtomusedAtom.keepAliveso one registry node per thread ever visited lived for the tab's lifetime, a released browser surface left a tombstone inbyTabIdper preview tab ever opened, and the server'sVcsStatusBroadcasterkept its per-cwd status cache for the process lifetime after the last poller subscriber released. Re-ports the still-applicable parts of #4176, which was closed when #2829 deleted its V1 base.Change
useThreadActions.ts): both delete paths (live and archived) now prune every per-thread store — preview state, right panel, diff panel, preview mini player, and ui state — and the archived path additionally releases composer draft uploads before clearing the draft, matching the live path's order.previewStateStore.ts):previewStateAtommoves fromAtom.keepAliveto a 5-minute idle TTL. Threads with live preview sessions stay resident because the observedactivePreviewSessionsAtomreads them (so the desktop webview host and automation hosts are unaffected); idle threads are collected and re-seed from EMPTY, repopulated by the next server snapshot viausePreviewSession'spreview.listreconcile. AddsremovePreviewThreadfor explicit cleanup on deletion.browserSurfaceStore.ts):releasedeletes the entry instead of writing a{ visible: false, owner: null }tombstone. All readers already treat a missing entry as not-visible/not-present, so this is behavior-preserving, and a stalepresent()from a released lease can no longer resurrect the entry.VcsStatusBroadcaster.ts): when the last poller subscriber releases, the cwd's cached status is dropped inside the samemodifyEffectcritical section that removes the poller, so a concurrentretainRemotePollercannot have its freshly seeded entry wiped. A later subscriber re-loads and re-seeds the cache (its poller starts with an immediate refresh).The orchestration read-model half of #4176 is intentionally not ported: orchestration V2 loads per-thread state from indexed SQLite inside each command's dispatch (per-thread keyed lock, one commit transaction) and holds no process-wide in-memory read model, so the O(N) scans and unbounded in-memory growth that change removed no longer exist.
No visible UI changes.
Scope and approval
Triaged bug issue: #4178. Maintainer approval of direction and scope: juliusmarminge on #4176 — "Once #2829 merges, please rebase onto
main, port the change to the V2 equivalent, and reopen (or open a fresh PR). Ping me and I'll prioritise the review. This is not a judgement on the change itself. Several of these are real gaps we still want fixed." These changes are the port of the parts whose underlying code still exists; the web and server changes are one fix because they are the client and server halves of the same unbounded-retention problem from that issue.Verification
apps/server/src/vcs/VcsStatusBroadcaster.test.ts— extended the existing last-subscriber-disconnects test: after the final poller releases, a new subscription reloads local status (localStatusCalls1 → 2). Verified against unmodifiedmainthat this assertion fails (cache retained, counter stays 1) and passes with the change.apps/web/src/previewStateStore.test.ts— new test:removePreviewThreadresets a deleted thread's state to EMPTY, leaves other threads intact, and a later event for the removed thread still lands.apps/web/src/uiStateStore.test.ts— new test:removeThreadUiStatedrops both per-thread records and is a reference-preserving no-op for unknown threads.apps/web/src/browser/browserSurfaceStore.test.ts— release now asserts the entry is gone (and a stalepresent()cannot resurrect it); the fitted-presentation release test updated to match.tsc --noEmitclean for@t3tools/webandt3; targetedvp lintclean (one pre-existing warning onmain, untouched lines).Closes #4178.
Done with GLM-5.3 via the Claude Code harness in T3 Code.
🤖 Generated with Claude Code