fix(frontend): release listeners and registries that outlive owners - #831
Merged
Neonforge98 merged 1 commit intoAug 17, 2026
Merged
Conversation
Lifecycle-leak sweep over WorkStation, engines and hooks. All fixes are behavior-neutral: subscribeSession disposer no longer unregisters a newer live listener Set after evictSessionCache; loadMore search listeners are released in finally (rejected requests leaked both); command-detection state is pruned when a terminal is removed; a deleted session's editor cache and active-repo pointer are disposed with its workspace; the Communication message-list memo moved from module-scope slots to a WeakMap keyed by the events array; partially activated WebGL addons are disposed on the throw path before their slot is released; TurnMetadataFooterSlot only touches the turn-metadata family with a real session id. Tests cover each. Full audit notes in docs/memory-audit-2026-08-16. Pre-commit hook ran. Total eslint: 0, total circular: 0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third frontend RAM pass: two thorough lifecycle sweeps (effects without cleanup, module-scope maps, registries holding DOM/xterm/EditorView, xterm/webview teardown, per-session atom families) over WorkStation, engines, hooks and scaffold. The codebase turned out disciplined; this PR fixes the genuine defects found — every change is behavior-neutral (nothing a user can see changes; only what is retained after it should have been released). Each has a regression test.
Problem
SnapshotCacheManager.subscribeSessionreturned a disposer that captured the listenerSetit was created with and deleted the registry entry whenever that Set emptied.evictSessionCache(reached by reload / manual compact / edit-message / sidebar delete while consumers are still mounted) drops the entry; a later subscriber installs a fresh Set, and the stale disposer then silently unregistered the live Set — mounted consumers stopped receivinges:changedpushes and pinned their last full snapshot.useSearchResults.loadMoreunlistened its two Tauri listeners only on the success path; a rejectedsearchCodeStreaming(invalid regex, repo unmounted) left both — each closing over the entire current result set — registered for the process lifetime and running on every latersearch-resultevent.removeCommandDetectionAtomhad zero callers.session:<id>editor cache / active-repo pointer (heap + a localStorage blob parsed at boot).Chat/Communication/config.tskept a module-scope single-slot memo of the last-built session's fullSessionEvent[]andMessageEntrytrees, outliving unmount / session switch / session close.loadAddon/activatewas never disposed while its budget slot was returned (orphaned GL context; the 8-slot budget could be exceeded).TurnMetadataFooterSlottouchedturnMetadataAtomFamilywithsessionId ?? "", creating empty-session-id entries that the loader's GC never retains or removes.Solution
snapshotCacheManager.ts).finally(useSearchResults.ts).removeCommandDetectionAtomcalled from both terminal-removal paths (terminal/index.ts).disposeEditorCacheForSessionAtom(editorCache.ts), wired into the single dispose callback both session-delete paths use (useWorkstationSidebarHandlers.ts).WeakMap(same hit semantics, no retention).webglAddon?.dispose()on the throw path interminalSetup.tsandXtermOutput/index.tsx.TurnMetadataFooterSlotsplit into a wrapper (null/visibility gate) + body that only reads the family with a real session id.Potential risks
WeakMapmemo hits are a superset of the old single-slot memo (never stale — keyed by array identity). Everything else is cleanup on close/error paths.cursorIdeTurnSummariesAtomFamilyretention for browsed Cursor sessions (needs mount-gated GC), two ~100 B/session maps with reconcile semantics, and the unreferencedsearch/fileTrackingAtom.ts+search/cacheAtom.tsmodules (dead-code deletion candidates).Validation / Test plan
pnpm typecheckclean; lint-staged (eslint + tsc) passed on commit.subscribeSessionDisposer.test.ts(stale disposer must not unregister a newer live Set),useSearchResults.loadMoreListeners.test.ts(both listeners released on rejection),commandDetectionLifecycle.test.ts(both removal paths prune),editorCache.test.tsdispose case (memory + storage),utils.test.tsmemo identity/independence cases.