187 lines
8.4 KiB
Markdown
187 lines
8.4 KiB
Markdown
# 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/<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 的每条消息:用户聊天、计划任务、重复任务、问题回复、内部系统消息。
|
||
|
||
```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 目录添加匹配的惰性迁移,并优先使用可空的列或带默认值的列,这样就不需要回填数据。
|