Files
boss/docs/superpowers/specs/2026-04-21-codex-desktop-thread-sync-design.md

182 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Codex Desktop 同线程消息镜像设计
目标:当用户在 Boss App 里对一个已绑定 `codexThreadRef` 的单线程会话发消息时,这条用户消息不仅进入 Boss 自己的项目账本和 `conversation_reply` 执行队列,也要被镜像进本机 Codex Desktop 的同一个线程历史里。这样用户稍后回到 Codex Desktop看见的是同一个线程下连续的聊天记录而不是 Boss 与 Desktop 两套割裂历史。
## 背景与现状
当前 Boss 的普通线程单聊主链是:
- Web / Android 调 `POST /api/v1/projects/[projectId]/messages`
- 服务端写入 Boss 项目消息账本
- 服务端排一个 `conversation_reply` 任务
- 本机 `local-agent` 认领任务后调用 `codex exec resume <targetCodexThreadRef>`
- Codex 线程完成后,再把线程回复回写到 Boss 项目账本
这条链现在已经能做到“Desktop 回复被 Boss 看见”,因为 heartbeat 扫描 `~/.codex/sessions/.../rollout-*.jsonl` 时,会把最近桌面 assistant 回复镜像回 Boss。缺口在反方向
- Boss App 发起的用户消息只存在于 Boss 项目账本
- `codex exec resume` 虽然会把 prompt 交给目标线程继续执行,但 Boss 发起的这条消息并不会先出现在 Desktop 线程历史
- 结果就是用户在 APP 和 Desktop 里看到的“同一个线程”并不是同一份完整聊天记录
## 方案对比
### 方案 1直接操控 Codex Desktop GUI 输入并发送
优点:
- 理论上最贴近“像用户在桌面端亲自发了一条消息”
缺点:
- 依赖窗口前台、焦点、输入法、系统权限
- 极易被 Codex Desktop UI 更新打断
- 无法稳定支持后台运行和多线程并发
不推荐作为主方案。
### 方案 2直接把 Boss 用户消息写入对应 Codex rollout JSONL再继续现有 `codex exec resume`
优点:
- 与当前 Desktop/CLI 共用的真实线程存储一致
- 不需要操控 GUI
- 可以保持现有 `local-agent -> codex exec resume` 主链不变
- 能与现有 heartbeat 读取 rollout 的能力形成闭环
缺点:
- 需要谨慎贴合 Codex rollout 事件格式
- 需要处理重复写入和 Desktop 刷新感知
这是本次推荐方案。
### 方案 3单独给 Desktop 再建一条镜像线程
优点:
- 对现有线程文件侵入最小
缺点:
- 用户要的是“同一个线程”,不是“另一个镜像线程”
- 历史会继续分叉
不满足目标。
## 本次设计
### 1. 保持 Boss 账本为移动端/UI 主真相
Boss 的项目消息账本、会话排序、未读数、主 Agent 协同逻辑继续基于现有 `boss-state.json`。这次不把 Boss UI 改成直接读取 `~/.codex`
### 2. 对单线程 `conversation_reply` 任务增加“写入 Desktop 线程历史”的镜像步骤
当任务满足以下条件时,在 `local-agent` 侧做一次 rollout 镜像:
- `task.taskType === "conversation_reply"`
- 存在 `targetCodexThreadRef`
- 存在用户原始消息文本
- 不属于 `relayViaMasterAgent === true` 的接管中转任务
镜像行为:
- 优先通过 `state_5.sqlite``threads.rollout_path` 定位目标 rollout 文件
- 如果本机 Codex 因版本/迁移差异无法稳定解析 `state_5.sqlite`,则回退扫描 `~/.codex/sessions/**/rollout-*-<threadId>.jsonl`
- 向该 rollout 文件追加一组 Codex 用户消息记录:`response_item / message(role=user)``event_msg / user_message`
- 事件内容使用 Boss 原始用户消息文本,而不是执行 prompt
- 事件时间优先使用 Boss 消息的 `sentAt`
- 事件写入成功后,再继续现有 `codex exec resume`
### 3. 任务负载补齐“Boss 原始消息”字段
现在任务里只有:
- `requestMessageId`
- `requestText`
- `executionPrompt`
这还不够稳,因为后续去重和 Desktop 镜像需要区分:
- 哪条 Boss 用户消息已经镜像过
- 这次镜像的真实显示文本是什么
- 这条消息的原始时间戳是什么
因此为 `MasterAgentTask` 增加:
- `sourceMessageId?: string`
- `sourceMessageBody?: string`
- `sourceMessageSentAt?: string`
- `mirrorBossUserMessageToCodexDesktop?: boolean`
对普通线程单聊:
- `sourceMessageId = message.id`
- `sourceMessageBody = message.body`
- `sourceMessageSentAt = message.sentAt`
- `mirrorBossUserMessageToCodexDesktop = true`
对主 Agent 直聊、`@主Agent`、托管中转等不应写进子线程 Desktop 历史的场景,不开启这个标记。
### 4. 去重策略
同一条 Boss 消息可能因为:
- 任务重试
- local-agent 重启
- claim / complete 重放
而被多次处理。为避免在 Desktop 线程里重复写入同一条用户消息本次采用“rollout 末尾去重”:
- 生成稳定镜像 key`boss-user:<threadRef>:<sourceMessageId>`
- 写入的 `event_msg` 中带上 `payload.metadata.bossSourceMessageId`
- 写入前读取 rollout 尾部固定窗口,检查最近是否已经存在同一 `bossSourceMessageId`
- 若存在,则跳过写入,仅继续 `codex exec resume`
这样不需要引入新的状态库,也能与 Codex 原始线程文件保持局部自洽。
### 5. 刷新感知
第一版只写 rollout 还不够稳,因为 Desktop 的线程列表排序和“最近活跃”判断通常还依赖 `threads.updated_at / updated_at_ms / has_user_event`。因此本次实现改为:
- rollout append 成功后,若 `state_5.sqlite` 可写且能命中该 thread则同步刷新
- `updated_at`
- `updated_at_ms`
- `has_user_event = 1`
- 如果当前机器上的 Codex 状态库不可用、字段不兼容或压根没有这条 thread 记录,则只保留 rollout 写入,不把整条消息链路判成失败
这样做的取舍是:
- 先保证 Boss -> Codex Desktop 同线程历史不丢
- 再尽可能提升 Desktop 侧的列表刷新和最近活跃感知
- 不引入 GUI 自动化,不依赖桌面窗口前台
## 涉及文件
- 新增 `local-agent/codex-thread-rollout-writer.mjs`
- 修改 `local-agent/codex-task-runner.mjs`
- 修改 `local-agent/server.mjs`
- 修改 `src/lib/boss-data.ts`
- 修改 `src/app/api/v1/projects/[projectId]/messages/route.ts`
- 修改 `src/lib/boss-master-agent.ts`(如果当前普通线程任务创建逻辑在这里有共用 helper也一起补齐
- 新增测试 `tests/local-agent-codex-rollout-writer.test.mjs`
- 修改测试 `tests/local-agent-codex-task-runner.test.mjs`
- 修改测试 `tests/single-thread-message-execution.test.ts`
## 边界
- 本次只处理“Boss App -> 已绑定 Codex Desktop 同线程”的用户消息镜像
- 不处理群聊镜像到 Desktop
- 不处理主 Agent 自己的回复写入 Desktop 子线程
- 不做 Codex Desktop GUI 自动输入
- 不把 Boss 会话列表直接改成读取 Desktop 原始线程文件
## 验收标准
- 普通单线程会话发消息后,生成的 `conversation_reply` 任务带有完整 `sourceMessage*` 字段
- local-agent 在执行 `codex exec resume` 前,能把这条 Boss 用户消息写进目标 rollout
- 同一 `sourceMessageId` 重试时不会重复写入 rollout
- 若状态库可用,镜像后会同步刷新 thread 的活跃时间和 `has_user_event`
- 若状态库不可用或这台机器上的线程索引不完整,仍可通过 `sessions` 回退找到 rollout 并完成消息镜像
- 现有普通线程回复链不回归Boss 仍能收到 Codex 线程回复
- 若目标线程缺失、只读或 cwd 不合法,仍保持现有 fail-closed 行为