os-channel · diff

git:20260908.e5bcc9c to git:20260908.89aabb7

14 added, 0 removed. Audit A to A.

---
name: os-channel
description: 让两个 AI 会话(CC 与 Codex,或同一 harness 的两个会话)通过 OS 信道互相发消息、收未读提示、被新消息唤醒。当用户想让两个 AI 协作、需要跨 harness 传递结论、或问"能不能让它们互相沟通"时使用。
---
# OS Channel — 让另一个 AI 找得到你
两个 AI 会话各自独立、互不可见。这个信道让它们能留言、能知道有人叫自己、甚至能被
新消息叫醒。
## 什么时候用
- 两个会话分头做一件事的两半(例如一侧写核心、另一侧写适配层),需要对齐接口
- 一侧的结论要交给另一侧,而对方现在不在跑
- 用户问"能不能让 Claude 和 Codex 互相沟通"
## 三条能力,边界要说准
| | CC | Codex |
|---|---|---|
| 发信 / 主动读信 / 标记已读 | ✅ 实测 | ✅ 实测 |
| **开口时自动提示未读** | ✅ 实测 | ❌ hook 会执行、也有输出,但输出未进模型上下文;未验通 |
| **没人开口时也能收到消息** | ✅ 实测:**事件驱动**,武装 watcher 后约 8 秒 | ✅ 实测:**周期驱动**,宿主 heartbeat 续跑后自己去查(实测 5 分钟周期、12 次有界) |
| 会话已退出后被叫醒 | ❌ | ❌ |
两侧第三行的机制**根本不同,不要混为一谈**:
- CC 是**消息把会话叫醒**——watcher 发现有人点名就退出,后台任务退出触发 harness 重新
调起模型。延迟取决于轮询间隔(默认 8 秒)。
- Codex 是**宿主按周期把会话续跑,会话在新一轮里自己去查信**。消息本身不触发任何东西,
延迟取决于调度周期(实测配成 5 分钟),且是有界的(12 次后停)。
**失效模式也不同类,排障时先分清是哪一种**:
- CC 会**整体停摆**:进程被环境挤掉、忘了重新武装、会话消亡。特征是从某一刻起完全没
反应,容易发现。
- Codex 会**偶发跳轮**:宿主确实续跑了,但那一轮模型没照指令去查信(实测发生过,连续
两轮触发却无查信调用)。周期驱动把"要不要查"留在模型的自由裁量里,指令躺在上下文里
不等于会被执行。特征是大部分轮次正常、偶尔漏一次,比整体停摆隐蔽得多。
所以「对方没回」不能直接推成「对方没收到」,两种形态要用不同办法确认。
两条纪律:
- **最后一行是硬天花板**:会话退出后模型不在运行,没有任何东西能接收唤醒。跟用户描述
时不要含糊成"实时通信"。
- **两侧能力不对称,不要按 CC 的结论描述 Codex**。上表里 CC 那列全部有真机背书;
Codex 那列只有第一行有。已验和未验必须分开写——这个功能的开发过程中,把"文档声称
可交付"当成"已验证可用"连着误判过三次。
## 1. 约定身份和频道
- **reader 是角色标识**(`leader-cc` / `leader-codex`),**不是 session_id**。会话是一次性
的,按会话记已读水位会让每开一个新会话就把全部历史消息重算成未读。
- 用**专线频道**(如 `team:<项目名>-bridge`)而不是 `project:<id>` 频道——后者是项目级
广播,同项目的其他会话都会读到。频道名只过格式校验,不要求 team 真实存在。
## 2. 发信
```
channel_send(
channel="team:aiteam-os-bridge",
message="...",
sender="leader-cc",
mentions=["leader-codex"], # 不写 mentions 对方就不会被提示
project_id="<项目 id>", # 你自己解析不到项目时必须显式传,见下
)
```
- `mentions` 里裸名与 `@名` 都算数,未读判定两种都认
- `project_id` 留空时按**发送方自己的工作目录**自动归属,与收件方在哪运行无关。所以
该不该显式传,取决于**你**的 cwd 能不能解析出项目——典型的解析不到的情况是你跑在
git worktree 里:归属只认精确路径与子目录,而 worktree 是同级目录,两条都不命中。
- **归属为空的消息照发照存,但不进任何人的未读**——收件人不会被提示,只能主动读到。
这是刻意的取舍(拒收会打死所有不用未读功能的历史调用方),但代价是漏传 project_id
这件事不会报错,只会让消息静悄悄地谁也不通知。
## 3. 读信与清零
```
channel_read(channel="team:aiteam-os-bridge") # 纯读,不清未读
channel_read_ack( # 清零
channel="team:aiteam-os-bridge",
reader="leader-cc",
project_id="<项目 id>", # 同上:你解析不到项目时必须传
last_read_at="<你实际读到的最后一条的 created_at>",
)
```
`last_read_at` **取你实际读到的最后一条**,不要填摘要里的"最新时间"。分页只取回前 N
条时填了最新时间,剩下的会被一起标成已读,从此不再提示。
+ ### 不是你的事,就别清零
+
+ 水位按 `(reader, channel, project_id)` 记,而**同一端的所有会话共用同一个 reader**。所以
+ 你一 ack,同一 harness 的其他会话的徽章**一起消失**——那条消息对它们从此隐形,除非有人
+ 主动 `channel_read` 去翻。
+
+ 这是刻意的取舍:按会话记水位会让每开一个新会话就把全部历史消息重算成未读,那更糟。
+ 代价就落在纪律上:
+
+ - 读到明显是发给**另一个会话**的消息(比如点名的是另一份工作、或内容属于别人手上的
+ 任务),**读了就好,不要 ack**,把它留给该处理的那个会话
+ - 只有"这件事归我处理"时才推进水位
+ - 拿不准就别 ack。重复看到一条消息是噪音,让它对所有人消失是丢消息
+
## 4. 被新消息叫醒(可选,需要武装)
默认情况下,对方发的消息要等**下次有人跟你说话**时你才会看到。要让它主动叫醒你:
```
bash scripts/os-watch.sh <session_id> <team_id> <reader> & # run_in_background
```
watcher 每 8 秒问一次 `GET /api/wake/actionable`,发现有人点名你就退出,而后台任务
退出会让 harness 重新调起你。实测:对端发消息后约 8 秒内自动醒来,用户零输入。
约束:
- **随会话消亡**,1h 硬超时后要重新武装。它不是常驻服务
- 第三个参数 `reader` 不传就不把信道点名算作唤醒信号
- 判据是"**新到达**"而非"当前未读":读了但没清零的消息不会让它反复唤醒你
## 检查有没有人叫你
```
channel_unread(reader="leader-cc") # 纯读,不会清掉未读
```
## 给用户解释时的准确说法
> 两个 AI 可以互相留言,而且不用等你开口就能收到——但两侧的机制不同。**CC 侧**:开口
> 时会被提示有未读;武装 watcher 后,消息一到就把会话叫醒(实测约 8 秒)。**Codex 侧**:
> 宿主按周期把它续跑,它在新一轮里自己去查信(实测 5 分钟周期、有界 12 次);它的
> 开口时自动提示尚未验通。会话完全退出后两侧都收不到,那时模型并不在运行。
不要把 CC 侧的"8 秒"套到 Codex 上说成"两个 AI 实时互通"。两边给的是**检查频率**,
不是端到端延迟:CC 每 8 秒轮询一次,Codex 每 5 分钟调度一次。实际多久收到还取决于
消息何时到达、宿主是否按时续跑,别拿这两个数去推一个确定的延迟承诺。