8.4 KiB
NanoClaw — 每个 Session 的 DB Schema
每个 session(会话)拥有的两个 SQLite 文件的参考:inbound.db(宿主机写入,容器读取)和 outbound.db(容器写入,宿主机读取)。请先阅读 db.md 了解三库概览、单一写入者规则以及跨挂载可见性约束。
Schema 位于 src/db/schema.ts 中,作为 INBOUND_SCHEMA 和 OUTBOUND_SCHEMA 常量。当新的 session 目录被配置时,两个文件都由 src/session-manager.ts 中的 ensureSchema() 创建。
1. Session 目录布局
data/v2-sessions/<agent_group_id>/<session_id>/
inbound.db ← 宿主机写入,容器读取(只读挂载)
outbound.db ← 容器写入,宿主机读取(只读打开)
.heartbeat ← 容器触碰的 mtime(非 DB 写入)
inbox/<message_id>/ ← 用户附件,从入站消息内容解码
outbox/<message_id>/ ← 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 的每条消息:用户聊天、计划任务、重复任务、问题回复、内部系统消息。
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。
写入者(宿主机): 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 以定位编辑和反应。
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)的投影。容器根据此表解析 to="name";如果行不存在,则拒绝发送并报 unknown destination。
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 未指定目的地时,出站消息的走向。
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 消息、系统动作。
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。
写入者(容器): 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 写入。
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 清除。
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 目录添加匹配的惰性迁移,并优先使用可空的列或带默认值的列,这样就不需要回填数据。