From 26b5e97614bc5da36ec7e91ed595f536159c91ef Mon Sep 17 00:00:00 2001 From: AI Bot Date: Mon, 1 Jun 2026 17:18:28 +0800 Subject: [PATCH] feat: surface codex thread config progress --- .../src/main/java/com/hyzq/boss/BossUi.java | 84 +++++++++++++++ .../boss/ProjectDetailActivityUiTest.java | 55 ++++++++++ docs/architecture/ai_handoff_index_cn.md | 2 +- .../codex_server_progress_card_cn.md | 6 +- .../current_runtime_and_deploy_status_cn.md | 4 +- local-agent/codex-app-server-runner.mjs | 100 ++++++++++++++++++ src/lib/boss-data.ts | 98 +++++++++++++++++ tests/fixtures/codex-app-server-runtime.mjs | 61 +++++++++++ ...cal-agent-codex-app-server-runner.test.mjs | 59 +++++++++++ .../master-agent-task-progress-route.test.ts | 76 +++++++++++++ 10 files changed, 540 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/BossUi.java b/android/app/src/main/java/com/hyzq/boss/BossUi.java index 3d1a098..ecdbae2 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossUi.java +++ b/android/app/src/main/java/com/hyzq/boss/BossUi.java @@ -1353,6 +1353,90 @@ public final class BossUi { } } + JSONObject threadGoal = progress == null ? null : progress.optJSONObject("threadGoal"); + JSONObject threadSettings = progress == null ? null : progress.optJSONObject("threadSettings"); + JSONObject compaction = progress == null ? null : progress.optJSONObject("compaction"); + boolean hasThreadConfig = threadGoal != null || threadSettings != null || compaction != null; + if (hasThreadConfig) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "线程配置")); + if (threadGoal != null) { + String status = threadGoal.optString("status", "").trim(); + String objective = threadGoal.optString("objective", "").trim(); + if ("cleared".equals(status)) { + card.addView(detailRow(context, "◎", "目标已清除", "", false)); + } else if (!TextUtils.isEmpty(status) || !TextUtils.isEmpty(objective)) { + card.addView(detailRow( + context, + "◎", + TextUtils.isEmpty(status) ? "目标" : "目标 " + status, + "", + false + )); + if (!TextUtils.isEmpty(objective)) { + card.addView(detailRow(context, "", objective, "", false, true)); + } + int tokensUsed = threadGoal.optInt("tokensUsed", 0); + int tokenBudget = threadGoal.optInt("tokenBudget", 0); + if (tokensUsed > 0 || tokenBudget > 0) { + StringBuilder budget = new StringBuilder(); + budget.append("预算 "); + if (tokensUsed > 0) { + budget.append(String.format(Locale.US, "%,d", tokensUsed)); + } else { + budget.append("0"); + } + if (tokenBudget > 0) { + budget.append(" / ").append(String.format(Locale.US, "%,d", tokenBudget)); + } + card.addView(detailRow(context, "", budget.toString(), "", false, true)); + } + } + } + if (threadSettings != null) { + String model = threadSettings.optString("model", "").trim(); + String provider = threadSettings.optString("modelProvider", "").trim(); + if (!TextUtils.isEmpty(model)) { + card.addView(detailRow( + context, + "◇", + TextUtils.isEmpty(provider) ? "模型 " + model : "模型 " + model + " · " + provider, + "", + false + )); + } + String approval = threadSettings.optString("approvalPolicy", "").trim(); + String sandbox = threadSettings.optString("sandboxPolicy", "").trim(); + if (!TextUtils.isEmpty(approval) || !TextUtils.isEmpty(sandbox)) { + String label = !TextUtils.isEmpty(approval) && !TextUtils.isEmpty(sandbox) + ? "审批 " + approval + " · 沙箱 " + sandbox + : !TextUtils.isEmpty(approval) ? "审批 " + approval : "沙箱 " + sandbox; + card.addView(detailRow(context, "◇", label, "", false)); + } + String collaborationMode = threadSettings.optString("collaborationMode", "").trim(); + String serviceTier = threadSettings.optString("serviceTier", "").trim(); + if (!TextUtils.isEmpty(collaborationMode) || !TextUtils.isEmpty(serviceTier)) { + String label = !TextUtils.isEmpty(collaborationMode) && !TextUtils.isEmpty(serviceTier) + ? "协作 " + collaborationMode + " · " + serviceTier + : !TextUtils.isEmpty(collaborationMode) ? "协作 " + collaborationMode : "服务 " + serviceTier; + card.addView(detailRow(context, "◇", label, "", false)); + } + } + if (compaction != null) { + String message = compaction.optString("message", "").trim(); + String status = compaction.optString("status", "").trim(); + if (!TextUtils.isEmpty(message) || !TextUtils.isEmpty(status)) { + card.addView(detailRow( + context, + "◷", + TextUtils.isEmpty(message) ? "上下文压缩 " + status : message, + "", + false + )); + } + } + } + JSONObject modelRoute = progress == null ? null : progress.optJSONObject("modelRoute"); JSONObject tokenUsage = progress == null ? null : progress.optJSONObject("tokenUsage"); JSONArray mcpServers = progress == null ? null : progress.optJSONArray("mcpServers"); diff --git a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java index 29e3e17..1509e18 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java @@ -1069,6 +1069,61 @@ public class ProjectDetailActivityUiTest { assertFalse(viewTreeContainsText(messageView, "sk-secret")); } + @Test + public void executionProgressMessageRendersCodexThreadGoalSettingsAndCompactionSections() throws Exception { + Intent intent = new Intent() + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "thread-config") + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程"); + TestProjectDetailActivity activity = Robolectric + .buildActivity(TestProjectDetailActivity.class, intent) + .setup() + .get(); + + JSONObject message = new JSONObject() + .put("id", "progress-thread-config-1") + .put("sender", "master") + .put("senderLabel", "主 Agent") + .put("body", "执行进度") + .put("kind", "execution_progress") + .put("sentAt", "2026-06-01T10:25:00+08:00") + .put("executionProgress", new JSONObject() + .put("status", "running") + .put("steps", new JSONArray() + .put(new JSONObject().put("text", "同步 Codex 线程配置").put("status", "running"))) + .put("threadGoal", new JSONObject() + .put("objective", "完成 App Server 线程目标同步") + .put("status", "active") + .put("tokenBudget", 120000) + .put("tokensUsed", 4800) + .put("timeUsedSeconds", 600)) + .put("threadSettings", new JSONObject() + .put("model", "gpt-5.5") + .put("modelProvider", "openai") + .put("approvalPolicy", "on-request") + .put("sandboxPolicy", "workspaceWrite") + .put("collaborationMode", "plan") + .put("serviceTier", "fast") + .put("cwd", "/Users/kris/code/boss/secret-project")) + .put("compaction", new JSONObject() + .put("status", "completed") + .put("message", "上下文已压缩"))); + + View messageView = ReflectionHelpers.callInstanceMethod( + activity, + "buildMessageView", + ReflectionHelpers.ClassParameter.from(JSONObject.class, message) + ); + + assertTrue(viewTreeContainsText(messageView, "线程配置")); + assertTrue(viewTreeContainsText(messageView, "目标 active")); + assertTrue(viewTreeContainsText(messageView, "完成 App Server 线程目标同步")); + assertTrue(viewTreeContainsText(messageView, "模型 gpt-5.5 · openai")); + assertTrue(viewTreeContainsText(messageView, "审批 on-request · 沙箱 workspaceWrite")); + assertTrue(viewTreeContainsText(messageView, "协作 plan · fast")); + assertTrue(viewTreeContainsText(messageView, "上下文已压缩")); + assertFalse(viewTreeContainsText(messageView, "/Users/kris")); + } + @Test public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception { Intent intent = new Intent() diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index e940c85..53b0461 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -153,7 +153,7 @@ - Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路 - 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败 - 当前量产方向已经明确为“Boss 企业控制面 + 可插拔执行协议”:多租户、权限、审批、审计、备份、回退和 Skill 治理由 Boss 承担,Codex App Server / Codex MCP / Codex CLI / Computer Use / 业务系统 API 都作为 provider 接入;详见 `docs/architecture/enterprise_ai_ops_architecture_cn.md` -- 当前 Codex App Server 已完成四批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started`、`item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated`、`thread/status/changed`、`thread/realtime/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,不保存 SDP、音频原始数据、raw item、remote installationId 或未清洗密钥。heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。 +- 当前 Codex App Server 已完成五批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started`、`item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated`、`thread/status/changed`、`thread/realtime/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId 或未清洗密钥。heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。 - 当前 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/codex_server_progress_card_cn.md b/docs/architecture/codex_server_progress_card_cn.md index 44a4b49..f35c509 100644 --- a/docs/architecture/codex_server_progress_card_cn.md +++ b/docs/architecture/codex_server_progress_card_cn.md @@ -1,6 +1,6 @@ # Codex Server 协议与 Boss 执行进度卡接入记录 -更新时间:`2026-05-31` +更新时间:`2026-06-01` ## 1. Codex 最新开放协议结论 @@ -16,7 +16,7 @@ Codex App Server 是更适合 Boss 长期接入的协议层,因为它面向富 - model/list、skills/list、plugin/list、app/list - command execution、file change、tool input、MCP tool-call approvals -Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写;2026-05-31 已继续把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved` 和 `item/fileChange/patchUpdated` 的安全摘要映射,APP 只展示审批状态、风险提醒和文件路径,不展示完整命令、diff、系统提示词或密钥。第三批已把 `thread/status/changed` 与 `thread/realtime/*` 归一成 `executionProgress.threadStatus / realtime`,APP 只展示活跃/等待审批/等待用户输入、realtime 文本摘要、音频片段计数和关闭/错误原因;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated` 和 `remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,用于 APP “运行状态”区块。`thread/realtime/sdp`、音频 base64、原始 realtime item、remote installationId 和未清洗的 MCP 错误不入账。 +Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写;2026-05-31 已继续把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved` 和 `item/fileChange/patchUpdated` 的安全摘要映射,APP 只展示审批状态、风险提醒和文件路径,不展示完整命令、diff、系统提示词或密钥。第三批已把 `thread/status/changed` 与 `thread/realtime/*` 归一成 `executionProgress.threadStatus / realtime`,APP 只展示活跃/等待审批/等待用户输入、realtime 文本摘要、音频片段计数和关闭/错误原因;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated` 和 `remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,用于 APP “运行状态”区块。2026-06-01 第五批已把 `thread/goal/updated|cleared`、`thread/settings/updated` 和 `thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`,用于 APP “线程配置”区块。`thread/realtime/sdp`、音频 base64、原始 realtime item、remote installationId、thread settings 的 `cwd`、compaction `turnId`、collaboration settings 内部 prompt 和未清洗的 MCP 错误不入账。 官方文档入口:`https://developers.openai.com/codex/app-server` @@ -75,6 +75,7 @@ APP 展示结构对齐截图: - `文件变更`:展示 App Server patchUpdated 中的文件路径和变更类型,不展示 diff - `线程状态`:展示 `active / idle / systemError / notLoaded` 以及 `waitingOnApproval / waitingOnUserInput` - `实时状态`:展示 realtime 启动、同步、关闭或错误状态,附带安全清洗后的 transcript 预览和计数 +- `线程配置`:展示 thread goal、模型 / provider、审批 / 沙箱、协作模式和上下文压缩状态 - `运行状态`:展示模型重路由、上下文用量、MCP 启动状态和远控连接状态 - `分支详情`:变更行、Git 操作、GitHub CLI 可用状态 - `生成结果`:从执行结果里提取文件、图片、APK、文档等产物名 @@ -125,6 +126,7 @@ UI 参考: - `local-agent/codex-app-server-runner.mjs` 已把 App Server 审批、Guardian warning 和 file-change patch 事件归一成 `executionProgress.approvals / warnings / fileChanges`;服务端和 Android 原生进度卡已支持展示,且测试覆盖了密钥和 diff 不外泄 - `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 SDP、音频原始数据和 raw item 不外泄 - `local-agent/codex-app-server-runner.mjs` 已把 App Server `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 installationId 和密钥不外泄 +- `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/goal/updated|cleared`、`thread/settings/updated`、`thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 cwd、turnId、内部 prompt 不外泄 - 新增实时进度入口 `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 - 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId` 时,App Server runner 会调用 `turn/steer`,并把 `turnControl=steer`、`turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start` diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index f68c675..20589ac 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -35,7 +35,7 @@ - `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist` - 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现 - 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准 -- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口,Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items`、`turn/steer`、`thread/realtime/*`、`command/exec` 和 `model/list` +- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口,Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items`、`turn/steer`、`thread/realtime/*`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`command/exec` 和 `model/list` - 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束,并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地 - 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime` - 当前已新增最小 `Telegram Gateway`:Boss 当前可直接暴露 Telegram webhook,把 Telegram 私聊或受控群聊文本桥接进 `master-agent` 或按群 / Topic 路由到指定 Boss 项目,并在主 Agent 异步任务完成后自动回推 Telegram;配置入口已接到 Web `/me/telegram` 和原生 Android `我的 > Telegram 接入` @@ -249,7 +249,7 @@ cd /Users/kris/code/boss - 当前 `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 互聊能力 - 当前 `local-agent` 对 `dispatch_execution` 任务会按 `orchestrationBackendId` 分流:默认走 `codex exec resume`;当任务显式选择 `omx-team` 且本机 `omxEnabled + omxCommand/omxArgs` 可用时,会改走 `OMX Team Runtime` JSON 协议执行并回写 `rawThreadReply / replyBody` -- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failed,Android 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起,Codex App Server 的 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed` 与 `thread/realtime/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要,并通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;complete 回写仍会携带最终进度兜底 +- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failed,Android 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 线程配置 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起,Codex App Server 的 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed` 与 `thread/realtime/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要;第五批已把 `thread/goal/*`、`thread/settings/updated` 和 `thread/compacted` 映射为线程配置摘要,只展示目标、模型、审批、沙箱、协作模式和上下文压缩状态,不保存 cwd、turnId 或 collaboration settings 内部 prompt。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;complete 回写仍会携带最终进度兜底 - 当前 `local-agent` heartbeat 已新增 Codex App Server capability discovery:按 TTL 拉取模型、provider 能力、Skill、Plugin、App 摘要,写入 `capabilities.codexAppServer.metadata`;Web 设备详情会展示 App Server 连接状态、模型数量、默认/快速/深度模型和扩展数量 - 当前 `MasterAgentTask` 已具备服务端租约和取消基础状态机:claim 会写入 `attemptCount / maxAttempts / leaseExpiresAt`,运行中任务租约过期后可被重新认领,超过重试上限会转 `timed_out`;`POST /api/v1/master-agent/tasks/[taskId]/cancel` 会把任务转 `canceled`,迟到的成功 complete 不会覆盖终态 - 当前 `local-agent` 对 `browser_control / desktop_control` 已从占位骨架升级成外部 runtime 桥:当本机配置了 `browserControlEnabled + browserControlCommand` 或 `computerUseEnabled + computerUseCommand` 时,会把标准化 JSON 请求透传给外部进程,并解析单行 JSON 结果;未启用时会 fail closed,返回明确的 runtime disabled 错误,不再假装执行成功 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index f3fdea2..fbdd697 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -763,6 +763,67 @@ function normalizeThreadStatusSnapshot(status) { }; } +function extractThreadGoalSnapshot(goal) { + if (!goal || typeof goal !== "object") { + return null; + } + const status = safeProgressText(goal.status, 40); + const objective = safeProgressText(goal.objective, 240); + if (!status && !objective) { + return null; + } + return { + ...(objective ? { objective } : {}), + status: status || "active", + ...(extractNumber(goal.tokenBudget) !== undefined ? { tokenBudget: extractNumber(goal.tokenBudget) } : {}), + ...(extractNumber(goal.tokensUsed) !== undefined ? { tokensUsed: extractNumber(goal.tokensUsed) } : {}), + ...(extractNumber(goal.timeUsedSeconds) !== undefined + ? { timeUsedSeconds: extractNumber(goal.timeUsedSeconds) } + : {}), + }; +} + +function extractSandboxPolicyName(sandboxPolicy) { + if (typeof sandboxPolicy === "string") { + return sandboxPolicy; + } + if (sandboxPolicy && typeof sandboxPolicy === "object") { + return sandboxPolicy.type; + } + return ""; +} + +function extractCollaborationModeName(collaborationMode) { + if (typeof collaborationMode === "string") { + return collaborationMode; + } + if (collaborationMode && typeof collaborationMode === "object") { + return collaborationMode.mode; + } + return ""; +} + +function extractThreadSettingsSnapshot(settings) { + if (!settings || typeof settings !== "object") { + return null; + } + const snapshot = { + model: safeProgressText(settings.model, 80) || undefined, + modelProvider: safeProgressText(settings.modelProvider, 80) || undefined, + approvalPolicy: + safeProgressText(typeof settings.approvalPolicy === "string" ? settings.approvalPolicy : "", 80) || undefined, + approvalsReviewer: safeProgressText(settings.approvalsReviewer, 80) || undefined, + sandboxPolicy: safeProgressText(extractSandboxPolicyName(settings.sandboxPolicy), 80) || undefined, + permissionProfile: safeProgressText(settings.activePermissionProfile?.id, 80) || undefined, + serviceTier: safeProgressText(settings.serviceTier, 80) || undefined, + effort: safeProgressText(settings.effort, 80) || undefined, + summary: safeProgressText(settings.summary, 80) || undefined, + collaborationMode: safeProgressText(extractCollaborationModeName(settings.collaborationMode), 80) || undefined, + personality: safeProgressText(settings.personality, 80) || undefined, + }; + return Object.values(snapshot).some(Boolean) ? snapshot : null; +} + function extractTokenUsageSnapshot(tokenUsage) { const total = tokenUsage?.total && typeof tokenUsage.total === "object" ? tokenUsage.total : {}; const totalTokens = extractNumber(total.totalTokens); @@ -1035,6 +1096,9 @@ function createProgressCollector() { let tokenUsage; const mcpServers = []; let remoteControl; + let threadGoal; + let threadSettings; + let compaction; const upsertArtifact = (artifact) => { if (!artifact || artifacts.some((item) => item.label === artifact.label)) { @@ -1305,6 +1369,33 @@ function createProgressCollector() { } return; } + if (message.method === "thread/goal/updated") { + const nextGoal = extractThreadGoalSnapshot(message.params?.goal); + if (nextGoal) { + threadGoal = nextGoal; + } + return; + } + if (message.method === "thread/goal/cleared") { + threadGoal = { + status: "cleared", + }; + return; + } + if (message.method === "thread/settings/updated") { + const nextSettings = extractThreadSettingsSnapshot(message.params?.threadSettings); + if (nextSettings) { + threadSettings = nextSettings; + } + return; + } + if (message.method === "thread/compacted") { + compaction = { + status: "completed", + message: "上下文已压缩", + }; + return; + } if (message.method === "thread/started") { upsertAgent(extractAgentFromThreadStarted(message.params)); } @@ -1357,6 +1448,15 @@ function createProgressCollector() { if (remoteControl) { result.remoteControl = { ...remoteControl }; } + if (threadGoal) { + result.threadGoal = { ...threadGoal }; + } + if (threadSettings) { + result.threadSettings = { ...threadSettings }; + } + if (compaction) { + result.compaction = { ...compaction }; + } return Object.keys(result).length > 0 ? result : undefined; }, }; diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 6c2707f..00f8587 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -187,6 +187,33 @@ export interface ExecutionProgressRemoteControl { environmentId?: string; } +export interface ExecutionProgressThreadGoal { + objective?: string; + status: string; + tokenBudget?: number; + tokensUsed?: number; + timeUsedSeconds?: number; +} + +export interface ExecutionProgressThreadSettings { + model?: string; + modelProvider?: string; + approvalPolicy?: string; + approvalsReviewer?: string; + sandboxPolicy?: string; + permissionProfile?: string; + serviceTier?: string; + effort?: string; + summary?: string; + collaborationMode?: string; + personality?: string; +} + +export interface ExecutionProgressCompaction { + status: string; + message?: string; +} + export interface ExecutionProgressSnapshot { taskId: string; projectId: string; @@ -211,6 +238,9 @@ export interface ExecutionProgressSnapshot { tokenUsage?: ExecutionProgressTokenUsage; mcpServers?: ExecutionProgressMcpServer[]; remoteControl?: ExecutionProgressRemoteControl; + threadGoal?: ExecutionProgressThreadGoal; + threadSettings?: ExecutionProgressThreadSettings; + compaction?: ExecutionProgressCompaction; updatedAt: string; } @@ -228,6 +258,9 @@ export interface ExecutionProgressInput { tokenUsage?: Partial; mcpServers?: Array & { name?: string }>; remoteControl?: Partial & { installationId?: unknown }; + threadGoal?: Partial; + threadSettings?: Partial & { cwd?: unknown }; + compaction?: Partial & { turnId?: unknown }; } export interface ForwardSource { @@ -3905,6 +3938,9 @@ function normalizeExecutionProgressSnapshot(raw: Partial value !== undefined && value !== "") ? settings : undefined; +} + +function normalizeExecutionProgressCompaction( + input?: ExecutionProgressInput["compaction"], +): ExecutionProgressCompaction | undefined { + if (!input) { + return undefined; + } + const status = safeExecutionProgressText(input.status); + const message = safeExecutionProgressText(input.message); + if (!status && !message) { + return undefined; + } + return { + status: status || "completed", + message: message || undefined, + }; +} + function defaultExecutionProgressStepTexts(task: Pick) { if (task.taskType === "browser_control") { return [ @@ -5706,6 +5801,9 @@ function buildExecutionProgressSnapshot( tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(input?.tokenUsage), mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(input?.mcpServers), remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(input?.remoteControl), + threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(input?.threadGoal), + threadSettings: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadSettings(input?.threadSettings), + compaction: nativeRemoteControl ? undefined : normalizeExecutionProgressCompaction(input?.compaction), updatedAt: nowIso(), }; } diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index 756fa37..eb77b1a 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -520,6 +520,67 @@ rl.on("line", (line) => { }, }); } + if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS === "1") { + send({ + method: "thread/goal/updated", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + goal: { + threadId: message.params?.threadId, + objective: "完成 App Server 线程目标同步", + status: "active", + tokenBudget: 120000, + tokensUsed: 4800, + timeUsedSeconds: 600, + createdAt: 1770000000, + updatedAt: 1770000300, + }, + }, + }); + send({ + method: "thread/settings/updated", + params: { + threadId: message.params?.threadId, + threadSettings: { + cwd: "/Users/kris/code/boss/secret-project", + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: ["/Users/kris/code/boss", "/Users/kris/.codex/memories"], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + activePermissionProfile: { + id: ":workspace", + extends: null, + }, + model: "gpt-5.5", + modelProvider: "openai", + serviceTier: "fast", + effort: "low", + summary: "concise", + collaborationMode: { + mode: "plan", + settings: { + developer_instructions: "internal prompt should not leak", + model_instructions_file: "/Users/kris/.codex/secret-instructions.md", + }, + }, + personality: "pragmatic", + }, + }, + }); + send({ + method: "thread/compacted", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + }, + }); + } send({ method: "item/agentMessage/delta", params: { diff --git a/tests/local-agent-codex-app-server-runner.test.mjs b/tests/local-agent-codex-app-server-runner.test.mjs index 58d6b65..90dfd12 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -470,6 +470,65 @@ test("codex app-server runner maps runtime status events without leaking interna } }); +test("codex app-server runner maps thread goal, settings, and compaction events without leaking local paths", async () => { + const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS; + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS = "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 result = await executeCodexAppServerTask(runnerConfig, { + taskId: "task-app-server-thread-config", + taskType: "conversation_reply", + targetCodexThreadRef: "019d-app-server-thread", + targetCodexFolderRef: repoRoot, + executionPrompt: "同步线程目标和设置", + }); + + assert.equal(result.status, "completed"); + assert.deepEqual(result.executionProgress.threadGoal, { + objective: "完成 App Server 线程目标同步", + status: "active", + tokenBudget: 120000, + tokensUsed: 4800, + timeUsedSeconds: 600, + }); + assert.deepEqual(result.executionProgress.threadSettings, { + model: "gpt-5.5", + modelProvider: "openai", + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandboxPolicy: "workspaceWrite", + permissionProfile: ":workspace", + serviceTier: "fast", + effort: "low", + summary: "concise", + collaborationMode: "plan", + personality: "pragmatic", + }); + assert.deepEqual(result.executionProgress.compaction, { + status: "completed", + message: "上下文已压缩", + }); + const serialized = JSON.stringify(result.executionProgress); + assert.equal(serialized.includes("/Users/kris"), false); + assert.equal(serialized.includes("internal prompt"), false); + assert.equal(serialized.includes("secret-instructions"), false); + } finally { + if (previous === undefined) { + delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS; + } else { + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS = previous; + } + } +}); + test("codex app-server runner bridges source thread context into target thread through inject_items", async () => { const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD; process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1"; diff --git a/tests/master-agent-task-progress-route.test.ts b/tests/master-agent-task-progress-route.test.ts index 357b411..a4c51d4 100644 --- a/tests/master-agent-task-progress-route.test.ts +++ b/tests/master-agent-task-progress-route.test.ts @@ -294,3 +294,79 @@ test("POST task progress preserves Codex runtime status summaries", async () => assert.equal(progress?.remoteControl?.status, "connected"); assert.equal(JSON.stringify(progress).includes("install-secret-should-not-persist"), false); }); + +test("POST task progress preserves Codex thread goal, settings, and compaction summaries", async () => { + const task = await data.queueMasterAgentTask({ + taskId: "route-progress-thread-config-task", + projectId: "group-progress-test", + taskType: "dispatch_execution", + requestMessageId: "msg-route-progress-thread-config", + requestText: "让目标线程同步目标和设置", + executionPrompt: "让目标线程同步目标和设置", + requestedBy: "krisolo", + requestedByAccount: "krisolo", + deviceId: "mac-studio", + targetProjectId: "master-agent", + targetThreadId: "master-agent-thread", + }); + await data.claimNextMasterAgentTask("mac-studio"); + + const response = await postProgress( + new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-boss-device-token": "boss-mac-studio-token", + }, + body: JSON.stringify({ + deviceId: "mac-studio", + status: "running", + executionProgress: { + steps: [{ text: "同步 Codex 线程配置", status: "running" }], + threadGoal: { + objective: "完成 App Server 线程目标同步", + status: "active", + tokenBudget: 120000, + tokensUsed: 4800, + timeUsedSeconds: 600, + }, + threadSettings: { + model: "gpt-5.5", + modelProvider: "openai", + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandboxPolicy: "workspaceWrite", + permissionProfile: ":workspace", + serviceTier: "fast", + effort: "low", + summary: "concise", + collaborationMode: "plan", + personality: "pragmatic", + cwd: "/Users/kris/code/boss/secret-project", + }, + compaction: { + status: "completed", + message: "上下文已压缩", + turnId: "turn-secret-should-not-persist", + }, + }, + }), + }), + { params: Promise.resolve({ taskId: task.taskId }) }, + ); + + assert.equal(response.status, 200); + + const state = await data.readState(); + const progress = state.projects + .find((project) => project.id === "master-agent") + ?.messages.find((message) => message.executionProgress?.taskId === task.taskId) + ?.executionProgress; + assert.equal(progress?.threadGoal?.objective, "完成 App Server 线程目标同步"); + assert.equal(progress?.threadSettings?.model, "gpt-5.5"); + assert.equal(progress?.threadSettings?.sandboxPolicy, "workspaceWrite"); + assert.equal(progress?.compaction?.status, "completed"); + const serialized = JSON.stringify(progress); + assert.equal(serialized.includes("/Users/kris"), false); + assert.equal(serialized.includes("turn-secret-should-not-persist"), false); +});