---
title: AI 对话格式差异：事件、快照与跨库消息定位
description: 对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。
image: "https://zengliangyi.com/og/blog/ai-conversation-format-divergence.png"
---

相同文件扩展名不保证相同消息语义，相同 SQLite 容器也不保证相同表和键。ChatCrystal 在适配器边界内统一输出，保留的内容仍受具体解析规则限制。[输出边界](/blog/ai-conversation-parser-boundaries/)检查了信息取舍；这里回到格式与定位，解释为什么只用“读取 JSONL”或“打开 state.vscdb”还不够。

## 五个固定入口，不是五份永久存储规范

下表描述固定提交的实现，不把第三方工具的当前所有版本或设计动机当作已验证事实。

| 来源 | 当前适配器读取的形状 | 定位与解释成本 |

| --- | --- | --- |

| Claude Code | 项目 JSONL 中带 uuid 的 user/assistant，content 可为字符串或块数组 | 识别消息类型、parentUuid 与选择性内容提取 |

| Codex | rollout JSONL 的 session_meta、event_msg、response_item 特定分支 | 区分事件、局部重复与工具标记，不是完整线程重放 |

| Cursor | workspace ItemTable 的 composer.composerData；global cursorDiskKV 的 bubbleId 前缀 | 会话索引和消息分属两处，另有孤立会话扫描 |

| Trae | ItemTable 的 memento/icube-ai-agent-storage；data.list 按 sessionId 定位 | 读取嵌套消息，按 turnIndex 排序并构造递增时间 |

| Copilot | .jsonl 的字面首行快照，或旧 .json 整文件 | kind=0/v 包装与旧形状分开，后续行不在当前重放路径 |

[parser/index.ts](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/index.ts)静态注册这五项。Claude/Codex 默认项目或 sessions 路径以及 Cursor/Trae/Copilot 的平台分支，均由各自实现决定；可配置覆盖路径也必须纳入定位验收。不要把一个 Windows 示例路径当作全部平台和部署方式。

## JSONL 是分帧容器，不是数据模型

逐行 JSON 适合追加与逐行处理，仍有解析开销。进程崩溃、缓冲未落盘或最后一行不完整，都需要具体持久化证据；“前面所有行必安全”不能从扩展名推出。

[Claude adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/claude-code.ts)读取带 uuid 的有效 user/assistant 分支，并选择性提取块。[Codex adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/codex.ts)选择特定事件，局部去重并把工具调用标记到最近助手；它没有从全部事件恢复完整因果线程。标题还可能借助 session_index.jsonl，而非只来自消息正文。

[Copilot adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/copilot.ts)对 JSONL 读取字面第一行，检查 kind 并取 v；不会把最后一行当最新完整快照。旧 JSON 另走整文件分支。后续补丁的实际作用需在上游版本与样本中核实，不能把没有读取的信息称为永久无价值。

## SQLite 相同，消息定位仍不同

[Cursor adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/cursor.ts)从工作区索引取得 composer heads，从全局 cursorDiskKV 读 bubbles，也尝试扫描没有工作区索引的孤立 composer。原稿的 TableItemTable 不是这里真实的表名。[Trae adapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapters/trae.ts)在 ItemTable 中读嵌套 data.list，不能套用同一个 composer/bubble 查询。

SQLite 的 SQL 与事务能力不保证复制一个运行中的主文件就得到所有最新消息。ChatCrystal 的读取是主文件字节快照；没有在该路径合并原生 WAL。取得一致样本与理解键内 JSON 是两个不同步骤。

## 用两份内存库列出关联与遗漏证据

保存 lesson.mjs，以 Node 24+ 运行，无额外 npm 依赖。它新建两个 Node SQLite 内存库，采用 Cursor 风格的表与键，仅做合成清单：不读取 state.vscdb、不调用真实适配器、不恢复完整会话，也不模拟实时写入或原生 WAL。



