SQLite 会话库

Agent CLI 的持久状态都在一个 SQLite 文件里:为什么用 Node 内置的 node:sqlite,库在哪、有哪些表,22 条只追加的迁移怎样按校验和记账,多进程并发与事务,故障注入,-c 与 --resume 的查询,以及恢复时怎样把消息和 part 重建成模型历史与读文件状态。

作者 David更新于 38 篇(共 47 篇)

ZCode 的 Agent 运行时把会话事件流放在内存里(apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:730 默认创建的是 createInMemorySessionEventStore()),进程退出后还要留着的东西——会话、消息与 part、输入账本、Todo、目标、检查点记录、用量——都写进同一个 SQLite 文件。读写它的是 SqliteSessionStore:一个类同时实现 SessionStorePortInputHistoryStorePortLocalSettingStorePortScriptWorkflowStorePortUsageStorePort 五个端口(apps/zcode-cli/packages/adapters/src/storage/session-store/sqlite-session-store.ts:225),动态工作流的 journal 端口由 workflowJournalStore() 另外交出(sqlite-session-store.ts:996)。主端口定义在 apps/zcode-cli/packages/contracts/src/interfaces/session-store.port.ts:1089,45 个方法里有 22 个带问号,旧宿主可以不实现。

代码在 apps/zcode-cli/packages/adapters/src/storage,34 个非测试文件、8907 行,其中 session-store/ 占 7115 行。事件怎样投影成消息与 part 落库,见会话事件流与持久化投影;本篇讲库本身、迁移与恢复。

位置职责
session-store/sqlite-session-store.ts打开连接、触发迁移,把端口方法分派给仓储;分叉、共享上下文导入等多表事务也在这里
session-store/migration-runner.tsmigrations.tsmigrations/迁移账本与 22 条迁移
session-store/repositories/按表读写:sessionsmessagessession-entriessession-inputstodosusagedwf-journal*
session-store/codecs.tsrows.tsjson.ts行结构与 JSON 编解码,读取时剥掉旧版字段
session-target.ts目标(Goal)表
fs-fault-injection.ts测试用的文件系统故障注入
index.ts另外导出工具结果文件存储 NodeToolArtifactStore

怎么用

库文件默认在 ~/.zcode/cli/db/db.sqliteapps/zcode-cli/packages/adapters/src/storage/session-store/paths.ts:7,配置默认值见 apps/zcode-cli/packages/contracts/src/config/index.ts:303),NOTICE 第三节写的也是这个位置(NOTICE.md:58)。要换位置,改配置键 storage.sessionDbPath,或设环境变量 ZCODE_SESSION_DB_PATHZCODE_SESSION_DB 同义,apps/zcode-cli/packages/adapters/src/config/env-config.adapter.ts:29);相对路径按进程工作目录解析(apps/zcode-cli/packages/bootstrap/src/app/session-store.ts:104)。库开着 WAL,旁边会多出 -wal-shm 两个文件,桌面端资源管理器把三者都归为“会话存储”并标成不可清理(packages/services/src/storage/domain/storageCatalog.ts:26storageCatalog.ts:56)。

接着之前的会话往下做,有这几个入口:

