182 lines
7.3 KiB
Markdown
182 lines
7.3 KiB
Markdown
# 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 行为
|