Skip to content
曾亮懿 ZengLiangYi Blog

Importing Cursor history: workspace metadata and global messages

Trace how ChatCrystal joins Cursor SQLite data, discovers orphan sessions, and extracts messages, then verify the import with a known conversation.

曾亮懿

曾亮懿

4 min read
Table of contents Show Hide

Importing Cursor history involves more than finding a chat text file. The current ChatCrystal adapter obtains session clues from workspaces and messages from a global database. Checking that both locations belong to the same user data is more useful than relying on an “import complete” message.

This article follows the Cursor adapter in ChatCrystal at commit 7642768. It describes handling of known storage shapes, not a stable public export format promised by Cursor.

Find the matching User directory

The adapter uses these default base directories:

Platform Base directory
Windows %APPDATA%\Cursor\User
macOS ~/Library/Application Support/Cursor/User
Linux ~/.config/Cursor/User

CURSOR_DATA_DIR overrides the base directory. Point it at the User level, rather than one hashed workspace directory. Like other local sources, the application server loads this variable at startup. Setting it before a client import command does not reconfigure an existing server.

User/
├── workspaceStorage/<workspace>/
│   ├── workspace.json
│   └── state.vscdb
└── globalStorage/
    └── state.vscdb

Do not combine databases from different machines or users. If moving directories, retain the workspace description alongside its matching database.

Workspace clues

Workspace scanning requires both workspace.json and state.vscdb. The adapter reads folder from the former, strips file:/// and URL-decodes it, then reads allComposers from the latter’s ItemTable entry composer.composerData. This simple conversion turns file:///tmp/project into tmp/project; it does not guarantee a preserved POSIX absolute path. Check project display names separately from usable filesystem paths.

The list supplies identifiers such as composerId. Project display names come from the final segment of the decoded folder path, rather than the hashed directory. Workspaces that cannot be opened or parsed are skipped, so the list does not establish complete history coverage.

A stored Composer name is also not necessarily the imported title. The current parse() returns slug: null; it does not use each Composer’s name to populate that field.

Messages from the global database

The global state.vscdb contains a cursorDiskKV table with keys shaped like bubbleId:<composerId>:<bubbleId>. The adapter finds messages by conversation ID and extracts text and supporting fields from their JSON values.

This constructed example illustrates fields; it is not an original record from a real import:

{
  "_v": 3,
  "type": 1,
  "text": "Why is this request running twice?",
  "createdAt": "2026-10-01T10:00:00Z"
}

The current code treats type === 1 as user and other values as assistant, skipping assistant bubbles without body text. Thinking is stored separately; tool and code fields set flags. This does not establish complete preservation of tool outputs or editor state.

When _v exceeds 3, the code warns and continues parsing known fields. That does not prove full compatibility with a new format. Inspect actual messages rather than universally treating version warnings as harmless.

Orphan discovery has limits

The adapter also looks for global sessions absent from known workspace Composer lists. This can recover some history whose workspace was removed while messages remain. It is not a recovery tool with guaranteed coverage.

The candidate check inspects at most 20 bubbles per session for nonempty text, without a SQL ordering clause. Missing text in that sample, an unreadable global database, or changed key shapes can affect discovery. Once a candidate is found, parsing reads all matching bubbles for that session.

The database reader loads the main database file into a sql.js memory instance and retries once after a short delay on failure. It does not precisely classify read failures, corruption, and unsupported shapes, or replay SQLite WAL here. Missing records do not directly establish that Cursor deleted them.

Verify a real conversation

Using the default local connection:

crystal import
crystal --json conversations --source cursor --limit 20

The second command returns an object containing items. Select a real conversation whose content you know, inspect its ID, project, and message count, then read actual messages through application details or GET /api/conversations/<id>. Local import has source-option limitations described in the Claude Code import guide; do not promise that the option restricts scanning to Cursor.

Include a normal workspace, history present only in the global database, and a file known to contain malformed data or empty assistant messages. Evaluate retained content, without treating unmeasured claims about seconds per batch as acceptance evidence.

Recheck updated history

Import uses file metadata and parsed content hashes to decide whether to skip or replace records. For known workspace sessions, the current Cursor scan prefers Composer creation time as fileMtime and uses global database size as fileSize. Metadata checks therefore cannot be promised to identify every content change, especially when the global file size stays unchanged.

Test an update to an imported session and confirm that new messages reach the application. Otherwise, retain the session ID, file state, and version as investigation evidence. Notes still require summarization and embedding; a changed imported record does not prove that an old note remains correct. See the getting-started workflow for configuration and single-note verification.

Share:

Related Posts

Parsing Cursor SQLite: Debugging Missing Messages Across Tables

Check paired databases, key prefixes, unknown versions, and orphan discovery to distinguish unreadable data, parsing omissions, and absent records.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Cursor #SQLite #Data parsing

AI Conversation Formats: JSONL Events vs SQLite Snapshots

Compare 5 AI adapter formats: separate JSONL events from SQLite keys, and inspect indexed, orphan, and unknown-version chat transcripts in memory stores.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

How Claude Code, Cursor, Copilot, and Trae Store Chats on Disk

Compare Claude Code, Cursor, Copilot, and Trae storage paths. Distinguish raw copies from exports, and verify migration retention with test manifests.

曾亮懿 曾亮懿
4 min read
#ChatCrystal #Node.js #SQLite

From writing to code

Explore ChatCrystal, developer tools and daily learning notes on my GitHub. Questions about the implementation are welcome.