feat: surface codex account runtime notices
This commit is contained in:
@@ -1437,6 +1437,65 @@ public final class BossUi {
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject accountStatus = progress == null ? null : progress.optJSONObject("accountStatus");
|
||||
JSONObject modelVerification = progress == null ? null : progress.optJSONObject("modelVerification");
|
||||
JSONArray verifications = modelVerification == null ? null : modelVerification.optJSONArray("verifications");
|
||||
boolean hasAccountStatus = accountStatus != null ||
|
||||
(verifications != null && verifications.length() > 0);
|
||||
if (hasAccountStatus) {
|
||||
card.addView(divider(context));
|
||||
card.addView(sectionTitle(context, "账号状态"));
|
||||
if (accountStatus != null) {
|
||||
String authMode = accountStatus.optString("authMode", "").trim();
|
||||
String planType = accountStatus.optString("planType", "").trim();
|
||||
if (!TextUtils.isEmpty(authMode) || !TextUtils.isEmpty(planType)) {
|
||||
String label = !TextUtils.isEmpty(authMode) && !TextUtils.isEmpty(planType)
|
||||
? "认证 " + authMode + " · " + planType
|
||||
: !TextUtils.isEmpty(authMode) ? "认证 " + authMode : "套餐 " + planType;
|
||||
card.addView(detailRow(context, "◇", label, "", false));
|
||||
}
|
||||
String limitName = accountStatus.optString("limitName", "").trim();
|
||||
int usedPercent = accountStatus.optInt("usedPercent", -1);
|
||||
if (!TextUtils.isEmpty(limitName) || usedPercent >= 0) {
|
||||
String label = "额度";
|
||||
if (!TextUtils.isEmpty(limitName)) {
|
||||
label += " " + limitName;
|
||||
}
|
||||
if (usedPercent >= 0) {
|
||||
label += " · " + usedPercent + "%";
|
||||
}
|
||||
card.addView(detailRow(context, "◷", label, "", false));
|
||||
}
|
||||
int windowDurationMins = accountStatus.optInt("windowDurationMins", 0);
|
||||
if (windowDurationMins > 0) {
|
||||
card.addView(detailRow(context, "", "窗口 " + windowDurationMins + " 分钟", "", false, true));
|
||||
}
|
||||
String creditsBalance = accountStatus.optString("creditsBalance", "").trim();
|
||||
if (!TextUtils.isEmpty(creditsBalance)) {
|
||||
card.addView(detailRow(context, "", "余额 " + creditsBalance, "", false, true));
|
||||
}
|
||||
}
|
||||
if (verifications != null && verifications.length() > 0) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < verifications.length(); i += 1) {
|
||||
String verification = verifications.optString(i, "").trim();
|
||||
if (TextUtils.isEmpty(verification)) {
|
||||
continue;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.append(verification);
|
||||
if (builder.length() > 120) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
card.addView(detailRow(context, "◇", "模型校验 " + builder, "", false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject modelRoute = progress == null ? null : progress.optJSONObject("modelRoute");
|
||||
JSONObject tokenUsage = progress == null ? null : progress.optJSONObject("tokenUsage");
|
||||
JSONArray mcpServers = progress == null ? null : progress.optJSONArray("mcpServers");
|
||||
|
||||
@@ -1069,6 +1069,58 @@ public class ProjectDetailActivityUiTest {
|
||||
assertFalse(viewTreeContainsText(messageView, "sk-secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionProgressMessageRendersCodexAccountStatusAndVerificationSections() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "thread-account")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "Boss开发主线程");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject message = new JSONObject()
|
||||
.put("id", "progress-account-1")
|
||||
.put("sender", "master")
|
||||
.put("senderLabel", "主 Agent")
|
||||
.put("body", "执行进度")
|
||||
.put("kind", "execution_progress")
|
||||
.put("sentAt", "2026-06-01T10:28:00+08:00")
|
||||
.put("executionProgress", new JSONObject()
|
||||
.put("status", "running")
|
||||
.put("steps", new JSONArray()
|
||||
.put(new JSONObject().put("text", "同步 Codex 账号运行态").put("status", "running")))
|
||||
.put("accountStatus", new JSONObject()
|
||||
.put("authMode", "chatgpt")
|
||||
.put("planType", "team")
|
||||
.put("limitName", "Codex")
|
||||
.put("usedPercent", 88)
|
||||
.put("windowDurationMins", 180)
|
||||
.put("resetsAt", 1770003600)
|
||||
.put("creditsBalance", "120.5")
|
||||
.put("hasCredits", true)
|
||||
.put("unlimitedCredits", false)
|
||||
.put("accessToken", "sk-secret-should-not-render"))
|
||||
.put("modelVerification", new JSONObject()
|
||||
.put("verifications", new JSONArray().put("trustedAccessForCyber"))
|
||||
.put("turnId", "turn-secret-should-not-render")));
|
||||
|
||||
View messageView = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildMessageView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, message)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(messageView, "账号状态"));
|
||||
assertTrue(viewTreeContainsText(messageView, "认证 chatgpt · team"));
|
||||
assertTrue(viewTreeContainsText(messageView, "额度 Codex · 88%"));
|
||||
assertTrue(viewTreeContainsText(messageView, "窗口 180 分钟"));
|
||||
assertTrue(viewTreeContainsText(messageView, "余额 120.5"));
|
||||
assertTrue(viewTreeContainsText(messageView, "模型校验 trustedAccessForCyber"));
|
||||
assertFalse(viewTreeContainsText(messageView, "sk-secret"));
|
||||
assertFalse(viewTreeContainsText(messageView, "turn-secret"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void executionProgressMessageRendersCodexThreadGoalSettingsAndCompactionSections() 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`、`thread/status/changed`、`thread/realtime/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId 或未清洗密钥。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/*`、`model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一到 Boss `execution_progress` 卡片;realtime 只保留状态、文本摘要和计数,运行状态只保留模型切换、上下文用量、MCP 状态和远控连接摘要,线程配置只保留目标、模型、审批、沙箱、协作模式和压缩状态,账号状态只保留认证方式、套餐、额度窗口、积分余额和模型校验摘要,不保存 SDP、音频原始数据、raw item、remote installationId、cwd、turnId、配置文件路径或未清洗密钥。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` 完成回写已补幂等,重复完成不会再向群聊重复追加结果
|
||||
|
||||
@@ -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`,把 `thread/status/changed`、`thread/realtime/started|transcript|outputAudio|itemAdded|error|closed` 归一成 `executionProgress.threadStatus / realtime`,并把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId 或未清洗的 MCP 错误。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`,把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`,并把 `thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成线程配置、账号状态、模型校验和安全提醒摘要;服务端 complete 回写会与本地 Git/GitHub 进度合并,且不保存 SDP、音频 base64、raw realtime item、remote installationId、cwd、turnId、配置路径或未清洗的 MCP 错误。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:
|
||||
|
||||
@@ -35,7 +35,7 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务
|
||||
- 本机 `codex --version`:`codex-cli 0.135.0-alpha.1`
|
||||
- 本机 `codex app-server --help` 已可用;本机 help 当前显示 `--listen` 支持 `stdio://`、`unix://`、`unix://PATH`、`ws://IP:PORT` 和 `off`
|
||||
- 本机 `codex app-server --help` 当前已经支持 `--ws-auth capability-token|signed-bearer-token`、`--ws-token-file`、`--ws-token-sha256`、`--ws-shared-secret-file`、issuer/audience/clock-skew 等 WebSocket 认证参数
|
||||
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,共识别 137 个协议方法;确认支持 `thread/inject_items`、`thread/rollback`、`thread/goal/*`、`turn/steer`、`command/exec`、`thread/realtime/*`、`model/list`
|
||||
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,共识别 137 个协议方法;确认支持 `thread/inject_items`、`thread/rollback`、`thread/goal/*`、`turn/steer`、`command/exec`、`thread/realtime/*`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`model/list`
|
||||
- Boss 当前默认仍以 `stdio` 作为本机 agent 接入方式;`ws://127.0.0.1:<port>` 和 `unix://PATH` 本地长驻 transport 已可灰度接入,WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`;非 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`、`plugin/list`、`app/list`,归一成设备 `capabilities.codexAppServer.metadata`;发现失败只记录 warn,不阻塞心跳
|
||||
@@ -127,6 +127,7 @@ UI 参考:
|
||||
- `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 不外泄
|
||||
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 归一成 `executionProgress.modelRoute / tokenUsage / mcpServers / remoteControl`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 installationId 和密钥不外泄
|
||||
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `thread/goal/updated|cleared`、`thread/settings/updated`、`thread/compacted` 归一成 `executionProgress.threadGoal / threadSettings / compaction`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖 cwd、turnId、内部 prompt 不外泄
|
||||
- `local-agent/codex-app-server-runner.mjs` 已把 App Server `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 归一成 `executionProgress.accountStatus / modelVerification / warnings`;服务端进度路由和 Android 原生进度卡已支持展示,测试覆盖配置路径、turnId 和密钥不外泄
|
||||
- 新增实时进度入口 `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`
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist`
|
||||
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
|
||||
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准
|
||||
- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口,Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items`、`turn/steer`、`thread/realtime/*`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`command/exec` 和 `model/list`
|
||||
- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口,Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.135.0-alpha.1` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.135.0-alpha.1/`,确认支持 WebSocket auth、`thread/inject_items`、`turn/steer`、`thread/realtime/*`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`command/exec` 和 `model/list`
|
||||
- 当前量产 B+ 架构开发文档已新增:`docs/architecture/enterprise_ai_ops_architecture_cn.md`。该文档把 PPT 中的主 Agent / 业务 Agent / 老板端 / 经理端 / 员工端 / 治理层 / 系统层 / 设备层 / 执行层 / 接入层整理成后续产品架构约束,并明确数据库备份、业务回退、Codex 协议扩展和 Skill 治理方向;它是规划文档,不代表当前全部已落地
|
||||
- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime`
|
||||
- 当前已新增最小 `Telegram Gateway`:Boss 当前可直接暴露 Telegram webhook,把 Telegram 私聊或受控群聊文本桥接进 `master-agent` 或按群 / Topic 路由到指定 Boss 项目,并在主 Agent 异步任务完成后自动回推 Telegram;配置入口已接到 Web `/me/telegram` 和原生 Android `我的 > Telegram 接入`
|
||||
@@ -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` 映射为审批、安全提醒和文件变更摘要;第三批已把 `thread/status/changed` 与 `thread/realtime/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要;第五批已把 `thread/goal/*`、`thread/settings/updated` 和 `thread/compacted` 映射为线程配置摘要,只展示目标、模型、审批、沙箱、协作模式和上下文压缩状态,不保存 cwd、turnId 或 collaboration settings 内部 prompt。所有进度均通过 `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/*` 安全映射为线程状态和实时状态摘要;第四批已把 `model/rerouted`、`thread/tokenUsage/updated`、`mcpServer/startupStatus/updated`、`remoteControl/status/changed` 安全映射为运行状态摘要;第五批已把 `thread/goal/*`、`thread/settings/updated` 和 `thread/compacted` 映射为线程配置摘要;第六批已把 `account/updated`、`account/rateLimits/updated`、`model/verification`、`warning`、`configWarning`、`deprecationNotice` 映射为账号状态、模型校验和安全提醒摘要。所有进度均通过 `POST /api/v1/master-agent/tasks/[taskId]/progress` 实时刷新;字段白名单会剥离 cwd、turnId、配置文件路径、内部 prompt 和未清洗密钥,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 错误,不再假装执行成功
|
||||
|
||||
@@ -846,6 +846,55 @@ function extractTokenUsageSnapshot(tokenUsage) {
|
||||
};
|
||||
}
|
||||
|
||||
function extractRateLimitWindowSnapshot(window) {
|
||||
if (!window || typeof window !== "object") {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
usedPercent: extractNumber(window.usedPercent),
|
||||
windowDurationMins: extractNumber(window.windowDurationMins),
|
||||
resetsAt: extractNumber(window.resetsAt),
|
||||
};
|
||||
}
|
||||
|
||||
function extractAccountRateLimitSnapshot(rateLimits) {
|
||||
if (!rateLimits || typeof rateLimits !== "object") {
|
||||
return null;
|
||||
}
|
||||
const primary = extractRateLimitWindowSnapshot(rateLimits.primary);
|
||||
const credits = rateLimits.credits && typeof rateLimits.credits === "object" ? rateLimits.credits : {};
|
||||
const snapshot = Object.fromEntries(Object.entries({
|
||||
limitId: safeProgressText(rateLimits.limitId, 80),
|
||||
limitName: safeProgressText(rateLimits.limitName, 80),
|
||||
planType: safeProgressText(rateLimits.planType, 80),
|
||||
rateLimitReachedType: safeProgressText(rateLimits.rateLimitReachedType, 80),
|
||||
usedPercent: primary.usedPercent,
|
||||
windowDurationMins: primary.windowDurationMins,
|
||||
resetsAt: primary.resetsAt,
|
||||
creditsBalance: safeProgressText(credits.balance, 80),
|
||||
...(typeof credits.hasCredits === "boolean" ? { hasCredits: credits.hasCredits } : {}),
|
||||
...(typeof credits.unlimited === "boolean" ? { unlimitedCredits: credits.unlimited } : {}),
|
||||
}).filter(([, value]) => value !== undefined && value !== ""));
|
||||
return Object.values(snapshot).some((value) => value !== undefined && value !== "") ? snapshot : null;
|
||||
}
|
||||
|
||||
function extractModelVerificationSnapshot(params) {
|
||||
const verifications = asArray(params?.verifications)
|
||||
.map((verification) => safeProgressText(verification, 120))
|
||||
.filter(Boolean)
|
||||
.slice(0, 8);
|
||||
return verifications.length > 0 ? { verifications } : null;
|
||||
}
|
||||
|
||||
function buildNoticeWarningMessage(summary, details) {
|
||||
const cleanSummary = safeProgressText(summary, 140);
|
||||
const cleanDetails = safeProgressText(details, 180);
|
||||
if (!cleanSummary) {
|
||||
return cleanDetails;
|
||||
}
|
||||
return cleanDetails ? `${cleanSummary}:${cleanDetails}` : cleanSummary;
|
||||
}
|
||||
|
||||
function buildServerRequestFallbackResponse(message) {
|
||||
const method = String(message?.method ?? "");
|
||||
if (/commandExecution\/requestApproval|execCommandApproval/i.test(method)) {
|
||||
@@ -1099,6 +1148,8 @@ function createProgressCollector() {
|
||||
let threadGoal;
|
||||
let threadSettings;
|
||||
let compaction;
|
||||
let accountStatus;
|
||||
let modelVerification;
|
||||
|
||||
const upsertArtifact = (artifact) => {
|
||||
if (!artifact || artifacts.some((item) => item.label === artifact.label)) {
|
||||
@@ -1396,6 +1447,66 @@ function createProgressCollector() {
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (message.method === "account/updated") {
|
||||
const authMode = safeProgressText(message.params?.authMode, 80);
|
||||
const planType = safeProgressText(message.params?.planType, 80);
|
||||
accountStatus = {
|
||||
...(accountStatus ?? {}),
|
||||
...(authMode ? { authMode } : {}),
|
||||
...(planType ? { planType } : {}),
|
||||
};
|
||||
return;
|
||||
}
|
||||
if (message.method === "account/rateLimits/updated") {
|
||||
const nextAccountStatus = extractAccountRateLimitSnapshot(message.params?.rateLimits);
|
||||
if (nextAccountStatus) {
|
||||
accountStatus = {
|
||||
...(accountStatus ?? {}),
|
||||
...nextAccountStatus,
|
||||
};
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "model/verification") {
|
||||
const nextVerification = extractModelVerificationSnapshot(message.params);
|
||||
if (nextVerification) {
|
||||
modelVerification = nextVerification;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "warning") {
|
||||
const warningMessage = safeProgressText(message.params?.message, 180);
|
||||
if (warningMessage) {
|
||||
pushWarning({
|
||||
id: `codex-warning-${warnings.length + 1}`,
|
||||
severity: "warning",
|
||||
message: warningMessage,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "configWarning") {
|
||||
const warningMessage = buildNoticeWarningMessage(message.params?.summary, message.params?.details);
|
||||
if (warningMessage) {
|
||||
pushWarning({
|
||||
id: `config-warning-${warnings.length + 1}`,
|
||||
severity: "warning",
|
||||
message: warningMessage,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "deprecationNotice") {
|
||||
const warningMessage = buildNoticeWarningMessage(message.params?.summary, message.params?.details);
|
||||
if (warningMessage) {
|
||||
pushWarning({
|
||||
id: `deprecation-notice-${warnings.length + 1}`,
|
||||
severity: "info",
|
||||
message: warningMessage,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message.method === "thread/started") {
|
||||
upsertAgent(extractAgentFromThreadStarted(message.params));
|
||||
}
|
||||
@@ -1457,6 +1568,12 @@ function createProgressCollector() {
|
||||
if (compaction) {
|
||||
result.compaction = { ...compaction };
|
||||
}
|
||||
if (accountStatus) {
|
||||
result.accountStatus = { ...accountStatus };
|
||||
}
|
||||
if (modelVerification) {
|
||||
result.modelVerification = { ...modelVerification };
|
||||
}
|
||||
return Object.keys(result).length > 0 ? result : undefined;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -175,6 +175,24 @@ export interface ExecutionProgressTokenUsage {
|
||||
contextPercent?: number;
|
||||
}
|
||||
|
||||
export interface ExecutionProgressAccountStatus {
|
||||
authMode?: string;
|
||||
planType?: string;
|
||||
limitId?: string;
|
||||
limitName?: string;
|
||||
usedPercent?: number;
|
||||
windowDurationMins?: number;
|
||||
resetsAt?: number;
|
||||
rateLimitReachedType?: string;
|
||||
creditsBalance?: string;
|
||||
hasCredits?: boolean;
|
||||
unlimitedCredits?: boolean;
|
||||
}
|
||||
|
||||
export interface ExecutionProgressModelVerification {
|
||||
verifications: string[];
|
||||
}
|
||||
|
||||
export interface ExecutionProgressMcpServer {
|
||||
name: string;
|
||||
status?: string;
|
||||
@@ -236,6 +254,8 @@ export interface ExecutionProgressSnapshot {
|
||||
realtime?: ExecutionProgressRealtime;
|
||||
modelRoute?: ExecutionProgressModelRoute;
|
||||
tokenUsage?: ExecutionProgressTokenUsage;
|
||||
accountStatus?: ExecutionProgressAccountStatus;
|
||||
modelVerification?: ExecutionProgressModelVerification;
|
||||
mcpServers?: ExecutionProgressMcpServer[];
|
||||
remoteControl?: ExecutionProgressRemoteControl;
|
||||
threadGoal?: ExecutionProgressThreadGoal;
|
||||
@@ -256,6 +276,8 @@ export interface ExecutionProgressInput {
|
||||
realtime?: Partial<ExecutionProgressRealtime>;
|
||||
modelRoute?: Partial<ExecutionProgressModelRoute>;
|
||||
tokenUsage?: Partial<ExecutionProgressTokenUsage>;
|
||||
accountStatus?: Partial<ExecutionProgressAccountStatus> & { accessToken?: unknown; apiKey?: unknown };
|
||||
modelVerification?: Partial<ExecutionProgressModelVerification> & { turnId?: unknown };
|
||||
mcpServers?: Array<Partial<ExecutionProgressMcpServer> & { name?: string }>;
|
||||
remoteControl?: Partial<ExecutionProgressRemoteControl> & { installationId?: unknown };
|
||||
threadGoal?: Partial<ExecutionProgressThreadGoal>;
|
||||
@@ -3936,6 +3958,10 @@ function normalizeExecutionProgressSnapshot(raw: Partial<ExecutionProgressSnapsh
|
||||
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(raw.realtime),
|
||||
modelRoute: nativeRemoteControl ? undefined : normalizeExecutionProgressModelRoute(raw.modelRoute),
|
||||
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(raw.tokenUsage),
|
||||
accountStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressAccountStatus(raw.accountStatus),
|
||||
modelVerification: nativeRemoteControl
|
||||
? undefined
|
||||
: normalizeExecutionProgressModelVerification(raw.modelVerification),
|
||||
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(raw.mcpServers),
|
||||
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(raw.remoteControl),
|
||||
threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(raw.threadGoal),
|
||||
@@ -5567,6 +5593,43 @@ function normalizeExecutionProgressTokenUsage(
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeExecutionProgressAccountStatus(
|
||||
input?: ExecutionProgressInput["accountStatus"],
|
||||
): ExecutionProgressAccountStatus | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
const status: ExecutionProgressAccountStatus = {
|
||||
authMode: safeExecutionProgressText(input.authMode) || undefined,
|
||||
planType: safeExecutionProgressText(input.planType) || undefined,
|
||||
limitId: safeExecutionProgressText(input.limitId) || undefined,
|
||||
limitName: safeExecutionProgressText(input.limitName) || undefined,
|
||||
usedPercent: normalizeOptionalNumber(input.usedPercent),
|
||||
windowDurationMins: normalizeOptionalNumber(input.windowDurationMins),
|
||||
resetsAt: normalizeOptionalNumber(input.resetsAt),
|
||||
rateLimitReachedType: safeExecutionProgressText(input.rateLimitReachedType) || undefined,
|
||||
creditsBalance: safeExecutionProgressText(input.creditsBalance) || undefined,
|
||||
hasCredits: typeof input.hasCredits === "boolean" ? input.hasCredits : undefined,
|
||||
unlimitedCredits: typeof input.unlimitedCredits === "boolean" ? input.unlimitedCredits : undefined,
|
||||
};
|
||||
return Object.values(status).some((value) => value !== undefined && value !== "") ? status : undefined;
|
||||
}
|
||||
|
||||
function normalizeExecutionProgressModelVerification(
|
||||
input?: ExecutionProgressInput["modelVerification"],
|
||||
): ExecutionProgressModelVerification | undefined {
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
const verifications = Array.isArray(input.verifications)
|
||||
? input.verifications
|
||||
.map((verification) => safeExecutionProgressText(verification))
|
||||
.filter(Boolean)
|
||||
.slice(0, 8)
|
||||
: [];
|
||||
return verifications.length > 0 ? { verifications } : undefined;
|
||||
}
|
||||
|
||||
function normalizeExecutionProgressMcpServers(input?: ExecutionProgressInput["mcpServers"]) {
|
||||
return (input ?? [])
|
||||
.map((server): ExecutionProgressMcpServer | null => {
|
||||
@@ -5799,6 +5862,10 @@ function buildExecutionProgressSnapshot(
|
||||
realtime: nativeRemoteControl ? undefined : normalizeExecutionProgressRealtime(input?.realtime),
|
||||
modelRoute: nativeRemoteControl ? undefined : normalizeExecutionProgressModelRoute(input?.modelRoute),
|
||||
tokenUsage: nativeRemoteControl ? undefined : normalizeExecutionProgressTokenUsage(input?.tokenUsage),
|
||||
accountStatus: nativeRemoteControl ? undefined : normalizeExecutionProgressAccountStatus(input?.accountStatus),
|
||||
modelVerification: nativeRemoteControl
|
||||
? undefined
|
||||
: normalizeExecutionProgressModelVerification(input?.modelVerification),
|
||||
mcpServers: nativeRemoteControl ? undefined : normalizeExecutionProgressMcpServers(input?.mcpServers),
|
||||
remoteControl: nativeRemoteControl ? undefined : normalizeExecutionProgressRemoteControl(input?.remoteControl),
|
||||
threadGoal: nativeRemoteControl ? undefined : normalizeExecutionProgressThreadGoal(input?.threadGoal),
|
||||
|
||||
61
tests/fixtures/codex-app-server-runtime.mjs
vendored
61
tests/fixtures/codex-app-server-runtime.mjs
vendored
@@ -581,6 +581,67 @@ rl.on("line", (line) => {
|
||||
},
|
||||
});
|
||||
}
|
||||
if (process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS === "1") {
|
||||
send({
|
||||
method: "account/updated",
|
||||
params: {
|
||||
authMode: "chatgpt",
|
||||
planType: "team",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "account/rateLimits/updated",
|
||||
params: {
|
||||
rateLimits: {
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
primary: {
|
||||
usedPercent: 88,
|
||||
windowDurationMins: 180,
|
||||
resetsAt: 1770003600,
|
||||
},
|
||||
secondary: null,
|
||||
credits: {
|
||||
hasCredits: true,
|
||||
unlimited: false,
|
||||
balance: "120.5",
|
||||
},
|
||||
planType: "team",
|
||||
rateLimitReachedType: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "model/verification",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
turnId: "turn-fixture",
|
||||
verifications: ["trustedAccessForCyber"],
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "warning",
|
||||
params: {
|
||||
threadId: message.params?.threadId,
|
||||
message: "模型切换提醒 token=sk-secret-should-not-leak",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "configWarning",
|
||||
params: {
|
||||
summary: "项目配置已忽略",
|
||||
details: "openai_base_url 不能放在项目配置里",
|
||||
path: "/Users/kris/code/boss/.codex/config.toml",
|
||||
},
|
||||
});
|
||||
send({
|
||||
method: "deprecationNotice",
|
||||
params: {
|
||||
summary: "on-failure 已废弃",
|
||||
details: "请改用 on-request",
|
||||
},
|
||||
});
|
||||
}
|
||||
send({
|
||||
method: "item/agentMessage/delta",
|
||||
params: {
|
||||
|
||||
@@ -529,6 +529,72 @@ test("codex app-server runner maps thread goal, settings, and compaction events
|
||||
}
|
||||
});
|
||||
|
||||
test("codex app-server runner maps account, quota, verification, and notices without leaking config paths", async () => {
|
||||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_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-account-notices",
|
||||
taskType: "conversation_reply",
|
||||
targetCodexThreadRef: "019d-app-server-thread",
|
||||
targetCodexFolderRef: repoRoot,
|
||||
executionPrompt: "同步账号与告警状态",
|
||||
});
|
||||
|
||||
assert.equal(result.status, "completed");
|
||||
assert.deepEqual(result.executionProgress.accountStatus, {
|
||||
authMode: "chatgpt",
|
||||
planType: "team",
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
usedPercent: 88,
|
||||
windowDurationMins: 180,
|
||||
resetsAt: 1770003600,
|
||||
creditsBalance: "120.5",
|
||||
hasCredits: true,
|
||||
unlimitedCredits: false,
|
||||
});
|
||||
assert.deepEqual(result.executionProgress.modelVerification, {
|
||||
verifications: ["trustedAccessForCyber"],
|
||||
});
|
||||
assert.deepEqual(result.executionProgress.warnings, [
|
||||
{
|
||||
id: "codex-warning-1",
|
||||
message: "模型切换提醒 token=[redacted]",
|
||||
severity: "warning",
|
||||
},
|
||||
{
|
||||
id: "config-warning-2",
|
||||
message: "项目配置已忽略:openai_base_url 不能放在项目配置里",
|
||||
severity: "warning",
|
||||
},
|
||||
{
|
||||
id: "deprecation-notice-3",
|
||||
message: "on-failure 已废弃:请改用 on-request",
|
||||
severity: "info",
|
||||
},
|
||||
]);
|
||||
const serialized = JSON.stringify(result.executionProgress);
|
||||
assert.equal(serialized.includes("/Users/kris"), false);
|
||||
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
|
||||
} else {
|
||||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_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";
|
||||
|
||||
@@ -370,3 +370,80 @@ test("POST task progress preserves Codex thread goal, settings, and compaction s
|
||||
assert.equal(serialized.includes("/Users/kris"), false);
|
||||
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
|
||||
});
|
||||
|
||||
test("POST task progress preserves Codex account, quota, verification, and notice summaries", async () => {
|
||||
const task = await data.queueMasterAgentTask({
|
||||
taskId: "route-progress-account-notices-task",
|
||||
projectId: "group-progress-test",
|
||||
taskType: "dispatch_execution",
|
||||
requestMessageId: "msg-route-progress-account-notices",
|
||||
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 账号运行态", status: "running" }],
|
||||
accountStatus: {
|
||||
authMode: "chatgpt",
|
||||
planType: "team",
|
||||
limitId: "codex",
|
||||
limitName: "Codex",
|
||||
usedPercent: 88,
|
||||
windowDurationMins: 180,
|
||||
resetsAt: 1770003600,
|
||||
creditsBalance: "120.5",
|
||||
hasCredits: true,
|
||||
unlimitedCredits: false,
|
||||
accessToken: "sk-secret-should-not-persist",
|
||||
},
|
||||
modelVerification: {
|
||||
verifications: ["trustedAccessForCyber"],
|
||||
turnId: "turn-secret-should-not-persist",
|
||||
},
|
||||
warnings: [
|
||||
{
|
||||
id: "config-warning-1",
|
||||
message: "项目配置已忽略:openai_base_url 不能放在项目配置里",
|
||||
severity: "warning",
|
||||
path: "/Users/kris/code/boss/.codex/config.toml",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{ 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?.accountStatus?.authMode, "chatgpt");
|
||||
assert.equal(progress?.accountStatus?.planType, "team");
|
||||
assert.equal(progress?.accountStatus?.usedPercent, 88);
|
||||
assert.equal(progress?.modelVerification?.verifications?.[0], "trustedAccessForCyber");
|
||||
assert.equal(progress?.warnings?.[0]?.message, "项目配置已忽略:openai_base_url 不能放在项目配置里");
|
||||
const serialized = JSON.stringify(progress);
|
||||
assert.equal(serialized.includes("/Users/kris"), false);
|
||||
assert.equal(serialized.includes("sk-secret-should-not-persist"), false);
|
||||
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user