Description
The WebKit WebContent renderer process (WKWebView) grows to 2.5 GB physical footprint after ~1.3 days of continuous runtime. This is a memory accumulation bug, not a crash-triggering leak — it degrades system performance over time as the process is paged out.
Physical footprint peak observed: 3.1 GB, stable at 2.5 GB before investigation.
Physical footprint: 2.5G
Physical footprint (peak): 3.1G
Live process analysis (vmmap PID 55616, macOS 26.5.1, Tauri variant):
| Region |
Dirty |
Swapped |
Physical Cost |
% of 2.5G |
| WebKit Malloc (DOM, styles, layers) |
183 MB |
1,500 MB |
~1,683 MB |
67% |
| Graphics buffers (offscreen renders) |
79 MB |
609 MB |
~688 MB |
27% |
| V8/JS heap (DefaultMallocZone + MALLOC_SMALL) |
9 MB |
21 MB |
~30 MB |
1% |
| JS VM Gigacage + JIT |
4 MB |
16 MB |
~20 MB |
1% |
| Everything else |
4 MB |
54 MB |
~58 MB |
4% |
2.7 million allocations in the WebKit Malloc zone, totaling 5.5 GB allocated.
Environment
- CodeNomad variant: Tauri
- OS: macOS 26.5.1 (25F80)
- Runtime: ~1.3 days (launched 2026-06-13 01:39)
- Observability: Renderer PID 55616, no remote debugging port open (no
--inspect flag)
Root Cause
Three independent issues interact to produce the 2.5 GB:
1. Tool outputs rendered as full DOM with no size limits (P0)
Every tool call's output (bash, write, edit, read, etc.) is rendered as complete DOM nodes when expanded. Default state is expanded for all non-read tools.
stores/preferences.tsx:150 — default preference:
toolOutputExpansion: "expanded"
components/tool-call.tsx:638-641 — output section always expanded:
const outputSectionExpanded = () => {
const override = outputSectionOverride()
if (override !== null) return override
return true // <-- always expanded
}
components/tool-call.tsx:609-622 — defaultExpandedForTool returns true for all non-read tools.
components/tool-call/renderers/bash.tsx:60 — no size cap:
return [command, outputResult?.text].filter(Boolean).join("\n")
components/tool-call/renderers/bash.tsx:91-98 — rendered as full innerHTML:
<Show when={finalAnsiHtml()} fallback={...}>
{(html) => <div innerHTML={html()} />}
</Show>
Message-level virtual scrolling (virtua, 800px overscan) exists, but within each visible message, every tool call's full output is a complete DOM subtree. WebKit's DOM tree + render layers + compositing multiplies text size by ~3-5x. This is why 67% of the physical footprint lives in WebKit Malloc — it's the DOM tree and render layers, not JS heap.
2. Message store never evicts data (P0)
The message store (stores/message-v2/instance-store.ts) accumulates all messages, parts, part data strings, permissions, questions, todos, and scroll state with zero automatic eviction.
instance-store.ts:578-579 — unbounded pending parts buffer:
function bufferPendingPart(entry: PendingPartEntry) {
setState("pendingParts", entry.messageId, (list = []) => [...list, entry])
}
instance-store.ts:714 — unbounded string concatenation during streaming:
part[input.field] = `${currentValue ?? ""}${input.delta}`
instance-store.ts:1162 — clearSession() exists but is only called on explicit user deletion.
instance-store.ts:1262 — clearInstance() only called on instance removal.
There is NO automatic eviction — no LRU, no TTL, no memory-pressure trigger.
3. Session state never cleaned on instance close (P1)
When an instance is stopped, removeInstance() (stores/instances.ts:626-668) cleans 7 stores but misses 15+ per-instance signals in session-state.ts:
function removeInstance(id: string) {
setInstances(...); // ✅
removeLogContainer(id); // ✅
clearCommands(id); // ✅
clearPermissionQueue(id); // ✅
clearRepliedPermissions(id); // ✅
clearQuestionQueue(id); // ✅
clearInstanceMetadata(id); // ✅
clearCacheForInstance(id); // ✅
messageStoreBus.unregisterInstance(id); // ✅
clearInstanceDraftPrompts(id); // ✅
// ❌ sessions, activeSessionId, activeParentSessionId,
// agents, providers, messagesLoaded, messageLoadErrors,
// sessionInfoByInstance, threadTotalsByInstance,
// expandedSessionParents, sessionPagination, sessionSearch,
// instanceIndicatorCounts, loading, sessionThreadCache
}
The sessions signal (stores/session-state.ts:37) is Map<string, Map<string, Session>> keyed by instanceId — when an instance is closed, its Map entry is never deleted. All sessions for that instance persist forever.
Steps to Reproduce
- Launch CodeNomad (Tauri or Electron variant)
- Create a workspace/session and run several agent conversations with tool calls (bash, file writes, diffs — the larger the output, the faster the growth)
- Keep the app running for 24+ hours
- Observe renderer memory via Activity Monitor or
vmmap:
vmmap <WebContent PID> -summary
The process will show 2-3 GB physical footprint dominated by WebKit Malloc and graphics memory.
Expected Behavior
- Renderer memory should stay below ~500 MB under sustained use
- Sessions not actively viewed should not retain full DOM
- Closed instances should not retain any session data
- Old sessions should be evicted from the in-memory message store
Suggested Fixes
P0 — Truncate / virtualize tool output rendering
- Change default to collapsed — Show collapsed tool outputs with a "Show output" button, especially for outputs >10 KB
- Add size caps on rendered output — Cap bash and other tool output DOM at ~10,000 characters with a "Show full output" expand button
- Use
IntersectionObserver for lazy DOM — Only create tool output DOM nodes when the tool call is near the viewport
P0 — Automatic message store eviction
- Auto-call
clearSession() when user navigates away from a session (or after inactivity timeout)
- Add per-instance and global memory caps — e.g., 50 MB per instance, 200 MB global, with oldest-session-first eviction
- Add periodic memory pressure check — poll
performance.memory?.usedJSHeapSize, trigger eviction when over threshold
P1 — Clean session state on instance close
- Add
purgeInstanceSessions(instanceId) to stores/session-state.ts that clears all 15+ per-instance signal entries when an instance is removed
- Call it from
removeInstance() in stores/instances.ts
P2 — Streaming delta optimization
- Buffer chunks in an array and join at end instead of repeated string concatenation (reduces O(n²) temporary allocations during large streaming responses)
Related Files
packages/ui/src/stores/message-v2/instance-store.ts — message store with no eviction
packages/ui/src/components/tool-call.tsx — tool call expansion defaults
packages/ui/src/components/tool-call/renderers/bash.tsx — no size cap on bash output
packages/ui/src/stores/session-state.ts — signals never cleaned on instance close
packages/ui/src/stores/instances.ts — removeInstance() missing session cleanup
packages/ui/src/stores/preferences.tsx — default toolOutputExpansion: "expanded"
Description
The WebKit WebContent renderer process (WKWebView) grows to 2.5 GB physical footprint after ~1.3 days of continuous runtime. This is a memory accumulation bug, not a crash-triggering leak — it degrades system performance over time as the process is paged out.
Physical footprint peak observed: 3.1 GB, stable at 2.5 GB before investigation.
Live process analysis (vmmap PID 55616, macOS 26.5.1, Tauri variant):
2.7 million allocations in the WebKit Malloc zone, totaling 5.5 GB allocated.
Environment
--inspectflag)Root Cause
Three independent issues interact to produce the 2.5 GB:
1. Tool outputs rendered as full DOM with no size limits (P0)
Every tool call's output (bash, write, edit, read, etc.) is rendered as complete DOM nodes when expanded. Default state is expanded for all non-read tools.
stores/preferences.tsx:150— default preference:components/tool-call.tsx:638-641— output section always expanded:components/tool-call.tsx:609-622—defaultExpandedForToolreturnstruefor all non-read tools.components/tool-call/renderers/bash.tsx:60— no size cap:components/tool-call/renderers/bash.tsx:91-98— rendered as full innerHTML:Message-level virtual scrolling (
virtua, 800px overscan) exists, but within each visible message, every tool call's full output is a complete DOM subtree. WebKit's DOM tree + render layers + compositing multiplies text size by ~3-5x. This is why 67% of the physical footprint lives in WebKit Malloc — it's the DOM tree and render layers, not JS heap.2. Message store never evicts data (P0)
The message store (
stores/message-v2/instance-store.ts) accumulates all messages, parts, part data strings, permissions, questions, todos, and scroll state with zero automatic eviction.instance-store.ts:578-579— unbounded pending parts buffer:instance-store.ts:714— unbounded string concatenation during streaming:instance-store.ts:1162—clearSession()exists but is only called on explicit user deletion.instance-store.ts:1262—clearInstance()only called on instance removal.There is NO automatic eviction — no LRU, no TTL, no memory-pressure trigger.
3. Session state never cleaned on instance close (P1)
When an instance is stopped,
removeInstance()(stores/instances.ts:626-668) cleans 7 stores but misses 15+ per-instance signals insession-state.ts:The
sessionssignal (stores/session-state.ts:37) isMap<string, Map<string, Session>>keyed by instanceId — when an instance is closed, itsMapentry is never deleted. All sessions for that instance persist forever.Steps to Reproduce
vmmap:The process will show 2-3 GB physical footprint dominated by WebKit Malloc and graphics memory.
Expected Behavior
Suggested Fixes
P0 — Truncate / virtualize tool output rendering
IntersectionObserverfor lazy DOM — Only create tool output DOM nodes when the tool call is near the viewportP0 — Automatic message store eviction
clearSession()when user navigates away from a session (or after inactivity timeout)performance.memory?.usedJSHeapSize, trigger eviction when over thresholdP1 — Clean session state on instance close
purgeInstanceSessions(instanceId)tostores/session-state.tsthat clears all 15+ per-instance signal entries when an instance is removedremoveInstance()instores/instances.tsP2 — Streaming delta optimization
Related Files
packages/ui/src/stores/message-v2/instance-store.ts— message store with no evictionpackages/ui/src/components/tool-call.tsx— tool call expansion defaultspackages/ui/src/components/tool-call/renderers/bash.tsx— no size cap on bash outputpackages/ui/src/stores/session-state.ts— signals never cleaned on instance closepackages/ui/src/stores/instances.ts—removeInstance()missing session cleanuppackages/ui/src/stores/preferences.tsx— defaulttoolOutputExpansion: "expanded"