Skip to content

feat(storage): import Cursor IDE sessions via local state.vscdb adapter #5964

Description

@ggbdpq

Motivation

The external session import system already brings local sessions from three tools into the desktop: Codex, Claude Code, and OpenCode. Cursor is a widely used AI IDE whose conversation history also lives entirely on disk — in a plain SQLite database — so importing it needs no network access, no auth, and no new dependencies beyond what the Codex adapter already uses (node:sqlite).

This proposes a fourth adapter, cursor-session-adapter, following the same contract and hardening rules as the existing three.

Measured data (macOS, 2026-10-04)

  • Database: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb — ordinary SQLite, readable with node:sqlite DatabaseSync (the Codex adapter already uses node:sqlite, so the toolchain is proven).
  • Tables: ItemTable, cursorDiskKV, composerHeaders.
  • cursorDiskKV key layout (value = JSON text):
    • composerData:<uuid> — 532 rows on my machine; session metadata. Sample fields: _v (measured 10), composerId, fullConversationHeadersOnly[] (bubble index), conversationMap{}, context{...}, status. Largest row observed: 822 KB.
    • bubbleId:<composerId>:<bubbleId> — 40435 rows; individual message bodies. Largest observed: 800 KB+.
  • Operational note: the path contains spaces; sqlite3 with a mode=ro URI fails on it (exit 14), while a plain read-only DatabaseSync open works.

Design sketch

  • detect(): the state DB exists and opens read-only.
  • listSessionPage(): page over key LIKE 'composerData:%' ORDER BY key (opaque pagination keys, reusing the stable-catalog approach); title/time from composerData with an fs-mtime fallback; reuse externalSessionMatchesQuery and the shared sanitization.
  • readSession(): walk fullConversationHeadersOnly and fetch each bubbleId:<composerId>:<bubbleId> → map to StoredMessage[]. A composer with empty headers is an empty session, not an error.
  • Options: stateDbPath? (default: the path above). Limit constants named in the existing CLAUDE_TRANSCRIPT_* style.
  • Registry: one entry added to createExternalSessionAdapterRegistry; no desktop UI changes expected (the import page lists registry sources).

Hardening, same as the existing adapters: all five limit kinds active with test-triggered paths per kind; title/cwd (≤4 KB bytes)/query text (≤200 chars) sanitized; path blacklist including zero-width and bidirectional controls (following CODEX_UNSAFE_PATH_CHARS); error types never carry source paths, transcript content, or underlying errors.

Risks

  1. state.vscdb is Cursor-internal, not a public contract. The same is true of the Codex and Claude Code sources already imported; precedent is consistent. Mitigation: field-level tolerance — missing/unknown fields degrade instead of failing the record.
  2. _v is a private schema version (measured 10). Dispatch parsing by version; unknown versions skip the unknown field rather than rejecting the record.
  3. A running Cursor holds the DB open for writing. The adapter opens read-only and never writes; test and real reads go through a read-only connection, and open failures are classified as retryable, not crashes.
  4. Single bubbles reach 800 KB+. The record_bytes limit must be sized accordingly and surface ExternalSessionLimitError when exceeded.

Verification commitment

  • Fixture: a mini state.vscdb built in code in a temp dir (composerData + bubbleId + an empty session + an over-limit record), with tests that fail against the missing adapter first.
  • All five limit kinds each have a triggering test; error messages asserted free of paths and content.
  • Local real-database check: detect() true, session listing in the ~532 magnitude, one real session imported end-to-end.
  • npm run format:check and npm run check:asf-headers green; anything not verifiable locally is stated as such.

Questions for maintainers

  1. Any objection to adding a fourth non-public local format on the same terms as the existing three? If yes, this stays a proposal.
  2. Is "no desktop UI change needed — the import page enumerates registry adapters" the right assumption, or does a new source need explicit UI treatment?

If the direction looks fine, I'll take this and open the implementation PR against this issue.

Activity

  1. ggbdpq commented on Oct 10, 2026

    @ggbdpq
    ContributorAuthor

    take

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions