From dbdaab8d0ff241e7c51fa22313ef03f237f775e0 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Wed, 3 Jun 2026 15:00:25 +0800 Subject: [PATCH] feat: mirror boss messages via app server --- docs/architecture/ai_handoff_index_cn.md | 1 + .../api_and_service_inventory_cn.md | 1 + .../codex_server_progress_card_cn.md | 1 + .../current_runtime_and_deploy_status_cn.md | 1 + local-agent/codex-app-server-runner.mjs | 64 +++++++++++++++++++ tests/fixtures/codex-app-server-runtime.mjs | 10 ++- ...cal-agent-codex-app-server-runner.test.mjs | 44 +++++++++++++ 7 files changed, 121 insertions(+), 1 deletion(-) diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index 52a1d64..536c1bd 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -171,6 +171,7 @@ - 当前已补 Codex App Server 受控线程改名:`POST /api/v1/projects/[projectId]/rename` 在 `mode=thread` 且绑定真实 `codexThreadRef` 时,会在本地 Boss 改名后创建 `intentCategory=thread_rename` 任务,`local-agent` 直接调用 `thread/name/set`;该链路不启动普通 turn,不把 thread 原始字段写回 APP,只提示“已同步 Codex 线程名称”。设备离线、并发冲突或 App Server 不可用不会回滚 Boss 本地改名。 - 当前已补 Codex App Server 受控线程 Git 元数据同步:`POST /api/v1/projects/[projectId]/thread-metadata` 会创建 `intentCategory=thread_metadata_sync` 任务,`local-agent` 直接调用 `thread/metadata/update`;当前只允许同步 `gitInfo.sha / branch / originUrl`,不会启动普通 turn,也不允许写入任意 metadata。 - 当前已补 Codex App Server 受控线程分叉:`POST /api/v1/projects/[projectId]/thread-fork` 会创建 `intentCategory=thread_fork` 任务,`local-agent` 直接调用 `thread/fork`;当前不允许远程覆盖 model、sandbox、instructions 或 config,也不会把 path、cwd、turns、instructionSources 写回 APP。新线程进入 Boss 会话列表仍依赖 thread discovery / 导入链路。 +- 当前已补 Codex App Server 版 Boss 用户消息镜像:普通单线程 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true` 时,`local-agent/codex-app-server-runner.mjs` 会在 `thread/resume` 后、`turn/start` 前调用 `thread/inject_items`,把 Boss APP 用户原文作为 `role=user` 的 Responses item 写入目标 Codex 线程模型可见历史;任务结果只回传 `threadHistorySync.threadId / injectedItemCount / source`,不回传消息 ID、内部 prompt 或用户原文。CLI rollout 镜像仍保留为 App Server 不可用前的 fallback 链路。 - 当前 boss-agent 已支持 Mac OTA:`local-agent/boss-agent-ota-runner.mjs` 默认开启,每 5 分钟检查服务端最新包;状态页可手动检查或下载并安装,安装时保留原绑定配置,只更新版本号和本机 runtime 路径。最新验证版本为 `20260516221619`,已在 MacBook Air `macbook-air` 上确认 OTA 下载校验、暂存、覆盖安装后不会误切到默认 `config.cloud.json`。正式分发脚本已预留 Developer ID 公证路径:`BOSS_AGENT_NOTARIZE=1` 配合 notary profile 或 Apple ID 凭据。 - 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA;`MasterAgentTask` claim 会记录 attempt 和 lease,运行中任务可按租约重试,超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。 - 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果 diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 224c1a2..3ac91be 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -134,6 +134,7 @@ - App Server heartbeat discovery 现在支持 `skills/extraRoots/set`:配置 `codexAppServerSkillExtraRoots` 或环境变量 `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 后,runner 会先把共享 Skill 根下发给 App Server,再刷新 `skills/list`,并写入 `capabilities.codexAppServer.metadata.skillExtraRootsSummary`。该字段用于 APP/后台展示企业共享 Skill 根是否已下发;只保留数量、basename 和状态,不保存根目录绝对路径、Skill 文件路径或配置原文。 - App Server heartbeat discovery 现在支持 `hooks/list`,写入 `capabilities.codexAppServer.metadata.hookSummary`。该字段用于 APP/后台展示本机 Codex hook 治理状态;只保留 workspace 数、hook 数、启用数、受管 / 可信 / 修改 / 未信任计数、warning / error 计数和事件 / handler 类型,不保存 hook key、command、sourcePath、statusMessage、hash、error message 或本地路径。 - 当前 Codex App Server runner 已新增第一版 Boss Inter-Thread Broker:任务携带 `intentCategory=thread_collaboration`、`sourceCodexThreadRef` 和 `targetCodexThreadRef` 时,会先 `thread/read` 源线程,再通过 `thread/inject_items` 向目标线程注入受控摘要,最后 `turn/start` 目标线程;服务端入口是 `POST /api/v1/projects/[projectId]/thread-collaboration`,负责权限、源/目标线程校验和任务排队。这不是假设官方线程 P2P,而是 Boss 自己做线程协作编排。 +- 当前 Codex App Server runner 已新增 Boss 用户消息镜像:普通 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true`、`sourceMessageBody` 和目标 `codexThreadRef` 时,会先 `thread/resume`,再 `thread/inject_items` 写入 `role=user` 的 Boss APP 用户原文,最后 `turn/start`;该链路用于让 APP 发起的对话进入 Codex Desktop 同一线程历史。执行结果只保存 `threadHistorySync` 安全摘要,不保存 App Server 原始 item、消息 ID、用户原文、系统提示词或内部调度字段。 - 当前 Codex App Server runner 已新增受控线程回滚:任务携带 `intentCategory=thread_rollback`、目标 `codexThreadRef` 和 `rollbackNumTurns` 时,会调用 `thread/rollback` 回滚目标线程最近 N 轮,不会启动新 turn,也不会把 App Server 返回的 thread/turn/items 写回 APP。服务端入口是 `POST /api/v1/projects/[projectId]/thread-rollback`,只保存回滚轮数、原因和执行摘要;边界是只回滚 Codex 线程历史,不自动还原本地文件变更。 - 当前 Codex App Server runner 已新增受控线程压缩:任务携带 `intentCategory=thread_compact` 和目标 `codexThreadRef` 时,会调用 `thread/compact/start` 发起上下文压缩,不会启动普通 turn,也不会把 contextCompaction item 的原始字段写回 APP。服务端入口是 `POST /api/v1/projects/[projectId]/thread-compact`,只保存压缩原因和执行摘要;边界是只压缩 Codex 线程上下文,不代表代码修改、文件恢复或版本发布完成。 - 当前 Codex App Server runner 已新增受控线程归档 / 恢复:任务携带 `intentCategory=thread_archive|thread_unarchive`、目标 `codexThreadRef` 和 `threadLifecycleAction` 时,会直接调用 `thread/archive` 或 `thread/unarchive`,不会先 resume 已归档线程,也不会启动普通 turn。服务端入口是 `POST /api/v1/projects/[projectId]/thread-archive`,只保存生命周期动作、原因和执行摘要;边界是只改变 Codex 线程生命周期状态,不代表代码修改、文件恢复或版本发布完成。 diff --git a/docs/architecture/codex_server_progress_card_cn.md b/docs/architecture/codex_server_progress_card_cn.md index ae7dd25..715d0cd 100644 --- a/docs/architecture/codex_server_progress_card_cn.md +++ b/docs/architecture/codex_server_progress_card_cn.md @@ -146,6 +146,7 @@ UI 参考: - `local-agent/codex-app-server-runner.mjs` 已把 App Server `windowsSandbox/setupCompleted` 归一成 `executionProgress.windowsSandbox`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 setup error 中的 token、本地 Windows 路径和 sourcePath 不外泄 - 新增实时进度入口 `POST /api/v1/master-agent/tasks/[taskId]/progress`,设备端可在任务执行中持续刷新同一张 `execution_progress` 卡;`local-agent` 的 App Server runner 已在收到协议进度事件时调用该接口,complete 仍携带最终进度作为兜底 - 新增服务端线程协作入口 `POST /api/v1/projects/[projectId]/thread-collaboration`,由 Boss 校验源/目标项目权限并创建 `intentCategory=thread_collaboration` 的 `conversation_reply` 任务;设备端继续通过 App Server runner 执行 `thread/read -> thread/inject_items -> turn/start`,避免把“线程互通”误做成无监管 P2P +- 新增 Boss APP 用户消息镜像到 Codex App Server 线程历史:普通 `conversation_reply` 任务已复用 `mirrorBossUserMessageToCodexDesktop` 标记;设备端通过 App Server runner 执行 `thread/resume -> thread/inject_items(user message) -> turn/start`,让 APP 发起的用户输入进入 Codex Desktop 同一线程的模型可见历史。执行结果只返回 `threadHistorySync` 安全摘要,不回写消息 ID、用户原文、内部 prompt 或 App Server 原始 item。 - 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId` 时,App Server runner 会调用 `turn/steer`,并把 `turnControl=steer`、`turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start` - 新增活跃 turn 中断:用户或管理员通过任务取消接口把任务转为 `canceled` 后,设备端会轮询 `GET /api/v1/master-agent/tasks/[taskId]/control-state`;如果当前任务已经启动 App Server turn,runner 会在同一个 JSON-RPC 连接上调用 `turn/interrupt`,并把返回的 `interrupted` 处理成干净取消,不再把用户主动取消误判成 runtime failure - 新增受控线程回滚:服务端入口 `POST /api/v1/projects/[projectId]/thread-rollback` 会创建 `intentCategory=thread_rollback` 任务;设备端通过 App Server 调用 `thread/rollback`,只返回“已回滚最近 N 轮”的用户可见摘要,不启动新 turn,不保存 App Server 返回的 thread/turn/items。该能力只回滚 Codex 线程历史,不自动还原本地文件变更,后续如需文件级撤回必须另走 Git / 文件快照恢复链路。 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index 3f5d45b..e2afa29 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -263,6 +263,7 @@ cd /Users/kris/code/boss - 当前已绑定真实 `codexThreadRef` 的普通单线程聊天,会在 `local-agent` 执行 `codex exec resume` 前,先把 Boss 用户消息镜像写入对应 Codex Desktop rollout;这样 APP 发起的消息也能进入桌面版同一线程历史,并按 `sourceMessageId` 去重。rollout 定位优先使用 `state_5.sqlite`,状态库不可用或索引缺失时回退扫描 `~/.codex/sessions`;写入后会尽量刷新 `threads.updated_at / updated_at_ms / has_user_event`,再通过 `codex://threads/{threadId}` 深链提示桌面版打开目标线程 - 当前 `local-agent` 已新增 `Codex App Server` provider:boss-agent 默认配置 `codexAppServerEnabled=true`,`conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server;长驻连接可通过 `codexAppServerAuthTokenFile` 或 `BOSS_CODEX_APP_SERVER_AUTH_TOKEN_FILE` 提供 bearer token。随后 runner 下发 `turn/start` 并收集流式 agent 回复;如果单个 JSON-RPC 请求返回 `-32001 / retry later`,runner 会先做指数退避重试;如果任务携带 `targetCodexTurnId`,会改用 `turn/steer` 干预活跃 turn;如果 App Server 在 turn 启动前失败,默认允许回退到 `codex exec resume`,如果 turn 已经启动则不再回退,避免同一轮用户消息被重复执行。桌面控制另有 `codexComputerUseEnabled=true`,默认先走 Codex Computer Use,再回退 CUA Driver。 - 当前已新增 Boss 自有 Inter-Thread Broker 第一版:服务端入口 `POST /api/v1/projects/[projectId]/thread-collaboration` 会创建带源/目标 Codex 线程引用的协作任务;App Server runner 执行 `thread/read(source) -> thread/inject_items(target) -> turn/start(target)`,用于让一个线程的结论受控进入另一个线程,不依赖官方任意线程 P2P 互聊能力 +- 当前已新增 App Server 版 Boss 用户消息镜像:普通 `conversation_reply` 任务携带 `mirrorBossUserMessageToCodexDesktop=true` 时,App Server runner 会在 `thread/resume` 后、`turn/start` 前调用 `thread/inject_items`,把 Boss APP 用户原文写成目标 Codex 线程的 `role=user` Responses item;任务结果只回写 `threadHistorySync.threadId / injectedItemCount / source`,不回写消息 ID、用户原文、内部调度 prompt 或系统约束。CLI rollout 写入仍作为 App Server 不可用前的兼容兜底。 - 当前已新增 Codex App Server 受控线程回滚:服务端入口 `POST /api/v1/projects/[projectId]/thread-rollback` 会创建 `intentCategory=thread_rollback` 任务;App Server runner 执行 `thread/rollback(target, numTurns)`,只回写“已回滚最近 N 轮”的用户可见摘要,不启动新 turn,不保存 App Server 返回的 thread/turn/items。该能力只回滚 Codex 线程历史,不自动还原本地文件变更。 - 当前已新增 Codex App Server 受控线程压缩:服务端入口 `POST /api/v1/projects/[projectId]/thread-compact` 会创建 `intentCategory=thread_compact` 任务;App Server runner 执行 `thread/compact/start(target)`,只回写“已发起上下文压缩”的用户可见摘要,不启动普通 turn,不保存 contextCompaction item 原始字段。该能力只压缩 Codex 线程上下文,不代表代码修改、文件恢复或版本发布完成。 - 当前已新增 Codex App Server 受控线程归档 / 恢复:服务端入口 `POST /api/v1/projects/[projectId]/thread-archive` 会创建 `intentCategory=thread_archive|thread_unarchive` 任务;App Server runner 直接执行 `thread/archive(target)` 或 `thread/unarchive(target)`,不先 resume 已归档线程,不启动普通 turn,不保存 App Server 返回的 thread 原始字段。该能力只改变 Codex 线程生命周期状态,不代表代码修改、文件恢复或版本发布完成。 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index fb2c22d..2da28b3 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -86,6 +86,17 @@ function isThreadForkTask(task) { return task?.intentCategory === "thread_fork" || task?.taskType === "thread_fork"; } +function shouldMirrorBossUserMessageToCodexThread(task) { + return ( + task?.mirrorBossUserMessageToCodexDesktop === true || + task?.syncUserMessageToCodexThread === true + ); +} + +function resolveBossUserMessageText(task) { + return trimToDefined(task?.sourceMessageBody) || trimToDefined(task?.requestText); +} + function resolveThreadRenameName(task) { return trimToDefined(task?.threadRenameName || task?.threadName || task?.name); } @@ -2896,6 +2907,50 @@ async function maybeInjectInterThreadContext({ request, task, targetThreadId }) }; } +function buildBossUserMessageInjectionItem({ task, text }) { + return { + type: "message", + role: "user", + content: [ + { + type: "input_text", + text, + }, + ], + metadata: { + boss_source: "boss_app", + boss_message_kind: "user_message", + ...(trimToDefined(task?.sourceMessageSentAt) + ? { boss_source_sent_at: trimToDefined(task.sourceMessageSentAt) } + : {}), + }, + }; +} + +async function maybeInjectBossUserMessage({ request, task, targetThreadId, targetTurnId, hasExistingThreadRef }) { + if ( + !targetThreadId || + targetTurnId || + !hasExistingThreadRef || + !shouldMirrorBossUserMessageToCodexThread(task) + ) { + return undefined; + } + const text = resolveBossUserMessageText(task); + if (!text) { + return undefined; + } + await request("thread/inject_items", { + threadId: targetThreadId, + items: [buildBossUserMessageInjectionItem({ task, text })], + }); + return { + threadId: targetThreadId, + injectedItemCount: 1, + source: "boss_user_message", + }; +} + export async function executeCodexAppServerTask(runnerConfig, task) { if (!shouldUseCodexAppServerTaskRunner(runnerConfig, task)) { return createFailure("CODEX_APP_SERVER_DISABLED", { canFallbackToCli: true }); @@ -3385,6 +3440,13 @@ export async function executeCodexAppServerTask(runnerConfig, task) { task, targetThreadId: threadId, }); + const threadHistorySync = await maybeInjectBossUserMessage({ + request, + task, + targetThreadId: threadId, + targetTurnId: targetTurnRef, + hasExistingThreadRef: Boolean(targetThreadRef), + }); const turnControl = targetTurnRef ? "steer" : "start"; const turnResult = targetTurnRef @@ -3420,6 +3482,7 @@ export async function executeCodexAppServerTask(runnerConfig, task) { transport: runnerConfig.transport, executionProgress: progressCollector.snapshot(), interThreadBroker, + threadHistorySync, canFallbackToCli: false, }; } @@ -3433,6 +3496,7 @@ export async function executeCodexAppServerTask(runnerConfig, task) { transport: runnerConfig.transport, executionProgress: progressCollector.snapshot(), interThreadBroker, + threadHistorySync, canFallbackToCli: false, }; } catch (error) { diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index 50da7b1..ae19d16 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -796,6 +796,12 @@ rl.on("line", (line) => { return; } const text = message.params?.input?.find?.((item) => item?.type === "text")?.text ?? ""; + const injectedUserMessageText = + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE === "1" + ? injectedItems + .flatMap((item) => item?.content ?? []) + .find((content) => content?.type === "input_text")?.text + : ""; send({ id: message.id, result: { @@ -1654,7 +1660,9 @@ rl.on("line", (line) => { threadId: message.params?.threadId, turnId: "turn-fixture", delta: - process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD === "1" + injectedUserMessageText + ? `INJECTED_USER_MESSAGE:${injectedUserMessageText}` + : process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD === "1" ? `INTER_THREAD_INJECTED:${JSON.stringify(injectedItems)}` : `APP_SERVER_REPLY:${text}`, }, diff --git a/tests/local-agent-codex-app-server-runner.test.mjs b/tests/local-agent-codex-app-server-runner.test.mjs index 5d2b8fb..2a53978 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -508,6 +508,50 @@ test("codex app-server runner resumes a thread and collects streamed agent text" assert.equal(result.transport, "stdio"); }); +test("codex app-server runner injects Boss user message into the target Codex thread before starting a turn", async () => { + const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE; + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE = "1"; + try { + const runnerConfig = getCodexAppServerRunnerConfig(process.env, { + codexAppServerEnabled: true, + codexAppServerCommand: process.execPath, + codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"], + codexAppServerWorkdir: repoRoot, + codexAppServerTimeoutMs: 5000, + masterAgentModel: "gpt-5.4", + }); + const task = { + taskId: "task-app-server-sync-user-message", + taskType: "conversation_reply", + targetCodexThreadRef: "019d-app-server-thread", + targetCodexFolderRef: repoRoot, + executionPrompt: "请修复登录页跳转问题", + mirrorBossUserMessageToCodexDesktop: true, + userMessageId: "boss-msg-1", + sourceMessageId: "boss-msg-1", + sourceMessageBody: "请修复登录页跳转问题", + userDisplayName: "你", + }; + + const result = await executeCodexAppServerTask(runnerConfig, task); + + assert.equal(result.status, "completed"); + assert.equal(result.threadId, "019d-app-server-thread"); + assert.equal(result.turnControl, "start"); + assert.equal(result.threadHistorySync?.threadId, "019d-app-server-thread"); + assert.equal(result.threadHistorySync?.injectedItemCount, 1); + assert.equal(result.threadHistorySync?.source, "boss_user_message"); + assert.match(result.replyBody, /INJECTED_USER_MESSAGE:请修复登录页跳转问题/); + assert.doesNotMatch(JSON.stringify(result), /boss-msg-1/); + } finally { + if (previous === undefined) { + delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE; + } else { + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_ECHO_INJECTED_USER_MESSAGE = previous; + } + } +}); + test("codex app-server runner converts protocol progress events into Boss execution progress", async () => { const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS; process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS = "1";