feat: map codex realtime thread status

This commit is contained in:
AI Bot
2026-05-31 03:54:43 +08:00
parent f333676c36
commit cee1e7938e
12 changed files with 505 additions and 7 deletions

View File

@@ -81,7 +81,7 @@
- 当前 `scripts/browser-control-smoke.mjs` 已经能对目标 URL 做一次真实最小探测:抓取页面标题并写回聊天结果;桌面 GUI 控制默认先走 `scripts/codex-computer-use-runtime.mjs`,由 Codex App Server 发起 Codex Computer Use 执行;失败后自动回退 `scripts/cua-driver-computer-use-runtime.mjs`,通过外部 `cua-driver` 执行 `launch_app -> get_window_state -> 可选 type_text/press_key -> get_window_state` 闭环;`scripts/computer-use-smoke.mjs` 仍保留为旧兜底和回归资产
- 受控 Mac 需要先安装并授权 `cua-driver`Boss runtime 会优先搜索 `PATH`,再搜索 `~/.local/bin/cua-driver``/usr/local/bin/cua-driver``/opt/homebrew/bin/cua-driver``/Applications/CuaDriver.app/Contents/MacOS/cua-driver`;如果仍找不到,会明确返回 `CUA_DRIVER_COMMAND_NOT_FOUND`,不会伪装成执行成功
- 当前默认本机配置已把 `browserAutomation / computerUse` 两项能力直接上报为在线起步态,所以 Boss App 里这台 Mac 会显示“可做浏览器控制 / 桌面控制”;如果某条链路要临时收起,只需要改 `local-agent/config.cloud.json`
- 当前 `local-agent` 已新增 `Codex App Server` runnerboss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:<port>``unix://PATH` 本机长驻 App ServerWebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`,优先用 `codexAppServerAuthTokenFile` 保存本地 token。失败时只在 turn 未启动前回退 `codex exec resume`,避免重复执行同一轮对话。设备 heartbeat 会单独上报 `codexAppServer` capability并按 `codexAppServerDiscoveryTtlMs` 缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要,供 APP/后台模型选择和治理页读取。2026-05-31 起runner 会吸收 App Server 的 plan / diff / item / subagent 事件并归一到 Boss `execution_progress` 进度卡,执行中通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;同日第二批已补 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated``approvals / warnings / fileChanges`Android 原生进度卡可显示安全提醒、审批状态和文件变更摘要且不展示完整命令、diff、系统提示词密钥。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成在 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`。同日新增第一版 Inter-Thread Broker任务携带源/目标 Codex 线程时可通过 `thread/read -> thread/inject_items -> turn/start` 完成受控线程协作;服务端新增 `POST /api/v1/projects/[projectId]/thread-collaboration` 作为 APP/后台可调用入口;任务携带 `targetCodexTurnId` 时 runner 会改用 `turn/steer` 干预活跃 turn。
- 当前 `local-agent` 已新增 `Codex App Server` runnerboss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:<port>``unix://PATH` 本机长驻 App ServerWebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`,优先用 `codexAppServerAuthTokenFile` 保存本地 token。失败时只在 turn 未启动前回退 `codex exec resume`,避免重复执行同一轮对话。设备 heartbeat 会单独上报 `codexAppServer` capability并按 `codexAppServerDiscoveryTtlMs` 缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要,供 APP/后台模型选择和治理页读取。2026-05-31 起runner 会吸收 App Server 的 plan / diff / item / subagent 事件并归一到 Boss `execution_progress` 进度卡,执行中通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;同日第二批已补 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated``approvals / warnings / fileChanges`第三批已补 `thread/status/changed``thread/realtime/*``threadStatus / realtime`Android 原生进度卡可显示线程状态、实时状态、安全提醒、审批状态和文件变更摘要且不展示完整命令、diff、系统提示词密钥、SDP、音频原始数据或 raw realtime item。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成在 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`。同日新增第一版 Inter-Thread Broker任务携带源/目标 Codex 线程时可通过 `thread/read -> thread/inject_items -> turn/start` 完成受控线程协作;服务端新增 `POST /api/v1/projects/[projectId]/thread-collaboration` 作为 APP/后台可调用入口;任务携带 `targetCodexTurnId` 时 runner 会改用 `turn/steer` 干预活跃 turn。
- `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill
- `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报
- `local-agent` 当前每 5 秒轮询一次本机 Skill lifecycle 请求;默认打开 `skillLifecycleEnabled=true`。远程 `install` 或带 `sourceUrl` 的更新必须命中 `skillLifecycleAllowedSources``skillLifecycleTrustedSources`,为空时只允许既有本地 Skill 的 `update / rollback / uninstall / version_lock`;请求携带 `checksum / expectedChecksum` 时会校验 `manifest.json``SKILL.md` 的 sha256失败会清理半安装目录或尽量恢复备份。卸载 / 更新 / 回滚前会在 `skillsDir/.boss-skill-backups` 保留备份,卸载仍限制在 `skillsDir` 目录内,版本锁写入 `.boss-skill-locks.json`

View File

