docs: add detailed answers for 21 learning roadmap questions

This commit is contained in:
2026-05-13 03:40:13 +00:00
parent a8d90d2980
commit c4753da8f5
10 changed files with 1308 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
# Q1-Q3: 全局架构
---
## Q1: 一条用户消息从 Slack 发出,到 agent 回复出现在聊天框里,完整路径是什么?
### 答案
一条消息的完整生命周期横跨四个阶段、两个进程、三个数据库。
**阶段一入站路由Host / Node**
1. Slack channel adapter 收到消息事件,调用 `routeInbound(event)``src/router.ts:158`
2. 应用线程策略非线程适配器折叠线程line 166
3. `getMessagingGroupWithAgentCount()``src/db/messaging-groups.ts:53`)通过一次 JOIN 查询中央库 `v2.db`,同时获得 messaging group 行和 wired agent 数量。自动创建规则:只有 @mention 时才自动创建 messaging group`router.ts:184-201`),普通消息静默丢弃
4. 如果 agent 数量为 0记录到 `unregistered_senders`reason 为 `no_agent_wired``router.ts:210-246`,写入 `src/db/dropped-messages.ts:16`
5. Sender resolver hook 执行upsert 用户行到中央库 `users` 表(`router.ts:252`
6. `getMessagingGroupAgents()` 从中央库取出所有 wired agent 行,按 `priority DESC` 排序(`messaging-groups.ts:193-196`
7. 对每个 agent 独立评估:`evaluateEngage()` 检查 trigger 模式(`router.ts:364`access gate 检查权限line 283通过则执行 `deliverToAgent()`line 287
8. `resolveSession()``session-manager.ts:92`)在中央库 `sessions` 表查找/创建 session必要时创建目录结构并初始化 `inbound.db` + `outbound.db`
9. `writeSessionMessage()``session-manager.ts:193`)写消息到 `inbound.db``messages_in` 表,使用**偶数 seq**;每次 open-write-close
10. `wakeContainer()``container-runner.ts:85`检查已运行容器、去重、spawn Docker 容器
**阶段二容器处理Container / Bun**
11. `container/agent-runner/src/poll-loop.ts:53``runPollLoop()`
- `getPendingMessages()``messages-in.ts:65`)只读打开 `inbound.db`,读 `status='pending'`
- `markProcessing()`line 101`processing_ack``outbound.db`
- 格式化消息,调 `provider.query()`line 170
- 流式过程中每 500ms poll 后续消息,调用 `provider.push()`
- `dispatchResultText()` 解析 `<message to="..">` XML`sendToDestination()``writeMessageOut()``outbound.db``messages_out`,使用**奇数 seq**
- `markCompleted()``outbound.db``processing_ack`
- `touchHeartbeat()` 更新 `.heartbeat` 文件的 mtime
**阶段三出站投递Host / Node**
12. `src/delivery.ts` 两层轮询:
- Active poll1s`pollActive()` 仅扫描正在运行容器的 session
- Sweep poll60s`pollSweep()` 扫描所有 active session
- `inflightDeliveries` Set 防止二者竞态line 50-51
13. `drainSession()`:只读打开 `outbound.db`,读写打开 `inbound.db`
- `getDueOutboundMessages()``outbound.db``messages_out`
-`inbound.db``delivered` 表做去重比对
- `deliverMessage()``deliveryAdapter.deliver()` 发到 Slack
- `markDelivered()``inbound.db``delivered`
- 清理 `outbox/` 目录
### 每个阶段涉及的数据库
| 阶段 | DB | 表 | Writer | Reader |
|------|-----|-----|--------|--------|
| 路由查找 | `v2.db` | `messaging_groups`, `messaging_group_agents` | Host | Host |
| 发送者解析 | `v2.db` | `users` | Host | Host |
| Session 解析 | `v2.db` | `sessions` | Host | Host |
| 写入站消息 | `inbound.db` | `messages_in` | Host | — |
| Container poll | `inbound.db` | `messages_in` | — | Container (RO) |
| 声明处理中 | `outbound.db` | `processing_ack` | Container | — |
| 写回复 | `outbound.db` | `messages_out` | Container | — |
| 读回复 | `outbound.db` | `messages_out` | — | Host (RO) |
| 跟踪投递 | `inbound.db` | `delivered` | Host | — |
| 丢弃记录 | `v2.db` | `unregistered_senders` | Host | — |
### 涉及进程
- **Node host 进程**:单进程,运行 `src/index.ts`,同时跑 router + delivery + host-sweep
- **Docker 容器**:每个活跃 session 一个独立容器,运行 `bun run /app/src/index.ts`
### 边界情况
- **Messaging group 不存在**:仅 @mention 时自动创建,普通聊天静默返回
- **频道被 owner 拒绝**`mg.denied_at` 已设置):静默丢弃
- **没有 agent 响应engage**:记录到 `unregistered_senders`reason 为 `no_agent_engaged`
- **投递失败**:重试最多 3 次,然后标记永久失败
- **`accumulate` 模式**:不 engage 但 `ignored_message_policy='accumulate'` 的消息写入 `trigger=0`,静默存为上下文
---
## Q2: 为什么 Host 用 NodeContainer 用 Bun两套运行时之间的"协议"是什么?
### 答案
**为什么 Host 用 Node**
- BaileysWhatsApp adapter依赖 `libsignal-node` 原生绑定和一个久经考验的 WebSocket/HTTP 栈。Bun 的 Node-API 兼容性已有改善,但风险仍然太高
- Host 负责所有平台级网络 I/ONode 的生态系统在这个领域最成熟
**为什么 Container 用 Bun**
1. `bun:sqlite` 是内建的——不需要每次重建镜像时编译 `better-sqlite3` 原生模块
2. TypeScript 源码直接运行——镜像构建和容器启动时不需要 `tsc` 编译步骤
3. `bun install``npm install` 快 5-10 倍
**两套运行时的"协议"——SQLite不是 IPC**
Host 和 Container **从不共享代码或模块**(各有自己的包树:`pnpm-lock.yaml` vs `container/agent-runner/bun.lock`)。它们之间的"协议"是两个 SQLite 文件:
```
Host ──写──▶ inbound.db ──读──▶ Container (Bun, 只读)
Host ◀──读── outbound.db ◀──写── Container (Bun)
```
没有 HTTP、没有 Unix socket、没有 stdin pipe。Container 的 `poll-loop.ts``{readonly: true}` 打开 `inbound.db``container/agent-runner/src/db/connection.ts:53`并轮询新行。Host 以 `{readonly: true}` 打开 `outbound.db``src/db/session-db.ts:30`)并轮询新行。
Host 在 spawn 容器时把 session 文件夹(`inbound.db``outbound.db``.heartbeat`)挂载到 `/workspace`agent group 文件夹挂载到 `/workspace/agent``src/container-runner.ts:267-271`)。
---
## Q3: `inbound.db` 和 `outbound.db` 为什么各只能有一个 writer`journal_mode=DELETE` 为什么是必须的seq 奇偶分配的规则是怎样的?
### 答案
**为什么每文件只有一个 writer**
`src/session-manager.ts:7-11` 声明了三条跨 mount 不变式:
1. `journal_mode=DELETE` — WAL 模式的 `-shm` 是内存映射的VirtioFS 不传播 mmap 一致性。Container 会卡在第一次读到的快照上,永远看不到新消息
2. Host 每次 open-write-close ——关闭连接会使 Container 的页缓存失效;长连接会冻结在第一次读取时的视图
3. 每文件一个 writer —— DELETE 模式的 journal 文件解链/重建在跨 mount 边界上不是原子的,并发 writer 会损坏数据库
**`journal_mode=DELETE` 为什么负载关键:**
`container/agent-runner/src/db/connection.ts:12-18` 说明WAL 的 `-shm` 内存映射不会被 VirtioFS 从 host 传播到 guest所以 WAL 模式的 `inbound.db` 会让 container reader 卡在早期快照上,永远看不到新的 host 消息。
Host 和 Container 两边都在每次打开 DB 时设置 `PRAGMA journal_mode = DELETE``session-db.ts:15,23,38` / `connection.ts:78`。Container 还额外设置 `mmap_size = 0``connection.ts:55`)禁用内存映射 I/O确保每次读都走内核无缓冲路径。
**Host open-write-close 每次操作:**
`src/session-manager.ts:189-192` 明确说明不要在多次调用间复用长连接——每次打开、写入、关闭使 SQLite 页缓存失效Container 才能看到最新写入。Container 对 `getPendingMessages()` 也使用 `openInboundDb()`(每次新连接),但用长期单例 `getInboundDb()` 读 host 只在 spawn 时写入一次的表destinations、session_routing
**seq 奇偶分配规则:**
- **Host偶数** `src/db/session-db.ts:89``nextEvenSeq()` 只读 `messages_in``MAX(seq)`输出0→2, 1→2, 2→4, 3→4, 4→6...
- **Container奇数** `container/agent-runner/src/db/messages-out.ts:45-56` — 读 `messages_in``messages_out` 两者的 `MAX(seq)`取较大者向上取奇数0→1, 1→3, 2→3, 3→5...
**为什么奇偶分配如此关键:** seq 是 agent 面对的消息 ID。当 agent 调用 `edit_message(seq=5)` 时,`getMessageIdBySeq()``messages-out.ts:90`)用奇偶性做快速路由:
- **奇数 → `messages_out`**container 发出的)
- **偶数 → `messages_in`**user/host 发出的)
奇偶性单字段即可区分消息归属,无需 JOIN 查询。