feat: summarize codex stream progress events

This commit is contained in:
AI Bot
2026-06-03 12:53:43 +08:00
parent bc9a586e81
commit 142fb2a4b3
11 changed files with 419 additions and 3 deletions

View File

@@ -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"); JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings");
if (warnings != null && warnings.length() > 0) { if (warnings != null && warnings.length() > 0) {
card.addView(divider(context)); card.addView(divider(context));

View File

@@ -1234,6 +1234,58 @@ public class ProjectDetailActivityUiTest {
assertFalse(viewTreeContainsText(messageView, "sk-secret-should-not-render")); 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 @Test
public void executionProgressMessageRendersCodexToolActivitySection() throws Exception { public void executionProgressMessageRendersCodexToolActivitySection() throws Exception {
Intent intent = new Intent() Intent intent = new Intent()

View File

@@ -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 放行动作。 - 第二十五批另补 `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 或线程生命周期动作。 - 第二十六批另补 `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 等能力分组;该字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。 - 第二十七批另补 `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 凭据。 - 当前 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

@@ -117,7 +117,7 @@
- 当前 `RemoteRuntimeAdapter` 还负责拦截固定模式的线程内部环境提示;命中后会直接改写成失败,避免把只读/cwd 这类脏文本写进聊天记录 - 当前 `RemoteRuntimeAdapter` 还负责拦截固定模式的线程内部环境提示;命中后会直接改写成失败,避免把只读/cwd 这类脏文本写进聊天记录
- 当前普通单线程 `conversation_reply` 在真正执行 `codex exec resume` 前,会先把 Boss 用户消息镜像进目标 Codex Desktop rollout定位优先走 `state_5.sqlite`,不可用时回退扫描 `~/.codex/sessions`,并按 `sourceMessageId` 去重 - 当前普通单线程 `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 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:<port>``codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Serverbearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。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 启动前失败可回退 CLIturn 启动后失败不回退避免重复执行。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:<port>``codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Serverbearer token 可通过 `codexAppServerAuthTokenFile` 读取并在握手时发送 `Authorization: Bearer <token>`。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 启动前失败可回退 CLIturn 启动后失败不回退避免重复执行。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 拉取 `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 拉取 `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。 - 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。

File diff suppressed because one or more lines are too long

View File

@@ -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 能力发现已新增审查、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 能力发现已新增 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 会把已验证进入当前协议快照的 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 能力发现已支持共享 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 或本地路径。 - 当前 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 治理方向;它是规划文档,不代表当前全部已落地 - 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地

View File

@@ -2143,6 +2143,7 @@ function createProgressCollector() {
let reasoningSummary; let reasoningSummary;
let accountStatus; let accountStatus;
let modelVerification; let modelVerification;
let streamEvents;
const upsertArtifact = (artifact) => { const upsertArtifact = (artifact) => {
if (!artifact || artifacts.some((item) => item.label === artifact.label)) { 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 { return {
observe(message) { observe(message) {
if (!message || typeof message !== "object") { if (!message || typeof message !== "object") {
return; 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") { if (message.method === "turn/plan/updated") {
const nextSteps = extractPlanItems(message.params) const nextSteps = extractPlanItems(message.params)
.map((item, index) => { .map((item, index) => {
@@ -2634,6 +2695,9 @@ function createProgressCollector() {
if (modelVerification) { if (modelVerification) {
result.modelVerification = { ...modelVerification }; result.modelVerification = { ...modelVerification };
} }
if (streamEvents) {
result.streamEvents = { ...streamEvents };
}
return Object.keys(result).length > 0 ? result : undefined; return Object.keys(result).length > 0 ? result : undefined;
}, },
}; };

View File

@@ -258,6 +258,17 @@ export interface ExecutionProgressReasoningSummary {
summary: string; summary: string;
} }
export interface ExecutionProgressStreamEvents {
status: string;
agentDeltaCount?: number;
planDeltaCount?: number;
reasoningDeltaCount?: number;
toolProgressCount?: number;
commandOutputChunkCount?: number;
terminalInteractionCount?: number;
fileOutputChunkCount?: number;
}
export interface ExecutionProgressSnapshot { export interface ExecutionProgressSnapshot {
taskId: string; taskId: string;
projectId: string; projectId: string;
@@ -291,6 +302,7 @@ export interface ExecutionProgressSnapshot {
threadCollaboration?: ExecutionProgressThreadCollaboration; threadCollaboration?: ExecutionProgressThreadCollaboration;
toolActivities?: ExecutionProgressToolActivity[]; toolActivities?: ExecutionProgressToolActivity[];
reasoningSummary?: ExecutionProgressReasoningSummary; reasoningSummary?: ExecutionProgressReasoningSummary;
streamEvents?: ExecutionProgressStreamEvents;
updatedAt: string; updatedAt: string;
} }
@@ -335,6 +347,15 @@ export interface ExecutionProgressInput {
content?: unknown; content?: unknown;
itemId?: unknown; itemId?: unknown;
}; };
streamEvents?: Partial<ExecutionProgressStreamEvents> & {
delta?: unknown;
text?: unknown;
content?: unknown;
output?: unknown;
command?: unknown;
itemId?: unknown;
turnId?: unknown;
};
} }
export interface ForwardSource { export interface ForwardSource {
@@ -4027,6 +4048,7 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
reasoningSummary: nativeRemoteControl reasoningSummary: nativeRemoteControl
? undefined ? undefined
: normalizeExecutionProgressReasoningSummary(raw.reasoningSummary), : normalizeExecutionProgressReasoningSummary(raw.reasoningSummary),
streamEvents: nativeRemoteControl ? undefined : normalizeExecutionProgressStreamEvents(raw.streamEvents),
updatedAt: raw.updatedAt ?? nowIso(), updatedAt: raw.updatedAt ?? nowIso(),
}; };
} }
@@ -5871,6 +5893,39 @@ function normalizeExecutionProgressReasoningSummary(
}; };
} }
function normalizeStreamEventCount(value: unknown) {
const count = normalizeOptionalNumber(value);
return count === undefined ? undefined : Math.max(0, Math.min(9999, count));
}
function normalizeExecutionProgressStreamEvents(
input?: ExecutionProgressInput["streamEvents"],
): ExecutionProgressStreamEvents | undefined {
if (!input) {
return undefined;
}
const streamEvents: ExecutionProgressStreamEvents = {
status: safeExecutionProgressText(input.status) || "streaming",
agentDeltaCount: normalizeStreamEventCount(input.agentDeltaCount),
planDeltaCount: normalizeStreamEventCount(input.planDeltaCount),
reasoningDeltaCount: normalizeStreamEventCount(input.reasoningDeltaCount),
toolProgressCount: normalizeStreamEventCount(input.toolProgressCount),
commandOutputChunkCount: normalizeStreamEventCount(input.commandOutputChunkCount),
terminalInteractionCount: normalizeStreamEventCount(input.terminalInteractionCount),
fileOutputChunkCount: normalizeStreamEventCount(input.fileOutputChunkCount),
};
const hasCount = [
streamEvents.agentDeltaCount,
streamEvents.planDeltaCount,
streamEvents.reasoningDeltaCount,
streamEvents.toolProgressCount,
streamEvents.commandOutputChunkCount,
streamEvents.terminalInteractionCount,
streamEvents.fileOutputChunkCount,
].some((count) => typeof count === "number" && count > 0);
return hasCount ? streamEvents : 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 [
@@ -6029,6 +6084,7 @@ function buildExecutionProgressSnapshot(
reasoningSummary: nativeRemoteControl reasoningSummary: nativeRemoteControl
? undefined ? undefined
: normalizeExecutionProgressReasoningSummary(input?.reasoningSummary), : normalizeExecutionProgressReasoningSummary(input?.reasoningSummary),
streamEvents: nativeRemoteControl ? undefined : normalizeExecutionProgressStreamEvents(input?.streamEvents),
updatedAt: nowIso(), updatedAt: nowIso(),
}; };
} }

View File

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

View File

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

View File

@@ -528,6 +528,78 @@ test("POST task progress preserves Codex thread collaboration summaries without
assert.equal(serialized.includes("turn-secret-should-not-persist"), false); 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 () => { test("POST task progress preserves Codex tool activity summaries without leaking raw payloads", async () => {
const task = await data.queueMasterAgentTask({ const task = await data.queueMasterAgentTask({
taskId: "route-progress-tool-activity-task", taskId: "route-progress-tool-activity-task",