275 lines
8.8 KiB
Markdown
275 lines
8.8 KiB
Markdown
# 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 仍可工作
|