8.5 KiB
NanoClaw 数据库架构 — 概述
数据模型的概览:三个数据库(database),它们如何协同工作,以及跨数据库保持不变的那些约束。表级 schema 请参阅下面的链接。
- db-central.md —
data/v2.db中的每一张表(身份标识、连接关系、审批、Chat SDK 状态)以及迁移系统。 - db-session.md — 每个 session(会话)的
inbound.db+outbound.db对、seq 奇偶规则以及 session 目录布局。
相关文档:architecture.md(高层设计);api-details.md(出入站消息内容格式);isolation-model.md(频道到 agent 的连接模式)。
1. 三个数据库
NanoClaw 使用三种 SQLite 数据库,全部位于宿主机文件系统上:
| DB | 位置 | 写入者 | 读取者 | 用途 |
|---|---|---|---|---|
| 中央库(Central) | data/v2.db |
宿主机 | 宿主机 | 身份、权限、路由、连接——管理平面 |
| Session 入站库 | data/v2-sessions/<agent_group_id>/<session_id>/inbound.db |
宿主机 | 宿主机(同步)、容器(只读) | 宿主机 → 容器消息 + 路由投影 |
| Session 出站库 | data/v2-sessions/<agent_group_id>/<session_id>/outbound.db |
容器 | 宿主机(轮询)、容器 | 容器 → 宿主机消息 + 处理状态 |
单一写入者规则。 每个 SQLite 文件有且仅有一个写入者。宿主机写入中央库和每个 inbound.db;容器只写入自己的 outbound.db。这消除了跨越 Docker/Apple Container 挂载边界的写入竞争——SQLite 锁在跨挂载场景下不可靠。
一切皆为消息。 宿主机和容器之间没有 IPC、stdin 管道或文件监视器。两个 session DB 是唯一的 IO 接口。心跳(heartbeat)是对 .heartbeat 的文件 touch(2),而非数据库写入。
日志模式。 Session DB 使用 journal_mode = DELETE(而非 WAL)。跨挂载的 WAL 可见性是个 bug 温床;DELETE 模式加上 open-write-close 会强制刷新页面缓存,使对端能看到变更。
2. 数据库映射
data/
v2.db ← 中央库(宿主机 ↔ 宿主机)
v2-sessions/
<agent_group_id>/
.claude-shared/ ← agent group 共享的 Claude 状态
agent-runner-src/ ← 每个 agent group 的 agent-runner 覆盖层
<session_id>/
inbound.db ← 宿主机写入,容器读取
outbound.db ← 容器写入,宿主机读取
.heartbeat ← 容器触碰的 mtime
inbox/<message_id>/ ← 已解码的用户附件
outbox/<message_id>/ ← agent 生成的附件
路径辅助函数:sessionDir()、inboundDbPath()、outboundDbPath()、heartbeatPath()——均在 src/session-manager.ts 中。
3. 中央库 vs. session:数据存放规则
| 数据类型 | 存放位置 | 原因 |
|---|---|---|
| 身份、角色、成员关系 | 中央库 | 稳定、跨 session、极少写入 |
| 频道连接、路由规则 | 中央库 | 管理平面 |
| 目的地 ACL | 中央库(+ 每个 session 的投影) | 中央为真源;session 本地快速查找 |
| Session 注册表(id、状态) | 中央库 | 宿主机编排生命周期 |
| 审批与待处理问题 | 中央库 | 能经受容器重启、管理员可见 |
| 丢弃消息审计 | 中央库 | 全局运维视图 |
| 入站消息、重试状态 | session inbound.db |
每个 session 的工作负载;宿主机为唯一写入者 |
| 出站消息、agent 状态 | session outbound.db |
容器为唯一写入者;宿主机轮询 |
| 投递结果 | session inbound.db(delivered) |
宿主机在成功时写入;容器读取以定位编辑目标 |
| 处理状态 | session outbound.db(processing_ack) |
容器不能写入 inbound.db |
启发式原则:如果一个值是消息、路由投影或运行时确认,则放入 per-session 的 DB。其他一切都在中央库。
4. 跨挂载可见性
Session DB 被 bind mount 到容器中。在修改 DB 代码之前,需要了解几条规则:
journal_mode = DELETE,而非 WAL。 WAL 文件不能可靠地跨越挂载边界,容器可能读到过时的页面。DELETE 模式强制每个写入者刷新主文件。- 宿主机侧 open-write-close。 宿主机对
inbound.db的写入是打开连接、写入、然后关闭。保持句柄打开会使缓存的页面对容器不可见。 - 容器读取为只读。 容器以
readonly: true打开inbound.db,且从不写入——所有容器→宿主机的状态通过outbound.db传递(见 db-session.md)。 - 心跳是文件 touch。
.heartbeat的 mtime 是存活信号,而非 DB 列。每次心跳做 DB 写入会串行化在其他写入者后面。
这些规则在 src/session-manager.ts 和 container/agent-runner/src/db/ 中通过约定强制执行。如果你要修改 DB 的打开方式,请先重新阅读这些代码。
5. 设计模式一览
- 两库 session 拆分。
inbound.db和outbound.db各有一个写入者,一个数据流向——无跨挂载锁竞争。 - Seq 奇偶规则。 偶数 = 宿主机,奇数 = 容器。两张表之间的不相交命名空间让 agent 可以仅凭
seq引用任何消息。详情见 db-session.md §3。 - 投影模式。
agent_destinations和session_routing在容器唤醒时从中央库投影到每个 session 的inbound.db中——容器获得快速、本地的读取路径,无需跨挂载查询。 - 通过反向通道确认。 容器从不写入
inbound.db。状态同步通过outbound.db中的processing_ack实现,由宿主机轮询并协调。 - 带外心跳。 对
.heartbeat的文件touch,而非 DB 写入,因此存活检查不串行在其他写入者后面。 - 惰性 session-DB 迁移。 中央库使用编号迁移;per-session 的 DB 使用
IF NOT EXISTS+ 针对旧 session 目录的临时ALTER TABLE辅助函数。 - ACL = 行的存在。
agent_destinations的成员关系本身就是权限——没有单独的permissions表。
6. 读取者与写入者 — 一览
| 表 | DB | 写入者 | 读取者 |
|---|---|---|---|
agent_groups |
central | src/db/agent-groups.ts |
session 解析器、投递、路由 |
messaging_groups |
central | src/db/messaging-groups.ts、频道设置 |
路由、投递、session 解析器 |
messaging_group_agents |
central | src/db/messaging-groups.ts |
路由 |
users |
central | src/db/users.ts、认证流程 |
权限检查 |
user_roles |
central | src/db/user-roles.ts |
src/access.ts、所有权限关卡 |
agent_group_members |
central | src/db/agent-group-members.ts |
成员资格检查 |
user_dms |
central | src/user-dm.ts(ensureUserDm) |
审批 + 配对投递 |
sessions |
central | src/db/sessions.ts、src/session-manager.ts |
投递、sweep、容器运行器 |
pending_questions |
central | src/db/sessions.ts(通过 ask_user_question) |
容器响应匹配器 |
agent_destinations |
central | src/db/agent-destinations.ts、迁移 004 回填 |
writeDestinations()、投递 ACL |
pending_approvals |
central | src/db/sessions.ts、src/onecli-approvals.ts |
管理员卡片投递、sweep |
unregistered_senders |
central | src/db/dropped-messages.ts |
运维工具 |
chat_sdk_* |
central | src/state-sqlite.ts |
Chat SDK 桥接 |
schema_version |
central | src/db/migrations/index.ts |
迁移运行器 |
messages_in |
inbound | src/db/session-db.ts |
container/agent-runner/src/db/messages-in.ts |
delivered |
inbound | src/db/session-db.ts(markDelivered) |
容器编辑/反应定位 |
destinations |
inbound | writeDestinations() 在 src/session-manager.ts |
容器路由 / ACL |
session_routing |
inbound | writeSessionRouting() 在 src/session-manager.ts |
容器 send_message 默认值 |
messages_out |
outbound | container/agent-runner/src/db/messages-out.ts |
src/delivery.ts 轮询循环 |
processing_ack |
outbound | container/agent-runner/src/db/messages-in.ts |
src/host-sweep.ts(syncProcessingAcks) |
session_state |
outbound | container/agent-runner/src/db/session-state.ts |
容器启动时 |