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

7.3 KiB
Raw Blame History

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.sqlitethreads.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 末尾去重”:

  • 生成稳定镜像 keyboss-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 行为