From 142fb2a4b363b392c0c34ef7a5016b34ace801ff Mon Sep 17 00:00:00 2001 From: AI Bot Date: Wed, 3 Jun 2026 12:53:43 +0800 Subject: [PATCH] feat: summarize codex stream progress events --- .../src/main/java/com/hyzq/boss/BossUi.java | 43 +++++++++++ .../boss/ProjectDetailActivityUiTest.java | 52 +++++++++++++ docs/architecture/ai_handoff_index_cn.md | 1 + .../api_and_service_inventory_cn.md | 2 +- .../codex_server_progress_card_cn.md | 3 +- .../current_runtime_and_deploy_status_cn.md | 2 +- local-agent/codex-app-server-runner.mjs | 64 +++++++++++++++ src/lib/boss-data.ts | 56 ++++++++++++++ tests/fixtures/codex-app-server-runtime.mjs | 77 +++++++++++++++++++ ...cal-agent-codex-app-server-runner.test.mjs | 50 ++++++++++++ .../master-agent-task-progress-route.test.ts | 72 +++++++++++++++++ 11 files changed, 419 insertions(+), 3 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 d06927c..d5a1fd6 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossUi.java +++ b/android/app/src/main/java/com/hyzq/boss/BossUi.java @@ -1679,6 +1679,49 @@ public final class BossUi { } } + JSONObject streamEvents = progress == null ? null : progress.optJSONObject("streamEvents"); + if (streamEvents != null) { + int agentDeltaCount = streamEvents.optInt("agentDeltaCount", 0); + int planDeltaCount = streamEvents.optInt("planDeltaCount", 0); + int reasoningDeltaCount = streamEvents.optInt("reasoningDeltaCount", 0); + int toolProgressCount = streamEvents.optInt("toolProgressCount", 0); + int commandOutputChunkCount = streamEvents.optInt("commandOutputChunkCount", 0); + int terminalInteractionCount = streamEvents.optInt("terminalInteractionCount", 0); + int fileOutputChunkCount = streamEvents.optInt("fileOutputChunkCount", 0); + boolean hasStreamEvents = agentDeltaCount > 0 || planDeltaCount > 0 || reasoningDeltaCount > 0 || + toolProgressCount > 0 || commandOutputChunkCount > 0 || terminalInteractionCount > 0 || + fileOutputChunkCount > 0; + if (hasStreamEvents) { + card.addView(divider(context)); + card.addView(sectionTitle(context, "流式增量")); + String status = streamEvents.optString("status", "").trim(); + if (!TextUtils.isEmpty(status)) { + card.addView(detailRow(context, "◇", "状态 " + status, "", false)); + } + if (agentDeltaCount > 0) { + card.addView(detailRow(context, "", "回复片段 " + agentDeltaCount, "", false, true)); + } + if (planDeltaCount > 0) { + card.addView(detailRow(context, "", "计划片段 " + planDeltaCount, "", false, true)); + } + if (reasoningDeltaCount > 0) { + card.addView(detailRow(context, "", "思考片段 " + reasoningDeltaCount, "", false, true)); + } + if (toolProgressCount > 0) { + card.addView(detailRow(context, "", "工具进度 " + toolProgressCount, "", false, true)); + } + if (commandOutputChunkCount > 0) { + card.addView(detailRow(context, "", "命令输出片段 " + commandOutputChunkCount, "", false, true)); + } + if (terminalInteractionCount > 0) { + card.addView(detailRow(context, "", "终端交互 " + terminalInteractionCount, "", false, true)); + } + if (fileOutputChunkCount > 0) { + card.addView(detailRow(context, "", "文件输出片段 " + fileOutputChunkCount, "", false, true)); + } + } + } + JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings"); if (warnings != null && warnings.length() > 0) { card.addView(divider(context)); 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 689f202..bac5c67 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java @@ -1234,6 +1234,58 @@ public class ProjectDetailActivityUiTest { assertFalse(viewTreeContainsText(messageView, "sk-secret-should-not-render")); } + @Test + public void executionProgressMessageRendersCodexStreamEventsSection() throws Exception { + Intent intent = new Intent() + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "stream-events") + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程"); + TestProjectDetailActivity activity = Robolectric + .buildActivity(TestProjectDetailActivity.class, intent) + .setup() + .get(); + + JSONObject message = new JSONObject() + .put("id", "progress-stream-events-1") + .put("sender", "master") + .put("senderLabel", "主 Agent") + .put("body", "执行进度") + .put("kind", "execution_progress") + .put("sentAt", "2026-06-03T11:20:00+08:00") + .put("executionProgress", new JSONObject() + .put("status", "running") + .put("steps", new JSONArray() + .put(new JSONObject().put("text", "接收 Codex 流式事件").put("status", "running"))) + .put("streamEvents", new JSONObject() + .put("status", "streaming") + .put("agentDeltaCount", 2) + .put("planDeltaCount", 1) + .put("reasoningDeltaCount", 3) + .put("toolProgressCount", 1) + .put("commandOutputChunkCount", 4) + .put("terminalInteractionCount", 1) + .put("fileOutputChunkCount", 1) + .put("delta", "secret delta should-not-render") + .put("output", "secret output should-not-render") + .put("content", "secret content should-not-render"))); + + View messageView = ReflectionHelpers.callInstanceMethod( + activity, + "buildMessageView", + ReflectionHelpers.ClassParameter.from(JSONObject.class, message) + ); + + assertTrue(viewTreeContainsText(messageView, "流式增量")); + assertTrue(viewTreeContainsText(messageView, "状态 streaming")); + assertTrue(viewTreeContainsText(messageView, "回复片段 2")); + assertTrue(viewTreeContainsText(messageView, "计划片段 1")); + assertTrue(viewTreeContainsText(messageView, "思考片段 3")); + assertTrue(viewTreeContainsText(messageView, "工具进度 1")); + assertTrue(viewTreeContainsText(messageView, "命令输出片段 4")); + assertTrue(viewTreeContainsText(messageView, "终端交互 1")); + assertTrue(viewTreeContainsText(messageView, "文件输出片段 1")); + assertFalse(viewTreeContainsText(messageView, "should-not-render")); + } + @Test public void executionProgressMessageRendersCodexToolActivitySection() 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 da374ab..06a782b 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -163,6 +163,7 @@ - 第二十五批另补 `mcpGovernanceSummary / userInteractionGovernanceSummary / guardianGovernanceSummary` MCP、用户交互和 Guardian 治理能力摘要:设备详情页会显示 MCP OAuth/resource/tool/elicitation、tool requestUserInput、Guardian denied action approval 和 permission request approval 等能力分组;这些字段只读,不在 heartbeat 中调用任何 MCP、用户输入或 Guardian 放行动作。 - 第二十六批另补 `runtimeEventSummary / extensionEventSummary / threadLifecycleEventSummary` 运行事件、扩展事件和线程生命周期事件能力摘要:设备详情页会显示 process output/exited、raw response completed、skills changed、plugin installed、thread started/closed/archived/unarchived/name updated 等能力分组;这些字段只读,不在 heartbeat 中主动触发进程、插件、Skill 或线程生命周期动作。 - 第二十七批另补 `streamDeltaEventSummary` 流式增量事件能力摘要:设备详情页会显示 agent delta、plan delta、reasoning delta、MCP progress、command output、terminal interaction 和 file output 等能力分组;该字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。 +- 当前任务执行态也已补 `executionProgress.streamEvents`:App Server runner 会把 agent / plan / reasoning / MCP / command / terminal / file 的流式 delta 归一成计数,Android 进度卡展示“流式增量”,不保存或渲染原始 delta、命令输出、终端输入、推理正文或文件输出。 - 当前 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 0bc2a0b..1d9993d 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -117,7 +117,7 @@ - 当前 `RemoteRuntimeAdapter` 还负责拦截固定模式的线程内部环境提示;命中后会直接改写成失败,避免把只读/cwd 这类脏文本写进聊天记录 - 当前普通单线程 `conversation_reply` 在真正执行 `codex exec resume` 前,会先把 Boss 用户消息镜像进目标 Codex Desktop rollout;定位优先走 `state_5.sqlite`,不可用时回退扫描 `~/.codex/sessions`,并按 `sourceMessageId` 去重 - 当前 Codex Desktop 同步新增常驻刷新桥:`scripts/codex-desktop-refresh-bridge-daemon.mjs` 通过 launchd 监听 `127.0.0.1:4318`,暴露 `POST /api/v1/codex-desktop/refresh`、`GET /api/v1/codex-desktop/events`、`GET /api/v1/codex-desktop/events/recent` 和 `GET /api/v1/codex-desktop/capabilities`;`local-agent` 会优先调用 refresh endpoint,失败时回退到 `scripts/codex-desktop-refresh-hint.mjs` 命令式刷新。SSE 事件只包含线程引用、消息 ID、状态、deep link 等安全元数据,不包含用户正文或内部 prompt;`scripts/codex-desktop-event-consumer.mjs` 可作为 Desktop 插件/IPC 接入前的订阅 smoke;`scripts/codex-desktop-integration-probe.mjs` 负责只读探测 Codex.app 能力 -- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer `。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`imageGeneration`、`hook/started|completed`、`windowsSandbox/setupCompleted`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution`、`ThreadItem.plan`、`ThreadItem.reasoning.summary` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作、上下文压缩、工具活动、图片产物、钩子生命周期、Windows 沙箱准备状态、计划步骤和思考摘要;新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 只归一为目标数量和 agent 状态集合。服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、receiverThreadIds、collab prompt、agentsStates 私有消息、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、imageGeneration revisedPrompt/result、hook sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths/本地绝对路径或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / hooks/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。 +- 当前新增 Codex App Server runner:`local-agent/codex-app-server-runner.mjs`。boss-agent 默认配置 `codexAppServerEnabled=true`,会接管 `conversation_reply / dispatch_execution`;它默认通过 stdio 启动 `codex app-server`,也支持 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:` 或 `codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server,bearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer `。runner 执行 `initialize -> thread/resume|thread/start -> turn/start|turn/steer`,并把 `item/agentMessage/delta` 或 `item/completed` 归一成 Boss 任务回复;当 App Server 对单个 JSON-RPC 请求返回 `-32001 / retry later` 时,runner 会做最多 3 次指数退避重试。turn 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice`、`ThreadItem.collabToolCall`、`ThreadItem.contextCompaction`、`mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`imageGeneration`、`hook/started|completed`、`windowsSandbox/setupCompleted`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution`、`ThreadItem.plan`、`ThreadItem.reasoning.summary` 归一成线程配置、账号状态、模型校验、安全提醒、线程协作、上下文压缩、工具活动、图片产物、钩子生命周期、Windows 沙箱准备状态、计划步骤和思考摘要;新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 只归一为目标数量和 agent 状态集合。2026-06-03 起,runner 还会把 `item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/summaryPartAdded|summaryTextDelta|textDelta`、`item/mcpToolCall/progress`、`command/exec/outputDelta`、`item/commandExecution/outputDelta|terminalInteraction` 和 `item/fileChange/outputDelta` 归一成 `executionProgress.streamEvents` 计数。服务端 complete/progress 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径、collab 源/目标线程 ID、receiverThreadIds、collab prompt、agentsStates 私有消息、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、原始 delta、terminal input、file output、imageGeneration revisedPrompt/result、hook sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths/本地绝对路径或未清洗的 MCP 错误。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / hooks/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`。 - App Server heartbeat discovery 现在还会按 TTL 拉取 `experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list`,写入 `capabilities.codexAppServer.metadata.experimentalFeatures / collaborationModes / permissionProfiles / mcpServers`。这些字段用于 APP/后台治理页展示 Codex 当前可用的实验特性、多 Agent/协作模式、权限 profile 和 MCP 服务健康;MCP 请求固定使用 `detail=toolsAndAuthOnly`,服务端状态里不保存 resource URI、工具参数、permission profile 文件规则、本地路径或密钥。 - App Server heartbeat discovery 现在还会按 TTL 拉取 `account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect`,写入 `capabilities.codexAppServer.metadata.accountSummary / rateLimitSummary / appConfigSummary / configRequirements / externalAgentMigration`。这些字段用于 APP/后台展示账号、额度、App 配置、企业托管要求和外部 Agent 迁移候选摘要;当前只做观测,不通过 Boss 远程写 `config.toml` 或执行外部 Agent 导入,且不保存邮箱、完整 config、API key、本地路径或迁移描述。 - App Server heartbeat discovery 现在还会按 TTL 拉取 `thread/list / thread/loaded/list`,写入 `capabilities.codexAppServer.metadata.threadSummary`。该字段用于 APP/后台展示 Codex 当前可见线程数量、加载态、活跃态和非归档线程轻量目录;目录只保留 `id / name / sourceKind / status / updatedAt / loaded`,不保存 cwd、本地路径、turn 内容、用户正文或内部 prompt。 diff --git a/docs/architecture/codex_server_progress_card_cn.md b/docs/architecture/codex_server_progress_card_cn.md index 20efa46..ebc032d 100644 --- a/docs/architecture/codex_server_progress_card_cn.md +++ b/docs/architecture/codex_server_progress_card_cn.md @@ -20,7 +20,7 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务 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 “线程配置”区块;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;第七批已把官方 `ThreadItem.collabToolCall` 归一成 `executionProgress.threadCollaboration`,并按官方建议把新版 `ThreadItem.contextCompaction` 映射回 `executionProgress.compaction`;第八批已把 `mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode` 和 `commandExecution` 归一成 `executionProgress.toolActivities`;第九批已把官方 `ThreadItem.plan` 的最终 `item/completed` 文本映射为 `executionProgress.steps`,并把 `ThreadItem.reasoning.summary` 映射为 `executionProgress.reasoningSummary`;第十批已把 `ThreadItem.imageGeneration` 安全映射为 `executionProgress.toolActivities` 的图像生成活动和 `executionProgress.artifacts` 的图片产物;第十一批已把 `hook/started|completed` 安全映射为 `executionProgress.toolActivities` 的钩子活动,供 APP 以“钩子”轻卡展示企业治理和插件生命周期状态;第十二批已把 `windowsSandbox/setupCompleted` 安全映射为 `executionProgress.windowsSandbox`,供 APP 在“运行状态”里展示 Windows 沙箱准备状态、setup mode 和脱敏错误摘要;第十三批已把 heartbeat discovery 扩展到 `experimentalFeature/list`、`collaborationMode/list`、`permissionProfile/list` 和 `mcpServerStatus/list`,供设备详情、APP 和 PC 后台看到实验特性、协作模式、权限 Profile 与 MCP 服务摘要;第十四批已把 `account/read`、`account/rateLimits/read`、`config/read`、`configRequirements/read` 和 `externalAgentConfig/detect` 纳入 heartbeat discovery,用于展示账号、套餐、额度、App 配置、托管要求和外部 Agent 迁移候选摘要;第十五批已把 `thread/list` 和 `thread/loaded/list` 纳入 heartbeat discovery,用于展示线程总数、已加载线程、活跃线程、归档线程和非归档可见线程轻量目录;第十六批已把 `thread/turns/list` 纳入 heartbeat discovery,用于展示总轮次、运行中轮次、完成轮次和每个非归档线程的最近 turn 状态摘要;第十七批已按 `codex-cli 0.136.0-alpha.2` 补齐 `skills/extraRoots/set`,支持把企业共享 Skill 根下发给 App Server 后再拉取 `skills/list`,并把新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 安全映射成目标数量和 agent 状态集合;第十八批已把 `hooks/list` 纳入 heartbeat discovery,用于展示本机 Codex hook 数、启用数、信任状态和 warning/error 计数;第十九批已新增线程操作能力摘要,用于展示 `thread/archive / thread/unarchive / thread/fork / thread/compact/start / thread/rollback / thread/name/set / thread/metadata/update / thread/shellCommand / thread/unsubscribe / turn/interrupt / turn/steer` 这类动作是否进入 Boss 受控能力目录;第二十批已新增插件治理能力摘要,用于展示 `plugin/install / plugin/uninstall / plugin/read / plugin/skill/read / plugin/share/*` 这类动作是否进入 Boss 受控能力目录;第二十一批已新增账号与配置治理能力摘要,用于展示 `account/login/* / account/logout / account/chatgptAuthTokens/refresh / account/sendAddCreditsNudgeEmail / config/value/write / config/batchWrite / config/mcpServer/reload / skills/config/write` 这类动作是否进入 Boss 受控能力目录;第二十二批已新增文件系统与命令会话治理能力摘要,用于展示 `fs/readFile / fs/writeFile / fs/remove / fs/watch / command/exec/write / command/exec/terminate` 这类动作是否进入 Boss 受控能力目录;第二十三批已新增外部 Agent 迁移、Marketplace 和实验特性治理能力摘要,用于展示 `externalAgentConfig/import / marketplace/add / marketplace/remove / marketplace/upgrade / experimentalFeature/enablement/set` 这类动作是否进入 Boss 受控能力目录;第二十四批已新增审查、Windows 沙箱和文件搜索事件能力摘要,用于展示 `review/start / windowsSandbox/readiness / windowsSandbox/setupStart / fuzzyFileSearch/session*` 是否进入 Boss 受控能力目录;第二十五批已新增 MCP、用户交互和 Guardian 治理能力摘要,用于展示 `mcpServer/oauth/login / mcpServer/tool/call / mcpServer/elicitation/request / item/tool/requestUserInput / thread/approveGuardianDeniedAction` 是否进入 Boss 受控能力目录;第二十六批已新增运行事件、扩展事件和线程生命周期事件能力摘要,用于展示 `process/outputDelta / process/exited / rawResponseItem/completed / skills/changed / plugin/installed / thread/started|closed|archived|unarchived|name/updated` 是否进入 Boss 受控能力目录;第二十七批已新增流式增量事件能力摘要,用于展示 `item/agentMessage/delta / item/plan/delta / item/reasoning/*Delta / item/mcpToolCall/progress / command/exec/outputDelta / item/commandExecution/terminalInteraction / item/fileChange/outputDelta` 是否进入 Boss 受控能力目录。 +2026-06-01 第五批已把 `thread/goal/updated|cleared`、`thread/settings/updated` 和 `thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`,用于 APP “线程配置”区块;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;第七批已把官方 `ThreadItem.collabToolCall` 归一成 `executionProgress.threadCollaboration`,并按官方建议把新版 `ThreadItem.contextCompaction` 映射回 `executionProgress.compaction`;第八批已把 `mcpToolCall`、`dynamicToolCall`、`webSearch`、`imageView`、`enteredReviewMode`、`exitedReviewMode` 和 `commandExecution` 归一成 `executionProgress.toolActivities`;第九批已把官方 `ThreadItem.plan` 的最终 `item/completed` 文本映射为 `executionProgress.steps`,并把 `ThreadItem.reasoning.summary` 映射为 `executionProgress.reasoningSummary`;第十批已把 `ThreadItem.imageGeneration` 安全映射为 `executionProgress.toolActivities` 的图像生成活动和 `executionProgress.artifacts` 的图片产物;第十一批已把 `hook/started|completed` 安全映射为 `executionProgress.toolActivities` 的钩子活动,供 APP 以“钩子”轻卡展示企业治理和插件生命周期状态;第十二批已把 `windowsSandbox/setupCompleted` 安全映射为 `executionProgress.windowsSandbox`,供 APP 在“运行状态”里展示 Windows 沙箱准备状态、setup mode 和脱敏错误摘要;第十三批已把 heartbeat discovery 扩展到 `experimentalFeature/list`、`collaborationMode/list`、`permissionProfile/list` 和 `mcpServerStatus/list`,供设备详情、APP 和 PC 后台看到实验特性、协作模式、权限 Profile 与 MCP 服务摘要;第十四批已把 `account/read`、`account/rateLimits/read`、`config/read`、`configRequirements/read` 和 `externalAgentConfig/detect` 纳入 heartbeat discovery,用于展示账号、套餐、额度、App 配置、托管要求和外部 Agent 迁移候选摘要;第十五批已把 `thread/list` 和 `thread/loaded/list` 纳入 heartbeat discovery,用于展示线程总数、已加载线程、活跃线程、归档线程和非归档可见线程轻量目录;第十六批已把 `thread/turns/list` 纳入 heartbeat discovery,用于展示总轮次、运行中轮次、完成轮次和每个非归档线程的最近 turn 状态摘要;第十七批已按 `codex-cli 0.136.0-alpha.2` 补齐 `skills/extraRoots/set`,支持把企业共享 Skill 根下发给 App Server 后再拉取 `skills/list`,并把新版 `ThreadItem.collabToolCall.receiverThreadIds / agentsStates` 安全映射成目标数量和 agent 状态集合;第十八批已把 `hooks/list` 纳入 heartbeat discovery,用于展示本机 Codex hook 数、启用数、信任状态和 warning/error 计数;第十九批已新增线程操作能力摘要,用于展示 `thread/archive / thread/unarchive / thread/fork / thread/compact/start / thread/rollback / thread/name/set / thread/metadata/update / thread/shellCommand / thread/unsubscribe / turn/interrupt / turn/steer` 这类动作是否进入 Boss 受控能力目录;第二十批已新增插件治理能力摘要,用于展示 `plugin/install / plugin/uninstall / plugin/read / plugin/skill/read / plugin/share/*` 这类动作是否进入 Boss 受控能力目录;第二十一批已新增账号与配置治理能力摘要,用于展示 `account/login/* / account/logout / account/chatgptAuthTokens/refresh / account/sendAddCreditsNudgeEmail / config/value/write / config/batchWrite / config/mcpServer/reload / skills/config/write` 这类动作是否进入 Boss 受控能力目录;第二十二批已新增文件系统与命令会话治理能力摘要,用于展示 `fs/readFile / fs/writeFile / fs/remove / fs/watch / command/exec/write / command/exec/terminate` 这类动作是否进入 Boss 受控能力目录;第二十三批已新增外部 Agent 迁移、Marketplace 和实验特性治理能力摘要,用于展示 `externalAgentConfig/import / marketplace/add / marketplace/remove / marketplace/upgrade / experimentalFeature/enablement/set` 这类动作是否进入 Boss 受控能力目录;第二十四批已新增审查、Windows 沙箱和文件搜索事件能力摘要,用于展示 `review/start / windowsSandbox/readiness / windowsSandbox/setupStart / fuzzyFileSearch/session*` 是否进入 Boss 受控能力目录;第二十五批已新增 MCP、用户交互和 Guardian 治理能力摘要,用于展示 `mcpServer/oauth/login / mcpServer/tool/call / mcpServer/elicitation/request / item/tool/requestUserInput / thread/approveGuardianDeniedAction` 是否进入 Boss 受控能力目录;第二十六批已新增运行事件、扩展事件和线程生命周期事件能力摘要,用于展示 `process/outputDelta / process/exited / rawResponseItem/completed / skills/changed / plugin/installed / thread/started|closed|archived|unarchived|name/updated` 是否进入 Boss 受控能力目录;第二十七批已新增流式增量事件能力摘要,用于展示 `item/agentMessage/delta / item/plan/delta / item/reasoning/*Delta / item/mcpToolCall/progress / command/exec/outputDelta / item/commandExecution/terminalInteraction / item/fileChange/outputDelta` 是否进入 Boss 受控能力目录。2026-06-03 已把这些流式 delta 从“能力目录”继续接入到任务执行态 `executionProgress.streamEvents`,APP 只显示 agent / plan / reasoning / MCP / command / terminal / file 的片段计数,不保存原始增量文本、命令输出、推理正文或文件输出。 `thread/realtime/sdp`、音频 base64、原始 realtime item、remote installationId、thread settings 的 `cwd`、compaction `turnId`、collaboration settings 内部 prompt、collabToolCall 源/目标线程 ID、`receiverThreadIds`、`agentsStates.message`、共享 Skill 根绝对路径、hook key/command/sourcePath/statusMessage/hash/error message、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning `content`、reasoning item id、imageGeneration 原始 result/revisedPrompt、hook id/sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths、本地绝对路径、permission profile 文件规则、MCP resource URI、账号邮箱、API key、完整 config、外部 Agent 迁移描述、turn id、turn items、turn 内容、用户正文、模型输出和未清洗的 MCP 错误不入账。 @@ -85,6 +85,7 @@ APP 展示结构对齐截图: - `线程协作`:展示 `collabToolCall` 的工具名、执行状态、目标类型、目标数量和智能体状态集合,不展示源/目标线程 ID、`receiverThreadIds`、prompt 或 agent 私有消息 - `工具活动`:展示 MCP / dynamic tool / web search / image view / image generation / hook / Review / command 的类型、名称、状态和安全摘要,不展示参数、结果、URL token、命令正文、命令输出、图像生成原始 result/revised prompt 或 hook 原始输出 - `思考摘要`:展示 Codex 官方 reasoning `summary` 和状态,不展示 raw reasoning `content`、item id 或密钥 +- `流式增量`:展示 agent 回复片段、计划片段、思考片段、MCP 进度、命令输出片段、终端交互和文件输出片段计数,不展示原始 delta、命令输出、推理正文或文件输出 - `运行状态`:展示模型重路由、上下文用量、MCP 启动状态、远控连接状态和 Windows 沙箱准备状态 - `分支详情`:变更行、Git 操作、GitHub CLI 可用状态 - `生成结果`:从执行结果里提取文件、图片、APK、文档等产物名 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index b6ecbf9..a8d9a3c 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -48,7 +48,7 @@ - 当前 App Server 能力发现已新增审查、Windows 沙箱和文件搜索事件能力摘要:local-agent 会把已验证进入当前协议快照的 review start、Windows sandbox readiness / setup start / setup completed、fuzzy file search updated / completed 写入设备 `codexAppServer.metadata.reviewGovernanceSummary / windowsSandboxGovernanceSummary / fuzzyFileSearchSummary`;设备详情页会显示“审查治理 / Windows 沙箱 / 文件搜索事件”。这些字段只读,不在 heartbeat 中调用任何审查启动、沙箱设置或文件搜索动作。 - 当前 App Server 能力发现已新增 MCP、用户交互和 Guardian 治理能力摘要:local-agent 会把已验证进入当前协议快照的 MCP OAuth / resource / tool / elicitation、tool requestUserInput、Guardian denied action approval 和 permission request approval 写入设备 `codexAppServer.metadata.mcpGovernanceSummary / userInteractionGovernanceSummary / guardianGovernanceSummary`;设备详情页会显示“MCP 治理 / 用户交互 / Guardian 治理”。这些字段只读,不在 heartbeat 中调用任何 MCP、用户输入或 Guardian 放行动作。 - 当前 App Server 能力发现已新增运行事件、扩展事件和线程生命周期事件能力摘要:local-agent 会把已验证进入当前协议快照的 process output / exited、raw response completed、skills changed、plugin installed、thread started / closed / archived / unarchived / name updated 写入设备 `codexAppServer.metadata.runtimeEventSummary / extensionEventSummary / threadLifecycleEventSummary`;设备详情页会显示“运行事件 / 扩展事件 / 线程生命周期”。这些字段只读,不在 heartbeat 中主动触发进程、插件、Skill 或线程生命周期动作。 -- 当前 App Server 能力发现已新增流式增量事件能力摘要:local-agent 会把已验证进入当前协议快照的 agent delta、plan delta、reasoning delta、MCP tool progress、command output、terminal interaction 和 file output 写入设备 `codexAppServer.metadata.streamDeltaEventSummary`;设备详情页会显示“流式增量”。这些字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。 +- 当前 App Server 能力发现已新增流式增量事件能力摘要:local-agent 会把已验证进入当前协议快照的 agent delta、plan delta、reasoning delta、MCP tool progress、command output、terminal interaction 和 file output 写入设备 `codexAppServer.metadata.streamDeltaEventSummary`;设备详情页会显示“流式增量”。同批已把执行中的 delta 事件接入 `executionProgress.streamEvents`,APP 进度卡只展示各类片段计数,不保存原始增量文本、命令输出、终端输入、推理正文或文件输出。 - 当前 App Server 能力发现已支持共享 Skill 根目录下发:配置 `codexAppServerSkillExtraRoots` / `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 时,local-agent 会先调用 `skills/extraRoots/set`,再刷新 `skills/list`,并把 `skillExtraRootsSummary` 写入设备 `codexAppServer.metadata`;设备详情页会显示“共享 Skill 根”。该链路只保存数量、basename 和状态,不保存根目录绝对路径、Skill 文件路径或配置原文。 - 当前 App Server 能力发现已新增 Hook 治理摘要:local-agent 会在 heartbeat discovery 中拉取 `hooks/list`,并把 hook 数、启用数、受管 / 可信 / 修改 / 未信任计数、warning / error 计数写入设备 `codexAppServer.metadata.hookSummary`;设备详情页会显示“Hook”。该链路不保存 hook key、command、sourcePath、statusMessage、hash、error message 或本地路径。 - 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束,并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index 16e8f19..8d6d6d2 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -2143,6 +2143,7 @@ function createProgressCollector() { let reasoningSummary; let accountStatus; let modelVerification; + let streamEvents; const upsertArtifact = (artifact) => { if (!artifact || artifacts.some((item) => item.label === artifact.label)) { @@ -2245,11 +2246,71 @@ function createProgressCollector() { } }; + const ensureStreamEvents = () => { + if (!streamEvents) { + streamEvents = { + status: "streaming", + agentDeltaCount: 0, + planDeltaCount: 0, + reasoningDeltaCount: 0, + toolProgressCount: 0, + commandOutputChunkCount: 0, + terminalInteractionCount: 0, + fileOutputChunkCount: 0, + }; + } + return streamEvents; + }; + + const incrementStreamEvent = (key) => { + const state = ensureStreamEvents(); + state.status = state.status === "completed" ? "completed" : "streaming"; + state[key] = Math.min(9999, Number(state[key] ?? 0) + 1); + }; + return { observe(message) { if (!message || typeof message !== "object") { return; } + if (message.method === "item/agentMessage/delta") { + incrementStreamEvent("agentDeltaCount"); + return; + } + if (message.method === "item/plan/delta") { + incrementStreamEvent("planDeltaCount"); + return; + } + if ( + message.method === "item/reasoning/summaryPartAdded" || + message.method === "item/reasoning/summaryTextDelta" || + message.method === "item/reasoning/textDelta" + ) { + incrementStreamEvent("reasoningDeltaCount"); + return; + } + if (message.method === "item/mcpToolCall/progress") { + incrementStreamEvent("toolProgressCount"); + return; + } + if (message.method === "command/exec/outputDelta" || message.method === "item/commandExecution/outputDelta") { + incrementStreamEvent("commandOutputChunkCount"); + return; + } + if (message.method === "item/commandExecution/terminalInteraction") { + incrementStreamEvent("terminalInteractionCount"); + return; + } + if (message.method === "item/fileChange/outputDelta") { + incrementStreamEvent("fileOutputChunkCount"); + return; + } + if (message.method === "turn/completed") { + if (streamEvents) { + streamEvents.status = "completed"; + } + return; + } if (message.method === "turn/plan/updated") { const nextSteps = extractPlanItems(message.params) .map((item, index) => { @@ -2634,6 +2695,9 @@ function createProgressCollector() { if (modelVerification) { result.modelVerification = { ...modelVerification }; } + if (streamEvents) { + result.streamEvents = { ...streamEvents }; + } return Object.keys(result).length > 0 ? result : undefined; }, }; diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 331555f..7f04b29 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -258,6 +258,17 @@ export interface ExecutionProgressReasoningSummary { summary: string; } +export interface ExecutionProgressStreamEvents { + status: string; + agentDeltaCount?: number; + planDeltaCount?: number; + reasoningDeltaCount?: number; + toolProgressCount?: number; + commandOutputChunkCount?: number; + terminalInteractionCount?: number; + fileOutputChunkCount?: number; +} + export interface ExecutionProgressSnapshot { taskId: string; projectId: string; @@ -291,6 +302,7 @@ export interface ExecutionProgressSnapshot { threadCollaboration?: ExecutionProgressThreadCollaboration; toolActivities?: ExecutionProgressToolActivity[]; reasoningSummary?: ExecutionProgressReasoningSummary; + streamEvents?: ExecutionProgressStreamEvents; updatedAt: string; } @@ -335,6 +347,15 @@ export interface ExecutionProgressInput { content?: unknown; itemId?: unknown; }; + streamEvents?: Partial & { + delta?: unknown; + text?: unknown; + content?: unknown; + output?: unknown; + command?: unknown; + itemId?: unknown; + turnId?: unknown; + }; } export interface ForwardSource { @@ -4027,6 +4048,7 @@ function normalizeExecutionProgressSnapshot(raw: Partial typeof count === "number" && count > 0); + return hasCount ? streamEvents : undefined; +} + function defaultExecutionProgressStepTexts(task: Pick) { if (task.taskType === "browser_control") { return [ @@ -6029,6 +6084,7 @@ function buildExecutionProgressSnapshot( reasoningSummary: nativeRemoteControl ? undefined : normalizeExecutionProgressReasoningSummary(input?.reasoningSummary), + streamEvents: nativeRemoteControl ? undefined : normalizeExecutionProgressStreamEvents(input?.streamEvents), updatedAt: nowIso(), }; } diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index 241d7a7..b4fea46 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -1401,6 +1401,83 @@ rl.on("line", (line) => { }, }); } + if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS === "1") { + send({ + method: "item/plan/delta", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "plan-delta-secret-should-not-leak", + delta: "secret plan delta should not leak", + }, + }); + send({ + method: "item/reasoning/summaryPartAdded", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "reasoning-delta-secret-should-not-leak", + summaryIndex: 0, + part: "secret reasoning summary part should not leak", + }, + }); + send({ + method: "item/reasoning/summaryTextDelta", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "reasoning-delta-secret-should-not-leak", + summaryIndex: 0, + delta: "secret reasoning summary delta should not leak", + }, + }); + send({ + method: "item/reasoning/textDelta", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "reasoning-delta-secret-should-not-leak", + delta: "secret raw reasoning delta should not leak", + }, + }); + send({ + method: "item/mcpToolCall/progress", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "mcp-progress-secret-should-not-leak", + message: "secret mcp progress should not leak", + }, + }); + send({ + method: "item/commandExecution/outputDelta", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "command-output-secret-should-not-leak", + stream: "stdout", + delta: "secret command output should not leak", + }, + }); + send({ + method: "item/commandExecution/terminalInteraction", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "terminal-interaction-secret-should-not-leak", + input: "secret terminal input should not leak", + }, + }); + send({ + method: "item/fileChange/outputDelta", + params: { + threadId: message.params?.threadId, + turnId: "turn-fixture", + itemId: "file-output-secret-should-not-leak", + delta: "secret file output should not leak", + }, + }); + } 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 e01a5cf..e5aa7a9 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -933,6 +933,56 @@ test("codex app-server runner maps collab tool calls and context compaction with } }); +test("codex app-server runner summarizes stream deltas without leaking raw delta content", async () => { + const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS; + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_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-stream-deltas", + taskType: "conversation_reply", + targetCodexThreadRef: "019d-app-server-thread", + targetCodexFolderRef: repoRoot, + executionPrompt: "检查 Codex 流式增量进度", + }); + + assert.equal(result.status, "completed"); + assert.deepEqual(result.executionProgress.streamEvents, { + status: "completed", + agentDeltaCount: 1, + planDeltaCount: 1, + reasoningDeltaCount: 3, + toolProgressCount: 1, + commandOutputChunkCount: 1, + terminalInteractionCount: 1, + fileOutputChunkCount: 1, + }); + const serialized = JSON.stringify(result.executionProgress); + assert.equal(serialized.includes("secret plan delta should not leak"), false); + assert.equal(serialized.includes("secret reasoning summary part should not leak"), false); + assert.equal(serialized.includes("secret reasoning summary delta should not leak"), false); + assert.equal(serialized.includes("secret raw reasoning delta should not leak"), false); + assert.equal(serialized.includes("secret mcp progress should not leak"), false); + assert.equal(serialized.includes("secret command output should not leak"), false); + assert.equal(serialized.includes("secret terminal input should not leak"), false); + assert.equal(serialized.includes("secret file output should not leak"), false); + } finally { + if (previous === undefined) { + delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS; + } else { + process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS = previous; + } + } +}); + test("codex app-server runner maps tool, search, image, review, and command activities without leaking payloads", async () => { const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS; process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS = "1"; diff --git a/tests/master-agent-task-progress-route.test.ts b/tests/master-agent-task-progress-route.test.ts index 66fa11b..3b1abfa 100644 --- a/tests/master-agent-task-progress-route.test.ts +++ b/tests/master-agent-task-progress-route.test.ts @@ -528,6 +528,78 @@ test("POST task progress preserves Codex thread collaboration summaries without assert.equal(serialized.includes("turn-secret-should-not-persist"), false); }); +test("POST task progress preserves Codex stream event counters without leaking raw deltas", async () => { + const task = await data.queueMasterAgentTask({ + taskId: "route-progress-stream-events-task", + projectId: "group-progress-test", + taskType: "dispatch_execution", + requestMessageId: "msg-route-progress-stream-events", + requestText: "检查 Codex 流式增量", + executionPrompt: "检查 Codex 流式增量", + 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" }], + streamEvents: { + status: "streaming", + agentDeltaCount: 2, + planDeltaCount: 1, + reasoningDeltaCount: 3, + toolProgressCount: 1, + commandOutputChunkCount: 4, + terminalInteractionCount: 1, + fileOutputChunkCount: 1, + delta: "secret delta should-not-persist", + text: "secret text should-not-persist", + output: "secret output should-not-persist", + content: "secret content should-not-persist", + command: "cat secret should-not-persist", + 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.deepEqual(progress?.streamEvents, { + status: "streaming", + agentDeltaCount: 2, + planDeltaCount: 1, + reasoningDeltaCount: 3, + toolProgressCount: 1, + commandOutputChunkCount: 4, + terminalInteractionCount: 1, + fileOutputChunkCount: 1, + }); + const serialized = JSON.stringify(progress); + assert.equal(serialized.includes("should-not-persist"), false); + assert.equal(serialized.includes("turn-secret"), false); +}); + test("POST task progress preserves Codex tool activity summaries without leaking raw payloads", async () => { const task = await data.queueMasterAgentTask({ taskId: "route-progress-tool-activity-task",