feat: map codex realtime thread status
This commit is contained in:
@@ -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` runner:boss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:<port>` 或 `unix://PATH` 本机长驻 App Server;WebSocket/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` runner:boss-agent 默认打开 `codexAppServerEnabled`,通过 `codex app-server` stdio 接入 `conversation_reply / dispatch_execution`,也可灰度切到 `ws://127.0.0.1:<port>` 或 `unix://PATH` 本机长驻 App Server;WebSocket/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`
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `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 启动前失败才回退 CLI,turn 启动后不重复执行;桌面远程控制默认先走 `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` 完成回写已补幂等,重复完成不会再向群聊重复追加结果
|
||||
|
||||
@@ -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 Server,bearer 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 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,并把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`;服务端 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 Server,bearer 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 启动前失败可回退 CLI,turn 启动后失败不回退,避免重复执行。2026-05-31 起,runner 会把 `turn/plan/updated`、`turn/diff/updated`、`item/started|completed`、`thread/started` 归一成 `executionProgress.steps / branch / artifacts / agents`,把 `item/*/requestApproval`、`item/autoApprovalReview/*`、`guardianWarning`、`serverRequest/resolved`、`item/fileChange/patchUpdated` 归一成 `executionProgress.approvals / warnings / fileChanges`,并把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`;服务端 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:
|
||||
|
||||
@@ -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 或后台。
|
||||
|
||||
@@ -249,7 +249,7 @@ cd /Users/kris/code/boss
|
||||
- 当前 `local-agent` 已新增 `Codex App Server` provider:boss-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 / failed,Android 原生聊天页会显示“进度 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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 / failed,Android 原生聊天页会显示“进度 / 线程状态 / 实时状态 / 安全提醒 / 审批状态 / 文件变更 / 分支详情 / 生成结果 / 后台智能体”。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 错误,不再假装执行成功
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
73
tests/fixtures/codex-app-server-runtime.mjs
vendored
73
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -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: {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user