@@ -1296,6 +1296,63 @@ public final class BossUi {
return card;
}
JSONObject threadStatus = progress == null ? null : progress.optJSONObject("threadStatus");
if (threadStatus != null) {
String type = threadStatus.optString("type", "").trim();
boolean waitingOnApproval = threadStatus.optBoolean("waitingOnApproval", false);
boolean waitingOnUserInput = threadStatus.optBoolean("waitingOnUserInput", false);
if (!TextUtils.isEmpty(type) || waitingOnApproval || waitingOnUserInput) {
card.addView(divider(context));
card.addView(sectionTitle(context, "线程状态"));
String label = "active".equals(type) ? "活跃" :
"idle".equals(type) ? "空闲" :
"systemError".equals(type) ? "系统异常" :
"notLoaded".equals(type) ? "未加载" : type;
card.addView(detailRow(context, "", TextUtils.isEmpty(label) ? "状态待同步" : label, "", false));
if (waitingOnApproval) {
card.addView(detailRow(context, "", "等待审批", "", false, true));
}
if (waitingOnUserInput) {
card.addView(detailRow(context, "", "等待用户输入", "", false, true));
}
}
}
JSONObject realtime = progress == null ? null : progress.optJSONObject("realtime");
if (realtime != null) {
String status = realtime.optString("status", "").trim();
if (!TextUtils.isEmpty(status)) {
card.addView(divider(context));
card.addView(sectionTitle(context, "实时状态"));
String closeReason = realtime.optString("closeReason", "").trim();
String lastError = realtime.optString("lastError", "").trim();
String statusLabel = "started".equals(status) ? "已启动" :
"streaming".equals(status) ? "同步中" :
"closed".equals(status) ? "已关闭" :
"error".equals(status) ? "异常" : status;
String trailing = !TextUtils.isEmpty(lastError) ? lastError : closeReason;
card.addView(detailRow(
context,
"",
TextUtils.isEmpty(trailing) ? statusLabel : statusLabel + " · " + trailing,
"",
"error".equals(status)
));
String transcript = realtime.optString("transcriptPreview", "").trim();
if (!TextUtils.isEmpty(transcript)) {
card.addView(detailRow(context, "", transcript, "", false, true));
}
int audioChunkCount = realtime.optInt("audioChunkCount", 0);
int itemCount = realtime.optInt("itemCount", 0);
if (audioChunkCount > 0) {
card.addView(detailRow(context, "", "音频片段 " + audioChunkCount, "", false, true));
}
if (itemCount > 0) {
card.addView(detailRow(context, "", "实时事件 " + itemCount, "", false, true));
}
}
}
JSONArray warnings = progress == null ? null : progress.optJSONArray("warnings");
if (warnings != null && warnings.length() > 0) {
card.addView(divider(context));

View File

@@ -958,6 +958,62 @@ public class ProjectDetailActivityUiTest {
assertFalse(viewTreeContainsText(messageView, "diff"));
}
@Test
public void executionProgressMessageRendersCodexThreadStatusAndRealtimeSections() throws Exception {
Intent intent = new Intent()
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "thread-realtime")
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程");
TestProjectDetailActivity activity = Robolectric
.buildActivity(TestProjectDetailActivity.class, intent)
.setup()
.get();
JSONObject message = new JSONObject()
.put("id", "progress-realtime-1")
.put("sender", "master")
.put("senderLabel", "主 Agent")
.put("body", "执行进度")
.put("kind", "execution_progress")
.put("sentAt", "2026-05-31T10:20:00+08:00")
.put("executionProgress", new JSONObject()
.put("status", "running")
.put("steps", new JSONArray()
.put(new JSONObject().put("text", "监听 Codex realtime 事件").put("status", "running")))
.put("threadStatus", new JSONObject()
.put("type", "active")
.put("activeFlags", new JSONArray()
.put("waitingOnApproval")
.put("waitingOnUserInput"))
.put("waitingOnApproval", true)
.put("waitingOnUserInput", true))
.put("realtime", new JSONObject()
.put("status", "closed")
.put("sessionId", "rt-session-1")
.put("version", "v2")
.put("transcriptRole", "assistant")
.put("transcriptPreview", "正在分析 Codex App Server 实时事件。")
.put("audioChunkCount", 1)
.put("itemCount", 1)
.put("closeReason", "completed")));
View messageView = ReflectionHelpers.callInstanceMethod(
activity,
"buildMessageView",
ReflectionHelpers.ClassParameter.from(JSONObject.class, message)
);
assertTrue(viewTreeContainsText(messageView, "线程状态"));
assertTrue(viewTreeContainsText(messageView, "活跃"));
assertTrue(viewTreeContainsText(messageView, "等待审批"));
assertTrue(viewTreeContainsText(messageView, "等待用户输入"));
assertTrue(viewTreeContainsText(messageView, "实时状态"));
assertTrue(viewTreeContainsText(messageView, "已关闭 · completed"));
assertTrue(viewTreeContainsText(messageView, "正在分析 Codex App Server 实时事件。"));
assertTrue(viewTreeContainsText(messageView, "音频片段 1"));
assertFalse(viewTreeContainsText(messageView, "audio-secret-payload"));
assertFalse(viewTreeContainsText(messageView, "v=0"));
}
@Test
public void nativeRemoteExecutionProgressDoesNotRenderCodexSections() throws Exception {
Intent intent = new Intent()

View File

@@ -153,7 +153,7 @@
- Web 和原生 Android 当前都已经接上“新设备导入草稿 -> 勾选 -> 决议预览 -> 应用导入”的前台页面;已绑定生产设备继续保留 heartbeat 自动导入链路
- 原生首页的刷新失败策略当前已改成按当前 tab 独立判错,不会再因为 `设备 / 设置 / OTA` 的旁路请求失败把会话页刷新一并判成失败
- 当前量产方向已经明确为“Boss 企业控制面 + 可插拔执行协议”:多租户、权限、审批、审计、备份、回退和 Skill 治理由 Boss 承担Codex App Server / Codex MCP / Codex CLI / Computer Use / 业务系统 API 都作为 provider 接入;详见 `docs/architecture/enterprise_ai_ops_architecture_cn.md`
- 当前 Codex App Server 已完成第一批和第二批接入boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio也可灰度连接 `ws://127.0.0.1:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started``item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated` 归一到 Boss `execution_progress` 卡片;heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。
- 当前 Codex App Server 已完成前三批接入boss-agent 默认开启 `local-agent/codex-app-server-runner.mjs` 作为 Codex 绑定入口,优先走 `codex app-server` stdio也可灰度连接 `ws://127.0.0.1:<port>``unix://PATH` 同机长驻 App Server长驻连接支持 `Authorization: Bearer <token>`,配置上优先使用 `codexAppServerAuthTokenFile`。turn 启动前失败才回退 CLIturn 启动后不重复执行;桌面远程控制默认先走 `codex-computer-use`,失败后回退 `cua-driver-computer-use`。2026-05-31 已按本机 `codex-cli 0.135.0-alpha.1` 生成协议快照 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,并把 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started``item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated``thread/status/changed``thread/realtime/*` 归一到 Boss `execution_progress` 卡片realtime 只保留状态、文本摘要和计数,不保存 SDP、音频原始数据或 raw item。heartbeat 已能缓存 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read` 的能力摘要;同批已补 `turn/steer` 活跃 turn 干预和 `POST /api/v1/projects/[projectId]/thread-collaboration` 服务端线程协作排队入口。
- 当前 boss-agent 已支持 Mac OTA`local-agent/boss-agent-ota-runner.mjs` 默认开启,每 5 分钟检查服务端最新包;状态页可手动检查或下载并安装,安装时保留原绑定配置,只更新版本号和本机 runtime 路径。最新验证版本为 `20260516221619`,已在 MacBook Air `macbook-air` 上确认 OTA 下载校验、暂存、覆盖安装后不会误切到默认 `config.cloud.json`。正式分发脚本已预留 Developer ID 公证路径:`BOSS_AGENT_NOTARIZE=1` 配合 notary profile 或 Apple ID 凭据。
- 当前量产治理已补设备撤权和任务可靠性底座:`revoke_device` 会清空设备 token、标记离线并阻断 heartbeat / 任务认领 / Skill 同步 / 日志上报 / boss-agent OTA`MasterAgentTask` claim 会记录 attempt 和 lease运行中任务可按租约重试超过上限转 `timed_out`,用户或管理员可通过 cancel 接口转 `canceled` 且迟到 complete 不覆盖终态。
- 当前群聊 `dispatch_execution` 完成回写已补幂等,重复完成不会再向群聊重复追加结果

View File

@@ -73,7 +73,7 @@
- 当前已支持聊天附件主链:输入框左侧 `+` 会打开底部抽屉,支持图片 / 视频 / 文件发送;图片 / 视频先确认,文件直接发送
- 当前附件消息支持下载、原生打开、手动分析和自动分析状态展示
- 当前线程聊天消息会按该线程绑定的 Codex 电脑显示来源头像:单线程会话使用项目绑定设备头像,多设备 / 群聊消息会优先根据发送人里的设备名匹配对应电脑头像;主 Agent 总入口自身仍保留主 Agent 对话样式
- 当前已支持 `execution_progress` 执行进度卡:普通线程对话、主 Agent 托管线程和群聊目标线程执行时,会在对应聊天窗口显示“进度 / 分支详情 / 生成结果 / 后台智能体”结构化卡片;线程过程噪音仍走 `thread_process` 折叠
- 当前已支持 `execution_progress` 执行进度卡:普通线程对话、主 Agent 托管线程和群聊目标线程执行时,会在对应聊天窗口显示“进度 / 线程状态 / 实时状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”结构化卡片;线程过程噪音仍走 `thread_process` 折叠
- `线程详情 / 运维调试` 仍保留对应原生活动页,但已退出主聊天面
- 当前已补上本地发送中气泡、发送按钮状态控制,以及“只有接近底部才自动滚到底”的消息流行为
- 当前根页导航:
@@ -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:<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`;服务端 complete 回写会与本地 Git/GitHub 进度合并。heartbeat 同时支持按 TTL 拉取 `model/list / skills/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`;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64 或 raw realtime item。heartbeat 同时支持按 TTL 拉取 `model/list / skills/list / plugin/list / app/list / modelProvider/capabilities/read`,并把摘要保存在 `capabilities.codexAppServer.metadata`
- 当前 Codex App Server runner 已新增第一版 Boss Inter-Thread Broker任务携带 `intentCategory=thread_collaboration``sourceCodexThreadRef``targetCodexThreadRef` 时,会先 `thread/read` 源线程,再通过 `thread/inject_items` 向目标线程注入受控摘要,最后 `turn/start` 目标线程;服务端入口是 `POST /api/v1/projects/[projectId]/thread-collaboration`,负责权限、源/目标线程校验和任务排队。这不是假设官方线程 P2P而是 Boss 自己做线程协作编排。
- 当前 boss-agent Mac OTA 已接入:`local-agent/boss-agent-ota-runner.mjs` 会用设备 token 调 Boss 服务端 `/api/v1/boss-agent/ota` 检查最新 Mac 运行包,`/api/v1/boss-agent/ota/apply` 会下载 `boss-agent-mac-latest.zip`、校验 sha256、暂存安装 wrapper并拉起本机安装器安装脚本会保留绑定配置并只更新版本号与本机 runtime 路径。安装器会优先沿用当前 LaunchAgent active config并保留所有 `config*.json`,避免多电脑场景中误绑定到默认设备配置。当前最新验证包为 `20260516221619`;构建脚本支持 `BOSS_AGENT_NOTARIZE=1` 的 Developer ID 公证路径。
- 当前 `local-agent` 还新增了两条统一电脑控制 runtime

View File

@@ -16,7 +16,7 @@ Codex App Server 是更适合 Boss 长期接入的协议层,因为它面向富
- model/list、skills/list、plugin/list、app/list
- command execution、file change、tool input、MCP tool-call approvals
Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写2026-05-31 已继续把 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated` 的安全摘要映射APP 只展示审批状态、风险提醒和文件路径不展示完整命令、diff、系统提示词或密钥。
Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务层。当前第一批已经新增 `local-agent/codex-app-server-runner.mjs`,把 App Server 的 `thread/resume | thread/start -> turn/start -> item/agentMessage/delta -> turn/completed` 映射成 Boss 的普通任务完成回写2026-05-31 已继续把 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started` 这类协议事件归一化为 Boss `execution_progress` 的步骤、分支变更、产物和后台智能体。同日第二批补齐 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated` 的安全摘要映射APP 只展示审批状态、风险提醒和文件路径不展示完整命令、diff、系统提示词或密钥。第三批已把 `thread/status/changed``thread/realtime/*` 归一成 `executionProgress.threadStatus / realtime`APP 只展示活跃/等待审批/等待用户输入、realtime 文本摘要、音频片段计数和关闭/错误原因;`thread/realtime/sdp`、音频 base64 和原始 realtime item 不入账。
官方文档入口:`https://developers.openai.com/codex/app-server`
@@ -73,6 +73,8 @@ APP 展示结构对齐截图:
- `安全提醒`:展示 Guardian warning 的用户可读摘要
- `审批状态`:展示命令 / 文件 / 权限审批与自动复核状态
- `文件变更`:展示 App Server patchUpdated 中的文件路径和变更类型,不展示 diff
- `线程状态`:展示 `active / idle / systemError / notLoaded` 以及 `waitingOnApproval / waitingOnUserInput`
- `实时状态`:展示 realtime 启动、同步、关闭或错误状态,附带安全清洗后的 transcript 预览和计数
- `分支详情`变更行、Git 操作、GitHub CLI 可用状态
- `生成结果`从执行结果里提取文件、图片、APK、文档等产物名
- `后台智能体`:预留 OMX / Hermes / explorer 等多智能体来源展示
@@ -120,6 +122,7 @@ UI 参考:
- 新增 `scripts/codex-app-server-protocol-snapshot.mjs`,可把本机 Codex App Server 的 help、JSON Schema、TypeScript bindings 和方法清单生成到 `docs/protocol-snapshots/codex-app-server/<version>/`
- `local-agent/codex-app-server-runner.mjs` 已吸收 App Server 协议进度事件,并把 plan、diff、artifact、subagent 归一成 Boss `executionProgress`,服务端 complete 回写会与本地 Git/GitHub 进度合并,不再覆盖协议原生进度
- `local-agent/codex-app-server-runner.mjs` 已把 App Server 审批、Guardian warning 和 file-change patch 事件归一成 `executionProgress.approvals / warnings / fileChanges`;服务端和 Android 原生进度卡已支持展示,且测试覆盖了密钥和 diff 不外泄
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/status/changed``thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 SDP、音频原始数据和 raw item 不外泄
- 新增实时进度入口 `POST /api/v1/master-agent/tasks/[taskId]/progress`,设备端可在任务执行中持续刷新同一张 `execution_progress` 卡;`local-agent` 的 App Server runner 已在收到协议进度事件时调用该接口complete 仍携带最终进度作为兜底
- 新增服务端线程协作入口 `POST /api/v1/projects/[projectId]/thread-collaboration`,由 Boss 校验源/目标项目权限并创建 `intentCategory=thread_collaboration``conversation_reply` 任务;设备端继续通过 App Server runner 执行 `thread/read -> thread/inject_items -> turn/start`,避免把“线程互通”误做成无监管 P2P
- 新增活跃 turn 干预:任务携带 `targetCodexTurnId` / `targetTurnId`App Server runner 会调用 `turn/steer`,并把 `turnControl=steer``turnId` 写回执行结果;没有活跃 turn id 时仍使用 `turn/start`
@@ -129,7 +132,7 @@ UI 参考:
后续建议按两步继续:
1. 把当前 runner 提升为完整 `CodexAppServerBackendAdapter`:继续补 MCP tool / realtime / account / rate-limit / config 事件映射,但保持 feature flag 默认关闭。
1. 把当前 runner 提升为完整 `CodexAppServerBackendAdapter`:继续补 MCP tool / account / rate-limit / config 事件映射,并把 realtime 字段纳入后台风险看板,但保持 feature flag 默认关闭。
2. 完善长驻 transport 灰度:`ws://127.0.0.1:<port>``unix://PATH` 和 bearer token handshake 已可用,下一步补 signed bearer JWT 的 issuer / audience 校验联调、断线重连和健康探测;失败自动回退 stdio。
3. 新增 `CodexMcpBackendAdapter`:让 `codex mcp-server` 成为 `ExecutionBackend` 的兼容实现,用于 App Server 不可用或只需要轻量会话时。
4. 每次 Codex 协议升级时生成 schema、跑映射测试、灰度打开新 capability避免把某个 Codex 版本写死到 APP 或后台。

View File

@@ -249,7 +249,7 @@ cd /Users/kris/code/boss
- 当前 `local-agent` 已新增 `Codex App Server` providerboss-agent 默认配置 `codexAppServerEnabled=true``conversation_reply / dispatch_execution` 会先通过 `codex app-server` 的 stdio JSON-RPC 恢复或创建线程,也可配置 `codexAppServerTransport=ws + codexAppServerUrl=ws://127.0.0.1:<port>``codexAppServerTransport=unix + codexAppServerUrl=unix:///absolute/path.sock` 连接同机长驻 App Server长驻连接可通过 `codexAppServerAuthTokenFile``BOSS_CODEX_APP_SERVER_AUTH_TOKEN_FILE` 提供 bearer token。随后 runner 下发 `turn/start` 并收集流式 agent 回复;如果单个 JSON-RPC 请求返回 `-32001 / retry later`runner 会先做指数退避重试;如果任务携带 `targetCodexTurnId`,会改用 `turn/steer` 干预活跃 turn如果 App Server 在 turn 启动前失败,默认允许回退到 `codex exec resume`,如果 turn 已经启动则不再回退,避免同一轮用户消息被重复执行。桌面控制另有 `codexComputerUseEnabled=true`,默认先走 Codex Computer Use再回退 CUA Driver。
- 当前已新增 Boss 自有 Inter-Thread Broker 第一版:服务端入口 `POST /api/v1/projects/[projectId]/thread-collaboration` 会创建带源/目标 Codex 线程引用的协作任务App Server runner 执行 `thread/read(source) -> thread/inject_items(target) -> turn/start(target)`,用于让一个线程的结论受控进入另一个线程,不依赖官方任意线程 P2P 互聊能力
- 当前 `local-agent``dispatch_execution` 任务会按 `orchestrationBackendId` 分流:默认走 `codex exec resume`;当任务显式选择 `omx-team` 且本机 `omxEnabled + omxCommand/omxArgs` 可用时,会改走 `OMX Team Runtime` JSON 协议执行并回写 `rawThreadReply / replyBody`
- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failedAndroid 原生聊天页会显示“进度 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起Codex App Server 的 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要,并通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新complete 回写仍会携带最终进度兜底
- 当前 `local-agent` 会在 Codex 任务执行中和完成时回传 `executionProgress`:服务端把同一任务的进度卡从 queued / running 更新到 completed / failedAndroid 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。2026-05-31 起Codex App Server 的 `turn/plan/updated``turn/diff/updated``item/started|completed``thread/started` 会直接映射为进度步骤、变更统计、生成产物和后台智能体;第二批已把 `item/*/requestApproval``item/autoApprovalReview/*``guardianWarning``serverRequest/resolved``item/fileChange/patchUpdated` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed``thread/realtime/*` 安全映射为线程状态和实时状态摘要,并通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新complete 回写仍会携带最终进度兜底
- 当前 `local-agent` heartbeat 已新增 Codex App Server capability discovery按 TTL 拉取模型、provider 能力、Skill、Plugin、App 摘要,写入 `capabilities.codexAppServer.metadata`Web 设备详情会展示 App Server 连接状态、模型数量、默认/快速/深度模型和扩展数量
- 当前 `MasterAgentTask` 已具备服务端租约和取消基础状态机claim 会写入 `attemptCount / maxAttempts / leaseExpiresAt`,运行中任务租约过期后可被重新认领,超过重试上限会转 `timed_out``POST /api/v1/master-agent/tasks/[taskId]/cancel` 会把任务转 `canceled`,迟到的成功 complete 不会覆盖终态
- 当前 `local-agent``browser_control / desktop_control` 已从占位骨架升级成外部 runtime 桥:当本机配置了 `browserControlEnabled + browserControlCommand``computerUseEnabled + computerUseCommand` 时,会把标准化 JSON 请求透传给外部进程,并解析单行 JSON 结果;未启用时会 fail closed返回明确的 runtime disabled 错误,不再假装执行成功

View File

@@ -743,6 +743,26 @@ function extractFileChangeEntries(params) {
.filter(Boolean);
}
function normalizeThreadStatusSnapshot(status) {
if (!status || typeof status !== "object") {
return null;
}
const type = safeProgressText(status.type, 40);
if (!type) {
return null;
}
const activeFlags = asArray(status.activeFlags)
.map((flag) => safeProgressText(flag, 60))
.filter(Boolean)
.slice(0, 6);
return {
type,
...(activeFlags.length > 0 ? { activeFlags } : {}),
...(activeFlags.includes("waitingOnApproval") ? { waitingOnApproval: true } : {}),
...(activeFlags.includes("waitingOnUserInput") ? { waitingOnUserInput: true } : {}),
};
}
function buildServerRequestFallbackResponse(message) {
const method = String(message?.method ?? "");
if (/commandExecution\/requestApproval|execCommandApproval/i.test(method)) {
@@ -987,6 +1007,8 @@ function createProgressCollector() {
const fileChanges = [];
const warnings = [];
const branch = {};
let threadStatus;
let realtime;
const upsertArtifact = (artifact) => {
if (!artifact || artifacts.some((item) => item.label === artifact.label)) {
@@ -1052,6 +1074,17 @@ function createProgressCollector() {
});
};
const ensureRealtime = () => {
if (!realtime) {
realtime = {
status: "streaming",
audioChunkCount: 0,
itemCount: 0,
};
}
return realtime;
};
return {
observe(message) {
if (!message || typeof message !== "object") {
@@ -1128,6 +1161,71 @@ function createProgressCollector() {
}
return;
}
if (message.method === "thread/status/changed") {
const nextStatus = normalizeThreadStatusSnapshot(message.params?.status);
if (nextStatus) {
threadStatus = nextStatus;
}
return;
}
if (message.method === "thread/realtime/started") {
const state = ensureRealtime();
state.status = "started";
const sessionId = safeProgressText(message.params?.realtimeSessionId, 120);
const version = safeProgressText(message.params?.version, 80);
if (sessionId) state.sessionId = sessionId;
if (version) state.version = version;
return;
}
if (message.method === "thread/realtime/transcript/delta") {
const state = ensureRealtime();
state.status = state.status === "closed" ? "closed" : "streaming";
const role = safeProgressText(message.params?.role, 40);
const delta = safeProgressText(message.params?.delta, 220);
if (role) state.transcriptRole = role;
if (delta) {
state.transcriptPreview = safeProgressText(`${state.transcriptPreview ?? ""}${delta}`, 220);
}
return;
}
if (message.method === "thread/realtime/transcript/done") {
const state = ensureRealtime();
state.status = state.status === "closed" ? "closed" : "streaming";
const role = safeProgressText(message.params?.role, 40);
const text = safeProgressText(message.params?.text, 220);
if (role) state.transcriptRole = role;
if (text) state.transcriptPreview = text;
return;
}
if (message.method === "thread/realtime/outputAudio/delta") {
const state = ensureRealtime();
state.status = state.status === "closed" ? "closed" : "streaming";
state.audioChunkCount = Math.min(999, Number(state.audioChunkCount ?? 0) + 1);
return;
}
if (message.method === "thread/realtime/itemAdded") {
const state = ensureRealtime();
state.status = state.status === "closed" ? "closed" : "streaming";
state.itemCount = Math.min(999, Number(state.itemCount ?? 0) + 1);
return;
}
if (message.method === "thread/realtime/error") {
const state = ensureRealtime();
state.status = "error";
const messageText = safeProgressText(message.params?.message, 180);
if (messageText) state.lastError = messageText;
return;
}
if (message.method === "thread/realtime/closed") {
const state = ensureRealtime();
state.status = "closed";
const reason = safeProgressText(message.params?.reason, 120);
if (reason) state.closeReason = reason;
return;
}
if (message.method === "thread/realtime/sdp") {
return;
}
if (message.method === "thread/started") {
upsertAgent(extractAgentFromThreadStarted(message.params));
}
@@ -1155,6 +1253,19 @@ function createProgressCollector() {
if (fileChanges.length > 0) {
result.fileChanges = fileChanges.slice(0, 12);
}
if (threadStatus) {
result.threadStatus = { ...threadStatus };
}
if (realtime) {
const normalizedRealtime = { ...realtime };
if (!normalizedRealtime.audioChunkCount) {
delete normalizedRealtime.audioChunkCount;
}
if (!normalizedRealtime.itemCount) {
delete normalizedRealtime.itemCount;
}
result.realtime = normalizedRealtime;
}
return Object.keys(result).length > 0 ? result : undefined;
},
};

View File

@@ -138,6 +138,27 @@ export interface ExecutionProgressFileChange {
status?: string;
}
export interface ExecutionProgressThreadStatus {
type: string;
activeFlags?: string[];
waitingOnApproval?: boolean;
waitingOnUserInput?: boolean;
}
export type ExecutionProgressRealtimeStatus = "started" | "streaming" | "closed" | "error";
export interface ExecutionProgressRealtime {
status: ExecutionProgressRealtimeStatus;
sessionId?: string;
version?: string;
transcriptRole?: string;
transcriptPreview?: string;
audioChunkCount?: number;
itemCount?: number;
lastError?: string;
closeReason?: string;
}
export interface ExecutionProgressSnapshot {
taskId: string;
projectId: string;
@@ -156,6 +177,8 @@ export interface ExecutionProgressSnapshot {
approvals?: ExecutionProgressApproval[];
warnings?: ExecutionProgressWarning[];
fileChanges?: ExecutionProgressFileChange[];
threadStatus?: ExecutionProgressThreadStatus;
realtime?: ExecutionProgressRealtime;
updatedAt: string;
}
@@ -167,6 +190,8 @@ export interface ExecutionProgressInput {
approvals?: Array<Partial<ExecutionProgressApproval> & { label?: string }>;
warnings?: Array<Partial<ExecutionProgressWarning> & { message?: string }>;
fileChanges?: Array<Partial<ExecutionProgressFileChange> & { path?: string }>;
threadStatus?: Partial<ExecutionProgressThreadStatus>;
realtime?: Partial<ExecutionProgressRealtime>;
}
export interface ForwardSource {
@@ -3838,6 +3863,8 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
approvals: nativeRemoteControl ? undefined : normalizeExecutionProgressApprovals(raw.approvals),
warnings: nativeRemoteControl ? undefined : normalizeExecutionProgressWarnings(raw.warnings),
fileChanges: nativeRemoteControl ? undefined : normalizeExecutionProgressFileChanges(raw.fileChanges),
threadStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadStatus(raw.threadStatus),
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(raw.realtime),
updatedAt: raw.updatedAt ?? nowIso(),
};
}
@@ -5364,6 +5391,60 @@ function normalizeExecutionProgressFileChanges(input?: ExecutionProgressInput["f
.slice(0, 12);
}
function normalizeExecutionProgressThreadStatus(
input?: ExecutionProgressInput["threadStatus"],
): ExecutionProgressThreadStatus | undefined {
if (!input) {
return undefined;
}
const type = safeExecutionProgressText(input.type);
if (!type) {
return undefined;
}
const activeFlags = Array.isArray(input.activeFlags)
? input.activeFlags
.map((flag) => safeExecutionProgressText(flag))
.filter(Boolean)
.slice(0, 6)
: [];
return {
type,
...(activeFlags.length > 0 ? { activeFlags } : {}),
...(input.waitingOnApproval === true || activeFlags.includes("waitingOnApproval")
? { waitingOnApproval: true }
: {}),
...(input.waitingOnUserInput === true || activeFlags.includes("waitingOnUserInput")
? { waitingOnUserInput: true }
: {}),
};
}
function normalizeExecutionProgressRealtime(
input?: ExecutionProgressInput["realtime"],
): ExecutionProgressRealtime | undefined {
if (!input) {
return undefined;
}
const status =
input.status === "started" || input.status === "streaming" || input.status === "closed" || input.status === "error"
? input.status
: undefined;
if (!status) {
return undefined;
}
return {
status,
sessionId: safeExecutionProgressText(input.sessionId) || undefined,
version: safeExecutionProgressText(input.version) || undefined,
transcriptRole: safeExecutionProgressText(input.transcriptRole) || undefined,
transcriptPreview: safeExecutionProgressText(input.transcriptPreview) || undefined,
audioChunkCount: normalizeOptionalNumber(input.audioChunkCount),
itemCount: normalizeOptionalNumber(input.itemCount),
lastError: safeExecutionProgressText(input.lastError) || undefined,
closeReason: safeExecutionProgressText(input.closeReason) || undefined,
};
}
function defaultExecutionProgressStepTexts(task: Pick<MasterAgentTask, "taskType" | "relayViaMasterAgent">) {
if (task.taskType === "browser_control") {
return [
@@ -5499,6 +5580,8 @@ function buildExecutionProgressSnapshot(
approvals: nativeRemoteControl ? undefined : normalizeExecutionProgressApprovals(input?.approvals),
warnings: nativeRemoteControl ? undefined : normalizeExecutionProgressWarnings(input?.warnings),
fileChanges: nativeRemoteControl ? undefined : normalizeExecutionProgressFileChanges(input?.fileChanges),
threadStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadStatus(input?.threadStatus),
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(input?.realtime),
updatedAt: nowIso(),
};
}

View File

@@ -394,6 +394,79 @@ rl.on("line", (line) => {
},
});
}
if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS === "1") {
send({
method: "thread/status/changed",
params: {
threadId: message.params?.threadId,
status: {
type: "active",
activeFlags: ["waitingOnApproval", "waitingOnUserInput"],
},
},
});
send({
method: "thread/realtime/started",
params: {
threadId: message.params?.threadId,
realtimeSessionId: "rt-session-1",
version: "v2",
},
});
send({
method: "thread/realtime/sdp",
params: {
threadId: message.params?.threadId,
sdp: "v=0 secret-sk-should-not-leak",
},
});
send({
method: "thread/realtime/transcript/delta",
params: {
threadId: message.params?.threadId,
role: "assistant",
delta: "正在分析 Codex ",
},
});
send({
method: "thread/realtime/transcript/done",
params: {
threadId: message.params?.threadId,
role: "assistant",
text: "正在分析 Codex App Server 实时事件。",
},
});
send({
method: "thread/realtime/outputAudio/delta",
params: {
threadId: message.params?.threadId,
audio: {
data: "audio-secret-payload",
sampleRate: 24000,
numChannels: 1,
samplesPerChannel: 480,
itemId: "audio-item-1",
},
},
});
send({
method: "thread/realtime/itemAdded",
params: {
threadId: message.params?.threadId,
item: {
type: "message",
text: "raw realtime item should not be persisted",
},
},
});
send({
method: "thread/realtime/closed",
params: {
threadId: message.params?.threadId,
reason: "completed",
},
});
}
send({
method: "item/agentMessage/delta",
params: {

View File

@@ -359,6 +359,57 @@ test("codex app-server runner maps guardian approval and file-change events with
}
});
test("codex app-server runner maps thread status and realtime events without leaking transport payloads", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_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-realtime",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "开启实时协作并回写状态",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.threadStatus, {
type: "active",
activeFlags: ["waitingOnApproval", "waitingOnUserInput"],
waitingOnApproval: true,
waitingOnUserInput: true,
});
assert.deepEqual(result.executionProgress.realtime, {
status: "closed",
sessionId: "rt-session-1",
version: "v2",
transcriptRole: "assistant",
transcriptPreview: "正在分析 Codex App Server 实时事件。",
audioChunkCount: 1,
itemCount: 1,
closeReason: "completed",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("audio-secret-payload"), false);
assert.equal(serialized.includes("v=0 secret"), false);
assert.equal(serialized.includes("raw realtime item should not be persisted"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS = previous;
}
}
});
test("codex app-server runner bridges source thread context into target thread through inject_items", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1";

View File

@@ -154,3 +154,67 @@ test("POST task progress preserves Codex approval, warning, and file-change summ
assert.equal(progress?.warnings?.[0]?.message, "检测到需要用户确认的命令执行。");
assert.equal(progress?.fileChanges?.[0]?.path, "src/app/page.tsx");
});
test("POST task progress preserves Codex thread status and realtime summaries", async () => {
const task = await data.queueMasterAgentTask({
taskId: "route-progress-realtime-task",
projectId: "group-progress-test",
taskType: "dispatch_execution",
requestMessageId: "msg-route-progress-realtime",
requestText: "让目标线程继续开发并回写实时状态",
executionPrompt: "让目标线程继续开发并回写实时状态",
requestedBy: "krisolo",
requestedByAccount: "krisolo",
deviceId: "mac-studio",
targetProjectId: "master-agent",
targetThreadId: "master-agent-thread",
});
await data.claimNextMasterAgentTask("mac-studio");
const response = await postProgress(
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-boss-device-token": "boss-mac-studio-token",
},
body: JSON.stringify({
deviceId: "mac-studio",
status: "running",
executionProgress: {
steps: [{ text: "监听 Codex realtime 事件", status: "running" }],
threadStatus: {
type: "active",
activeFlags: ["waitingOnApproval", "waitingOnUserInput"],
waitingOnApproval: true,
waitingOnUserInput: true,
},
realtime: {
status: "streaming",
sessionId: "rt-session-1",
version: "v2",
transcriptRole: "assistant",
transcriptPreview: "正在分析 Codex App Server 实时事件。",
audioChunkCount: 1,
itemCount: 1,
},
},
}),
}),
{ params: Promise.resolve({ taskId: task.taskId }) },
);
assert.equal(response.status, 200);
const state = await data.readState();
const progress = state.projects
.find((project) => project.id === "master-agent")
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
?.executionProgress;
assert.equal(progress?.threadStatus?.type, "active");
assert.equal(progress?.threadStatus?.waitingOnApproval, true);
assert.equal(progress?.threadStatus?.waitingOnUserInput, true);
assert.equal(progress?.realtime?.status, "streaming");
assert.equal(progress?.realtime?.transcriptPreview, "正在分析 Codex App Server 实时事件。");
assert.equal(progress?.realtime?.audioChunkCount, 1);
});