---
title: "Parsing Cursor SQLite: Debugging Missing Messages Across Tables"
description: Check paired databases, key prefixes, unknown versions, and orphan discovery to distinguish unreadable data, parsing omissions, and absent records.
image: "https://zengliangyi.com/og/blog/en/cursor-sqlite-parser-pitfalls.png"
---

When imported messages are missing, identify the layer that did not produce expected data: file reading, table queries, conversation key matching, or field extraction. An empty result does not directly establish that the source tool deleted records, and a version warning cannot automatically be ignored.

This article follows [ChatCrystal’s Cursor adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/cursor.ts) at [`7642768`](https://github.com/ZengLiangYi/ChatCrystal/tree/764276814e5c5a33ecbfeae63754f74b9261a3a8). See the [Cursor import guide](/en/blog/cursor-conversation-import) for installation, directories, and full import steps. Here the focus is parser diagnosis and coverage.

## Establish that files belong to the same user data

Workspace databases supply Composer metadata; the global database supplies bubble messages. The adapter also obtains project directories from `workspace.json`. Copying only one store, or mixing in another machine’s global database, can leave conversation IDs without matching messages.

The [database reader](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/vscdb.ts) loads the main file into a sql.js memory instance, retries after 500 milliseconds on failure, and then returns null if it fails again. This is not precise classification of locks, permissions, or corruption, and it does not replay WAL.

SQLite’s [WAL reference](https://www.sqlite.org/wal.html) explains that committed changes can remain in the log. Investigating a live database requires a consistent view; consult the [official backup mechanism](https://www.sqlite.org/backup.html). A main-file-only read is not evidence of every latest record. Do not experiment by deleting files from the original database.

## Query the right table and key

Inspect tables in a test copy, then query keys recognized by the adapter. The following session ID is constructed:



```sql
-- Workspace database  
  SELECT   value   FROM   ItemTable  
  WHERE   [key]   =   'composer.composerData'  ;  
  
  -- Global database  
  SELECT   [key],   value   FROM   cursorDiskKV  
  WHERE   [key]   LIKE   'bubbleId:sample-session:%'  ;
```



A missing `cursorDiskKV` table differs from an existing table with no matching rows. Keys have the shape `bubbleId:<composerId>:<bubbleId>`. A prefix matches a whole conversation, while an exact known key can use equality. This storage does not require LIKE for every query.

For a new implementation, bind parameters and handle the wildcard meaning of `%` and `_` separately. The current adapter interpolates strings; this recommendation is not an already implemented protection. Ordinary UUIDs lack these wildcards, but that does not make every abnormal ID safe to handle identically.

## Version and role checks are not full schema validation

For `_v > 3`, the code warns and continues reading known fields. This does not establish full compatibility with a new structure. Construct samples with `_v: 4`, renamed fields, and incorrect text types; inspect retained content and whether failures can be diagnosed.

The current code treats `type === 1` as user and everything else as assistant, skipping assistant bubbles without body text. This is an implementation rule, not correct classification of every unknown type. An empty body alone also does not prove a streaming intermediate.

## Thinking retention has prerequisites

When `allThinkingBlocks` is a nonempty array, the code collects each block’s `thinking` or `text`; otherwise it can fall back to top-level `thinking`. However, assistant bubbles without bodies are skipped before thinking extraction. An assistant record containing only reasoning can therefore be absent from imported messages.

Test body plus reasoning, top-level reasoning only, and a nonempty block array without usable text separately. Format support describes these known paths; it is not complete preservation of every reasoning record.

## Orphan checks sample candidates

Global discovery extracts Composer IDs from keys, excludes known workspace conversations, and checks at most 20 candidate bubbles for nonempty text. The `bubbleId:` prefix has nine characters, so SQL `SUBSTR([key], 10)` begins at the ID.

This check has no ordering clause and does not inspect every bubble in the conversation. Placing usable text beyond the first 20 rows is an important failure sample. The LIMIT optimization cannot be described as lossless coverage. Only after discovery does parsing read every matching row.

## Compare changed records, not only totals

Workspace paths are stripped of their prefix and URL-decoded for project display. The current conversion turns `file:///tmp/project` into `tmp/project`, losing the POSIX root slash; it does not guarantee a usable absolute path. Invalid encoding can cause a workspace to be skipped; a directory hash is not a reliable project name. Also inspect incremental import metadata: a known Composer’s creation time can serve as `fileMtime`, and combining that with global file size cannot establish detection of every content change.

Prepare a conversation with known messages, modify content in a test copy, import again, and compare actual message updates. Record files, tables, keys, fields, versions, and imported IDs to turn a missing result into a specific diagnosis. After parsing checks, inspect whether generated notes retain critical conditions. See [Claude Code JSONL filtering](/en/blog/claude-code-jsonl-noise-filter) for related event-filtering methods.

Share:

##  Related Posts

[##  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.          曾亮懿       October 3, 2026      4 min read

#ChatCrystal

#Cursor

#SQLite

#Data parsing](/en/blog/cursor-conversation-import)    [##  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": "Parsing Cursor SQLite: Debugging Missing Messages Across Tables",
  "description": "Check paired databases, key prefixes, unknown versions, and orphan discovery to distinguish unreadable data, parsing omissions, and absent records.",
  "url": "https://zengliangyi.com/en/blog/cursor-sqlite-parser-pitfalls/",
  "image": "https://zengliangyi.com/images/blog/cursor-sqlite-parser-pitfalls.svg",
  "datePublished": "2026-10-03T00:00:00.000Z",
  "dateModified": "2026-10-23T00: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-sqlite-parser-pitfalls/"
  }
}
{
  "@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": "Parsing Cursor SQLite: Debugging Missing Messages Across Tables",
      "item": "https://zengliangyi.com/en/blog/cursor-sqlite-parser-pitfalls/"
    }
  ]
}
```