入口行为
zcode -c--continue取当前目录最近更新的一个根会话;没有就报 No resumable session found for 加目录(apps/zcode-cli/packages/cli/src/resume.ts:16
zcode --resume <sessionId>直接用给定 ID,存不存在要到真正恢复时才查(src/resume.ts:12
两者同时给--resume and --continue cannot be used together. 后退出(apps/zcode-cli/packages/cli/src/run.ts:368
TUI 里的 /resume弹出当前目录最近 50 个根会话的选择列表(apps/zcode-cli/packages/cli/src/tui-command-data.ts:45apps/zcode-cli/packages/cli/src/command-center/create.ts:333
TUI 里的 /resume <id>/continue按 ID 恢复,或恢复最近一个(create.ts:345
-p 无头模式走同一套解析,在解析出的会话里接着跑这次提示词(apps/zcode-cli/packages/cli/src/prompt-command.ts:165

“根会话”指 parent_id 为空的会话。两个列表查询都带 roots: trueapps/zcode-cli/packages/bootstrap/src/sessions.ts:21apps/zcode-cli/packages/bootstrap/src/sessions.ts:45),SQL 是按 time_updated desc, id desc 排序、默认排除已归档(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/sessions.ts:217repositories/sessions.ts:228)。分叉出的子会话都记了 parent_id(见下一篇),所以它们既不在 /resume 列表里,也不会被 -c 选中,只能用 --resume 加 ID 打开。

为什么是 node:sqlite

整个仓库没有一处文字交代为什么选 Node 内置的 node:sqlite,但代码里能读出几条理由:

  • 没有原生依赖。仓库根与 apps/zcode-cli 的两份 pnpm-lock.yaml 里都找不到 sqlite 字样,不需要 better-sqlite3 这类按 Node ABI 与平台编译的原生模块。从打包脚本推断,这能省掉单文件可执行(SEA)的一块麻烦:TUI 用到的 koffi 就得按目标平台单独挑出 koffi.node 塞进包里(apps/zcode-cli/packages/cli/scripts/sea-tui-assets.mjs:294)。
  • 同步 APIDatabaseSync 的每个调用都是同步的,仓储函数虽然标着 async,函数体里并不等待 I/O。动态工作流的 journal 端口干脆设计成同步接口,注释说它“正好贴合 node:sqlite 的 DatabaseSync”(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/dwf-journal.ts:4)。
  • 运行时已经锁定。CLI 固定在 Node 24.14.0(apps/zcode-cli/package.json:47mise.toml:2),桌面端服务层的任务索引库也用 node:sqlite,还特意用 createRequire 加载,防止打包器把它改写成 npm 上的 sqlite 包(packages/services/src/session/tasksDatabase/startup.ts:5)。

代价是 Node 24 里这个模块仍会打印 ExperimentalWarning: SQLite is an experimental feature,CLI 在 stderr 边界把这一行吞掉(apps/zcode-cli/packages/cli/src/runtime-warnings.ts:8)。

打开连接时先把忙等超时交给驱动,再跑迁移(sqlite-session-store.ts:243):

    try {
      ensureParentDir(this.dbPath);
      maybeThrowStorageFsFault({ operation: "sqliteOpen", path: this.dbPath });
      // 多个本地或远程 Agent 会共享同一个 session DB;timeout 必须在执行首条
      // PRAGMA 前生效,否则并发启动会在 migration prelude 直接抛 database is locked。
      this.db = new DatabaseSync(this.dbPath, { timeout: startupLockTimeoutMs });
    } catch (error) {
      // ...
    }
    try {
      if (startupToken !== deferredStartup)
        runSqliteSessionMigrations(this.db, this.dbPath, startupLockTimeoutMs);

startupLockTimeoutMs 默认 5000 毫秒(apps/zcode-cli/packages/adapters/src/storage/session-store/migration-runner.ts:13)。构造函数走同步迁移;应用启动走 SqliteSessionStore.openStartup,用一个模块私有的 symbol 跳过构造期迁移,再跑异步版本,保证“所有 Repo/业务只可能拿到 COMMIT 后的连接”(sqlite-session-store.ts:276)。

有哪些表

业务表都由迁移创建,一共 22 张,另有迁移运行器自己建的账本 schema_migration。核心几张的关系:

图表加载中…

逐张列出来(“建表行”指 apps/zcode-cli/packages/adapters/src/storage/session-store/migrations.ts 里对应 create table 语句的行号):

建表行用途关键列
session14会话主记录project_idworkspace_idparent_iddirectorytitletitle_sourcerevert(对话回退游标)、permissiontask_typetrace_idtime_archived
message41消息;除 ID、时间和序号外,整条消息以 JSON 存在 datasession_idsequence
part52消息的组成部分:文本、推理、工具调用、文件、时间线等,同样整条存 JSONmessage_idsession_idsequence
session_entry77会话级的类型化条目:模型选择、执行状态、检查点、命令幂等事实等typedata
session_input691输入账本:立即开始、引导、排队三种输入的持久生命周期,0017、0018 各重建一次deliverystatusadmitted_sequencepromoted_message_id
todo64Todo 列表主键 (session_id, position)
session_target166目标、Token 预算与活跃运行记账,0005 重建objectivestatustoken_budgettokens_used
input_history97输入框历史,全库最多保留 100 条project_idtextkindattachments
local_setting116按作用域存的本地设置,如项目权限规则与模式(scope, scope_id, namespace, key)
permission90旧的项目权限表,只作读取回退project_iddata
model_usageturn_usagetool_usage396、448、481模型请求、回合、工具调用的用量与耗时事实,保留 30 天trace_idturn_id、各类 Token 数
workflow_* 四张与 session_task_link234 起旧的脚本工作流运行、活动、事件与父子会话链接
dwf_rundwf_actordwf_nodedwf_event831 起动态工作流 journal,不设外键动态工作流(三)

session_entrytype 是开放字符串,端口里登记了七种常量,比如 runtime/model_selectionruntime/execution_stateruntime/workspace_checkpointsession-store.port.ts:802),分叉与共享上下文导入另外用到 v4/command_factv4/shared_context_import 等类型。两个数字的出处:输入历史的 100 条在 apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/input-history.ts:12,删除时不分项目;用量的 30 天在 apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/usage.ts:17,每次写用量都会顺手清一遍过期行(usage.ts:163usage.ts:335)。

迁移:22 条,只追加

迁移注册表 SQLITE_MIGRATIONSapps/zcode-cli/packages/adapters/src/storage/session-store/migrations.ts:9)是一个 { appVersion, id, sql } 数组,编号从 0001_base_session_store0022_backfilled_session_reasoningappVersion 从 0.2.0 一路到 0.16.5。执行在一个 begin immediate 事务里完成(migration-runner.ts:159):

    yield* acquire(() => db.exec("begin immediate"));
    transactionStarted = true;
    db.exec(`create table if not exists schema_migration (
      id text primary key, checksum text not null, app_version text, time_applied integer not null
    )`);
    // ...
    for (const migration of SQLITE_MIGRATIONS) {
      try {
        const checksum = migrationChecksum(migration.sql);
        const applied = readAppliedMigration(db, migration.id);
        if (applied) {
          ensureMigrationChecksum(migration.id, applied.checksum, checksum, dbPath);
          completed++;
          continue;
        }
        // ...
        db.exec(migration.sql);
        if (migrationFacts) migrationFacts.executedCount++;
        db.prepare(
          "insert into schema_migration (id, checksum, app_version, time_applied) values (?, ?, ?, ?)",
        ).run(migration.id, checksum, migration.appVersion, Date.now());
        completed++;
      } catch (error) {
        throw normalizeMigrationError(error, dbPath, migration.id);
      }
    }

几个要点:

  • 账本与校验和。校验和是 SQL 去掉首尾空白后的 sha256(migration-runner.ts:296)。已执行的迁移每次启动都重新核对,对不上就以 checksum_mismatch 拒绝启动,报错原文是“Historical migrations are immutable; add a new migration instead.”(migration-runner.ts:308)。
  • 拿锁之后再读账本。进事务前先把库切到 WAL、打开外键(migration-runner.ts:137migration-runner.ts:141),再做一次只读预检,判断这次是 noneinitialize 还是 upgrade,只用于展示(migration-runner.ts:318);真正要跑哪些,以拿到写锁后的账本为准。两个进程同时启动,后到的那个等锁、读账本、发现都已提交,就什么也不做。失败则整体回滚,一条都不留(migration-runner.ts:206)。
  • 只重试 SQLITE_BUSY。等锁时退避间隔从 10 毫秒翻倍到最多 200 毫秒(migration-runner.ts:16),SQLITE_LOCKED 不重试(migration-runner.ts:122)。同步版本总共只等 5 秒;异步版本把驱动自身的忙等压到 25 毫秒、让出事件循环,总预算一小时,结束后恢复 5 秒(migration-runner.ts:45migration-runner.ts:66migration-runner.ts:92)。每个阶段(checkingwaiting_for_lockmigratingcommittingreadyfailed)都会回调进度:普通启动写进启动日志(apps/zcode-cli/packages/bootstrap/src/app/session-store.ts:84),协议与存储准备模式则以控制帧报给桌面端,见文末。

migrations/ 目录里只有 0020 到 0022 三个文件:0001 到 0019 都以内联 SQL 写在 migrations.ts 里,后三条是改写 JSON 字段的数据迁移,SQL 很长,0020 与 0022 还要用 TypeScript 拼出来(比如 0020 把旧的模型身份字段改写成 modelSelection,并把 builtin: 前缀的 provider 映射成新 ID,apps/zcode-cli/packages/adapters/src/storage/session-store/migrations/0020-provider-model-selection.ts:16),于是各自成文件;文件头强调“冻结的数据迁移只生成 SQL,checksum 覆盖最终 SQL”(0020-provider-model-selection.ts:1)。编号为什么恰好从 0020 接着排,0019 的注释给了答案(migrations.ts:803):

    // 本条是 beta 前把开发期 0019–0030 十二条迁移**压成的单一基线**:四张表一次建齐、形状即
    // 0030 之后的终态。中间态(三次为放宽 CHECK 的整表重建、0028 的删列改名)只存在于
    // 内部预览库里,收敛办法是删掉四张 dwf_* 表并清掉 schema_migration 里的 dwf 记账行,
    // 下次启动由本条重建。beta 之后本条
    // 不可再改:runner 按 checksum 记账,历史迁移只能追加。
    //
    // 只为占住 0019 这个槽位,让
    // staging 后续迁移从 0020 起编号,功能分支合回时 ledger 不会撞号。四张表在功能落地前闲置无害。

也就是说,动态工作流在开发期用掉了 0019 到 0030 十二个号,发布前压成一条 0019,占住这个槽位,staging 分支上的后续迁移因此从 0020 起编。迁移里还能看到新旧二进制并存的痕迹:0015 的注释记录了一次实测,本机有 1690 行 message、5933 行 part 的 sequence 为空,“主要嫌疑是旧版本二进制并存写同一 DB”,于是补了两个 AFTER INSERT 触发器兜底(migrations.ts:598migrations.ts:656)。

并发与事务

同一个库会被多个进程同时打开,注释原话是“多个本地或远程 Agent 会共享同一个 session DB”(sqlite-session-store.ts:246)。协调全靠 SQLite 自己:WAL 允许并发读,写锁由 busy_timeout 等待。在这个前提下,写路径有几条规矩:

  • 单条语句靠自动提交,多行写入用 begin immediate。Todo 整表替换(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/todos.ts:30)、输入历史插入并截断、输入账本的批量改写与“提升”、分叉提交、完全访问授权提交、用量清理都各开一个立即写事务。完全访问的提交还特意说明事务内不 await,“取消检查和提交之间没有异步重入窗口”(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/permission-full-access.ts:5)。
  • 序号在插入语句里算。消息与 part 的 sequence 用子查询取同一会话(或同一消息)的最大值加一,冲突更新时保留原序号,只有跨会话改绑才换新序号;注释解释了为什么不能用 coalesce:会把历史里为空的行在任何一次重存时挪到队尾(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/messages.ts:35)。读取时按 sequence is null, sequence, time_created, rowid 排序(messages.ts:230)。
  • 时间只增不减。每存一条消息或 part 都会“触碰”会话的 time_updated,写法是 max(time_updated, ?)repositories/sessions.ts:341),这就是 -c/resume 列表的排序依据;路径自愈这类维护写入也用旧值做比较交换,避免把并发写进来的新标题、权限覆盖掉(repositories/sessions.ts:285)。
  • 给回滚留快照。旧版读取器会无条件读 user.modeltoModel.providerID 之类的字段,新代码写入时补上最小的旧对象,冲突更新时保留旧快照(messages.ts:16messages.ts:46)。换句话说,库要能被上一个版本的二进制继续打开。
  • 幂等靠事实行。分叉等命令在父会话里写一条 v4/command_fact 条目,ID 形如 v4_command_fact:child: 加父会话 ID 和命令 ID;重复提交时先查这一行,存在就直接返回已创建的子会话(sqlite-session-store.ts:336sqlite-session-store.ts:395)。输入账本也是同一个思路:admitted 之后要么在同一事务里和用户消息一起“提升”为 promoted,要么收口为 cancelleddiscardedfailed,迟到的丢弃不能把已提升的记录改回去(apps/zcode-cli/packages/adapters/src/storage/session-store/repositories/session-inputs.ts:1session-inputs.ts:323)。

故障注入

存储层的关键操作之前都有一道 maybeThrowStorageFsFault,规则来自环境变量 ZCODE_E2E_FS_FAULTS,是一个 JSON 数组(apps/zcode-cli/packages/adapters/src/storage/fs-fault-injection.ts:1)。每条规则指定错误码、要拦的操作(mkdirwriteFileappendFilerenamermsqliteOpensqliteRunany)、路径匹配条件,以及最多命中几次,缺省 1 次(fs-fault-injection.ts:4fs-fault-injection.ts:98)。它只在 ZCODE_ENV=test 或同时设了 ZCODE_E2E_FS_FAULTS_ALLOW=1 时生效,否则规则被忽略(fs-fault-injection.ts:219)。

在会话库里,打开数据库对应 sqliteOpen,大部分写方法开头的 throwBeforeWrite() 对应 sqliteRunsqlite-session-store.ts:300);日志写盘和文件系统适配器也接了同一套钩子(apps/zcode-cli/packages/adapters/src/logging/index.ts:132apps/zcode-cli/packages/adapters/src/fs/index.ts:123)。不过并不是每个写方法都过这道闸,目标(Goal)与用量的写入就没有调用它(sqlite-session-store.ts:742sqlite-session-store.ts:838)。

另有两个只给测试用的构造选项:forkCommitFaultAt 让分叉事务在子会话、消息、目标、条目、输入、命令事实写完后或提交前的任一阶段抛错,用来验证全有或全无;startupLockTimeoutMs 调短启动锁等待(apps/zcode-cli/packages/adapters/src/storage/session-store/options.ts:12)。

恢复:从行到内存历史

--resume-c 只是定出一个会话 ID,再以 resume: true 创建应用。这样启动时恢复是惰性的:等到第一次真实执行(提交提示词、启动工作流等)的边界才调用 resumeFromStorecreate-app.ts:502create-app.ts:514);TUI 里的 /resume 换上新 App 后则立即显式调用(create.ts:346)。进 core 之前,bootstrap 先读 runtime/model_selection 条目恢复模型选择(create-app.ts:484)。core 里的流程(apps/zcode-cli/packages/core/src/runtime/methods/resume.ts:59):

图表加载中…

几处细节:会话已归档也算找不到(methods/resume.ts:73);环境信息取第一条带 contextSnapshot 的用户消息里存下的那份,而不是当前机器的(methods/resume.ts:114);账本里还处于 admitted 的输入一律以 session_resumed 为由丢弃,“重启不保留队列”(apps/zcode-cli/packages/core/src/runtime/methods/steering.ts:1346);恢复出的目标会以附件形式提醒模型,并叮嘱计划或清单做完不等于目标完成(methods/resume.ts:443)。

消息历史hydrateMessageHistoryFromSessionapps/zcode-cli/packages/core/src/agent/session-history-hydrator.ts:54)先用 activeSessionMessages 裁出活跃分支:先按回退游标取分支,再从分支里最后一个压缩边界往后取(session-history-hydrator.ts:240);没有游标的旧数据保持先压缩、后分支的老顺序(session-history-hydrator.ts:211)。然后逐条回放。用户消息里的合成提醒还原成附件,文本附件还原成 prompt_attachment 提醒,图片、视频放到文字之后,与实时路径的顺序一致(session-history-hydrator.ts:271);尚未附着的共享上下文不进历史(session-history-hydrator.ts:84)。助手消息还原文本、推理和工具调用,工具结果按状态分三种(session-history-hydrator.ts:149):

    for (const part of toolParts) {
      const providerToolName = providerToolNameFromPart(part);
      if (part.state.status === "completed") {
        // ...
        const content = projectedMediaContent ?? part.state.output;
        input.history.addToolResult(part.callID, providerToolName, content, true);
        continue;
      }

      if (part.state.status === "error") {
        const persistedModelContent = part.state.metadata?.modelContent;
        // 实时链路使用 ToolExecutionResult.modelContent,但旧恢复逻辑只重放
        // 面向 UI / 日志的 state.error;优先使用持久化字符串并兼容旧 session。
        input.history.addToolResult(
          part.callID,
          providerToolName,
          typeof persistedModelContent === "string" ? persistedModelContent : part.state.error,
          false,
        );
        continue;
      }

      interruptedToolCount++;
      input.history.addToolResult(part.callID, providerToolName, INTERRUPTED_TOOL_RESULT, false);
    }

