Summary
The Brain panel memory list grows RSS unbounded during hover interaction and does not reclaim memory after the panel closes. ~956 MiB RSS observed; closing the panel does not reduce it.
Root causes (in order of impact)
1. document.addEventListener accumulation (critical)
renderMemoryList() adds one document.addEventListener('click', ..., { once: false }) for every memory item on every render pass (line 956). With 50 items and 10 renders, 500 persistent document-level listeners accumulate, each holding a closure over a dropdown DOM element. These are never removed. Zero removeEventListener calls exist anywhere in memory.js (51 add / 0 remove).
2. No event listener cleanup on re-render
memoryList.innerHTML = '' at line 685 destroys the old item DOM nodes, but all their event listeners remain alive in memory until the GC happens to collect them. Because Qt does not forward OS memory pressure, that GC never fires during normal use.
3. Animations run when modal is hidden
All ::after sweep animations on memory items continue running when #memory-modal gains .hidden. The compositor keeps raster tiles allocated for all items (including off-screen). Qt never evicts them.
4. No cleanup or GC trigger on panel close
Closing the Brain modal only toggles .hidden. No event listeners are released, no open dropdowns are removed, no GC is triggered.
Fix
- Add an
AbortController scoped to each renderMemoryList() call. Abort the previous controller before re-rendering to release all item listener closures before the DOM is cleared. Thread { signal } through all 14 item-level addEventListener calls.
- Move the
document.addEventListener for outside-click dropdown close inside the menuBtn.click handler with { once: true, signal } — eliminates per-render accumulation and ensures it fires at most once per dropdown open.
- Add a
MutationObserver on #memory-modal that runs cleanup (close open dropdown, abort controller, trigger GC) when the modal becomes hidden.
- CSS: add
animation-play-state: paused when #memory-modal.hidden to stop all compositor animation work while the panel is not visible.
- Add
odysseus:modal-closed CustomEvent in modalManager.js (parallel to existing odysseus:modal-opened).
Summary
The Brain panel memory list grows RSS unbounded during hover interaction and does not reclaim memory after the panel closes. ~956 MiB RSS observed; closing the panel does not reduce it.
Root causes (in order of impact)
1. document.addEventListener accumulation (critical)
renderMemoryList()adds onedocument.addEventListener('click', ..., { once: false })for every memory item on every render pass (line 956). With 50 items and 10 renders, 500 persistent document-level listeners accumulate, each holding a closure over a dropdown DOM element. These are never removed. ZeroremoveEventListenercalls exist anywhere in memory.js (51 add / 0 remove).2. No event listener cleanup on re-render
memoryList.innerHTML = ''at line 685 destroys the old item DOM nodes, but all their event listeners remain alive in memory until the GC happens to collect them. Because Qt does not forward OS memory pressure, that GC never fires during normal use.3. Animations run when modal is hidden
All
::aftersweep animations on memory items continue running when#memory-modalgains.hidden. The compositor keeps raster tiles allocated for all items (including off-screen). Qt never evicts them.4. No cleanup or GC trigger on panel close
Closing the Brain modal only toggles
.hidden. No event listeners are released, no open dropdowns are removed, no GC is triggered.Fix
AbortControllerscoped to eachrenderMemoryList()call. Abort the previous controller before re-rendering to release all item listener closures before the DOM is cleared. Thread{ signal }through all 14 item-level addEventListener calls.document.addEventListenerfor outside-click dropdown close inside themenuBtn.clickhandler with{ once: true, signal }— eliminates per-render accumulation and ensures it fires at most once per dropdown open.MutationObserveron#memory-modalthat runs cleanup (close open dropdown, abort controller, trigger GC) when the modal becomes hidden.animation-play-state: pausedwhen#memory-modal.hiddento stop all compositor animation work while the panel is not visible.odysseus:modal-closedCustomEvent in modalManager.js (parallel to existingodysseus:modal-opened).