Heavy agent use grows state.vscdb without bound, and past some size it degrades
the whole app into renderer crashes and failed subagent dispatch with nothing
anywhere naming the cause. I spent an evening diagnosing this and think the
underlying cause is a retention bug rather than my usage, so here’s what I found.
Setup: macOS, heavy agent + subagent use, several long-running agent chats
open across days.
The symptoms, none of which point at the database
- Repeated renderer/window crashes with no consistent trigger. Each one kills
every agent running in that window. - Subagent dispatch failing with
Error: Timeout waiting for bubble creation.
This is the only symptom that points at a write. - Subagents stuck at “Starting up” forever, accumulating into the hundreds —
four chats held roughly 127, 66, 23, and 100 of them. Each appears to be a
half-created bubble record left behind by a timed-out write. - ~82 GB consumed by a single file, which is easy to miss if you’re looking at
project directories.
The cause: ~50 stored rows per visible message
Everything lives in ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb,
table cursorDiskKV, keys bubbleId:*. At diagnosis that was ~82 GB across
7.47M rows, 5.94M of them bubbleId:*.
The ratio is the part I’d flag as a bug. Individual chats showed on the order of
40,000 messages in their own header list against ~2,200,000 bubbleId rows —
roughly 50 rows per visible message. That looks like every superseded revision
of every bubble being retained as an agent streams and edits it, with the dead
ones never pruned.
It’s also extremely concentrated rather than spread across history. Of 6,897,718
bubbleId rows, three chats held 5,858,083 — 85%. Fourth place was 158K, 9x
smaller than third. All three were long-running agent chats still being written
to. So an ordinary long chat is fine; what’s expensive is a single chat staying
open across days of high-volume agent writing.
The regrowth rate, which is why this isn’t a one-off
I’ve been sampling the file since the cleanup. From 2026-09-01 22:18 to
2026-09-03 15:17 — about 41 hours — it went from 67.61 GiB to 89.66 GiB, +22.05
GiB, averaging ~0.54 GiB/hour under continuous agent use. The file now reads
96,277,479,424 bytes (96.3 GB / 89.7 GiB), which is larger than the size
that triggered the original incident, reached in under two days.
Growth is bursty rather than steady — hourly buckets ranged about 10x, and the
largest single 2-minute interval added 666 MB — so it’s individual large blobs
landing rather than gradual accretion.
The remedy exists but is hard to find and harder to trust
The fix is an undocumented Command Palette command, GC Agent KV Blobs. Three
things about it each cost me real time:
- It only reclaims orphans. It took the file from ~82 GB to ~66.66 GB — 18%.
The remaining 66 GB was live content, and I could find no retention lever for
it anywhere in the product. Deleting a chat in the UI doesn’t visibly reclaim
its blob storage. - It takes hours with no honest progress signal. The
Scanning composer data
phase shows a long counter that reads as the main event, but slower phases
follow it — so a nearly-complete counter means nearly none of the work is
done. My first run also crashed partway through; it completed on a second
attempt. - The write-ahead log actively misleads. Rewriting a 66 GB database produced
a 66.59 GB-walfile, so free disk space did not move at all until a
checkpoint. A window reload does not checkpoint — only a full application quit
does — and that quit presents as a hang: windows gone, process still in
alt-tab, Activity Monitor reporting “not responding”, for as long as the merge
takes. The two natural readings of that state are “the GC failed” and “Cursor
is hung, force-kill it”. Both are wrong, and force-killing there looks like a
good way to corrupt the database.
One useful diagnostic for anyone else landing here: freelist_count is not the
right instrument. It measures space already freed inside the file, so it reads
near zero whether or not collectable garbage exists. The probe that actually
answers “will a GC help?” is counting bubbleId:* rows whose composer has no
surviving composerData:* row.
What I’d ask for
- Bound the table, or expose retention — age-based pruning, or a per-chat
delete that actually reclaims blob storage. Pruning superseded bubble
revisions would address the 50x ratio at its source. - Surface the condition before it degrades the app — a warning at some size
threshold beats leaving it to present as unexplained crashes. - Make the GC discoverable, with a truthful progress indicator that includes
the checkpoint phase and warns that the quit will look like a hang. - Fail loudly on a
bubbleIdwrite timeout rather than leaving a
half-created agent sitting in the UI forever.
Happy to supply the full sample series or run any query that would help.