进程死在工具执行中途的调用,会被补上一条 [Tool execution was interrupted before resume] 作为结果(session-history-hydrator.ts:45),保证历史里每个工具调用都有一条配对的结果。

文件 partfilePartToContentBlockapps/zcode-cli/packages/core/src/agent/file-part-hydration.ts:8)先看 part 自己的 URL 是不是有效的 data URL,不是就按 zcode-artifact:// 地址从 artifact 存储读回(file-part-hydration.ts:113)。读到了,图片、视频、PDF 分别还原成对应的内容块;只有这三类会还原成二进制输入,注释提到曾因把白名单放宽到所有非文本类型,导致音频在冷恢复后意外变成 provider 文件输入(file-part-hydration.ts:32)。文本类型用存下的预览文字,其余一律退化成 [Attached <mime>: <名字>] 的占位文字。完成的工具结果若带附件,还要按持久化的内容布局重排,布局缺失或损坏就退回旧的文字输出(session-history-hydrator.ts:161)。

读文件状态。“先读后写”约束依赖运行时记住读过哪些文件(见读、写、改、搜),重启后这张表也要重建。hydrateReadFileStateFromSessionapps/zcode-cli/packages/core/src/agent/read-file-state-hydrator.ts:28)先清空,再扫活跃分支里已完成的 ReadWriteEdit 工具 part,只恢复带结构化元数据(修改时间、修订号、大小、内容)的完整读取;带偏移或行数限制的局部读取跨恢复一律不认(read-file-state-hydrator.ts:90read-file-state-hydrator.ts:170)。它也从不去读当前磁盘来“补全”,理由写在注释里:外部手动保存会被误认成 Agent 已读(read-file-state-hydrator.ts:125)。结果里的 skippedUnreadableEditCount 在当前代码里初始化为 0 之后再没有递增过(read-file-state-hydrator.ts:50)。

