Where does the bug appear (feature/product)?
Cursor IDE
Describe the Bug
- This is a follow up on a previous bug report I did yesterday. I am documenting the crashes, and I outline how I hot patched it locally.
One glass window, left up with local agent loops, died twice in the same day. Both deaths were the renderer JavaScript heap at the 4096 MB cap. Last-resort GC freed almost nothing. Exit code 132 (SIGILL) is V8’s fatal OOM path, not a bad CPU. The kernel did not OOM-kill it. The main process stayed up.
These are three separate retainers. Hiding inline diffs does not stop the first. A 15-minute idle GC does not stop any of them.
Crash 1 — 2026-09-16 01:31:36 WEST
Renderer pid 790635, up since 11:23 the previous day. Glass mode. Extension isolation forced off.
Heap: used=3912.3MB limit=4096.0MB Mark-Compact (reduce) 3912.3 (4010.5) → 3912.3 (4010.5) MB, last resort V8 javascript OOM (Reached heap limit) CodeWindow: renderer process gone (reason: crashed, code: 132)
RSS went from about 1.2 GB at 11:23 to 6.7–7.0 GB by midnight, then sat there. The renderer log has 3,081 potential listener LEAK detected errors. Every stack is new oF → ModelService._createModelData in workbench.glass.main.js. Each text model adds a language-selection listener that only comes off on dispose, and a tree-sitter parser starts on onModelAdded, on the UI thread, even if no editor is open.
A restart at 09:23:24, before any new prompt, logged:
Potential working copy LEAK detected, having 513 working copies already. Most frequent owner (258): file#
at nYl.acquire → nYl.doCreateReferencedObject → _vc.doResolve → new _9e → registerWorkingCopy
typeId is "" (iwt=""), so every file text model prints as file#<no typeId>. The “258” is not a second owner. Tracking starts at 256, so 513 − 256 + 1 = 258 tracked registrations of the same stack. The reporter prints once and then goes silent. LEAK_TRACKING_THRESHOLD is 256, LEAK_REPORTING_THRESHOLD is 512. It only logs. It does not evict.
Crash 2 — 2026-09-16 19:52:52 WEST
Renderer pid 915788, started 11:24 the same day, after a local patch that stopped the startup diff restore (below). It still died, 8.5 hours later. RSS peak 8.1 GB. UI blocked about 18 seconds (RendererPing blocked ~18036 ms).
Heap: used=3911.9MB limit=4096.0MB Mark-Compact 3951.7 → 3902.5 MB, 1717 ms Mark-Compact 3965.7 → 3900.5 MB, 1528 ms V8 javascript OOM (Ineffective mark-compacts near heap limit) CodeWindow: renderer process gone (reason: crashed, code: 132)
The JavaScript stack at the abort, from the user journal:
get length compute dynamicProgrammingDiffing.js computeDiff aZS / ReviewChangesResource (sjv) getResources_AllAgentChangesFromConversationState
That function Promise.alls every entry in conversationState.fileStatesV2, decodes the before blob and the after blob, reads the live file, and constructs a ReviewChangesResource per file. The constructor immediately runs computeDiff on the full texts (maxComputationTimeMs is 1000, but the matrix is still allocated on the UI thread). Each resource then hydrate()s and _registerURIs the original and the modified URI, which createModelReferences and keeps the ref in _textModelDisposables.
The same 513-copy warning came back at 12:30:28 on that window, and again at 20:31:42 on the next start, two minutes after launch. The stack stops at nYl.acquire because the acquire is awaited. The owner split (file URIs plus one unique chat-multi-diff-… scheme per original) matches _registerURI keeping a pair per file.
Why “disable diffs” does not fix crash 1
Glass registers editor host JJC with alwaysHidesInlineDiffDecorations = true, so shouldHideInlineDiffs() is already true. The user setting noInlineDiffs is the same flag. In glass it changes nothing.
That flag only skips painting. computeDecorations clears decorations and returns. loadInlineDiffs never reads it. With park_inline_diffs defaulting off, startup calls restoreAllStoredDiffsEagerly, which createModelReferences every stored diff URI in one Promise.allSettled before any dispose. Every stored diff we had was hideDecorations: true. After the first acquire, the loop disposes it and calls addDecorationsOnlyDiff, which acquires a second ref and stores it on xAe.modelRef in diffHandlers with no dispose. The accept/reject UI that would call remove() and dispose that ref is the UI glass just hid.
On disk that store was 1,354 records, 15.1 MB. LawScrape alone was 497 unique file URIs. The multi-gigabyte heap is the live text model, the piece tree, the undo stack, and the tree-sitter parse, not the JSON.
While agents run, the file-change handler disposes its own temporary ref in finally, then calls addDecorationsOnlyDiff with no third argument. streamDiff pulls the ref out of its using helper with transferOut() so finally does not dispose it, then addDiff keeps it. Notebook file changes do return early when diffs are hidden. Ordinary files do not.
The park branch is already in loadInlineDiffs. It stores the record and does not open a file unless a model is already loaded. getDescriptors() already appends parked diffs. restoreParkedDiff → restoreStoredDiff already loads one file for accept/reject and remove() disposes it. Glass just does not take that branch.
Why crash 2 is a different bug
Parking the diffs made Review Changes worse. With no live diff handlers, the pane treats the set as “show everything” and falls through to getResources_AllAgentChangesFromConversationState instead of a pending set. Every file the agent has touched is decoded and diffed at once, not the ones on screen. updateResources then calls hydrateSkeleton on every skeleton, and NBi’s constructor calls hydrate() immediately.
fileStatesV2 blob ids are enough to list the files and to tell added from modified from deleted. The bytes and the text models are only needed for the file that is actually open.
Why a long agent loop still grows after those two are fixed
A third window, started 2026-09-17 00:47 after the two patches below, did not crash and did not log a working-copy leak. RSS still went from 2.2 GB at 00:49 to 4.2 GB by 10:08, about 200 MB an hour, almost all anonymous private memory. Agent-loop wakelocks were held the whole time. Five were still active at 10:14.
The loaded-agent cap defaults to 5 (glass_loaded_agent_lru_cap, fallback bum=5). Eviction skips any agent with hasActiveReferences. A running loop is that reference, so the same agents stay resident. Each new message is written with p("conversationMap", bubbleId, bubble). There is no conversationMap.delete on the append path. Headers grow the same way.
The transcript virtualizer does evict, but only for the mounted transcript, 750 ms after a row leaves the viewport (DXa=750). It keeps the generating bubble, the active tool bubble, and a few rows of overscan. A 72-hour goal that is not the open chat never mounts that list, so nothing is evicted.
The unload primitive already exists and is safe for a live turn. unloadBubblesByIds skips generatingBubbleIds and any message that is dirty or still persisting. loadBubblesByIds reads the body back from composerMessageStorageService. Append already calls storeMessage. Streaming updates call updateMessage. Prompt history already calls loadBubblesByIds if a human body is missing.
On disk at the time of that measurement: 658,432 bubbleId keys, 5.0 GB, and 413,556 agentKv keys, 11.4 GB. The renderer was not holding all of that. It was holding the loaded slice, and that slice only got longer.
codeBlockData and conversationState.fileStatesV2 are not bubble bodies. Unloading bubbles does not touch them. originalFileStates was 36 MB. That is not the heap.
What we changed locally, and what it did
All three are splices in workbench.glass.main.js only. They are not a product fix. A Cursor update overwrites them. We did not change extension hosts.
-
Park hidden diffs (
pedro-park-diffs).loadInlineDiffsforces_parkInlineDiffs = true, so it does not callrestoreAllStoredDiffsEagerly.addDecorationsOnlyDiffparks and returns, withoutcreateModelReference, when the third argument is unset andcomposerMetadataanduriare set.restoreStoredDiffstill passes the third argument, so accepting one diff still loads that one file and then disposes it.Before: restart at 09:23, LawScrape materialized, 513 working copies within 8 seconds, RSS on the way to 5 GB within an hour. After: restart at 11:24, same workspaces materialized, no working-copy leak, no listener leak, about 1.3 GB at a minute and a half. The 19:52 crash still happened, via Review Changes, which this patch does not see.
-
Stop Review Changes from materializing every file (
pedro-review-cap).getResources_AllAgentChangesFromConversationStatebuilds the list from blob ids only. NogetBlob, no decode, nocreateModelReference. Rows stay unique viamodifiedUri= the file URI.NBi.hydratedoes not call_registerURIunless that side has contents.aZSskipscomputeDiffwhen the two strings together exceed 200,000 characters, so the other review paths cannot allocate the matrix that killed the renderer.The next start did not log the 513-copy warning and did not die overnight. RSS still climbed about 200 MB an hour from chat state.
-
Keep the last 30 messages per loaded composer (
pedro-bubble-cap). Every 30 seconds, for each loaded composer, anything older than the last 30 headers is passed tounloadBubblesByIds. Generating and dirty bubbles are left alone by that function. Bodies stay on disk.Restart at 10:47. First sample peaked at 3.2 GB, then the five-minute average fell to 1.1 GB by 10:52 and sat between 1.0 and 1.3 GB for the next half hour. Live RSS at 11:19 was 1.5 GB. No working-copy leak, no listener leak, no heap-limit line.
A 15-minute idle sweep that force-zeroed the model-reference counter was tried first. It did not help. Those refs are held on purpose, the idle timer never opens while agents re-acquire files, and writing counter = 0 under a live acquire is the wrong shape. GC cannot free this. The 01:31 last-resort GC freed 0 bytes.
Suggested product fix
Must
- In glass, park inline diffs by default.
shouldHideInlineDiffs()andnoInlineDiffsmust skip model creation, not only decorations.loadInlineDiffs,restoreStoredDiff, andaddDecorationsOnlyDiffmust not callcreateModelReferencefor a hidden diff.xAemust not holdmodelReffor one. Accept and reject of one diff load that one model, apply, and dispose. - Virtualize Review Changes. Do not
Promise.alleveryfileStatesV2blob. Do not run an in-process dynamic-programming diff of two full files for every path in the resource constructor. Do notcreateModelReferencea row that is not on screen. List paths from the blob ids. Diff the file that is open. - Evict old bubble bodies for every loaded agent, not only the mounted transcript.
unloadBubblesByIds/loadBubblesByIdsalready do the right thing. Call them on a memory or tail cap whilehasActiveReferencesis true, so a multi-day loop does not pin the whole transcript. KeepgeneratingBubbleIdsand dirty messages. Do not require the viewport virtualizer to be mounted. - Do not tree-sitter-parse a model that is not attached to a visible editor.
onModelAddedof an unattached model must not startparser.parseon the UI thread.
Should
- Do not call
restoreStoredDifffrom load just becausegetModel(uri) !== null. That re-pins a hidden diff the moment anything else has the file open. - File tabs should not default to
alwaysMounted: true. That was not the 513-copy burst (two file tabs were persisted), but each mounted tab holds_latestModelReffor the life of the handle. - The working-copy leak reporter should evict or at least keep logging. Printing once at 512, then staying silent while the set grows, hid the afternoon growth.
- Idle release of a model reference must run only when the last acquire drops. Never write
counter = 0while another acquire is outstanding.
Steps to Reproduce
Steps to reproduce
Cursor 3.20.17, glass mode, Linux. Do not reload the window.
- Use a glass window whose agents have already written inline diffs. Ours had 1,354 stored diffs, all
hideDecorations: true, across LawScrape (497), Canvas (247), enterprise-release (141), grok-writing-campaign (117), ArXiv (115), SciScrape (78), and others. - Quit and start Cursor. Do not open a file by hand.
- Within about 8 seconds,
renderer.logshows listener leaks, thenPotential working copy LEAK detected, having 513 working copies already. - Leave local agent loops running in that window. Do not reload.
- RSS climbs and does not come back down. Overnight, the renderer hits the 4096 MB V8 heap and aborts with code 132. The second death, after diffs were parked, is
getResources_AllAgentChangesFromConversationState→computeDiffonce Review Changes loadsfileStatesV2for an agent that has touched hundreds of files. - For the slow growth after those two are fixed: leave several agent loops running for hours, including ones that are not the open transcript. RSS rises about 200 MB an hour.
conversationMapon the loaded composers is append-only. The viewport virtualizer never runs for the background ones.
Expected
A hidden diff is a record, not a live text model, until that one diff is opened or accepted. Review Changes lists files without decoding every blob and without a text model per row. A multi-day agent loop does not keep every previous turn’s body in the renderer heap. GC can reclaim what the UI is not showing.
Actual
Hidden diffs become live models at startup and again on every agent write. Review Changes decodes and diffs every touched file at once, then pins a model per pair. Loaded agents keep every bubble for the life of the loop. All three fill the same 4096 MB renderer heap. Forcing a GC does not help, because the objects are still referenced.
Operating System
Linux
Version Information
Cursor IDE 3.20.17 (stable)
Commit: 0c32194e3fb5ffaced9fb36430b860ec301e1fc0
VS Code: 1.128.0
Date: 2026-09-12T03:16:10.634Z
Build: x64
OS: Linux 7.2.5-200.fc44.x86_64 (Fedora 44, Wayland)
Does this stop you from using Cursor
Sometimes - I can sometimes use Cursor