pi 的 Node.js SQLite 会话后端:从 0.81.0 到 0.84.4 的演进与实现解析
本文以 sqlite-node 会话后端的 CHANGELOG 为主线,梳理 @earendil-works/pi-session-backend-sqlite-node 从最初落地(0.81.0)到 v4 lane-based 存储契约(0.84.0)、再到 SQL 层性能与查询能力增强(0.84.1)的完整演进过程,并结合仓库源码还原每个版本变更背后的具体实现:lane/记录/事实/租约的数据模型、fenced writer lease 的并发保护机制、可组合的 sql 模板标签,以及 WAL 与 covering index 等底层调优。读完本文,你能理解该后端每一版的变更动机、可复用的 API 用法与默认参数,以及如何在 pi 项目中阅读和扩展这套 SQLite 存储层。
包概览与版本时间线
该包位于 packages/session-backends/sqlite-node,是 pi agent harness 会话(@earendil-works/pi-agent-core sessions)的 Node.js 专用存储后端。README 概括其提供的能力:node:sqlite 适配器(SqliteDatabase 实现)、SQLite 会话仓库(SqliteSessionRepository)、数据库迁移、物化会话视图,以及可选的 FTS 全文搜索。
package.json 显示当前版本为 0.84.4,且 "engines": { "node": ">=22.19.0" }——这是一个重要前提:node:sqlite(DatabaseSync)是 Node.js 22.5+ 才引入的实验性内置模块,后端直接构建在它之上,因此整个包没有引入任何第三方 SQLite 驱动依赖。
对照 CHANGELOG,关键版本节点如下:
| 版本 | 日期 | 核心变更 |
|---|---|---|
| 0.81.0 | 2026-07-21 | 首次加入 Node.js SQLite 存储后端,含迁移与物化会话视图 |
| 0.82.0 ~ 0.83.0 | 2026-07-24 ~ 2026-07-29 | 常规迭代(无独立变更记录) |
| 0.84.0 | 2026-08-06 | 破坏性变更:包重命名 + 换用 v4 lane-based SessionRepo 契约 |
| 0.84.1 | 2026-08-07 | 新增可组合 sql 模板标签;分支查询下推到 SQL、加 covering index |
| 0.84.2 ~ 0.84.4 | 2026-08-14 ~ 2026-08-28 | 后续迭代(当前版本) |
下面的章节按版本顺序展开,每一节都会给出对应的源码证据。
0.81.0:Node.js SQLite 后端首次落地
CHANGELOG 中 0.81.0(2026-07-21)的 Added 条目写道:
Added a Node.js SQLite storage backend for agent harness sessions, including migrations and materialized session views。
这对应了包的最初形态:把 agent 会话持久化到 SQLite 文件,并提供迁移脚本与物化视图。仓库中至今保留的迁移文件 001_initial.sql 就是该后端的数据模型基础。构建脚本会把它一并拷贝进发布产物(prepare-dist.mjs 由 npm run build 触发,见 package.json 的 build 脚本:tsgo -p tsconfig.build.json && node ./scripts/prepare-dist.mjs copy-sqlite-migrations),迁移由 migrations.ts 在首次打开数据库时自动应用。
0.84.0:破坏性变更——包重命名与 v4 lane-based SessionRepo
CHANGELOG 的 0.84.0(2026-08-06)是整个后端历史中最重要的一版,包含两条 Breaking Changes:
- 包重命名:
@earendil-works/pi-storage-sqlite-node→@earendil-works/pi-session-backend-sqlite-node。这与当前 package.json 中的name字段完全一致。命名变化本身透露了定位转变:从泛化的“存储包”收窄为“agent 会话后端”,目录也从packages/session-backends/sqlite-node体现这一层级。 - 存储契约替换:legacy SQLite session schema 与 repository 被整体替换为 v4 lane-based
SessionRepo契约,且明确说明“Existing work-in-progress databases are not migrated”——旧库不迁移,属于一次彻底切换。
v4 契约在源码中的落点
repo.ts 中 SqliteSessionRepository 直接实现 agent-core 的 SessionRepository 接口(第 669-672 行),对外暴露 create / open / list / delete / fork / repairBranchCache 等方法。v4 契约新增的能力(CHANGELOG Added 条目:“bounded active-branch queries, durable operation records, global facts, shared sequence allocation, session statistics, fenced writer leases”)都能在迁移 SQL 中找到一一对应的表:
| v4 能力 | 对应表(001_initial.sql) | 源码实现 |
|---|---|---|
| lane(可并行的写入轨道) | lanes(session_id, lane, leaf_id, open_operation_id) |
lanes.ts |
| 持久化操作记录 | records + lane_moves |
records.ts |
| 全局事实(会话名、标签) | facts(session_id, seq, kind, key, value) |
facts.ts |
| 共享序号分配 | session_sequences(session_id, next_seq) |
session-sequences.ts |
| 会话统计 | session_stats(message_count、三类 tokens、cost_total) |
session-stats.ts |
| fenced writer lease | writer_leases(session_id, owner_id, fence, expires_at_ms) |
writer-leases.ts |
| 有界分支读 | branch_entries 物化缓存 + branch_tips |
branch-cache.ts、branch-entries.ts |
几个值得注意的设计细节:
- entries 是 canonical 数据:
entries(session_id, seq, id, parent_id, type, timestamp, payload)上(session_id, seq)唯一,父链接(parent_id)是唯一的分支结构事实。branch_entries表在 SQL 注释中被明确称为 “Derived branch cache… this cache exists only to make branch scans cheap”——它是物化派生缓存,repo.ts提供了repairBranchCache用于从 canonical 父链接重建它。 - 单一序号空间:条目(entry)、记录(record)、lane 移动(lane move)、事实(fact)共用
session_sequences分配的 seq,getLog(repo.ts 第 568 行起)就是把四类行按 seq 归并排序后回放成统一日志。 - fork 语义:
fork支持scope: "tree"(整树复制,含所有 lane 与分支尖端)与默认的单 lane 复制两种模式,且复制的是分支缓存中从根到目标 tip 的整条路径(repo.ts 第 797-909 行)。
0.84.0 的修复:会话列表不再抢占写者租约
CHANGELOG 0.84.0 的 Fixed 条目记录了 PR #7655 的修复:SQLite 会话列表不再获取 writer claim,且包含当前会话名,从而在会话存在活跃写者时也能完成清单读取。对应源码是 list() 方法的注释与实现(repo.ts 第 763-772 行):“Reads the session catalog without acquiring or renewing per-session writer leases”——它只读 sessions 表,而会话名从 global facts(readLatestFact(db, sessionId, "name", null))投影出来(见 types.ts 中 SqliteSessionMetadata.name 的字段注释)。这是一个典型的“清单读与写互不阻塞”的解耦。
fenced writer lease:多进程写者保护
“fenced writer leases” 是 0.84.0 的关键安全机制,其参数与行为都定义在 repo.ts 中:
export interface SqliteWriterLeaseOptions {
/** Time without a successful heartbeat before another writer may take over. Default: 30 seconds. */
ttlMs?: number;
/** Idle heartbeat cadence. Default: 10 seconds. Must be less than ttlMs. */
heartbeatIntervalMs?: number;
}
实现要点(均可在 repo.ts 中核对):
resolveWriterLeaseOptions(第 114-122 行):默认ttlMs = 30_000、heartbeatIntervalMs = 10_000,并强制校验heartbeatIntervalMs为正且小于ttlMs。- fence 的作用:
writer_leases表中的fence列(每次新 owner 接管时递增,SQL 注释为 “The fence prevents an expired owner from writing after a new owner takes over”)。每次写入事务都先调用renewWriterLease(第 382-391 行)校验租约归属,过期被接管后的旧 owner 会收到writer lease was lost的SessionError,从根源上避免双写。 - 心跳:
scheduleHeartbeat(第 394-415 行)按heartbeatIntervalMs周期性续租,计时器unref()不阻塞进程退出;心跳失败只重试,因为“每一次写仍会事务内校验归属”。 - 单写者约束:同一会话被第二个写者
open时,acquireWriterLease失败,抛出 “already has an active writer”(第 124-126 行)。 - 这些行为的回归测试见 writer-leases.test.ts。
0.84.1:sql 模板标签与查询下推
CHANGELOG 0.84.1(2026-08-07)有两条变更,分别对应查询构造与查询性能两个层面。
新增可组合、参数化的 sql 模板标签
实现位于 sql.ts(仅 66 行,设计紧凑):
/** Builds a parameterized query. Nested queries are inlined; other interpolations become `?` parameters. */
export function sql(strings: TemplateStringsArray, ...values: SqlTemplateValue[]): SqlQuery {
// 嵌套的 SqlQuery 直接内联其 queryText 并拼接参数;
// 其它插值一律生成 "?" 占位符并 push 进 params。
...
return new SqlQuery(queryText, params);
}
要点:
SqlQuery是一个不可变的参数化查询对象,持有queryText与params两个只读字段,并提供exec / run / get / all / iterate五种执行方式(第 15-34 行)。exec明确禁止带参,抛TypeError——这与node:sqlite中exec不支持绑定的语义一致。- 嵌套组合:模板标签中插入另一个
SqlQuery时,其文本被内联、参数按原顺序并入,因此可以像搭积木一样组合出动态 WHERE 子句等片段;非查询值(字符串、数字等)则一律走?绑定,杜绝字符串拼接注入。 joinSqlFragments(第 56-66 行)用于把多个“可信”片段按分隔符拼接并保持参数顺序,例如组装IN (?, ?, ?)或OR链。- 该标签已贯穿整个后端:
configureSqliteDatabase用sql\PRAGMA journal_mode=WAL`.exec(db)这类写法执行 PRAGMA,NodeSqliteDatabase.transaction用它包裹BEGIN IMMEDIATE / COMMIT / ROLLBACK`(src/index.ts 第 77-94 行)。参数化行为与组合规则的单测在 sql.test.ts 中。
分支查询下推到 SQL 与 covering index
CHANGELOG 同版 Fixed 条目(PR #7727)写道:
Fixed SQLite branch queries to apply filters, cursors, and limits in SQL; bounded log reads; and added covering indexes for session, record, branch, and fact queries。
这条修复解决了三个问题:
-
过滤、游标、limit 全部在 SQL 层执行。此前分支查询如果先取回再在 JS 里过滤/截断,会随分支长度线性放大小结果集的内存与 IO。现在
findEntriesOnBranch(repo.ts 第 535-549 行)通过queryCachedBranchRows把EntryQuery的type / cursor / order / limit直接编进 SQL,取回行数即为返回行数。 -
有界的日志读取:
getLog对 entries、records、lane_moves、facts 四个来源分别以afterSeq + limit参数化查询,避免“无界拉全量再截断”。 -
covering index 补齐。对照 001_initial.sql 中的索引定义可以看到覆盖性设计的密度:
- sessions:
idx_sessions_created_at、idx_sessions_cwd_created_at(支撑按时间倒序与按 cwd 过滤的列表查询); - records:多达 6 个索引(
session+lane+seq、session+type+seq、session+type+op_kind+seq、session+lane+type+op_kind+seq等),几乎每种RecordQuery组合都有一条可覆盖的复合索引; - branch_entries:4 个索引覆盖“按分支按 seq”“按 entry 反查所属分支”“按分支+type”“按分支+custom_type”四类扫描;
- facts:
idx_facts_session_kind_key_seq支撑“取某 kind/key 的最新事实”。
注意其中
idx_entries_session_type_seq、idx_records_session_lane_type_op_kind_seq这类把查询谓词列放在索引前列的写法,正是让LIMIT能尽早终止扫描的关键。 - sessions:
这些行为的验证集中在 branch-query.test.ts、log-query.test.ts、facts-query.test.ts 三个测试文件中。
Node 适配器与数据库调优
CHANGELOG 中 0.81.0 的 “including migrations and materialized session views” 在 Node 侧的载体是 src/index.ts 里的 NodeSqliteDatabase / NodeSqliteStatement:它把 node:sqlite 的 DatabaseSync 包装成后端的 SqliteDatabase 抽象(见 types.ts 第 12-30 行的接口定义),并做三件实事:
- 参数形态归一:自动区分“命名参数对象”与“位置参数列表”(
isNamedParameters),对外只暴露unknown[]的可变参数签名; - 结果归一:
run返回{ changes, lastInsertRowid },lastInsertRowid由可能的 BigInt 转为 number; - 同步事务:
transaction用BEGIN IMMEDIATE开启写事务,且显式拒绝返回 Promise 的回调(SQLite transaction callbacks must be synchronous),失败时回滚并保留原始错误。
对外部使用者,入口是 createNodeSqliteFactory():返回一个 SqliteDatabaseFactory,open(path) 直接 new DatabaseSync(path)。仓库打开后的统一配置在 configureSqliteDatabase(repo.ts 第 172-176 行):
sql`PRAGMA journal_mode=WAL`.exec(db); // 读写并发:读不阻塞写
sql`PRAGMA synchronous=FULL`.exec(db); // 持久性优先:checkpoint 时全量同步
sql`PRAGMA busy_timeout=5000`.exec(db); // 遇到锁等待至多 5 秒
这套组合表明该后端的设计取向是持久性与写安全优先:WAL 让 list 这类纯读操作不与活跃写者互斥(呼应 0.84.0 的 Fixed 条目),FULL + writer lease fence 保证崩溃恢复与双写防护。
使用方式与契约验证
结合 README 的最小示例与上文源码,一个典型的用法是:
// repository 惰性持有一条共享数据库连接;
// search 是同一 canonical 数据库上的独立服务,repository 不暴露 search()。
await using repository = new SqliteSessionRepository(options);
const search = createSqliteSessionSearch(options);
const session = await repository.create({ cwd });
await session.appendMessage(message);
const hits = [];
for await (const hit of search.search("needle")) hits.push(hit);
其中 options 即上文 SqliteSessionRepositoryOptions:env(提供 absolutePath / createDir / exists 的 FileSystem 能力子集)、sqlite(数据库工厂,Node 场景传入 createNodeSqliteFactory())、databasePath,以及可选的 writerLease: { ttlMs, heartbeatIntervalMs }。README 还强调 FTS 的懒加载策略:FTS 表与触发器在第一次非空白搜索时才创建,创建时从 canonical 条目做一次性全量重建,之后由 SQLite 触发器随 canonical 条目的插入、删除、payload 更新自动同步——搜索功能因此对未用到它的部署完全零成本。
质量保障方面,除前述各功能测试外,conformance.test.ts 对整个后端做契约级回归,确保它稳定满足 agent-core 的 SessionRepo 语义;测试运行方式为包内的 npm test(即 vitest --run,配置见 vitest.config.ts)。
小结:如何沿 CHANGELOG 阅读这个后端
- 0.81.0 奠定了“
node:sqlite+ 迁移 + 物化视图”的基本盘,前提是 Node.js >= 22.19; - 0.84.0 是一次契约换代:重命名为
pi-session-backend-sqlite-node、切换到 v4 lane-basedSessionRepo,用 lanes/records/facts/sequences/stats 五类表加 fenced writer lease 重构了存储模型,并让会话列表摆脱写者租约;旧库不做迁移; - 0.84.1 打磨查询层:引入可组合、参数化的
sql模板标签,把分支查询的过滤/游标/limit 下推 SQL,并用一组 covering index 覆盖 session/record/branch/fact 的高频查询路径。
由于 0.84.2 至 0.84.4 未记录独立变更,从 CHANGELOG 可以推断这三个版本属于常规维护迭代;如需确认具体行为差异,应以上下文中的源码与测试文件为准。整体来看,这份 CHANGELOG 虽短,却勾勒出 pi 会话存储从“能用”到“多写者安全 + 查询下推”的一条清晰演进路径,配合 001_initial.sql 与 repo.ts 即可完整还原每一版变更的工程含义。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00