Renderer listener leak in _createModelData path leads to runaway CPU/memory during multi-subagent sessions

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

The renderer accumulates undisposed event listeners during long agent sessions that dispatch many subagents. VS Code’s built-in leak detector fires 732 times with a single identical stack signature, rooted in observable/listener registration during editor text-model creation.

Roughly 3.5 hours into one session the renderer reached 446% CPU and 2921 MB, the window became unresponsive, and it restarted ~10 minutes later. These warnings are logged at [error] level by Cursor’s own logger.

Stack — grouping all 732 warning blocks by their frames yields exactly one group:

at KAv.create                     (workbench.glass.main.js:53:13900)
at x8e._event [as event]          (workbench.glass.main.js:55:2324)
at x8e.onFirstObserverAdded       (workbench.glass.main.js:156:33582)
at x8e.addObserver                (workbench.glass.main.js:156:15322)
at Object.onWillAddFirstListener  (workbench.glass.main.js:53:11657)
at OSs._event [as onDidChange]    (workbench.glass.main.js:55:2576)
at new fL                         (workbench.glass.main.js:224:34913)
at wgc._createInstance            (workbench.glass.main.js:21536:1504)
at wgc.createInstance             (workbench.glass.main.js:21536:933)

A deeper capture of the same site also shows bjl._createModelData (workbench.glass.main.js:21061:2522) plus React frames from react-dom/esm-client-production.js.

An observable (x8e) registers a listener on OSs.onDidChange via onFirstObserverAddedonWillAddFirstListener. The listener is never removed when the owning object is disposed, so the emitter’s listener count grows monotonically. The detector only fires above a per-emitter threshold (175 listeners by default), so 732 firings means the count crossed that threshold 732 separate times.

Accumulation over the session (warnings per hour):

18:00     1
19:00     8
20:00    81
21:00   113
22:00    57
23:00    15

[GlassDiffService] Refresh reports missing git repository warnings appear interleaved throughout, which is consistent with the diff-rendering path being the driver.

Resource peak — from Cursor’s own process-monitor telemetry; peak sample window 299.8s / 30 subsamples:

CPU Peak memory Processes
Total 523% 7283 MB 61
Renderer 446% (85%) 2921 MB (40%) 1
Main process 40% 413 MB 1
Subagent-owned (ownerAgentId set) 17pp (3%) 2217 MB (30%) 18

The renderer alone accounts for 85% of CPU. The 18 processes actually attributable to subagents (Xcode toolchain, swift-frontend, SWBBuildService) account for 3%. A new renderer window log directory is created ~10 minutes after this window, indicating the window restarted.

Steps to Reproduce

Not reduced to a minimal case. Observed conditions:

  1. Long-running agent session (3+ hours) in a Swift/Xcode workspace
  2. Many subagents dispatched (~16), each producing file edits
  3. Each edit renders a diff in the editor

Leak warnings begin within the first hour and accumulate steadily.

To check your own logs:

grep -rc 'listener LEAK' ~/Library/Application\ Support/Cursor/logs

