@morlay/session-rdb
RDB(SQLite / PostgreSQL)持久会话后端(ctx.sessionPersistence):实现上游
SessionHandle 模型(create/open/flush/stat/list),支持配置选择
SQLite 或 PostgreSQL 后端。表结构、原样存储与各条流程的设计见
设计总览,决策见 .agents/adrs/。
配置
配置就是这一行的 config(cordis.patch.yml 的 session-rdb 行,或 profile patch /
设置页改的那份 entry config):
- id: session-rdb
name: "@morlay/session-rdb"
config:
type: sqlite
# path 省略时回落 bundle patch 的默认($DSH_HOME/sessions/sessions.sqlite,
# 由 patch 的 !!js 表达式求值);自定义路径请用绝对路径字符串。
path: /absolute/path/to/sessions.sqlite
journalMode: wal
busyTimeout: 5000
PostgreSQL 同理:type: postgres + connectionString。
上游 0.1.7 取消了「settings namespace 覆盖插件 config」那套机制(settings 现在只做
volatile 字段的表单编辑,持久化落回 profile patch)。老库的 settings.yaml 由上游在
启动时一次性导入到同 id 的行 config,内容不丢;此后要改就改行 config 或设置页。
Config 类型:
type Config =
| {
type: "sqlite";
/** SQLite 数据库文件路径;`:memory:` 用于测试。 */
path: string;
/** journal_mode:`wal`(默认)/ `delete` / `truncate` / `persist`。 */
journalMode?: "wal" | "delete" | "truncate" | "persist";
/** 写锁竞争等待毫秒数(默认 5000)。 */
busyTimeout?: number;
/** 投影 checkpoint 的写入节奏(`writeEveryEvents` / `writeIntervalMs`)。 */
projectionCache?: ProjectionCacheOptions;
}
| {
type: "postgres";
/** node-postgres 连接串;首次打开自动建表并写入 store 身份。 */
connectionString: string;
/** 目标 schema(默认 public,必须已存在)。 */
schema?: string;
/** 投影 checkpoint 的写入节奏(`writeEveryEvents` / `writeIntervalMs`)。 */
projectionCache?: ProjectionCacheOptions;
};
分支能力(session-branch 闭环)
除 ctx.sessionPersistence 外,本包还实现 @morlay/session-branch 的 provider
抽象并随插件自动注册 ctx.sessionBranch(SessionBranchRdb),在不修改上游
代码的前提下提供 rewind / retry / fork 的持久化闭环(原语:forkFrom 纯 append
派生、rewind 后端事务截断)——语义、坐标论证与已知限制见
分支能力。
上层编排(edit / reroll / retry / rewind / fork 完整功能)由
@morlay/ui-conversation-message-actions 提供,或直接在 ctx.sessionBranch /
ctx.sessionEditor 之上编程。
会话删除(仅已归档)
delete 面由本包自持:deleteSession(id) 只允许删除已归档会话(未归档报
SESSION_NOT_ARCHIVED),live(有打开的 handle 或未 materialize)报 SESSION_LIVE。
删除只落该会话的行(桥接行、workspace 归属行、投影 checkpoint、会话行)——被删会话的事件行
成为孤儿留在 t_events,不影响删除后的可见性,回收见下节。
web 模式经 POST /api/session.delete(body { sessionId })暴露,状态映射:
200 已删除 / 404 不存在 / 409 未归档或 live。决策与边界见
ADR-会话删除仅限已归档且硬删 与
ADR-删除不再清孤儿与vacuum独立成gc通道。
会话列表(两条路)
| 用途 | 接缝 | 内容 |
|---|
官方 UI(上游 ui-*) | session/list → ctx.sessionQuery.listSessions(本包接管,默认排除已归档) | 未归档的可见语料,标题走投影(projections.title) |
| 我们自己的管理面 | POST /api/session.rows(本包路由) | 完整语料(含归档) + 标题 + 最后活动时间 + 工作区归属,搜索 / 子代理过滤 / 分页都在后端 |
排除归档是本部署的策略:归档是「收起来」的会话,不该混进日常列表与搜索。管理动作(取消归档 / 删除 /
导入)需要完整集合,所以另给一条自有路由,两条路不混。决策与取舍见
ADR-会话列表两条路。
session.rows 的请求体是 { query?, includeSubagents?, page?, pageSize? }(pageSize 默认 20、上限 200),
响应是 { items, total, page, pageSize }——搜索与分页都在后端:前端分页等于每次拉全量,数据一多就崩。
排序键是会话行的 f_last_event_at(由写路径维护、老数据由迁移一次性回填、没有事件的会话回落
f_created_at)。早先这里对每行算一次「该会话最后事件时间」的相关子查询:真实库 243 行要 4.2s,
加 LIMIT 也救不了(排序要求全表先算出来);物化之后同一条分页查询是 0.2ms。
导出
POST /api/session.export(body { sessionId })直接把会话日志打成 zip 响应体:
content-type: application/zip + Content-Disposition 文件名,包内 artifact 与导入通道读的
是同一份(readRaw 的 session.vN.jsonl)。会话不存在 404,body 非法 400。只读,不限归档状态。
孤儿回收与 VACUUM(GC)
POST /api/session.gc 一次做完四件事:让所有运行中的 agent 退场
(ctx.agents.list() 逐个 cancel 并等 whenIdle)→ collectOrphanSessions() 回收父已不存在的
subagent 会话(origin = 'subagent'、父不在表里;有 open handle / pending 的跳过)→
collectOrphans() 回收已无桥接行引用的事件行 → vacuum()(SQLite VACUUM、Postgres
VACUUM ANALYZE,均不得在事务内执行)。顺序上先删会话再清事件行,被删会话独占的事件行才刚成为孤儿。
响应 { orphanSessions, orphanEvents, stoppedAgents, vacuumed }。
为什么独立成通道(而不是删除时顺带做):实测删除的成本大头就是那句全库孤儿清理
(1 万行会话:545ms → 加孤儿清理后 2.3s),且它无法让库文件变小。代价是删除后库体积不降,
直到执行 GC;入口在「对话管理」页,执行期间阻塞界面。
用量统计
POST /api/session.usage 一次回报三份数据:totals(含 subagent / human 拆分)、按
天 × provider/model × subagent 的桶、按会话的行(活动计数:轮次 / 步骤 / 用户输入 / 工具调用)。
统计读侧只读三张可销毁重建的派生表,读侧维度(会话 / 模型 / 本地日 / 是否被引用 / 是否子代理)
全部在写入时物化,查询不做 EXISTS、也不回连事件表:
t_event_usage——一条 assistant/message 事件行一行(去重口径的总量与「天 × 模型」桶读它);
t_session_usage——会话 × 本地日 × 模型的 token 汇总(按会话的行读它,含 fork 继承前缀);
t_session_counts——会话 × 本地日的四项活动计数。
写入时在事务内落用量行、事务后旁路累加两张汇总表;rewind / fork / 删除会话之后按会话重算并全量重算
引用标记;表为空(首次 / 迁移后 / 手动清空)在打开库时全量回填——迁移对这三张表先删后建。真实库实测
单次统计从 1.6s → ~140ms → ~30ms(口径不变,旧口径逐项交叉核对一致),升级后首次打开付一次回填
(2.4s / 3.09 万用量行,此后不再发生)。口径与取舍见
ADR-统计衍生表物化归属与汇总。
查询接管(ctx.sessionQuery)
官方 session-query-sqlite 由 @morlay/better-session 的 patch 禁用,本包注册 ctx.sessionQuery:
精确读 / 过滤 / 血缘复用上游 SessionQueryEngine 基类,全文检索恒 disabled
(抛 SESSION_QUERY_SEARCH_DISABLED,不引入派生索引库)。决策见
ADR-接管会话查询服务所有权。
storages 接管(workspace 与投影缓存)
$DSH_HOME/storages 不再产生文件:官方 storage-json 与
session-projection-cache 由 @morlay/better-session 的 patch 禁用,数据落本包的
语义专用表——workspace 域经本包注册的 rdb KV 后端,投影 checkpoint 经本包的
ctx.sessionProjectionCache 服务。表结构见
表结构,决策见
ADR-接管storages到rdb语义表。
写节流参数(默认 200 / 5000,与官方 base 装配一致)同样写在行 config 里:
- id: session-rdb
name: "@morlay/session-rdb"
config:
type: sqlite
path: /absolute/path/to/sessions.sqlite
projectionCache:
writeEveryEvents: 200
writeIntervalMs: 5000
旧 storages JSON 的导入是包内 API(先停掉 dsh,旧文件保留不删)——没有 CLI
入口、也不在 exports 里:
await importStorages(repository, { dshHome }); // 用法见 src/import-storages.ts