# NanoClaw — 每个 Session 的 DB Schema 每个 session(会话)拥有的两个 SQLite 文件的参考:`inbound.db`(宿主机写入,容器读取)和 `outbound.db`(容器写入,宿主机读取)。请先阅读 [db.md](db.md) 了解三库概览、单一写入者规则以及跨挂载可见性约束。 Schema 位于 `src/db/schema.ts` 中,作为 `INBOUND_SCHEMA` 和 `OUTBOUND_SCHEMA` 常量。当新的 session 目录被配置时,两个文件都由 `src/session-manager.ts` 中的 `ensureSchema()` 创建。 --- ## 1. Session 目录布局 ``` data/v2-sessions/// inbound.db ← 宿主机写入,容器读取(只读挂载) outbound.db ← 容器写入,宿主机读取(只读打开) .heartbeat ← 容器触碰的 mtime(非 DB 写入) inbox// ← 用户附件,从入站消息内容解码 outbox// ← agent 生成的附件 ``` 一个 session = 一个目录 = 一对 DB。`agent_group_id` 父目录还存放每个 agent group 共享的状态(`.claude-shared/`、`agent-runner-src/`),这些状态在该 agent group 的所有 session 间共享。 `src/session-manager.ts` 中的路径辅助函数:`sessionDir()`、`inboundDbPath()`、`outboundDbPath()`、`heartbeatPath()`。 --- ## 2. 入站库 (`inbound.db`) 宿主机拥有,容器只读。Schema 常量:`src/db/schema.ts` 中的 `INBOUND_SCHEMA`。 ### 2.1 `messages_in` 抵达 session 的每条消息:用户聊天、计划任务、重复任务、问题回复、内部系统消息。 ```sql CREATE TABLE messages_in ( id TEXT PRIMARY KEY, seq INTEGER UNIQUE, -- 仅偶数(宿主机分配)——见 §3 kind TEXT NOT NULL, timestamp TEXT NOT NULL, status TEXT DEFAULT 'pending', -- pending|completed|failed|paused process_after TEXT, recurrence TEXT, -- 重复任务的 cron 表达式 series_id TEXT, -- 将重复任务的发生次数分组 tries INTEGER DEFAULT 0, trigger INTEGER NOT NULL DEFAULT 1, -- 0 = 仅上下文(不唤醒),1 = 唤醒 agent platform_id TEXT, channel_type TEXT, thread_id TEXT, content TEXT NOT NULL, -- JSON;格式取决于 kind source_session_id TEXT, -- agent 到 agent 的返回路径 on_wake INTEGER NOT NULL DEFAULT 0 -- 1 = 仅在容器的首次轮询时投递 ); CREATE INDEX idx_messages_in_series ON messages_in(series_id); ``` 内容格式:见 [api-details.md §Session DB Schema Details](api-details.md#session-db-schema-details)。 **写入者(宿主机):** `insertMessage()`、`insertTask()`、`insertRecurrence()`——均在 `src/db/session-db.ts` 中。每个都调用 `nextEvenSeq()`。 **读取者(容器):** `container/agent-runner/src/db/messages-in.ts`——轮询 `status='pending' AND (process_after IS NULL OR process_after <= now)`。 ### 2.2 `delivered` 宿主机在将 `messages_out` 行交给频道适配器后写入此处。容器读取 `platform_message_id` 以定位编辑和反应。 ```sql CREATE TABLE delivered ( message_out_id TEXT PRIMARY KEY, platform_message_id TEXT, status TEXT NOT NULL DEFAULT 'delivered', -- delivered|failed delivered_at TEXT NOT NULL ); ``` 写入者:`src/db/session-db.ts` 中的 `markDelivered()` / `markDeliveryFailed()`。较旧的 session DB 由 `migrateDeliveredTable()` 惰性地升级 schema。 ### 2.3 `destinations` 本 session 的 agent 对应的中央 `agent_destinations` 表(见 [db-central.md §1.10](db-central.md#110-agent_destinations))的投影。容器根据此表解析 `to="name"`;如果行不存在,则拒绝发送并报 `unknown destination`。 ```sql CREATE TABLE destinations ( name TEXT PRIMARY KEY, display_name TEXT, type TEXT NOT NULL, -- 'channel' | 'agent' channel_type TEXT, -- 用于 type='channel' platform_id TEXT, -- 用于 type='channel' agent_group_id TEXT -- 用于 type='agent' ); ``` 在每次容器唤醒时以及连接配置在 session 中途变更时,由 `writeDestinations()` 整体重写(事务中的 DELETE + INSERT)。`src/db/schema.ts` 中该表的注释是刷新语义的规范陈述。 ### 2.4 `session_routing` 单行(`id=1`)默认路由:当 agent 未指定目的地时,出站消息的走向。 ```sql CREATE TABLE session_routing ( id INTEGER PRIMARY KEY CHECK (id = 1), channel_type TEXT, platform_id TEXT, thread_id TEXT ); ``` 由 `writeSessionRouting()` 在每次容器唤醒时写入,数据来源于 `sessions.messaging_group_id` + `sessions.thread_id`。 --- ## 3. 序列号奇偶规则 每条消息(入站或出站)获得一个单调递增的整数 `seq`,在 session 内跨两张表唯一。 - **宿主机写入偶数 seq**(2、4、6、…)到 `messages_in`——`src/db/session-db.ts:75` 中的 `nextEvenSeq()`。 - **容器写入奇数 seq**(1、3、5、…)到 `messages_out`——逻辑在 `container/agent-runner/src/db/messages-out.ts:54`(`max % 2 === 0 ? max + 1 : max + 2`),跨*两张*表读取 `MAX(seq)` 以保持全局顺序。 为什么不相交?`seq` 是 agent 视角的消息 ID。当 agent 调用 `edit_message(seq=5)` 或 `add_reaction(seq=6)` 时,`getMessageIdBySeq()` 利用奇偶来路由查找:奇数 → `messages_out`,偶数 → `messages_in`。单凭奇偶就能消除歧义,无需连接。冲突会破坏编辑功能。 如果你添加了一条写入任一张表的代码路径,请保持奇偶规则——该规则不是通过约束强制执行的,只有两个辅助函数在维护。 --- ## 4. 出站库 (`outbound.db`) 容器拥有,宿主机只读。Schema 常量:`src/db/schema.ts` 中的 `OUTBOUND_SCHEMA`。 ### 4.1 `messages_out` agent 生成的所有内容:聊天回复、编辑、反应、卡片、问题发送、agent 到 agent 消息、系统动作。 ```sql CREATE TABLE messages_out ( id TEXT PRIMARY KEY, seq INTEGER UNIQUE, -- 仅奇数(容器分配)——见 §3 in_reply_to TEXT, timestamp TEXT NOT NULL, deliver_after TEXT, recurrence TEXT, kind TEXT NOT NULL, -- chat|chat-sdk|system|… platform_id TEXT, channel_type TEXT, thread_id TEXT, content TEXT NOT NULL -- JSON;操作类型包含在其中(edit/reaction/card/…) ); ``` 内容格式:见 [api-details.md §Session DB Schema Details](api-details.md#session-db-schema-details)。 **写入者(容器):** `container/agent-runner/src/db/messages-out.ts` 中的 `writeMessageOut()`。 **读取者(宿主机):** `src/delivery.ts`(轮询投递),`getMessageIdBySeq()` / `getRoutingBySeq()` 用于编辑/反应定位。 ### 4.2 `processing_ack` 容器侧对它所接触的每个 `messages_in.id` 的状态记录。宿主机轮询此表并将状态同步回 `messages_in`——这避免了容器向 `inbound.db` 写入。 ```sql CREATE TABLE processing_ack ( message_id TEXT PRIMARY KEY, status TEXT NOT NULL, -- processing|completed|failed status_changed TEXT NOT NULL ); ``` 崩溃恢复:容器启动时,陈旧的 `processing` 条目被清除。宿主机侧同步:`src/host-sweep.ts` 中的 `syncProcessingAcks()`。 ### 4.3 `session_state` 持久化的容器拥有的 KV 存储。主要消费者是 Chat SDK session ID——将其存储在这里可以让 agent 的对话在容器重启后恢复。可通过 `/clear` 清除。 ```sql CREATE TABLE session_state ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ); ``` 访问方式:`container/agent-runner/src/db/session-state.ts`。 --- ## 5. Schema 演化 与中央库不同,session DB **不**经过编号迁移。`INBOUND_SCHEMA` 和 `OUTBOUND_SCHEMA` 都使用 `CREATE TABLE IF NOT EXISTS`,因此新的 session 总是获得最新的 schema。对于在较旧版本下创建的 session 目录,列级别的差异在打开时惰性修补——例如 `src/db/session-db.ts` 中的 `migrateDeliveredTable()` 如果缺少 `platform_message_id` 和 `status` 列,则会将其添加到 `delivered` 表。 如果你向任一 schema 添加了一个列,请为现有的 session 目录添加匹配的惰性迁移,并优先使用可空的列或带默认值的列,这样就不需要回填数据。