From de9f85bd21202d1ab65917e5a8bd104e3a20b287 Mon Sep 17 00:00:00 2001 From: AI Bot Date: Thu, 4 Jun 2026 14:09:46 +0800 Subject: [PATCH] feat: sync app server thread replies --- README.md | 3 +- docs/architecture/ai_handoff_index_cn.md | 4 +- .../api_and_service_inventory_cn.md | 3 +- .../codex_server_progress_card_cn.md | 6 +- .../current_runtime_and_deploy_status_cn.md | 3 +- local-agent/codex-app-server-runner.mjs | 65 +++++++++- local-agent/server.mjs | 111 +++++++++++++++++- tests/fixtures/codex-app-server-runtime.mjs | 18 +++ ...cal-agent-codex-app-server-runner.test.mjs | 8 ++ ...ocal-agent-heartbeat-capabilities.test.mjs | 98 ++++++++++++++++ 10 files changed, 310 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c9afb21..b9cf537 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,8 @@ - 当前 App Server heartbeat discovery 已扩展到 `experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list`,设备详情页会展示“治理:实验特性 / 协作模式 / MCP / 权限”摘要;MCP 只保留服务名、工具数量、资源数量和认证状态,permission profile 只保留 id/description,不保存本地路径、resource URI、文件规则、token 或工具参数。 - 当前 App Server heartbeat discovery 已继续扩展到 `account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect`,设备详情页会展示账号、套餐、额度、App 配置、托管要求和外部 Agent 迁移候选摘要;该链路只保存计数、开关和状态,不保存邮箱、API key、完整 config、本地路径、迁移描述或外部 Agent 原始内容。 - 当前 App Server heartbeat discovery 已新增 `thread/list / thread/loaded/list` 线程可见性摘要,设备详情页会展示线程总数、已加载线程、活跃线程和最新更新时间;metadata 只保留非归档线程的 `id / name / sourceKind / status / updatedAt / loaded` 轻量目录,不保存 cwd、本地路径、turn 内容、用户正文或内部 prompt。 -- 当前 App Server heartbeat discovery 已新增 `thread/turns/list` turn 运行态摘要,设备详情页会展示总轮次、运行中轮次、完成轮次和最新 turn 更新时间;请求固定使用 `itemsView=notLoaded`,metadata 只保留每个线程的 turn 计数、最近状态和更新时间,不保存 turn id、items、用户输入、模型输出或内部 prompt。 +- 当前 App Server heartbeat discovery 已新增 `thread/turns/list` turn 运行态摘要,设备详情页会展示总轮次、运行中轮次、完成轮次和最新 turn 更新时间;请求固定使用 `itemsView=summary`,metadata 只保留每个线程的 turn 计数、最近状态、更新时间和最终 `agentMessage` 安全摘要,不保存用户输入、reasoning 原文、命令输出、原始 items、内部 prompt 或系统提示词。 +- 当前 App Server heartbeat discovery 会把非归档可见线程的最终 `agentMessage` 合并进 `projectCandidates.recentAssistantMessages`;服务端据此把 Codex Desktop 自己产生的新回复反向同步到 Boss APP 会话列表、preview、lastMessageAt 和未读数。已有本地扫描候选优先保留 folder/thread 映射,App Server 只补充最新回复摘要。 - 当前 App Server heartbeat discovery 已新增线程操作能力摘要,设备详情页会展示“线程操作”;该摘要只来自 runner 安全 catalog 和协议快照证明,不会在 heartbeat 中调用 `thread/archive / thread/compact/start / thread/shellCommand / turn/interrupt` 等写操作。 - 当前 App Server heartbeat discovery 已支持 `skills/extraRoots/set` 共享 Skill 根目录下发摘要,设备详情页会展示“共享 Skill 根”;metadata 不保存根目录绝对路径、Skill 文件路径、token 或配置原文。 - 当前 App Server heartbeat discovery 已支持 `hooks/list` 钩子治理摘要,设备详情页会展示“Hook”;metadata 只保留 hook 数、启用数、受管 / 可信 / 修改 / 未信任计数、warning / error 计数和事件 / handler 类型,不保存 hook key、command、sourcePath、statusMessage、hash、error message 或本地路径。 diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index 536c1bd..6075892 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -153,7 +153,9 @@ - Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路 - 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败 - 当前量产方向已经明确为“Boss 企业控制面 + 可插拔执行协议”:多租户、权限、审批、审计、备份、回退和 Skill 治理由 Boss 承担,Codex App Server / Codex MCP / Codex CLI / Computer Use / 业务系统 API 都作为 provider 接入;详见 `docs/architecture/enterprise_ai_ops_architecture_cn.md` -- 当前 Codex App Server 已完成二十七批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-06-03 已按本机 `codex-cli 0.136.0-alpha.2` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`,并把 `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`、`windowsSandbox/setupCompleted`、`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`、`enteredReviewMode`、`exitedReviewMode`、`commandExecution`、`ThreadItem.plan`、`ThreadItem.reasoning.summary` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态、远控连接摘要和 Windows 沙箱准备状态,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,线程协作只保留工具名、状态、目标类型、目标数量和智能体状态集合,工具活动只保留类型、名称、状态和安全摘要,图像生成只保留状态与安全文件名,钩子生命周期只保留事件名、处理器类型、状态、来源、执行模式和耗时,思考摘要只保留官方 summary 文本和状态,账号状态只保留认证方式、套餐、额度窗口、积分余额和模型校验摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径、collab 源/目标线程 ID、collab receiverThreadIds、collab prompt、agentsStates 私有消息、共享 Skill 根绝对路径、hook key/command/sourcePath/statusMessage/hash/error message、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、imageGeneration revisedPrompt/result、hook sourcePath/statusMessage/entries、Windows sandbox sourcePath/samplePaths/本地绝对路径或未清洗密钥。heartbeat 已能缓存 `model/list / skills/list / skills/extraRoots/set / hooks/list / plugin/list / app/list / modelProvider/capabilities/read / experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list / account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect / thread/list / thread/loaded/list / thread/turns/list` 的能力摘要;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。MCP、权限、账号、配置、外部 Agent、线程、turn、Hook、Skill extra roots、运行事件、扩展事件、线程生命周期和流式增量 discovery 只保留安全摘要,不保存 resource URI、权限文件规则、工具参数、邮箱、完整 config、本地路径、迁移描述、turn id、turn items、用户正文、模型输出、hook 命令、共享 Skill 根绝对路径、原始增量文本、命令输出、推理正文、文件输出或 token。 +- 当前 Codex App Server 已完成二十八批接入:boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio,也可灰度连接 `ws://127.0.0.1:` 或 `unix://PATH` 同机长驻 App Server;长驻连接支持 `Authorization: Bearer `,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-06-03 已按本机 `codex-cli 0.136.0-alpha.2` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`。 +- App Server runner 已把 plan、diff、item、approval、warning、file change、thread status、realtime、model route、token usage、MCP、remote control、thread goal、settings、compaction、account、model verification、collab、tool activity、reasoning summary、image generation、hook、Windows sandbox 和 stream delta 归一到 Boss `execution_progress` 卡片;字段白名单只保留安全摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径、collab 源/目标线程 ID、receiverThreadIds、collab prompt、agentsStates 私有消息、共享 Skill 根绝对路径、hook key/command/sourcePath/statusMessage/hash/error message、tool arguments/result/contentItems、web URL token、命令正文/输出、raw reasoning content、reasoning item id、imageGeneration revisedPrompt/result、Windows sandbox sourcePath/samplePaths、本地绝对路径或未清洗密钥。 +- Heartbeat discovery 已能缓存 `model/list / skills/list / skills/extraRoots/set / hooks/list / plugin/list / app/list / modelProvider/capabilities/read / experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list / account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect / thread/list / thread/loaded/list / thread/turns/list` 的能力摘要。`thread/turns/list` 固定使用 `itemsView=summary`,只额外提取最终 `agentMessage` 安全摘要,并合并进 `projectCandidates.recentAssistantMessages` 让 Codex Desktop 自己产生的新回复反向同步到 Boss APP;不保存用户正文、reasoning 原文、命令输出、原始 items、内部 prompt 或系统提示词。同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。 - 第十九批另补 `threadActionSummary` 线程操作能力摘要:设备详情页会显示 archive / unarchive / fork / compact / rollback / rename / metadata / steer / interrupt / shell / unsubscribe 等能力分组;该字段只读,不在 heartbeat 中调用任何会改变线程状态的 App Server API。 - 第二十批另补 `pluginGovernanceSummary` 插件治理能力摘要:设备详情页会显示 install / uninstall / read / skill-read / share 等能力分组;该字段只读,不在 heartbeat 中调用任何插件安装、卸载或共享写 API。 - 第二十一批另补 `accountGovernanceSummary / configGovernanceSummary` 账号与配置治理能力摘要:设备详情页会显示 login / logout / token refresh / add credits nudge / config write / MCP reload / Skill config write 等能力分组;这些字段只读,不在 heartbeat 中调用任何账号或配置写 API。 diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 3ac91be..90b73e5 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -121,7 +121,8 @@ - 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。 -- App Server heartbeat discovery 现在还会按 TTL 对非归档可见线程拉取 `thread/turns/list`,写入 `capabilities.codexAppServer.metadata.threadTurnSummary`。该字段用于 APP/后台展示 Codex 当前线程 turn 运行态;请求固定 `itemsView=notLoaded`,只保留 turn 计数、运行中 / 完成计数、最近状态和更新时间,不保存 turn id、items、用户正文、模型输出或内部 prompt。 +- App Server heartbeat discovery 现在还会按 TTL 对非归档可见线程拉取 `thread/turns/list`,写入 `capabilities.codexAppServer.metadata.threadTurnSummary`。该字段用于 APP/后台展示 Codex 当前线程 turn 运行态;请求固定 `itemsView=summary`,只保留 turn 计数、运行中 / 完成计数、最近状态、更新时间和最终 `agentMessage` 安全摘要,不保存用户正文、reasoning 原文、命令输出、原始 items、内部 prompt 或系统提示词。 +- App Server heartbeat discovery 现在还会把最终 `agentMessage` 安全摘要合并进 `projectCandidates.recentAssistantMessages`。服务端根据 `codexThreadRef` 将 Codex Desktop 自己产生的新回复反向同步到 Boss APP 对应会话、preview、lastMessageAt 和未读数;已有本地扫描候选的 folder/thread 映射优先,App Server 只补充最新回复摘要。 - App Server heartbeat discovery 现在还会写入 `capabilities.codexAppServer.metadata.threadActionSummary`。该字段用于 APP/后台展示当前协议下可接入的线程治理动作数量和分组,覆盖 archive / unarchive / fork / compact / rollback / rename / metadata / steer / interrupt / shell / unsubscribe;它只来自 runner 安全 catalog 和协议快照,不会在 heartbeat 中调用这些写操作。 - App Server heartbeat discovery 现在还会写入 `capabilities.codexAppServer.metadata.pluginGovernanceSummary`。该字段用于 APP/后台展示当前协议下可接入的插件治理动作数量和分组,覆盖 install / uninstall / read / skill-read / share;它只来自 runner 安全 catalog 和协议快照,不会在 heartbeat 中调用插件安装、卸载或共享写操作。 - App Server heartbeat discovery 现在还会写入 `capabilities.codexAppServer.metadata.accountGovernanceSummary / configGovernanceSummary`。这些字段用于 APP/后台展示当前协议下可接入的账号与配置治理动作数量和分组,覆盖 login / logout / token refresh / add credits nudge / config write / MCP reload / Skill config write;它们只来自 runner 安全 catalog 和协议快照,不会在 heartbeat 中调用账号或配置写操作。 diff --git a/docs/architecture/codex_server_progress_card_cn.md b/docs/architecture/codex_server_progress_card_cn.md index 715d0cd..6fa66f5 100644 --- a/docs/architecture/codex_server_progress_card_cn.md +++ b/docs/architecture/codex_server_progress_card_cn.md @@ -22,7 +22,9 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务 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 错误不入账。 +2026-06-03 第二十八批已把 heartbeat `thread/turns/list` 从 `itemsView=notLoaded` 升级为 `itemsView=summary`,但只提取最终 `agentMessage` 安全摘要,并合并到 `projectCandidates.recentAssistantMessages`;服务端据此把 Codex Desktop 自己产生的新最终回复反向同步到 Boss APP 对应会话、preview、lastMessageAt 和未读数。 + +除用于 Boss APP 会话反向同步的最终 `agentMessage` 安全摘要外,`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 错误不入账。 官方文档入口:`https://developers.openai.com/codex/app-server` @@ -44,7 +46,7 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务 - 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`,共识别 138 个协议方法;确认支持 `thread/inject_items`、`thread/rollback`、`thread/archive`、`thread/unarchive`、`thread/fork`、`thread/compact/start`、`thread/name/set`、`thread/metadata/update`、`thread/shellCommand`、`thread/unsubscribe`、`thread/goal/*`、`turn/steer`、`turn/interrupt`、`command/exec`、`command/exec/write`、`command/exec/resize`、`command/exec/terminate`、`command/exec/outputDelta`、`process/outputDelta|exited`、`rawResponseItem/completed`、`thread/realtime/*`、`thread/started|closed|archived|unarchived|name/updated`、`item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/*Delta`、`item/mcpToolCall/progress`、`item/commandExecution/terminalInteraction`、`item/fileChange/outputDelta`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`model/list`、`skills/changed`、`skills/extraRoots/set`、`hooks/list`、`plugin/installed`、`plugin/install`、`plugin/uninstall`、`plugin/read`、`plugin/skill/read`、`plugin/share/*`、`config/value/write`、`config/batchWrite`、`config/mcpServer/reload`、`skills/config/write`、`fs/*`、`externalAgentConfig/import`、`marketplace/add|remove|upgrade`、`experimentalFeature/enablement/set`、`review/start`、`windowsSandbox/readiness|setupStart`、`fuzzyFileSearch/session*`、`mcpServer/oauth*`、`mcpServer/resource/read`、`mcpServer/tool/call`、`mcpServer/elicitation/request`、`item/tool/requestUserInput` 和 `thread/approveGuardianDeniedAction` - Boss 当前默认仍以 `stdio` 作为本机 agent 接入方式;`ws://127.0.0.1:` 和 `unix://PATH` 本地长驻 transport 已可灰度接入,WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer `;非 loopback signed bearer/JWT、自动重连和健康探测仍保留为后续增强,不直接替换当前稳定链路 - 官方文档提示 WebSocket ingress 满载时会返回 JSON-RPC `-32001 / Server overloaded; retry later.`;Boss runner 已对该错误做最多 3 次指数退避重试,避免长驻连接瞬时拥塞直接把用户任务打失败 -- Boss heartbeat 已新增 App Server 能力发现缓存:按 `codexAppServerDiscoveryTtlMs` 拉取 `model/list`、`modelProvider/capabilities/read`、`skills/list`、`hooks/list`、`plugin/list`、`app/list`、`experimentalFeature/list`、`collaborationMode/list`、`permissionProfile/list`、`mcpServerStatus/list`、`account/read`、`account/rateLimits/read`、`config/read`、`configRequirements/read`、`externalAgentConfig/detect`、`thread/list`、`thread/loaded/list` 和 `thread/turns/list`;配置了 `codexAppServerSkillExtraRoots` / `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 时,会先调用 `skills/extraRoots/set` 再拉取 `skills/list`,归一成设备 `capabilities.codexAppServer.metadata`;发现失败只记录 warn,不阻塞心跳。MCP discovery 使用 `detail=toolsAndAuthOnly`,turn discovery 固定 `itemsView=notLoaded`,账号、配置、线程、Hook 和 Skill extra root discovery 只保留安全摘要,不保存邮箱、resource URI、工具参数、完整 config、本地路径、迁移描述、turn id、turn items、用户正文、模型输出、hook 命令或共享 Skill 根绝对路径。 +- Boss heartbeat 已新增 App Server 能力发现缓存:按 `codexAppServerDiscoveryTtlMs` 拉取 `model/list`、`modelProvider/capabilities/read`、`skills/list`、`hooks/list`、`plugin/list`、`app/list`、`experimentalFeature/list`、`collaborationMode/list`、`permissionProfile/list`、`mcpServerStatus/list`、`account/read`、`account/rateLimits/read`、`config/read`、`configRequirements/read`、`externalAgentConfig/detect`、`thread/list`、`thread/loaded/list` 和 `thread/turns/list`;配置了 `codexAppServerSkillExtraRoots` / `BOSS_CODEX_APP_SERVER_SKILL_EXTRA_ROOTS` 时,会先调用 `skills/extraRoots/set` 再拉取 `skills/list`,归一成设备 `capabilities.codexAppServer.metadata`;发现失败只记录 warn,不阻塞心跳。MCP discovery 使用 `detail=toolsAndAuthOnly`,turn discovery 固定 `itemsView=summary`,账号、配置、线程、Hook 和 Skill extra root discovery 只保留安全摘要;turn discovery 只额外保留最终 `agentMessage` 摘要,并会合并进 `projectCandidates.recentAssistantMessages` 用于 Codex Desktop 回复反向同步,不保存邮箱、resource URI、工具参数、完整 config、本地路径、迁移描述、用户正文、reasoning 原文、命令输出、原始 items、内部 prompt、系统提示词、hook 命令或共享 Skill 根绝对路径。 - Boss 第一批只用 App Server 做任务级 provider,不直接复用 ChatGPT Mobile 到 Codex App 的官方 relay;官方移动控制链路仍属于 ChatGPT App 与 Codex App 同账号/工作区之间的产品能力,不是第三方 Boss 可以稳定依赖的私有通道 下一轮再核对版本时,不要只看 npm 包版本号;必须同时读取 App Server schema / TypeScript 定义,并把 protocol snapshot 保存到 `docs/protocol-snapshots/codex-app-server//`。 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index e2afa29..ba0613b 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -39,7 +39,8 @@ - 当前 App Server 能力发现已新增治理摘要:local-agent 会在 heartbeat discovery 中拉取 `experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list`,并把实验特性、协作模式、权限 Profile 与 MCP 服务状态写入设备 `codexAppServer.metadata`;设备详情页会显示“治理”摘要。该链路只保留安全摘要,不保存 MCP resource URI、permission profile 文件规则、本地路径、token 或工具参数。 - 当前 App Server 能力发现已新增账号与配置摘要:local-agent 会在 heartbeat discovery 中拉取 `account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect`,并把账号登录方式、套餐、额度使用率、App 配置计数、托管要求数量和外部 Agent 迁移候选数量写入设备 `codexAppServer.metadata`;设备详情页会显示“账号 / 配置”摘要。该链路只读不写,不保存账号邮箱、完整 config、API key、本地路径或迁移描述。 - 当前 App Server 能力发现已新增线程可见性摘要:local-agent 会在 heartbeat discovery 中拉取 `thread/list / thread/loaded/list`,并把线程总数、已加载线程数、活跃线程数、归档线程数、最新更新时间和非归档线程轻量目录写入设备 `codexAppServer.metadata.threadSummary`;设备详情页会显示“线程”摘要。该链路不保存 cwd、本地路径、turn 内容、用户正文或内部 prompt。 -- 当前 App Server 能力发现已新增 turn 运行态摘要:local-agent 会在 heartbeat discovery 中对非归档可见线程拉取 `thread/turns/list`,请求固定 `itemsView=notLoaded`,并把总轮次、运行中轮次、完成轮次、最新 turn 更新时间和每个线程的最近 turn 状态写入设备 `codexAppServer.metadata.threadTurnSummary`;设备详情页会显示“轮次”摘要。该链路不保存 turn id、turn items、用户正文、模型输出或内部 prompt。 +- 当前 App Server 能力发现已新增 turn 运行态摘要:local-agent 会在 heartbeat discovery 中对非归档可见线程拉取 `thread/turns/list`,请求固定 `itemsView=summary`,并把总轮次、运行中轮次、完成轮次、最新 turn 更新时间、每个线程的最近 turn 状态和最终 `agentMessage` 安全摘要写入设备 `codexAppServer.metadata.threadTurnSummary`;设备详情页会显示“轮次”摘要。该链路不保存用户正文、reasoning 原文、命令输出、原始 items、内部 prompt 或系统提示词。 +- 当前 App Server discovery 还会把最终 `agentMessage` 合并进 heartbeat `projectCandidates.recentAssistantMessages`。服务端已有 `codexThreadRef` 匹配时会把 Codex Desktop 自己产生的新回复反向同步到 Boss APP 对应会话,并刷新 preview、lastMessageAt 和未读数;已有本地扫描候选的 folder/thread 映射优先保留,App Server 只补充最新回复摘要。 - 当前 App Server 能力发现已新增线程操作能力摘要:local-agent 会把已验证进入当前协议快照的 archive / unarchive / fork / compact / rollback / rename / metadata / steer / interrupt / shell / unsubscribe 写入设备 `codexAppServer.metadata.threadActionSummary`;设备详情页会显示“线程操作”。该字段只读,不在 heartbeat 中调用任何线程写 API。 - 当前 App Server 能力发现已新增插件治理能力摘要:local-agent 会把已验证进入当前协议快照的 install / uninstall / read / skill-read / share 写入设备 `codexAppServer.metadata.pluginGovernanceSummary`;设备详情页会显示“插件治理”。该字段只读,不在 heartbeat 中调用任何插件写 API。 - 当前 App Server 能力发现已新增账号与配置治理能力摘要:local-agent 会把已验证进入当前协议快照的 login / logout / token refresh / add credits nudge / config write / MCP reload / Skill config write 写入设备 `codexAppServer.metadata.accountGovernanceSummary / configGovernanceSummary`;设备详情页会显示“账号治理 / 配置治理”。这些字段只读,不在 heartbeat 中调用任何账号或配置写 API。 diff --git a/local-agent/codex-app-server-runner.mjs b/local-agent/codex-app-server-runner.mjs index 2da28b3..cf07d63 100644 --- a/local-agent/codex-app-server-runner.mjs +++ b/local-agent/codex-app-server-runner.mjs @@ -1928,6 +1928,68 @@ function extractDiscoveryTurnStatus(turn) { ); } +const MAX_DISCOVERY_RECENT_ASSISTANT_MESSAGES = 6; + +function extractDiscoveryTurnItems(turn) { + if (asArray(turn?.items).length > 0) { + return asArray(turn.items); + } + if (asArray(turn?.summary?.items).length > 0) { + return asArray(turn.summary.items); + } + return []; +} + +function normalizeDiscoveryAssistantMessageText(item) { + if (!item || typeof item !== "object" || item.type !== "agentMessage") { + return ""; + } + const text = trimToDefined(item.text ?? item.content ?? item.message); + return text ? safeProgressText(text, 4000) : ""; +} + +function normalizeDiscoveryAssistantPhase(item) { + const phase = trimToDefined(item?.phase); + return phase === "final_answer" || phase === "commentary" ? phase : undefined; +} + +function extractDiscoveryRecentAssistantMessages(threadId, turns) { + const messages = []; + for (const turn of turns) { + const turnId = trimToDefined(turn?.id ?? turn?.turnId) || "unknown-turn"; + const sentAt = trimToDefined(turn?.updatedAt ?? turn?.lastUpdatedAt ?? turn?.createdAt) || ""; + if (!sentAt) { + continue; + } + for (const item of extractDiscoveryTurnItems(turn)) { + const body = normalizeDiscoveryAssistantMessageText(item); + if (!body) { + continue; + } + const itemId = trimToDefined(item?.id) || crypto + .createHash("sha256") + .update(`${threadId}:${turnId}:${body}`) + .digest("hex") + .slice(0, 16); + messages.push({ + messageId: `codex-app-server:${threadId}:${turnId}:${itemId}`, + body, + sentAt, + phase: normalizeDiscoveryAssistantPhase(item), + }); + } + } + const deduped = new Map(); + for (const message of messages) { + if (!deduped.has(message.messageId)) { + deduped.set(message.messageId, message); + } + } + return [...deduped.values()] + .sort((left, right) => left.sentAt.localeCompare(right.sentAt)) + .slice(-MAX_DISCOVERY_RECENT_ASSISTANT_MESSAGES); +} + function normalizeDiscoveryThreadTurnSummary(threadTurnResults, limit) { const threadSummaries = threadTurnResults .map(({ threadId, result }) => { @@ -1951,6 +2013,7 @@ function normalizeDiscoveryThreadTurnSummary(threadTurnResults, limit) { completedTurnCount: turns.filter((turn) => /completed|complete|success|succeeded/i.test(turn.status)).length, latestTurnStatus: latestTurn?.status || "unknown", latestTurnUpdatedAt: latestTurn?.updatedAt || "", + recentAssistantMessages: extractDiscoveryRecentAssistantMessages(threadId, rawTurns), }; }) .sort((left, right) => String(right.latestTurnUpdatedAt).localeCompare(String(left.latestTurnUpdatedAt))) @@ -2137,7 +2200,7 @@ export async function discoverCodexAppServerCapabilities(runnerConfig) { threadId: thread.id, limit: turnProbeLimit, sortDirection: "desc", - itemsView: "notLoaded", + itemsView: "summary", }), })), ); diff --git a/local-agent/server.mjs b/local-agent/server.mjs index 83d7094..b565863 100755 --- a/local-agent/server.mjs +++ b/local-agent/server.mjs @@ -122,6 +122,102 @@ async function resolveHeartbeatProjects(config, runtime) { } } +function trimToDefined(value) { + const trimmed = String(value ?? "").trim(); + return trimmed ? trimmed : undefined; +} + +function mergeRecentAssistantMessages(left = [], right = []) { + const messages = new Map(); + for (const message of [...left, ...right]) { + const messageId = trimToDefined(message?.messageId); + const body = trimToDefined(message?.body); + if (!messageId || !body) { + continue; + } + messages.set(messageId, { + messageId, + body, + sentAt: trimToDefined(message?.sentAt) || new Date().toISOString(), + ...(trimToDefined(message?.phase) ? { phase: trimToDefined(message.phase) } : {}), + }); + } + return [...messages.values()].sort((a, b) => a.sentAt.localeCompare(b.sentAt)).slice(-6); +} + +function resolveCandidateKey(candidate) { + return trimToDefined(candidate?.codexThreadRef) || trimToDefined(candidate?.threadId); +} + +function mergeHeartbeatProjectCandidates(existingCandidates = [], appServerCandidates = []) { + const candidateMap = new Map(); + for (const candidate of [...existingCandidates, ...appServerCandidates]) { + const key = resolveCandidateKey(candidate); + if (!key) { + continue; + } + const existing = candidateMap.get(key); + if (!existing) { + candidateMap.set(key, candidate); + continue; + } + const recentAssistantMessages = mergeRecentAssistantMessages( + existing.recentAssistantMessages, + candidate.recentAssistantMessages, + ); + candidateMap.set(key, { + ...candidate, + ...existing, + lastActiveAt: [existing.lastActiveAt, candidate.lastActiveAt].filter(Boolean).sort().at(-1), + suggestedImport: existing.suggestedImport ?? candidate.suggestedImport ?? true, + ...(recentAssistantMessages.length > 0 ? { recentAssistantMessages } : {}), + }); + } + return [...candidateMap.values()].sort((a, b) => + String(b.lastActiveAt ?? "").localeCompare(String(a.lastActiveAt ?? "")), + ); +} + +function buildCodexAppServerProjectCandidates(metadata) { + const visibleThreads = Array.isArray(metadata?.threadSummary?.visibleThreads) + ? metadata.threadSummary.visibleThreads + : []; + const turnSummaries = Array.isArray(metadata?.threadTurnSummary?.threads) + ? metadata.threadTurnSummary.threads + : []; + const turnSummaryByThreadId = new Map( + turnSummaries + .map((thread) => [trimToDefined(thread?.threadId), thread]) + .filter(([threadId]) => Boolean(threadId)), + ); + return visibleThreads + .map((thread) => { + const threadId = trimToDefined(thread?.id); + if (!threadId) { + return null; + } + const turnSummary = turnSummaryByThreadId.get(threadId); + const threadName = trimToDefined(thread?.name) || threadId; + const recentAssistantMessages = mergeRecentAssistantMessages( + [], + turnSummary?.recentAssistantMessages, + ); + return { + folderName: threadName, + threadId, + threadDisplayName: threadName, + codexThreadRef: threadId, + lastActiveAt: + trimToDefined(turnSummary?.latestTurnUpdatedAt) || + trimToDefined(thread?.updatedAt) || + new Date().toISOString(), + suggestedImport: true, + ...(recentAssistantMessages.length > 0 ? { recentAssistantMessages } : {}), + }; + }) + .filter(Boolean); +} + async function postHeartbeat(config, runtime, heartbeatProjects) { const now = new Date().toISOString(); const preferredExecutionMode = @@ -142,6 +238,17 @@ async function postHeartbeat(config, runtime, heartbeatProjects) { const guiConnected = config.guiConnected === true || (config.guiConnected !== false && heartbeatProjects.guiConnected === true); + const appServerCandidates = buildCodexAppServerProjectCandidates(codexAppServerMetadata); + const mergedProjectCandidates = mergeHeartbeatProjectCandidates( + heartbeatProjects.projectCandidates, + appServerCandidates, + ); + const mergedProjects = [ + ...new Set([ + ...heartbeatProjects.projects, + ...mergedProjectCandidates.map((candidate) => candidate.folderName).filter(Boolean), + ]), + ]; const response = await fetch(`${config.controlPlaneUrl.replace(/\/$/, "")}/api/device-heartbeat`, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -184,8 +291,8 @@ async function postHeartbeat(config, runtime, heartbeatProjects) { }, }, preferredExecutionMode, - projects: heartbeatProjects.projects, - projectCandidates: heartbeatProjects.projectCandidates, + projects: mergedProjects, + projectCandidates: mergedProjectCandidates, endpoint: config.endpoint, }), }); diff --git a/tests/fixtures/codex-app-server-runtime.mjs b/tests/fixtures/codex-app-server-runtime.mjs index ae19d16..872a216 100644 --- a/tests/fixtures/codex-app-server-runtime.mjs +++ b/tests/fixtures/codex-app-server-runtime.mjs @@ -505,6 +505,7 @@ rl.on("line", (line) => { if (message.method === "thread/turns/list") { const threadId = message.params?.threadId; if (threadId === "thr-active") { + const includeSummaryItems = message.params?.itemsView === "summary" || !message.params?.itemsView; send({ id: message.id, result: { @@ -522,6 +523,23 @@ rl.on("line", (line) => { status: "completed", createdAt: "2026-06-03T08:00:00.000Z", updatedAt: "2026-06-03T08:10:00.000Z", + ...(includeSummaryItems + ? { + items: [ + { + type: "userMessage", + id: "private-user-message-should-not-leak", + content: [{ type: "text", text: "private user summary text should not leak" }], + }, + { + type: "agentMessage", + id: "agent-final-app-server-reply", + text: "App Server 最终回复已完成同步。", + phase: "final_answer", + }, + ], + } + : {}), }, ], nextCursor: null, diff --git a/tests/local-agent-codex-app-server-runner.test.mjs b/tests/local-agent-codex-app-server-runner.test.mjs index 2a53978..ce38ca9 100644 --- a/tests/local-agent-codex-app-server-runner.test.mjs +++ b/tests/local-agent-codex-app-server-runner.test.mjs @@ -113,6 +113,14 @@ test("codex app-server discovery includes governance and MCP summaries without l completedTurnCount: 1, latestTurnStatus: "running", latestTurnUpdatedAt: "2026-06-03T08:21:00.000Z", + recentAssistantMessages: [ + { + messageId: "codex-app-server:thr-active:turn-active-1:agent-final-app-server-reply", + body: "App Server 最终回复已完成同步。", + sentAt: "2026-06-03T08:10:00.000Z", + phase: "final_answer", + }, + ], }); assert.deepEqual(metadata.threadActionSummary, { actionCount: 11, diff --git a/tests/local-agent-heartbeat-capabilities.test.mjs b/tests/local-agent-heartbeat-capabilities.test.mjs index 417cc9b..2a2130c 100644 --- a/tests/local-agent-heartbeat-capabilities.test.mjs +++ b/tests/local-agent-heartbeat-capabilities.test.mjs @@ -271,3 +271,101 @@ test("local-agent heartbeat reports Codex App Server discovered models, skills, await rm(runtimeRoot, { recursive: true, force: true }); } }); + +test("local-agent heartbeat enriches Codex App Server threads with recent final replies", async () => { + const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-app-server-recent-replies-")); + const skillsDir = path.join(runtimeRoot, "skills"); + await mkdir(skillsDir, { recursive: true }); + + const mockControlPlane = await startMockControlPlane(); + const exampleConfig = JSON.parse( + await readFile(path.join(repoRoot, "local-agent", "config.example.json"), "utf8"), + ); + const configPath = path.join(runtimeRoot, "config.json"); + await writeFile( + configPath, + JSON.stringify( + { + ...exampleConfig, + bindHost: "127.0.0.1", + port: 0, + controlPlaneUrl: `http://127.0.0.1:${mockControlPlane.port}`, + heartbeatIntervalMs: 60_000, + masterAgentPollIntervalMs: 60_000, + masterAgentEnabled: false, + codexSessionDiscoveryEnabled: false, + codexAppServerEnabled: true, + codexAppServerCommand: process.execPath, + codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"], + codexAppServerWorkdir: repoRoot, + codexAppServerTimeoutMs: 5000, + codexAppServerDiscoveryTtlMs: 60_000, + projects: ["boss"], + projectCandidates: [ + { + folderName: "boss", + folderRef: repoRoot, + threadId: "thr-active", + threadDisplayName: "Boss App Server rollout", + codexFolderRef: repoRoot, + codexThreadRef: "thr-active", + lastActiveAt: "2026-06-03T08:20:00.000Z", + suggestedImport: true, + }, + ], + skillsDir, + }, + null, + 2, + ), + "utf8", + ); + + const child = spawn(process.execPath, ["local-agent/server.mjs", configPath], { + cwd: repoRoot, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + + try { + const heartbeatRequest = await Promise.race([ + mockControlPlane.heartbeatReceived, + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error(`timed out waiting for heartbeat\n${stderr}`)); + }, 8000); + }), + ]); + + const payload = JSON.parse(heartbeatRequest.bodyText); + const serializedPayload = JSON.stringify(payload); + const candidate = payload.projectCandidates.find((item) => item.codexThreadRef === "thr-active"); + + assert.ok(candidate, "expected heartbeat to keep the imported Codex App Server thread candidate"); + assert.equal(candidate.folderName, "boss"); + assert.deepEqual(candidate.recentAssistantMessages, [ + { + messageId: "codex-app-server:thr-active:turn-active-1:agent-final-app-server-reply", + body: "App Server 最终回复已完成同步。", + sentAt: "2026-06-03T08:10:00.000Z", + phase: "final_answer", + }, + ]); + assert.ok(!serializedPayload.includes("private active turn text should not leak")); + assert.ok(!serializedPayload.includes("private item content should not leak")); + assert.ok(!serializedPayload.includes("private user summary text should not leak")); + } finally { + child.kill("SIGTERM"); + await new Promise((resolve) => { + child.once("close", resolve); + }).catch(() => null); + await new Promise((resolve) => { + mockControlPlane.server.close(resolve); + }); + await rm(runtimeRoot, { recursive: true, force: true }); + } +});