Resource data is in ~/Library/Application Support/Cursor/process-monitor/*.log (fields: sampleStart, sampleEnd, rows[].sampleCpuTimeMs, rows[].samplePeakMemMb, rows[].ownerAgentId). CPU% per row = sampleCpuTimeMs / (sampleEnd - sampleStart) * 100.

Expected Behavior

Listeners registered in the onFirstObserverAdded → onWillAddFirstListener path should be disposed when the owning instance (fL, created via wgc.createInstance) is disposed.

Renderer CPU and memory should not grow monotonically with the number of diffs rendered over a session.

Operating System

MacOS

Version Information

Cursor 3.18.9 (build 3.18.9)
macOS 26.6.1 (25G76)
Apple M4, 16 GB RAM

Additional Information

Same workload through Cursor CLI on the same machine: ~495 MB RSS idle, ~1180 MB peak, no CPU excursion. cursor-agent has no renderer, and its 16 subagents run in a single Node process. This isolates the cost to the renderer rather than to agent execution.

What this does not prove: the correlation between the leak warnings and the CPU peak comes from a single session, and I have not attached a profiler to confirm that GC over leaked listeners is what consumed the 446%. The leak itself is confirmed independently by the built-in detector; the causal link to the CPU peak is inference from timing.

Suggestion: 732 [error]-level leak warnings in one session is a signal already present in the logs. Surfacing it in CI or telemetry would catch regressions of this class without needing user reports.

Raw renderer.log available on request.

Does this stop you from using Cursor

Yes - Cursor is unusable

Hey @X_Gugu, thanks for the detailed report.

This is a known issue we are tracking with the Agents window renderer during long sessions that produce many edits and diffs. Some memory improvements shipped recently, but the listener growth you captured is a separate part of it that is still being worked on, and your report has been added to the investigation.

Two artifacts from your setup would help pin down exactly what is being retained:

  1. Next time CPU or memory starts climbing, focus the affected window and open Toggle Developer Tools from the Command Palette.
  2. In the Performance tab, record about 30 seconds while the machine is busy, then export the profile using the down-arrow icon.
  3. In the Memory tab, take a heap snapshot and save it.
  4. Reply here with both files, plus the raw renderer.log you mentioned.

In the meantime, running Reload Window from the command palette on the affected window should reclaim some CPU and memory. Note that it stops any local agents still running in that window, so finish or pause those first if you can.

Thanks @colin – really appreciate the quick and detailed response, and good to know
the listener growth is being tracked as its own piece.

Before I set up a repro for the profile and snapshot, I went back through the data I
already had and found a few things that might narrow the search. It all comes from
Cursor’s own process-monitor telemetry and renderer.log, so there’s nothing
sensitive in any of it.

Renderer memory ratchets and never recovers

Taking the lowest peak-memory sample in each hour (the floor, which filters out
transient allocation):

16:00    529 MB
17:00    662 MB   +133
18:00    828 MB   +166
19:00   1204 MB   +376
20:00   1585 MB   +381
21:00   1701 MB   +116
22:00   2216 MB   +515      <- 22:55 peak: 2921 MB / 446% CPU
---------------- window restarted ~23:05 ----------------
23:00   1173 MB  -1043

Seven hours, the floor rises 4.2x, and only a window restart brings it back down.

GC looks healthy the whole time – memoryDuringSamplePeakMb dips to 405, 448, 528 and
539 MB at various points in that same period. Transient allocation is being collected
fine; something else is being retained that GC can’t reach.

The growth tracks diff volume, not session or workspace count

Two windows point the same way:

  • 20:30-21:30 – 348 leak warnings, with only 7 workspace lifecycle events.
  • 18:30-19:00 – 15 workspace materializations and 14 deregistrations, but only
    2 leak warnings.

So switching workspaces or sessions doesn’t appear to be the driver.

Teardown runs, but doesn’t reclaim

This was the part I found most suggestive. In that same 18:30-19:00 window – 15
materializations, 14 deregistrations – the memory floor went up, 828 → 1204 MB.
Workspaces were torn down and the memory didn’t come back.

That seems to fit with the single use-after-dispose error in the log:

[error] AgentReference used after dispose (stableId=<redacted>, access=header)
    at Gm (react-runtime/react-dom/esm-client-production.js:10:115486)

Note access=header. The leak stack in my original post runs through
get header (workbench.glass.main.js:22477:24140) – the same accessor. If I’m reading
it right, something holds a reference to a disposed AgentReference and reaches it
through that path, which would mean the retaining object outlives its owner rather
than simply never being disposed. Happy to be corrected on that reading.

What this doesn’t establish: I haven’t confirmed which objects are retained. The
ratchet and the teardown result show that retention exists and survives disposal, but
identifying the retained set is exactly what the heap snapshot would settle.

Attached

Redacted renderer.log – absolute paths, workspace names and UUIDs replaced, nothing
else touched. All 532 leak warnings from this window are intact. (Small correction to
my original post: the 732 figure was across all log directories; this single window
has 532.)

renderer-redacted.log (509.3 KB)

On the profile and heap snapshot

Glad to get you both – two things I wanted to flag first rather than just going quiet
on it.

A heap snapshot of the Agents window will contain every live string in the renderer,
and since the retained objects look like editor text models, that means file contents,
conversation text and paths. I’d rather not attach that to a public thread. Is there a
private channel I could send it to? Alternatively I’m happy to reproduce on a
throwaway workspace with dummy content, which would make the snapshot safe to post
here – just slower to set up.

Second, I’ve since moved this workload over to cursor-agent CLI, so I don’t hit the
state naturally anymore. Reproducing means deliberately running a 2-3 hour GUI session
with heavy subagent activity, which I’m happy to do – it’ll just take me a little while
to arrange. If the data above is enough to make progress in the meantime, that’s
probably the faster path; otherwise let me know and I’ll get it queued up.

Thanks again for taking this seriously.

Hey @X_Gugu, thanks for going back through the data!

I can open a DM with you here on the forum, and we can sort out a private way to send the files. No need for the throwaway-workspace setup. When you capture them, let me know roughly how big the heap snapshot and profile end up (they compress well with zip or gzip), so I can make sure the transfer method can handle them.

No rush on the repro session. What you’ve already posted is useful on its own, so queue it up whenever it’s convenient.

No rush on the repro session. We are actively tracking this leak, so it’s just helpful at this point, not critical for us to understand where we should be looking!

Hi @colin, thanks a lot for following up and for offering the DM – really appreciate the attention on this.

Since you’re already tracking the leak and know where to look, I won’t set up a dedicated repro session. I’ve mostly moved my heavy work over to the CLI, so I don’t hit this state in normal use anymore. If I do run into it again on the GUI, I’ll grab a Performance profile and heap snapshot at the time and reach out via DM.

Thanks again for the help – looking forward to the fix in an upcoming release.