# NanoClaw 数据库架构 — 概述 数据模型的概览:三个数据库(database),它们如何协同工作,以及跨数据库保持不变的那些约束。表级 schema 请参阅下面的链接。 - **[db-central.md](db-central.md)** — `data/v2.db` 中的每一张表(身份标识、连接关系、审批、Chat SDK 状态)以及迁移系统。 - **[db-session.md](db-session.md)** — 每个 session(会话)的 `inbound.db` + `outbound.db` 对、seq 奇偶规则以及 session 目录布局。 相关文档:[architecture.md](architecture.md)(高层设计);[api-details.md](api-details.md)(出入站消息内容格式);[isolation-model.md](isolation-model.md)(频道到 agent 的连接模式)。 --- ## 1. 三个数据库 NanoClaw 使用**三种 SQLite 数据库**,全部位于宿主机文件系统上: | DB | 位置 | 写入者 | 读取者 | 用途 | |----|----------|--------|---------|---------| | **中央库(Central)** | `data/v2.db` | 宿主机 | 宿主机 | 身份、权限、路由、连接——管理平面 | | **Session 入站库** | `data/v2-sessions///inbound.db` | 宿主机 | 宿主机(同步)、容器(只读) | 宿主机 → 容器消息 + 路由投影 | | **Session 出站库** | `data/v2-sessions///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/ / .claude-shared/ ← agent group 共享的 Claude 状态 agent-runner-src/ ← 每个 agent group 的 agent-runner 覆盖层 / inbound.db ← 宿主机写入,容器读取 outbound.db ← 容器写入,宿主机读取 .heartbeat ← 容器触碰的 mtime inbox// ← 已解码的用户附件 outbox// ← 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](db-session.md#52-processing_ack))。 - **心跳是文件 touch。** `.heartbeat` 的 mtime 是存活信号,而非 DB 列。每次心跳做 DB 写入会串行化在其他写入者后面。 这些规则在 `src/session-manager.ts` 和 `container/agent-runner/src/db/` 中通过约定强制执行。如果你要修改 DB 的打开方式,请先重新阅读这些代码。 --- ## 5. 设计模式一览 1. **两库 session 拆分。** `inbound.db` 和 `outbound.db` 各有一个写入者,一个数据流向——无跨挂载锁竞争。 2. **Seq 奇偶规则。** 偶数 = 宿主机,奇数 = 容器。两张表之间的不相交命名空间让 agent 可以仅凭 `seq` 引用任何消息。详情见 [db-session.md §3](db-session.md#3-sequence-numbering-invariant)。 3. **投影模式。** `agent_destinations` 和 `session_routing` 在容器唤醒时从中央库投影到每个 session 的 `inbound.db` 中——容器获得快速、本地的读取路径,无需跨挂载查询。 4. **通过反向通道确认。** 容器从不写入 `inbound.db`。状态同步通过 `outbound.db` 中的 `processing_ack` 实现,由宿主机轮询并协调。 5. **带外心跳。** 对 `.heartbeat` 的文件 `touch`,而非 DB 写入,因此存活检查不串行在其他写入者后面。 6. **惰性 session-DB 迁移。** 中央库使用编号迁移;per-session 的 DB 使用 `IF NOT EXISTS` + 针对旧 session 目录的临时 `ALTER TABLE` 辅助函数。 7. **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` | 容器启动时 |