给人看的“转录”是另一条投影:TUI 恢复后显示的历史来自 projectSessionTranscript,它跳过摘要消息和只给模型看的用户消息(apps/zcode-cli/packages/bootstrap/src/session-transcript.ts:59),与上面喂给模型的历史不是同一份。

存储准备模式

--prepare-storageapps/zcode-cli/packages/cli/src/arguments.ts:77)目前只有桌面端调用。桌面 Host 启动时有一道数据库启动门:先在 Worker 线程里迁移自己的任务索引库 tasks-index.sqlite,再对每个候选工作目录,在 Worker 里跑同一个 CLI bundle 的 app-server --stdio --prepare-storage --cwd <目录>,最后才启动服务(packages/desktop/src/host/hostDatabaseStartup.ts:46packages/desktop/src/host/storagePreparationProcesses.ts:131)。按目录逐个准备,是因为相对路径的 sessionDbPath 要按各自的工作目录解析;同一次准备里真实路径相同的库只迁移一次(storagePreparationProcesses.ts:171)。这个模式下 CLI 不改进程名、不装协议生命周期、不准备 SEA 运行时工具和 provider 环境(apps/zcode-cli/packages/cli/src/main.ts:19),bootstrap 也只做存储(apps/zcode-cli/packages/bootstrap/src/zcode-protocol-entrypoint.ts:85)。

