---
title: "Importing Cursor history: workspace metadata and global messages"
description: Trace how ChatCrystal joins Cursor SQLite data, discovers orphan sessions, and extracts messages, then verify the import with a known conversation.
image: "https://zengliangyi.com/og/blog/en/cursor-conversation-import.png"
---

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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/cursor.ts) in [ChatCrystal](/en/projects/chatcrystal) at commit [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8). 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.



```text
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:



```json
{  
    "_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](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/vscdb.ts) 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:



```bash
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](/en/blog/claude-code-conversation-import); 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](/en/blog/chatcrystal-getting-started) 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.          曾亮懿       October 3, 2026      4 min read

#ChatCrystal

#Cursor

#SQLite

#Data parsing](/en/blog/cursor-sqlite-parser-pitfalls)    [##  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.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-format-divergence)    [##  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.          曾亮懿       October 7, 2026      4 min read

#ChatCrystal

#Node.js

#SQLite](/en/blog/ai-conversation-management-options)

##  From writing to code

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

[Visit my GitHub](https://github.com/ZengLiangYi)

```json
{
  "@context": "https://schema.org",
  "@type": "WebSite",
  "name": "ZengLiangYi Blog",
  "alternateName": [
    "Zeng Liangyi",
    "ZengLiangYi",
    "zengliangyi.com"
  ],
  "url": "https://zengliangyi.com",
  "description": "曾亮懿的个人网站。记录 AI Native 探索、开发者工具与全栈产品实践。"
}
{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Importing Cursor history: workspace metadata and global messages",
  "description": "Trace how ChatCrystal joins Cursor SQLite data, discovers orphan sessions, and extracts messages, then verify the import with a known conversation.",
  "url": "https://zengliangyi.com/en/blog/cursor-conversation-import/",
  "image": "https://zengliangyi.com/images/blog/cursor-conversation-import.svg",
  "datePublished": "2026-10-03T00:00:00.000Z",
  "dateModified": "2026-10-03T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/en/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/en/blog/cursor-conversation-import/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://zengliangyi.com/en"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "Blog",
      "item": "https://zengliangyi.com/en/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "Importing Cursor history: workspace metadata and global messages",
      "item": "https://zengliangyi.com/en/blog/cursor-conversation-import/"
    }
  ]
}
```
