feat: surface codex thread config progress

This commit is contained in:
AI Bot
2026-06-01 17:18:28 +08:00
parent 591638f35f
commit 26b5e97614
10 changed files with 540 additions and 5 deletions

View File

@@ -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 modelRoute = progress == null ? null : progress.optJSONObject("modelRoute");
JSONObject tokenUsage = progress == null ? null : progress.optJSONObject("tokenUsage"); JSONObject tokenUsage = progress == null ? null : progress.optJSONObject("tokenUsage");
JSONArray mcpServers = progress == null ? null : progress.optJSONArray("mcpServers"); JSONArray mcpServers = progress == null ? null : progress.optJSONArray("mcpServers");

View File

@@ -1069,6 +1069,61 @@ public class ProjectDetailActivityUiTest {
assertFalse(viewTreeContainsText(messageView, "sk-secret")); 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 @Test
public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception { public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception {
Intent intent = new Intent() Intent intent = new Intent()

View File

@@ -153,7 +153,7 @@
- Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路 - Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路
- 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败 - 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败
- 当前量产方向已经明确为“Boss 企业控制面 + 可插拔执行协议”:多租户、权限、审批、审计、备份、回退和 Skill 治理由 Boss 承担Codex App Server / Codex MCP / Codex CLI / Computer Use / 业务系统 API 都作为 provider 接入;详见 `docs/architecture/enterprise_ai_ops_architecture_cn.md` - 当前量产方向已经明确为“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:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `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:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `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 凭据。 - 当前 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 不覆盖终态。 - 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA`MasterAgentTask` claim 会记录 attempt 和 lease运行中任务可按租约重试超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。
- 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果 - 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果

View File

@@ -1,6 +1,6 @@
# Codex Server 协议与 Boss 执行进度卡接入记录 # Codex Server 协议与 Boss 执行进度卡接入记录
更新时间:`2026-05-31` 更新时间:`2026-06-01`
## 1. Codex 最新开放协议结论 ## 1. Codex 最新开放协议结论
@@ -16,7 +16,7 @@ Codex App Server 是更适合 Boss 长期接入的协议层,因为它面向富
- model/list、skills/list、plugin/list、app/list - model/list、skills/list、plugin/list、app/list
- command execution、file change、tool input、MCP tool-call approvals - 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` 官方文档入口:`https://developers.openai.com/codex/app-server`
@@ -75,6 +75,7 @@ APP 展示结构对齐截图:
- `文件变更`:展示 App Server patchUpdated 中的文件路径和变更类型,不展示 diff - `文件变更`:展示 App Server patchUpdated 中的文件路径和变更类型,不展示 diff
- `线程状态`:展示 `active / idle / systemError / notLoaded` 以及 `waitingOnApproval / waitingOnUserInput` - `线程状态`:展示 `active / idle / systemError / notLoaded` 以及 `waitingOnApproval / waitingOnUserInput`
- `实时状态`:展示 realtime 启动、同步、关闭或错误状态,附带安全清洗后的 transcript 预览和计数 - `实时状态`:展示 realtime 启动、同步、关闭或错误状态,附带安全清洗后的 transcript 预览和计数
- `线程配置`:展示 thread goal、模型 / provider、审批 / 沙箱、协作模式和上下文压缩状态
- `运行状态`展示模型重路由、上下文用量、MCP 启动状态和远控连接状态 - `运行状态`展示模型重路由、上下文用量、MCP 启动状态和远控连接状态
- `分支详情`变更行、Git 操作、GitHub CLI 可用状态 - `分支详情`变更行、Git 操作、GitHub CLI 可用状态
- `生成结果`从执行结果里提取文件、图片、APK、文档等产物名 - `生成结果`从执行结果里提取文件、图片、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 审批、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 `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 `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/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 - 新增服务端线程协作入口 `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` - 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId`App Server runner 会调用 `turn/steer`,并把 `turnControl=steer``turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start`

View File

@@ -35,7 +35,7 @@
- `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist` - `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现 - 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准 - 当前生产主链仍然沿用 `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 治理方向;它是规划文档,不代表当前全部已落地 - 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime` - 当前 `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 接入` - 当前已新增最小 `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` providerboss-agent 默认配置 `codexAppServerEnabled=true``conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>``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。 - 当前 `local-agent` 已新增 `Codex App Server` providerboss-agent 默认配置 `codexAppServerEnabled=true``conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>``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 互聊能力 - 当前已新增 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``dispatch_execution` 任务会按 `orchestrationBackendId` 分流:默认走 `codex exec resume`;当任务显式选择 `omx-team` 且本机 `omxEnabled + omxCommand/omxArgs` 可用时,会改走 `OMX Team Runtime` JSON 协议执行并回写 `rawThreadReply / replyBody`
- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failedAndroid 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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 / failedAndroid 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 线程配置 / 运行状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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 连接状态、模型数量、默认/快速/深度模型和扩展数量 - 当前 `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 不会覆盖终态 - 当前 `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 错误,不再假装执行成功 - 当前 `local-agent``browser_control / desktop_control` 已从占位骨架升级成外部 runtime 桥:当本机配置了 `browserControlEnabled + browserControlCommand``computerUseEnabled + computerUseCommand` 时,会把标准化 JSON 请求透传给外部进程,并解析单行 JSON 结果;未启用时会 fail closed返回明确的 runtime disabled 错误,不再假装执行成功

View File

@@ -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) { function extractTokenUsageSnapshot(tokenUsage) {
const total = tokenUsage?.total && typeof tokenUsage.total === "object" ? tokenUsage.total : {}; const total = tokenUsage?.total && typeof tokenUsage.total === "object" ? tokenUsage.total : {};
const totalTokens = extractNumber(total.totalTokens); const totalTokens = extractNumber(total.totalTokens);
@@ -1035,6 +1096,9 @@ function createProgressCollector() {
let tokenUsage; let tokenUsage;
const mcpServers = []; const mcpServers = [];
let remoteControl; let remoteControl;
let threadGoal;
let threadSettings;
let compaction;
const upsertArtifact = (artifact) => { const upsertArtifact = (artifact) => {
if (!artifact || artifacts.some((item) => item.label === artifact.label)) { if (!artifact || artifacts.some((item) => item.label === artifact.label)) {
@@ -1305,6 +1369,33 @@ function createProgressCollector() {
} }
return; 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") { if (message.method === "thread/started") {
upsertAgent(extractAgentFromThreadStarted(message.params)); upsertAgent(extractAgentFromThreadStarted(message.params));
} }
@@ -1357,6 +1448,15 @@ function createProgressCollector() {
if (remoteControl) { if (remoteControl) {
result.remoteControl = { ...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; return Object.keys(result).length > 0 ? result : undefined;
}, },
}; };

View File

@@ -187,6 +187,33 @@ export interface ExecutionProgressRemoteControl {
environmentId?: string; 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 { export interface ExecutionProgressSnapshot {
taskId: string; taskId: string;
projectId: string; projectId: string;
@@ -211,6 +238,9 @@ export interface ExecutionProgressSnapshot {
tokenUsage?: ExecutionProgressTokenUsage; tokenUsage?: ExecutionProgressTokenUsage;
mcpServers?: ExecutionProgressMcpServer[]; mcpServers?: ExecutionProgressMcpServer[];
remoteControl?: ExecutionProgressRemoteControl; remoteControl?: ExecutionProgressRemoteControl;
threadGoal?: ExecutionProgressThreadGoal;
threadSettings?: ExecutionProgressThreadSettings;
compaction?: ExecutionProgressCompaction;
updatedAt: string; updatedAt: string;
} }
@@ -228,6 +258,9 @@ export interface ExecutionProgressInput {
tokenUsage?: Partial<ExecutionProgressTokenUsage>; tokenUsage?: Partial<ExecutionProgressTokenUsage>;
mcpServers?: Array<Partial<ExecutionProgressMcpServer> & { name?: string }>; mcpServers?: Array<Partial<ExecutionProgressMcpServer> & { name?: string }>;
remoteControl?: Partial<ExecutionProgressRemoteControl> & { installationId?: unknown }; remoteControl?: Partial<ExecutionProgressRemoteControl> & { installationId?: unknown };
threadGoal?: Partial<ExecutionProgressThreadGoal>;
threadSettings?: Partial<ExecutionProgressThreadSettings> & { cwd?: unknown };
compaction?: Partial<ExecutionProgressCompaction> & { turnId?: unknown };
} }
export interface ForwardSource { export interface ForwardSource {
@@ -3905,6 +3938,9 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(raw.tokenUsage), tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(raw.tokenUsage),
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(raw.mcpServers), mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(raw.mcpServers),
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(raw.remoteControl), remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(raw.remoteControl),
threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(raw.threadGoal),
threadSettings: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadSettings(raw.threadSettings),
compaction: nativeRemoteControl ? undefined : normalizeExecutionProgressCompaction(raw.compaction),
updatedAt: raw.updatedAt ?? nowIso(), updatedAt: raw.updatedAt ?? nowIso(),
}; };
} }
@@ -5565,6 +5601,65 @@ function normalizeExecutionProgressRemoteControl(
}; };
} }
function normalizeExecutionProgressThreadGoal(
input?: ExecutionProgressInput["threadGoal"],
): ExecutionProgressThreadGoal | undefined {
if (!input) {
return undefined;
}
const status = safeExecutionProgressText(input.status);
const objective = safeExecutionProgressText(input.objective);
if (!status && !objective) {
return undefined;
}
return {
objective: objective || undefined,
status: status || "active",
tokenBudget: normalizeOptionalNumber(input.tokenBudget),
tokensUsed: normalizeOptionalNumber(input.tokensUsed),
timeUsedSeconds: normalizeOptionalNumber(input.timeUsedSeconds),
};
}
function normalizeExecutionProgressThreadSettings(
input?: ExecutionProgressInput["threadSettings"],
): ExecutionProgressThreadSettings | undefined {
if (!input) {
return undefined;
}
const settings: ExecutionProgressThreadSettings = {
model: safeExecutionProgressText(input.model) || undefined,
modelProvider: safeExecutionProgressText(input.modelProvider) || undefined,
approvalPolicy: safeExecutionProgressText(input.approvalPolicy) || undefined,
approvalsReviewer: safeExecutionProgressText(input.approvalsReviewer) || undefined,
sandboxPolicy: safeExecutionProgressText(input.sandboxPolicy) || undefined,
permissionProfile: safeExecutionProgressText(input.permissionProfile) || undefined,
serviceTier: safeExecutionProgressText(input.serviceTier) || undefined,
effort: safeExecutionProgressText(input.effort) || undefined,
summary: safeExecutionProgressText(input.summary) || undefined,
collaborationMode: safeExecutionProgressText(input.collaborationMode) || undefined,
personality: safeExecutionProgressText(input.personality) || undefined,
};
return Object.values(settings).some((value) => 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<MasterAgentTask, "taskType" | "relayViaMasterAgent">) { function defaultExecutionProgressStepTexts(task: Pick<MasterAgentTask, "taskType" | "relayViaMasterAgent">) {
if (task.taskType === "browser_control") { if (task.taskType === "browser_control") {
return [ return [
@@ -5706,6 +5801,9 @@ function buildExecutionProgressSnapshot(
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(input?.tokenUsage), tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(input?.tokenUsage),
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(input?.mcpServers), mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(input?.mcpServers),
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(input?.remoteControl), 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(), updatedAt: nowIso(),
}; };
} }

View File

@@ -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({ send({
method: "item/agentMessage/delta", method: "item/agentMessage/delta",
params: { params: {

View File

@@ -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 () => { 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; const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1"; process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1";

View File

@@ -294,3 +294,79 @@ test("POST task progress preserves Codex runtime status summaries", async () =>
assert.equal(progress?.remoteControl?.status, "connected"); assert.equal(progress?.remoteControl?.status, "connected");
assert.equal(JSON.stringify(progress).includes("install-secret-should-not-persist"), false); 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);
});