stdout 上是一问一答的控制帧(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/storage-startup.ts:105):

  try {
    await write({ method: "startup/storagePath", params: { path: options.dbPath } });
    const reuse = await acknowledgement;
    clearTimeout(timer!);
    lines.close();
    // reuse 仅由同一次 Host 准备的成功路径集合授予;不打开连接,也不写永久跳过标记。
    if (!reuse) {
      store = await openProtocolStartupStorage(options);
      store.close();
      store = undefined;
    }
    await write({ method: "startup/storagePrepared", params: {} });

先报库路径,等 Host 回一行 startup/storagePathReady(Host 要先给这个路径的磁盘占用采个基线,hostDatabaseStartup.ts:30),30 秒收不到就以 startup_status_timeout 失败(storage-startup.ts:78);随后迁移,每个阶段都以 startup/storageState 帧报告,最后发 startup/storagePrepared。失败原因被归成 12 种错误码,比如 storage_fullcorruptlock_timeoutchecksum_mismatch,跨进程只传错误码和迁移 ID,不带 SQL 或文件内容(packages/shared/src/database-startup.ts:3database-startup.ts:43);其中损坏、校验和不符、传输中断等几种不给手动重试,只能退出重开(database-startup.ts:164)。桌面端这道门的界面与服务启动见桌面应用

最后一个细节印证了“打开即迁移”:apps/zcode-cli/scripts/shadow-replay.mjs 要拿真实库做冷恢复对账,默认先把库连同 -wal-shm 复制到临时目录再打开,因为“store 打开时会跑迁移”(apps/zcode-cli/scripts/shadow-replay.mjs:6);调试界面读库则用只读连接(apps/zcode-cli/packages/debug/server/sources.ts:75)。

下一篇:检查点、回退与分叉——Write 与 Edit 留下的文件快照怎样撤销,“回退对话”为什么只移游标不删消息,分叉又复制了哪些行。

本页目录