Incident report: runaway file descriptors on Cursor globalStorage/state.vscdb (SpecStory extension 1.0.4)
Tracking issue: specstoryai/getspecstory#208 — [EXTENSION/CLI] CPU spikes issue (Cursor). Reports in that thread match what we see: extreme memory and CPU, many specstory_darwin_arm64 processes, and heavy system load. This document adds independent evidence on extension 1.0.4: hundreds of duplicate FDs on Cursor globalStorage/state.vscdb and -wal, FD counts that grow during short polls, and a multi-process breakdown (extension-host lineage vs orphans). Discussion belongs in #208; this gist is a companion attachment for that issue.
- What’s going wrong: Under Cursor,
specstory_darwin_arm64 watchcan hold hundreds of duplicate open file descriptors on~/Library/Application Support/Cursor/User/globalStorage/state.vscdbandstate.vscdb-wal, and those counts creep upward over short polls — strong signal of sqlite / handle churn, not a stable long-lived DB handle. - Where to look first: The native CLI
watchpath (bundledbin/specstory_darwin_arm64). The 1.0.4 extension spawns that binary but the minified extension bundle does not referencestate.vscdb, so the pattern does not look like accidental sqlite access from the JS layer. - Why CPU/RAM explode: Each leaked or churning handle costs kernel + userspace work; many concurrent
watchprocesses (extension-host children + orphans) multiply the effect — consistent with pending child processes when reloading or toggling providers too quickly. - Version / scope: Evidence captured on SpecStory 1.0.4, Cursor 3.1.0-pre.12.patch.0 (arm64; commit
157ffe1c6dac02720a7c5d875229565305625f10, same as Help → About /cursor --version), macOS arm64. VSIX and binary SHA256s are listed below for bit-for-bit matching.
Audience: SpecStory / getspecstory maintainers
Extension: specstory.specstory-vscode 1.0.4
Platform observed: macOS darwin arm64 (Apple Silicon)
Severity: High — hundreds of duplicate FDs per specstory_darwin_arm64 watch process, sustained CPU, large RSS, multi-process proliferation
While running the SpecStory VS Code extension 1.0.4 under Cursor, we observed specstory_darwin_arm64 watch processes holding ~150+ open file descriptors each on the same two paths:
~/Library/Application Support/Cursor/User/globalStorage/state.vscdb~/Library/Application Support/Cursor/User/globalStorage/state.vscdb-wal
FD counts increased over short polling windows, indicating handle churn rather than a stable small set of DB handles. Multiple specstory_darwin_arm64 processes were present (including orphans), each with similar FD profiles.
Static review of the 1.0.4 extension bundle shows the extension spawns the bundled CLI via child_process.spawn and manages a watch child process through a dedicated service (kill on stop / dispose). The extension bundle does not reference state.vscdb by string, suggesting the duplicate sqlite-related FDs originate in the native CLI (bin/specstory_darwin_arm64) during watch, not in the TypeScript layer directly.
- Host OS: Darwin (macOS), arm64
- Editor: Cursor (VS Code–compatible)
- Cursor IDE: 3.1.0-pre.12.patch.0 (arm64); commit 157ffe1c6dac02720a7c5d875229565305625f10 — from
cursor --version/ Cursor → About (matchesCursor.appon the machine used for this report) - Extension ID / version: SpecStory 1.0.4 (VSIX SHA256 below)
- CLI:
specstory_darwin_arm64from the extensionbin/directory,watchmode with JSON output and cloud sync flags (exact argv captured in logs; tokens redacted in shared artifacts)
Download VSIX 1.0.4 from the Marketplace API (response is gzip; decompress to obtain the ZIP VSIX).
Expected SHA256 (decompressed VSIX):
e72b6b707d429b8fed72ab470a073506ccfa16ee09cf590c47f39cf8f2eb71c6
See VSIX-1.0.4-PROVENANCE.md (companion file in the same gist as this report) for URLs and commands.
With SpecStory enabled and watch running (typical: auto-save / cloud sync paths enabled):
- List SpecStory processes:
ps aux | rg specstory_darwin - Pick a
watchPID (child of Cursor extension host). - FD snapshot:
lsof -p <PID> 2>/dev/null | rg 'state\.vscdb' | sort | uniq -c | sort -nr - Repeat step 3 after 10–30 seconds of normal IDE activity.
Expected failure signal: counts ofstate.vscdb/state.vscdb-walincrease without a corresponding user action that would justify new DB connections.
Optional: sample <PID> 5 -file /tmp/specstory.sample.txt — stacks often show pread, kevent, pthread traffic (consistent with sqlite + event loop in the native binary).
A redacted collector and narrative report are available alongside this document (local workspace paths; the sanitized repro script is also 02-specstory_sanitized_repro.py in the gist with this report):
specstory-memory-fd-report.mdspecstory-evidence-20260406-164600/specstory-investigation/specstory_sanitized_repro.py
| Observation | Detail |
|---|---|
| Duplicate FDs | ~150+ each on state.vscdb and state.vscdb-wal for a single watch PID |
| Growth | FD totals rose between consecutive samples during steady-state use |
| Multi-process | Dozens of specstory_darwin_arm64 processes visible; mix of current extension-host lineage and orphans (suggesting lifecycle / restart issues across windows or crashes) |
| Memory | Individual watch processes in the hundreds of MB RSS range in our sample |
Exact tables and command output are in specstory-memory-fd-report.md (tokens redacted).
- Native CLI (
specstory_darwin_arm64)watchcommand — sqlite access to Cursor’sglobalStorage/state.vscdbwith repeated opens, missing connection reuse, or a retry loop that allocates new DB handles without closing old ones. - Amplification — multiple concurrent
watchprocesses (extension restarts, multiple windows/workspaces, or processes not exiting on extension deactivate) multiply FD and CPU cost.
Extension-side note (1.0.4 static review): The extension explicitly spawns watch in background mode and stores the ChildProcess; stop / dispose call kill on that handle. If FD growth persists with exactly one stable watch PID over time, that points away from pure extension duplication and toward native sqlite handle lifecycle in the CLI.
- Ensure at most one sqlite connection (or a small pooled set) to
state.vscdb, reused for the lifetime ofwatch. - Audit any periodic reconnect, per-session open, or per-provider open patterns.
- Confirm WAL mode file handles are not duplicated on each query or poll.
- Add a debug metric (behind
--debug): count of open FDs onstate.vscdb*over time.
- Guard against overlapping
start()calls (debounce / mutex) so full-sync +watchcannot stack if configuration events fire rapidly. - On
restartCliWatch, ensurestop()fully terminates the prior CLI before starting a new one (currently structured asawait stop(); await start()— verify no race with asyncinitialize()/ remote service).
- Document expected maximum
specstory_darwin_arm64watch count per workspace (ideally 1).
- This report (sanitized)
-
VSIX-1.0.4-PROVENANCE.md(hashes) - Redacted
specstory-memory-fd-report.mdor excerpt - Optional:
lsof+sampleexcerpts with no tokens
| Artifact | SHA256 |
|---|---|
VSIX specstory.specstory-vscode-1.0.4.vsix |
e72b6b707d429b8fed72ab470a073506ccfa16ee09cf590c47f39cf8f2eb71c6 |
bin/specstory_darwin_arm64 |
c14ef0fd618678e71292d784add1496cdd56bf539e1003f0551788fd502581b9 |
This document is an independent technical incident report prepared from runtime observation and review of the published 1.0.4 extension bundle. It is not an official statement from SpecStory.