```js
import   assert   from   'node:assert/strict'  ;  
  import   { DatabaseSync }   from   'node:sqlite'  ;  
  const   workspace   =   new   DatabaseSync  (  ':memory:'  );  
  const   global   =   new   DatabaseSync  (  ':memory:'  );  
  try   {  
    workspace.  exec  (  'CREATE TABLE ItemTable(key TEXT PRIMARY KEY, value TEXT)'  );  
    global.  exec  (  'CREATE TABLE cursorDiskKV(key TEXT PRIMARY KEY, value TEXT)'  );  
    workspace  
      .  prepare  (  'INSERT INTO ItemTable VALUES (?, ?)'  )  
      .  run  (  
        'composer.composerData'  ,  
        JSON  .  stringify  ({ allComposers: [{ composerId:   'known'  , name:   'fixture session'   }] })  
      );  
    const   insert   =   global.  prepare  (  'INSERT INTO cursorDiskKV VALUES (?, ?)'  );  
    insert.  run  (  'bubbleId:known:u1'  ,   JSON  .  stringify  ({ type:   1  , text:   'question'  , _v:   3   }));  
    insert.  run  (  'bubbleId:known:a1'  ,   JSON  .  stringify  ({ type:   2  , text:   'answer'  , _v:   3   }));  
    insert.  run  (  'bubbleId:orphan:a2'  ,   JSON  .  stringify  ({ type:   2  , text:   'unindexed answer'  , _v:   99   }));  
    insert.  run  (  'bubbleId:known:bad'  ,   'broken JSON'  );  
    const   row   =   workspace  
      .  prepare  (  'SELECT value FROM ItemTable WHERE key = ?'  )  
      .  get  (  'composer.composerData'  );  
    const   heads   =   new   Map  (  JSON  .  parse  (row.value).allComposers.  map  ((  head  )   =>   [head.composerId, head]));  
    const   groups   =   new   Map  ();  
    const   errors   =   [];  
    const   unknownVersions   =   [];  
    for   (  const   row   of   global  
      .  prepare  (  "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'"  )  
      .  all  ()) {  
      const   [,   id  ,   messageId  ]   =   row.key.  split  (  ':'  );  
      let   bubble;  
      try   {  
        bubble   =   JSON  .  parse  (row.value);  
      }   catch   {  
        errors.  push  (row.key);  
        continue  ;  
      }  
      if   ((bubble._v   ??   0  )   >   3  ) unknownVersions.  push  (row.key);  
      const   group   =   groups.  get  (id)   ??   { id, head: heads.  get  (id)   ??   null  , messages: [] };  
      group.messages.  push  ({ messageId, bubble });  
      groups.  set  (id, group);  
    }  
    assert.  equal  (groups.  get  (  'known'  ).messages.  length  ,   2  );  
    assert.  equal  (groups.  get  (  'known'  ).head.name,   'fixture session'  );  
    assert.  ok  (groups.  has  (  'orphan'  ));  
    assert.  equal  (groups.  get  (  'orphan'  ).head,   null  );  
    assert.  equal  (groups.  get  (  'orphan'  ).messages.  length  ,   1  );  
    assert.  deepEqual  (errors, [  'bubbleId:known:bad'  ]);  
    assert.  deepEqual  (unknownVersions, [  'bubbleId:orphan:a2'  ]);  
    assert.  throws  (()   =>   global.  prepare  (  'SELECT value FROM ItemTable'  ).  all  (),   /  no such table  /  );  
    console.  log  (  'two-store inventory, orphan data, and unknown-version evidence pass'  );  
  }   finally   {  
    workspace.  close  ();  
    global.  close  ();  
  }
```



known 有索引和两条有效消息，orphan 只有全局消息。坏 JSON 被单独记录，未知 _v=99 也保留证据；版本警告不等于兼容成功。把消息库误当含 ItemTable 的库，查询确实报表不存在。

若在分组前丢弃没有 head 的 id，孤立组存在断言失败；去掉未知版本记录，其列表断言失败。恢复后通过。真实 Cursor 孤立扫描有自己的窗口、缓存和样本限制，不能把这个小清单当作它的完整实现。

## 统一输出需要版本与损失记录

[SourceAdapter](https://github.com/ZengLiangYi/ChatCrystal/blob/764276814e5c5a33ecbfeae63754f74b9261a3a8/server/src/parser/adapter.ts)隔离 detect/scan/parse 的入口形状，但 scan 可能查询或读取内容，接口不是外部数据的运行时 schema 验证。新增来源还涉及共享来源联合类型、配置、远端载荷与 watcher/UI，并非注册一行就自动完成。

统一输出方便下游摘要与检索；它没有承诺保留所有工具、思考、父引用、附件和真实时间。为每个支持版本保存来源、输入副本、提取规则和遗漏原因，再检查输出与导入后的状态。跨工具迁移的选择可以继续看[对话管理方案](/blog/ai-conversation-management-options/)。

分享：

##  相关文章

[##  AI 对话管理方案：四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入，区分原始副本、可读导出与知识库，并用合成清单验证身份和信息保留。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/ai-conversation-management-options)    [##  sql.js WASM 持久化：内存、导出、外键与保存点

对照 ChatCrystal 的数据库生命周期，运行 sql.js 1.14.2 样本，区分 SQL 提交、二进制导出、文件写入、外键恢复和嵌套回滚。          曾亮懿       2026年10月7日      阅读约 5 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/sqljs-wasm-persistence)    [##  Vectra 索引内部：元数据文件、取消更新与实例缓存

核对 Vectra 0.15.0 的真实查询和存储路径，在临时索引中观察外部元数据、取消后的文件残留，以及不同实例的缓存状态。          曾亮懿       2026年10月7日      阅读约 4 分钟

#ChatCrystal

#Node.js

#SQLite](/blog/vectra-search-internals)

##  从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录，欢迎交流实现细节。

[访问我的 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": "AI 对话格式差异：事件、快照与跨库消息定位",
  "description": "对照五个固定适配器，区分 JSONL 容器、事件语义与 SQLite 键布局，用两个内存数据库观察消息索引、孤立数据和未知版本。",
  "url": "https://zengliangyi.com/blog/ai-conversation-format-divergence/",
  "image": "https://zengliangyi.com/images/blog/ai-conversation-format-divergence.svg",
  "datePublished": "2026-10-07T00:00:00.000Z",
  "dateModified": "2026-10-07T00:00:00.000Z",
  "author": {
    "@type": "Person",
    "name": "曾亮懿",
    "url": "https://zengliangyi.com/about"
  },
  "publisher": {
    "@type": "Organization",
    "name": "ZengLiangYi Blog",
    "logo": {
      "@type": "ImageObject",
      "url": "https://zengliangyi.com/favicon-96x96.png"
    }
  },
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://zengliangyi.com/blog/ai-conversation-format-divergence/"
  }
}
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "首页",
      "item": "https://zengliangyi.com/"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "文章",
      "item": "https://zengliangyi.com/blog"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "AI 对话格式差异：事件、快照与跨库消息定位",
      "item": "https://zengliangyi.com/blog/ai-conversation-format-divergence/"
    }
  ]
}
```
