# Codex Desktop Thread Sync Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 让 Boss App 发往单线程 Codex 会话的用户消息,在继续现有 `conversation_reply -> codex exec resume` 主链前,同步镜像进本机 Codex Desktop 的同一个线程历史。 **Architecture:** 服务端继续以 Boss 项目账本为主真相,但给普通单线程 `conversation_reply` 任务补齐 `sourceMessage*` 元数据和显式镜像开关。`local-agent` 在执行 `codex exec resume` 前,按 `targetCodexThreadRef` 解析目标 rollout 文件,先做一次本地 user_message append + 去重,再继续原有执行链。rollout 定位优先使用 `state_5.sqlite`,若本机 Codex CLI/Desktop 版本导致状态库不可用,则回退扫描 `~/.codex/sessions`;rollout 写入后尽量刷新 `threads.updated_at / updated_at_ms / has_user_event`,但不依赖 GUI 自动化。 **Tech Stack:** Next.js App Router, TypeScript, Node.js, sqlite, local-agent Node runtime, `tsx --test`, Node test runner --- ### Task 1: 给 conversation task 补齐 Desktop 镜像元数据 **Files:** - Modify: `src/lib/boss-data.ts` - Modify: `src/lib/boss-master-agent.ts` - Modify: `src/app/api/v1/projects/[projectId]/messages/route.ts` - Test: `tests/single-thread-message-execution.test.ts` - [x] **Step 1: 写失败测试,要求普通单线程消息返回的任务带 sourceMessage 元数据** ```ts assert.equal(task?.sourceMessageId, message.id); assert.equal(task?.sourceMessageBody, "请同步一下当前阻塞情况"); assert.equal(task?.sourceMessageSentAt, message.sentAt); assert.equal(task?.mirrorBossUserMessageToCodexDesktop, true); ``` - [x] **Step 2: 运行测试确认失败** Run: `npx tsx --test tests/single-thread-message-execution.test.ts` Expected: FAIL,提示 `sourceMessageBody/sourceMessageSentAt/mirrorBossUserMessageToCodexDesktop` 不存在或断言失败。 - [x] **Step 3: 写最小实现** 在 `MasterAgentTask` 和状态序列化/反序列化里补字段: ```ts sourceMessageId?: string; sourceMessageBody?: string; sourceMessageSentAt?: string; mirrorBossUserMessageToCodexDesktop?: boolean; ``` 在 `queueThreadConversationReplyTask` 中透传: ```ts sourceMessageId: params.sourceMessageId, sourceMessageBody: params.sourceMessageBody, sourceMessageSentAt: params.sourceMessageSentAt, mirrorBossUserMessageToCodexDesktop: params.relayViaMasterAgent ? undefined : true, ``` 在消息 route 调用时补: ```ts const queuedTask = await queueThreadConversationReplyTask({ projectId, requestMessageId: message.id, requestText: message.body, requestedBy: session.displayName || session.account, requestedByAccount: session.account, sourceMessageId: message.id, sourceMessageBody: message.body, sourceMessageSentAt: message.sentAt, }); ``` - [x] **Step 4: 运行测试确认通过** Run: `npx tsx --test tests/single-thread-message-execution.test.ts` Expected: PASS ### Task 2: 新增 rollout writer,并确保重复任务不重复写 Desktop 线程 **Files:** - Create: `local-agent/codex-thread-rollout-writer.mjs` - Test: `tests/local-agent-codex-rollout-writer.test.mjs` - [x] **Step 1: 写失败测试,约束 writer 会写入 user_message 且按 sourceMessageId 去重** ```js test("appendBossUserMessageToCodexThreadRollout writes one user_message event and dedupes by source message id", async () => { const first = await appendBossUserMessageToCodexThreadRollout({ ... }); const second = await appendBossUserMessageToCodexThreadRollout({ ... }); assert.equal(first.status, "written"); assert.equal(second.status, "duplicate"); }); ``` - [x] **Step 2: 运行测试确认失败** Run: `node --test tests/local-agent-codex-rollout-writer.test.mjs` Expected: FAIL,提示模块不存在或导出函数不存在。 - [x] **Step 3: 写最小实现** 实现 writer 逻辑: ```js export async function appendBossUserMessageToCodexThreadRollout(params) { const rolloutPath = await resolveThreadRolloutPath(params); const duplicate = await hasBossSourceMessageInRolloutTail(rolloutPath, params.sourceMessageId); if (duplicate) return { status: "duplicate", rolloutPath }; const responseItem = JSON.stringify({ timestamp: params.sentAt, type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: params.message }], }, }); const event = JSON.stringify({ timestamp: params.sentAt, type: "event_msg", payload: { type: "user_message", message: params.message, images: [], local_images: [], text_elements: [], metadata: { bossSourceMessageId: params.sourceMessageId, bossMirroredFrom: "boss-app", }, }, }); await appendFile(rolloutPath, `${responseItem}\n${event}\n`, "utf8"); return { status: "written", rolloutPath }; } ``` - [x] **Step 4: 运行测试确认通过** Run: `node --test tests/local-agent-codex-rollout-writer.test.mjs` Expected: PASS ### Task 3: 在 local-agent 执行 resume 前追加 Desktop 线程镜像 **Files:** - Modify: `local-agent/codex-task-runner.mjs` - Modify: `tests/local-agent-codex-task-runner.test.mjs` - [x] **Step 1: 写失败测试,要求 prepare 阶段保留镜像计划,且 relay task 不启用** ```js assert.deepEqual(result.execution.desktopMirror, { enabled: true, sourceMessageId: "msg-1", sourceMessageBody: "请继续推进", }); ``` - [x] **Step 2: 运行测试确认失败** Run: `node --test tests/local-agent-codex-task-runner.test.mjs` Expected: FAIL,提示 `desktopMirror` 不存在。 - [x] **Step 3: 写最小实现** 在 `buildCodexTaskExecution` 返回值中增加: ```js desktopMirror: shouldMirrorBossUserMessageToDesktop(task) ? { enabled: true, sourceMessageId: task.sourceMessageId, sourceMessageBody: task.sourceMessageBody, sourceMessageSentAt: task.sourceMessageSentAt, targetThreadRef, } : { enabled: false } ``` 其中 `shouldMirrorBossUserMessageToDesktop(task)` 需要保证: - `task.taskType === "conversation_reply"` - `task.mirrorBossUserMessageToCodexDesktop === true` - `task.relayViaMasterAgent !== true` - `targetThreadRef/sourceMessageId/sourceMessageBody` 全部存在 - [x] **Step 4: 运行测试确认通过** Run: `node --test tests/local-agent-codex-task-runner.test.mjs` Expected: PASS ### Task 4: 在实际任务执行前调用 rollout writer,并保持现有 resume/complete 主链不回归 **Files:** - Modify: `local-agent/server.mjs` - Modify: `tests/local-agent-codex-task-runner.test.mjs` - Modify: `tests/single-thread-message-execution.test.ts` - [x] **Step 1: 写失败测试,要求 server 在 spawn codex 前先执行 rollout 镜像** ```js assert.equal(writerCalls.length, 1); assert.equal(writerCalls[0].sourceMessageId, "msg-1"); assert.equal(writerCalls[0].message, "请继续推进"); ``` - [x] **Step 2: 运行测试确认失败** Run: `node --test tests/local-agent-codex-task-runner.test.mjs` Expected: FAIL,提示 writer 未被调用。 - [x] **Step 3: 写最小实现** 在 `runMasterAgentTask` 的 `spawn("codex", ...)` 前增加: ```js if (codexExecution.desktopMirror?.enabled) { await appendBossUserMessageToCodexThreadRollout({ stateDbPath: config.codexStateDbPath, targetThreadRef: codexExecution.desktopMirror.targetThreadRef, sourceMessageId: codexExecution.desktopMirror.sourceMessageId, message: codexExecution.desktopMirror.sourceMessageBody, sentAt: codexExecution.desktopMirror.sourceMessageSentAt ?? new Date().toISOString(), }); } ``` 如果镜像失败: - 不吞掉错误 - 直接按任务失败返回,让链路保持 fail-closed - [x] **Step 4: 运行定向测试确认通过** Run: ```bash node --test tests/local-agent-codex-rollout-writer.test.mjs node --test tests/local-agent-codex-task-runner.test.mjs npx tsx --test tests/single-thread-message-execution.test.ts ``` Expected: PASS ### Task 5: 回归验证与文档同步 **Files:** - Modify: `README.md`(如需补一句运行时说明) - Modify: `docs/architecture/current_runtime_and_deploy_status_cn.md` - Modify: `docs/architecture/api_and_service_inventory_cn.md` - [x] **Step 1: 跑仓库要求的基线验证** Run: ```bash npm run lint npm run build ``` Expected: PASS - [x] **Step 2: 补文档** 在运行时/服务清单文档补一句: - Boss 普通线程单聊现在会在 local-agent 执行 `codex exec resume` 前,把 Boss 用户消息镜像进目标 Codex Desktop 线程 rollout - 该能力仅针对已绑定 `codexThreadRef` 的单线程会话 - [x] **Step 3: 完成最终自检** 检查: - 没有把主 Agent 会话或 takeover relay 错写进 Desktop 子线程 - 没有重复写 rollout - 现有 heartbeat 读取 recent desktop replies 仍可工作