chore: rebuild release package
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
[mcp_servers.codebase-memory-mcp]
|
[mcp_servers.codebase-memory-mcp]
|
||||||
type = "stdio"
|
|
||||||
command = "/home/hdzx/.local/bin/codebase-memory-mcp"
|
command = "/home/hdzx/.local/bin/codebase-memory-mcp"
|
||||||
args = []
|
args = []
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
85
.planning/multi-agent-v2-audit/findings.md
Normal file
85
.planning/multi-agent-v2-audit/findings.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# 调研发现
|
||||||
|
|
||||||
|
## 上游 Multi-agent V2
|
||||||
|
|
||||||
|
- 本机 `codex-cli` 版本为 `0.144.1`。
|
||||||
|
- `codex features list` 显示:`multi_agent` 已稳定且启用;`multi_agent_v2` 为 `under development` 且默认关闭;旧 `collaboration_modes` 已移除但配置值仍显示启用。
|
||||||
|
- 因此 V2 目前应按实验性、显式开关能力看待,不能假设所有 app-server 客户端都自动获得新行为。
|
||||||
|
- 本机二进制进一步暴露了 V2 的配置面:并发线程上限、等待超时上下限/默认值、用量提示、根/子代理提示文案、工具命名空间、隐藏 `spawn_agent` 元数据、仅非 code mode 启用,以及 `custom` / `explicitRequestOnly` / `proactive` 三种 `multiAgentMode`。
|
||||||
|
- 二进制中的 app-server 类型仍沿用 `CollabAgentToolCall` 和 `SubAgentActivity`,并新增/保留 `agentNickname`、`agentRole`、`agentPath`、`depth`、`parentThreadId` 等代理身份与层级元数据。
|
||||||
|
- 已从 OpenAI 官方 `openai/codex` 仓库的 `rust-v0.144.1` 标签对应提交 `44918ea1` 下载源码归档。V2 不是单一 UI 开关:涉及 feature config、上下文提示、agent control、独立的 `multi_agents_v2` handlers、CSV 批量派工、app-server `MultiAgentMode` 协议字段及线程状态/通知。
|
||||||
|
- V2 工具语义发生实质变化:`spawn_agent` 强制 `task_name` 并返回规范任务路径;默认继承全部历史,也可用 `fork_turns=none|all|N`;`fork_context` 在 V2 明确报错。
|
||||||
|
- V1 的 `send_input`、`resume_agent`、`close_agent` 被 V2 的 `send_message`(不触发新 turn)、`followup_task`(触发/续跑 turn)、`interrupt_agent`(中断但保留代理)、`list_agents` 取代。
|
||||||
|
- V2 `wait_agent` 不再接收目标列表,也不返回代理最终正文;它等待整个代理树的 mailbox 活动,用户 steer 也会提前唤醒。最终内容应经 mailbox/事件链消费。
|
||||||
|
- V2 引入规范层级路径(如 `/root/task1/task3`)和嵌套代理树,可按路径前缀列举;这会把 cc-web 从“按 agent id 展示卡片”推进到“代理树 + mailbox 事件”模型。
|
||||||
|
- `MultiAgentMode` 有 `custom`、`explicitRequestOnly`、`proactive` 三种策略,并作为 developer 上下文片段注入;默认 Ultra 推理会选 proactive,其余选 explicit-request-only(除非配置自定义提示)。
|
||||||
|
- 另有 `spawn_agents_on_csv` 批量任务:按 CSV 每行派生 worker、并发执行、worker 用 `report_agent_job_result` 回报、最后导出结果 CSV。它是独立的批处理能力,不是基础聊天 UI 必须支持的协议。
|
||||||
|
- app-server v2 schema 已出现 `multiAgentMode`,但 0.144.1 的 `thread/start` 处理器暂时把入参绑定为 `_multi_agent_mode`,并在 start/resume/fork 响应中固定回 `explicitRequestOnly`;还需确认 `turn/start` 是否才是当前有效入口。
|
||||||
|
- 已确认 `thread/start.multiAgentMode` 与 `turn/start.multiAgentMode` 都在 0.144.1 源码中标为 deprecated/ignored,注释明确要求用 Ultra reasoning effort;因此 cc-web 不应实现新的 `multiAgentMode` 控件或依赖其返回值。
|
||||||
|
- 相比 `rust-v0.143.0`,V2 工具 schema 基本未变,0.144.1 最重要的协议变化是事件标准化:`wait_agent` 从专用 `collab_waiting_begin/end` 改为 `item/started` + `item/completed` 的 `collabAgentToolCall(tool=wait)`;spawn/message/interrupt 从专用 `sub_agent_activity` event 改为 completed `subAgentActivity` turn item。
|
||||||
|
- 这意味着 cc-web 若只监听旧顶层 collab/sub-agent 通知会漏状态;若已有统一 `item/started`/`item/completed` 分发并识别这两种 item,则基本兼容。
|
||||||
|
- 官方提交记录对应三项连续迁移:canonical sub-agent activity(#31299)、canonical collab tool call(#31300)、canonical collab wait(#31301);另有 Ultra + 高并发提示(#31621)。
|
||||||
|
- OpenAI 最新稳定版实际已是 `0.145.0`(2026-07-21 发布),而非本机的 0.144.1。官方发布说明明确称“Stabilized the opt-in multi-agent V2 experience”,覆盖可配置子代理模型/推理等级/并发、角色恢复和代理导航,并在 #34383 将 V2 标记为 stable。
|
||||||
|
- 0.145.0 同时包含:统一多代理设置到 `agents`(#33550)、遵循子代理模型默认值(#33631)、恢复 V2 agent roles(#33657)、父线程持有的子代理线程只读(#33841)、agent picker 存活性/路径选择(#33921/#33922),并移除了 CSV-backed agent jobs(#34413)。因此 0.144.1 的 `spawn_agents_on_csv` 不能作为最新稳定版能力建议。
|
||||||
|
- 已获取 0.145.0 官方源码提交 `25af12f7`。V2 的核心工具仍是 `spawn_agent` / `send_message` / `followup_task` / `interrupt_agent` / `list_agents` / `wait_agent`,`fork_turns` 与 mailbox wait 语义保持不变;canonical `subAgentActivity` / `collabAgentToolCall` 事件路线也仍存在。
|
||||||
|
- 0.145.0 新增 `[agents]` 统一配置,包含 enabled、最大线程数、最大嵌套深度、默认子代理模型、默认子代理推理强度、interrupt message 等;旧 `features.multi_agent_v2` 仍保留兼容配置和部分 UI/工具参数。
|
||||||
|
- 精确配置语义:`multi_agent_v2` 在 0.145.0 为 stable 但 `default_enabled=false`;`agents.enabled` 默认 true 且旧 feature 开关优先。`agents.max_depth` 的 schema 明确写“V2 忽略”,所以 V2 的路径树不能按 V1 深度上限推断。
|
||||||
|
- V2 的 spawn `model` / `reasoning_effort` 在 0.145.0 默认不暴露,只有 `features.multi_agent_v2.expose_spawn_agent_model_overrides=true` 才进入工具 schema;`agent_type` 也只有实际配置角色时才暴露。cc-web 不应在固定 developer instructions 中假设这些字段总可用。
|
||||||
|
- 0.145.0 允许 full-history fork 叠加 model/reasoning override,只禁止 full-history 同时覆盖 agent_type;这再次证明 cc-web 当前硬编码的 `fork_context`/override 规则已经过时,最佳修复是让模型遵循运行时工具 schema,而不是在 cc-web 重述易漂移规则。
|
||||||
|
- 0.145.0 新增 child terminal turn → direct parent 的标准 completion envelope,结果以不触发新 turn 的 inter-agent communication 写入父代理 mailbox。因此即使 cc-web 没抓到 child 的独立 app-server 通知,Codex 核心仍可让 `wait_agent` 唤醒并把结果交给父代理;服务端 child route 缺口应定级为“实时卡片/状态观测缺口”,不是核心代理结果丢失。
|
||||||
|
- app-server v2 仍以 canonical TurnItem 生命周期为主,旧 `SubAgentActivity` 顶层 event 只保留给兼容/raw consumers。cc-web 当前使用 `item/completed(subAgentActivity)` 的方向正确。
|
||||||
|
- 已将当前用户实际使用的 Codex 从 0.144.1 升级为 0.145.0,路径仍为 `~/.local/bin/codex`;npm 包和 CLI 版本一致。升级后 `multi_agent_v2` 状态为 stable/false,未自动开启实验/opt-in 能力。
|
||||||
|
- 升级后仍有两个 2026-07-02/07-18 启动的 app-server 子进程,其 `/proc/<pid>/exe` 指向已删除的旧安装目录;它们不会热切换到 0.145.0。当前 ccweb 只检测到本轮一个 running 对话,但本轮不重启服务,避免中断正在进行的会话。
|
||||||
|
|
||||||
|
## cc-web 当前实现
|
||||||
|
|
||||||
|
- codebase-memory 初始索引只含 10 个源码文件,漏掉 `server.js` 与 `lib/codex-app-runtime.js`;执行 full reindex 后节点数仍未变化。`.cbmignore` 并未排除这些源码,推测是索引器对超大 JS 文件的提取限制。本轮对未索引文件按仓库规则降级到 `rg`/定点源码读取。
|
||||||
|
- 前端 `public/app.js` 已识别 `subAgentActivity`,并把它归并到 `collab_agent_tool_call` 子代理卡片;回归脚本覆盖 agentPath、agentThreadId、role、taskDescription、各 activity kind 与普通工具防误判。
|
||||||
|
- 后端 `lib/codex-app-runtime.js` 已明确处理 `item/started`、`item/completed`、`collabAgentToolCall`、`subAgentActivity`,并保留 started→completed 的输入合并与持久化。
|
||||||
|
- `server.js` 已有 canonical item lifecycle 路由和 collab 状态归并;说明 0.144.1 最关键的事件迁移在 cc-web 中已有针对性实现与回归。
|
||||||
|
- `lib/codex-app-runtime.js` 的 lifecycle 处理可兼容 V2:`item/started` 建立工具项,`item/completed` 合并并结束;`subAgentActivity` 会把 agentPath/agentThreadId/prompt/role 规范化成子代理卡片状态,V2 wait 的 `collabAgentToolCall` 也走统一路径。
|
||||||
|
- **明确缺口:** `CODEX_APP_COLLABORATION_INSTRUCTIONS` 仍注入 V1 规则:要求使用/推断 `fork_context`,并要求 `wait_agent` 持续等待“最终状态”。V2 会拒绝 `fork_context`,且 wait 只报告 mailbox 活动、不返回最终正文。这些开发者指令会直接诱导 V2 工具调用失败或空转等待,应优先改为版本中立/V2 兼容指令。
|
||||||
|
- `codexAppCollabToolName` 与 collab fallback 仍主要识别 V1 动作(spawn/wait/send_input/resume/close),未显式识别 V2 的 `send_message`、`followup_task`、`interrupt_agent`、`list_agents`。其中 `send_message` 被折叠为 `send_input`,其余需评估对历史恢复和状态归并的影响。
|
||||||
|
- **明确缺口:Ultra 被 cc-web 丢弃。** 上游 V2 用 `ReasoningEffort::Ultra` 触发 proactive,但 `server.js` 的允许集合、Codex App 模型字符串解析、`lib/agent-runtime.js` 的 CLI 参数解析,以及前端推理强度选项都只到 `xhigh`。即使 `~/.codex/config.toml` 配置 `model_reasoning_effort = "ultra"`,cc-web 也不会把它编码进 session model/turn collaboration settings。
|
||||||
|
- mock/regression 目前反而断言了旧的“重复 wait_agent 直到 final”指导,说明修复 V2 指令时必须同步调整 mock 摘要与回归断言,避免测试把旧语义锁死。
|
||||||
|
- **明确缺口:V2 spawn 的 child thread 未进入服务端路由表。** `handleCodexAppNotification` 只对 `collabAgentToolCall` 调 `syncCcwebMcpChildAgentsFromCollabItem`;0.144.1 V2 spawn 发的是 completed `subAgentActivity`。runtime/前端虽能画卡片,但 `ccwebMcpChildThreads` 没有登记该 child,随后 child 自己的 agentMessage/turn 通知会成为 unrouted,实时最终消息与状态无法可靠回填。
|
||||||
|
- 现有 mock 的多代理场景仍模拟 V1 `collabAgentToolCall(tool=spawn_agent)` 后再发 child turn;没有模拟“parent 收到 V2 subAgentActivity → 注册 child → 路由 child turn”的真实链路。因此当前回归通过不能证明 V2 live routing 完整。
|
||||||
|
- **嵌套代理树也未覆盖。** 即便 child 已被登记,`processCcwebMcpChildNotification` 只处理其 agentMessage 与 turn 生命周期,不处理 child 发出的 `subAgentActivity`;V2 grandchild 的创建/消息会被忽略。基础一层代理修复应为 P0,完整嵌套树可作为 P1。
|
||||||
|
- `recoverCcwebMcpChildThreadsFromPersistedToolCalls` 能从已持久化的 `subAgentActivity` 恢复 child map,但只在 cc-web 启动时的残留 turn 恢复路径调用;它不能弥补当前活跃 turn 中 V2 child 的即时注册缺口。
|
||||||
|
- V2 `SubAgentActivityItem` 本身不含原始 task prompt,cc-web 目前只能用 agentPath basename 生成标题;若要保留任务描述,需要额外关联同 call id 的 spawn 请求或接受“仅路径标题”的降级。这属于体验优化,不是协议阻断。
|
||||||
|
- 本机 `~/.codex/config.toml` 当前模型为 `gpt-5.6-sol`、推理强度 `xhigh`,且未配置 `multi_agent_v2`;结合 `codex features list` 可确认当前 V2 实际未启用。因此这些缺口目前不会破坏现有 V1 会话,但会阻断/削弱未来显式启用 V2。
|
||||||
|
|
||||||
|
## 差距与建议
|
||||||
|
|
||||||
|
### P0:启用 V2 前必须跟进
|
||||||
|
|
||||||
|
1. Ultra 全链路:`CODEX_REASONING_LEVELS`、Codex/Codex App 模型字符串解析、CLI `model_reasoning_effort` 参数、前端 Thinking picker 和回归用例全部加入 `ultra`。
|
||||||
|
2. 删除 V1 字段级 developer instructions:不再注入 `fork_context` 和“wait 直到 final”的规则;仅保留 cc-web 自有 title 规则,并要求遵循当前运行时工具 schema/描述。
|
||||||
|
|
||||||
|
### P1:建议同步完成
|
||||||
|
|
||||||
|
1. parent 收到 `subAgentActivity(kind=started)` 时登记 child thread,随后能路由 child 的 agentMessage/turn 通知并回填实时卡片;同一路径递归处理 child 发出的 grandchild activity。
|
||||||
|
2. mock 改成真实 V2 序列:completed-only `subAgentActivity`、空 receiver 的 wait item、child completion/mailbox、嵌套路径;保留 V1 用例做双栈回归。
|
||||||
|
3. UI 的“关闭”在 V2 中实际只是 interrupt,不能伪装成销毁;应调整文案/状态或明确隐藏与可恢复语义。代理展示应保留 canonical task path,避免只取 basename 后同名冲突。
|
||||||
|
|
||||||
|
### 无需跟进
|
||||||
|
|
||||||
|
- 不新增 `multiAgentMode` 控件:0.145.0 仍标记 deprecated/ignored,proactive 由 Ultra 驱动。
|
||||||
|
- 不做 CSV fanout UI:`spawn_agents_on_csv` 已从 0.145.0 移除。
|
||||||
|
- 不默认强开 `multi_agent_v2`:它虽 stable 但仍 opt-in/default false;应继续尊重用户 Codex 配置。
|
||||||
|
- `[agents]` 配置由 Codex app-server 直接读取,cc-web 暂无需复制一套配置管理 UI。
|
||||||
|
|
||||||
|
### 已兼容
|
||||||
|
|
||||||
|
- canonical `item/started` / `item/completed` 分发。
|
||||||
|
- `collabAgentToolCall` 与 `subAgentActivity` 的 runtime/前端归并和基础卡片渲染。
|
||||||
|
- app-server `collaborationMode.settings` 的 model/reasoning/developer_instructions 形状及线程级 MCP 注入。
|
||||||
|
|
||||||
|
## 实施结果
|
||||||
|
|
||||||
|
- Ultra 已贯通本地 Codex 配置、会话模型字符串、Codex CLI `model_reasoning_effort`、Codex App `collaborationMode.settings.reasoning_effort` 与前端 Thinking picker。
|
||||||
|
- 固定注入的 V1 `fork_context` / 重复 `wait_agent` 指导已删除,改为只要求遵循当前 runtime tool schema 与工具描述。
|
||||||
|
- 服务端会从父级或任意已登记 child 的 `subAgentActivity` 递归登记子线程,保留直接 `parentThreadId`、`spawnToolId`、canonical `agentPath` 与任务描述。
|
||||||
|
- 子代理操作与状态改为“中断 / 已中断”,保留后续 turn 恢复可能;前端卡片 tooltip/footer 展示 canonical path,避免同名 basename 混淆。
|
||||||
|
- mock 同时保留 V1 `collabAgentToolCall(spawn_agent)` 序列,并新增 V2 completed-only activity、child→grandchild、空 receiver wait 与终态回填序列。
|
||||||
|
- 静态检查、定向 subagent 回归和完整 `npm run regression` 均通过;`multi_agent_v2` 仍保持 opt-in/default false,未由 cc-web 强制开启。
|
||||||
38
.planning/multi-agent-v2-audit/progress.md
Normal file
38
.planning/multi-agent-v2-audit/progress.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 进度记录
|
||||||
|
|
||||||
|
- 2026-07-23:已读取 `openai-docs` 与 `planning-with-files` 技能说明。
|
||||||
|
- 2026-07-23:已确认工作区存在用户既有未跟踪 CSV 文件,后续不触碰。
|
||||||
|
- 2026-07-23:开始阶段 1,上游与本机 Codex 证据收集。
|
||||||
|
- 2026-07-23:确认本机 Codex 0.144.1 中 `multi_agent_v2` 为默认关闭的开发中功能。
|
||||||
|
- 2026-07-23:官方手册 helper 遇到 HEAD 403,记录后切换为更窄的官方/本机取证路径。
|
||||||
|
- 2026-07-23:从本机二进制提取到 V2 配置形状、模式枚举以及 app-server 代理元数据线索。
|
||||||
|
- 2026-07-23:获取并定位 OpenAI Codex `rust-v0.144.1` 官方源码中的 Multi-agent V2 实现文件。
|
||||||
|
- 2026-07-23:完成 V1/V2 工具语义、任务路径、等待模型、策略模式和 CSV 批处理的第一轮源码对比。
|
||||||
|
- 2026-07-23:完成 0.143.0→0.144.1 定点差异比对,锁定 Multi-agent V2 的 turn-item 事件迁移。
|
||||||
|
- 2026-07-23:阶段 1 完成,进入 cc-web 现有实现定位。
|
||||||
|
- 2026-07-23:发现 codebase-memory 对超大 JS 主文件覆盖不完整;full reindex 未改善,按约定对未索引文件降级为定点文本校验。
|
||||||
|
- 2026-07-23:初步确认 cc-web 已覆盖 canonical `collabAgentToolCall` / `subAgentActivity` item 生命周期。
|
||||||
|
- 2026-07-23:发现 cc-web 注入的子代理开发者指令仍是 V1 语义,与 V2 `fork_turns` / mailbox wait 冲突,列为首要跟进项。
|
||||||
|
- 2026-07-23:确认 cc-web 全链路缺少 Ultra 推理强度支持,导致无法通过官方入口启用 proactive Multi-agent V2。
|
||||||
|
- 2026-07-23:确认 V2 `subAgentActivity` 仅完成了 UI/runtime 适配,服务端 child 路由注册仍依赖 V1 spawn item;一层实时 child 通知存在漏路由风险,嵌套代理也未覆盖。
|
||||||
|
- 2026-07-23:确认已有 child 恢复逻辑只服务于进程重启后的残留状态,不能替代 V2 live routing 注册。
|
||||||
|
- 2026-07-23:阶段 2 完成;当前本机 V2 未启用,进入风险分级和最小跟进范围评估。
|
||||||
|
- 2026-07-23:发现最新稳定版为 Codex 0.145.0,发布说明正式稳定化 opt-in Multi-agent V2;审计基线从本机 0.144.1 上调到 0.145.0。
|
||||||
|
- 2026-07-23:获取 0.145.0 源码并确认 V2 工具/事件主语义未回退,新增重点在统一 agents 配置与角色/模型恢复。
|
||||||
|
- 2026-07-23:确认 0.145.0 的 spawn 字段是按配置动态暴露,cc-web 固定注入字段级规则会持续与上游漂移。
|
||||||
|
- 2026-07-23:确认 0.145.0 会把 child 终态标准化投递给父代理 mailbox;cc-web 路由缺口主要影响实时 UI,而非核心结果交付。
|
||||||
|
- 2026-07-23:升级后回归通过;发现既有 app-server 仍驻留旧二进制。运行对话检查仅有当前对话,本轮未重启 cc-web。
|
||||||
|
- 2026-07-23:完成 0.145.0 差距分级:2 项 P0、3 项 P1,并明确 4 类无需跟进项;本轮未修改 cc-web 产品代码。
|
||||||
|
- 2026-07-23:用户授权实施适配并在完成后重启;已创建 `Multi-agent V2 适配 TO DO list.csv`,进入测试先行阶段。
|
||||||
|
- 2026-07-23:用户明确授权升级 Codex;开始以单次 proxyd npm registry 安装 0.145.0,不重启 cc-web。
|
||||||
|
- 2026-07-23:系统级 npm 前缀安装因 EACCES 失败且未产生有效升级;切换到 `--prefix ~/.local` 的用户级安装路径。
|
||||||
|
- 2026-07-23:用户级 Codex 0.145.0 安装成功,CLI/包版本/可执行路径均已核验;进入 0.145.0 兼容性最终比对。
|
||||||
|
- 2026-07-23:计划审查通过;补齐 Ultra、V1 指导移除、V2 child/grandchild 路由、真实 V2 mock 和中断/path 展示失败回归。
|
||||||
|
- 2026-07-23:完成 Ultra 全链路实现,并用进程 spawn 日志验证 Codex CLI 收到 `model_reasoning_effort="ultra"`。
|
||||||
|
- 2026-07-23:完成版本中立的子代理 developer instructions,移除 `fork_context` 与重复 wait 旧规则。
|
||||||
|
- 2026-07-23:完成 parent/child `subAgentActivity` 递归登记、嵌套通知路由及 canonical agentPath/parentThreadId 同步。
|
||||||
|
- 2026-07-23:完成 V2 mock、前端“中断”语义与路径展示;保留 V1 双栈回归。
|
||||||
|
- 2026-07-23:6 个改动 JS 文件语法检查、`git diff --check`、定向回归和完整 `npm run regression` 通过。
|
||||||
|
- 2026-07-23:重启前通过 ccweb MCP 确认仅当前对话 running、无 pending reply/child;执行 `pm2 restart ccweb --update-env`。
|
||||||
|
- 2026-07-23:重启后 ccweb PID 1272312 online,HTTP 8002 返回 200,线上 app.js 含 Ultra/中断语义;新 app-server 使用有效的 Codex 0.145.0 二进制。
|
||||||
|
- 2026-07-23:`multi_agent_v2` 保持 stable/false,未擅自开启;发现 7 月 2 日启动的外部旧 app-server 指向 deleted 二进制,因不属于本次 ccweb 进程未终止。
|
||||||
37
.planning/multi-agent-v2-audit/task_plan.md
Normal file
37
.planning/multi-agent-v2-audit/task_plan.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Multi-agent V2 与 cc-web 兼容性审计
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
完成 cc-web 对 Codex 0.145.0 Multi-agent V2 的兼容适配,验证后在无其他运行对话时重启服务并验收。
|
||||||
|
|
||||||
|
## 实施计划
|
||||||
|
|
||||||
|
- [x] 补充 Multi-agent V2 失败回归用例
|
||||||
|
- [x] 实现 Ultra 推理强度全链路支持
|
||||||
|
- [x] 替换过时的 V1 子代理指导
|
||||||
|
- [x] 实现 subAgentActivity 子代理路由与嵌套状态同步
|
||||||
|
- [x] 更新 V2 mock 与关闭/路径展示语义
|
||||||
|
- [x] 运行回归和静态检查
|
||||||
|
- [x] 检查运行会话并重启 cc-web
|
||||||
|
- [x] 执行重启后版本与服务验收
|
||||||
|
|
||||||
|
## 约束
|
||||||
|
|
||||||
|
- 上游事实优先采用 OpenAI 官方文档或本机安装包的可验证行为。
|
||||||
|
- 代码理解优先使用 codebase-memory-mcp,`rg` 仅做文本与行号校验。
|
||||||
|
- 保留工作区既有未提交文件,只修改本任务涉及的产品代码和回归。
|
||||||
|
- 用户已明确授权用户级全局 Codex 包升级;仅使用单次代理注册表,不改写 npm 持久配置。
|
||||||
|
- 重启前必须确认除当前对话外没有其他 running 对话;不启用 `multi_agent_v2` feature flag。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
- 官方 Codex 手册 helper 对 `HEAD https://developers.openai.com/codex/codex-manual.md` 返回 HTTP 403;后续改用已缓存手册、官方 GET 或本机安装包证据降级。
|
||||||
|
- 一次过宽的 `/home/hdzx` 文件系统扫描超过 60 秒无输出,已中止;后续收窄到已知安装路径与有限目录。
|
||||||
|
- 浅克隆官方源码目录只留下无提交的 `.git`,未形成可检出的 `HEAD`;后续改为查询精确标签后定点 fetch,避免重复同一失败路径。
|
||||||
|
- 一次工具脚本因 JavaScript 对象引号拼写错误未执行;已立即修正,同类命令后续采用更短参数块。
|
||||||
|
- 一次 GitHub release JSON 的 `jq` 表达式把数组结果继续索引 `.body`,导致解析报错;已改为分别输出元组和正文,确认最新稳定版为 0.145.0。
|
||||||
|
- 一次计划文件补丁把“错误记录”误定位到 findings 文件,校验未通过且未写入;已按文件职责拆分后成功更新。
|
||||||
|
- 一次读取 0.145.0 child completion 源码的工具脚本重复出现 JavaScript 对象引号拼写错误;命令未执行,随即修正并成功获取证据。
|
||||||
|
- 一次补写 child completion 发现时再次把 task_plan 的错误记录定位到 findings,补丁校验未通过;已拆分为正确文件更新。
|
||||||
|
- 一次补写驻留 app-server 发现时误带了空的 skill 文件 hunk,补丁整体校验失败且未写入;已移除无关 hunk 后成功更新。
|
||||||
|
- 首次 `npm install -g` 使用 npm 默认系统前缀 `/usr/local`,因 EACCES 在 rename 阶段失败;未使用 sudo,改为对当前实际 Codex 路径对应的 `~/.local` 显式用户前缀安装。
|
||||||
18
.planning/subagent-plan-progress/findings.md
Normal file
18
.planning/subagent-plan-progress/findings.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 发现记录
|
||||||
|
|
||||||
|
## 2026-07-24
|
||||||
|
|
||||||
|
- `processCcwebMcpChildNotification` 当前仅处理子线程 turn 和 agent message,忽略计划通知。
|
||||||
|
- `processCodexAppNotification` 已兼容 `plan/updated`、`turn/plan/updated`、`item/plan/updated`、`item/todoList/updated`。
|
||||||
|
- `planUpdateItemFromParams`、`normalizeTodoListFromPlanItem`、`isTodoListPlanDone` 可作为纯解析链路复用。
|
||||||
|
- 子代理公开状态通过 `ccwebMcpChildPublicState`、`mergeCcwebMcpChildIntoTool` 和现有 child update 事件传给前端,适合承载精简计划字段。
|
||||||
|
- 前端已有 `createPlanProgressElement`,可抽出由 `{ completed, total }` 直接渲染的基础函数。
|
||||||
|
- 当前工作区已有未提交改动和用户自有 CSV,必须保留且不能误清理。
|
||||||
|
- 子代理恢复路径当前只从原始 `subAgentActivity` input 恢复;精简计划应写进工具 `input.agentsStates[threadId]`,并在恢复时读取对应 agent state。
|
||||||
|
- `rememberCollabAgentStateFromChild` 会 spread 子代理公开状态,字段可进入缓存;但 `mergeCollabAgentTaskState` 需要确认不会在卡片工具合并时丢失计划字段。
|
||||||
|
- 现有 Plan List 进度点只在总数超过 12 时显示计数;子代理卡片需求明确要求始终展示 `completed/total`,基础渲染函数需要可配置计数显示。
|
||||||
|
- 样式文件实际为 `public/style.css`,后续 CSS 契约应使用现有 `PUBLIC_STYLE_PATH`。
|
||||||
|
- `normalizeIdentifier` 会把 `inProgress` / `in_progress` 都归一为 `inprogress` / `in_progress`;当前完成状态判断已经兼容多种形状。
|
||||||
|
- 纯解析 API 适合返回 `{ item, todoList, progress, currentStep, done }`:父线程用 `todoList` 更新工具,子线程只保留 `progress/currentStep`。
|
||||||
|
- 现有 Plan List 回归只断言 items 的 `completed` 和 progress,可以在不改变 todo payload 形状的前提下从原始 entries 提取当前步骤。
|
||||||
|
- Plan List 的桌面进度点单点实际占用约 22px;在 220px 子卡片内最多 12 点会溢出,因此子卡片保留同一语义和主题色,但将点尺寸收敛为 8px。
|
||||||
25
.planning/subagent-plan-progress/progress.md
Normal file
25
.planning/subagent-plan-progress/progress.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 进度日志
|
||||||
|
|
||||||
|
## 2026-07-24T06:22:38+08:00
|
||||||
|
|
||||||
|
- 恢复前一轮上下文并核对工作区状态。
|
||||||
|
- 完整读取 `planning-with-files` 与 `todo-list-csv` 技能说明。
|
||||||
|
- 已建立 7 步实施计划;当前执行失败回归阶段。
|
||||||
|
- 本地审查计划通过:范围和验收项完整,无需调整步骤。
|
||||||
|
- 已确认父计划解析、子通知处理、子状态恢复和前端卡片的精确修改点。
|
||||||
|
- 新增运行时纯解析 API 和前端简版进度的契约断言。
|
||||||
|
- `npm run regression -- --target subagent-card-metadata` 按预期失败:当前缺少 `createPlanProgressElementFromProgress`。
|
||||||
|
- 已在 `codex-app-runtime` 暴露 `planUpdateFromNotification`,并将父线程计划通知改为复用该纯解析 API。
|
||||||
|
- 纯解析验证通过:三步计划得到 `1/3` 且当前步骤为“同步”。
|
||||||
|
- 子通知处理器已识别计划更新,并将 `planProgress` / `planCurrentStep` / `planUpdatedAt` 同步到公开状态和持久化工具结果。
|
||||||
|
- 持久化恢复现在优先读取工具 result 中的最新 agent state,可恢复计划摘要。
|
||||||
|
- 已抽取 `createPlanProgressElementFromProgress`,子代理卡片复用进度点并始终显示计数。
|
||||||
|
- 卡片新增当前步骤单行省略展示;定向 `subagent-card-metadata` 回归通过。
|
||||||
|
- 首次全量回归在旧 Plan List DOM 契约处失败:断言仍提取 wrapper,已改为检查抽取后的基础渲染函数。
|
||||||
|
- V2 mock 现在由子线程发送三步 `turn/plan/updated`;集成断言验证父卡片收到 `1/3` 和当前步骤。
|
||||||
|
- 新增前端行为断言:3 个进度点、`1/3` 计数与当前步骤均正确渲染。
|
||||||
|
- 全量 `npm run regression` 在 60 秒限制内通过,包含 V2 计划状态持久化验证。
|
||||||
|
- 差异审查发现 Plan List 原 14px 点在子卡片中可能溢出,已添加高优先级 8px 卡片内覆盖,不影响原 Plan List。
|
||||||
|
- 当前修订已通过全量回归、`subagent-card-metadata` / `plan-list-progress` 定向回归、全部改动 JS 语法检查和 `git diff --check`。
|
||||||
|
- 重启前 ccweb running 列表仅有当前会话,安全门禁通过。
|
||||||
|
- `pm2 restart ccweb --update-env` 已执行;命令回传因服务自身重启中断,事后验收确认 ccweb online、8002 返回 200、线上 `app.js` 包含新进度函数。
|
||||||
30
.planning/subagent-plan-progress/task_plan.md
Normal file
30
.planning/subagent-plan-progress/task_plan.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# 子代理卡片计划进度
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
当子代理通过 `update_plan` 更新计划时,把精简进度同步到父会话的子代理卡片;页面刷新后仍可恢复,并在完成回归后安全重启 ccweb。
|
||||||
|
|
||||||
|
## 阶段
|
||||||
|
|
||||||
|
| 阶段 | 状态 | 验收标准 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. 补充子代理计划进度失败回归 | complete | 回归能证明当前缺少子代理计划同步与卡片展示 |
|
||||||
|
| 2. 暴露并复用计划通知解析 | complete | 子代理和父代理使用同一套计划通知解析逻辑 |
|
||||||
|
| 3. 实现子代理计划状态同步与持久化 | complete | 子代理公开状态、工具状态、恢复状态都包含精简计划 |
|
||||||
|
| 4. 实现卡片简版进度渲染 | complete | 卡片显示进度点、完成数和当前步骤 |
|
||||||
|
| 5. 更新 V2 mock 与集成断言 | complete | mock 子代理发送计划通知,集成回归验证父会话收到状态 |
|
||||||
|
| 6. 运行回归和静态检查 | complete | JS 语法、diff 检查和 60 秒内全量回归通过 |
|
||||||
|
| 7. 检查运行会话、重启并验收 | complete | 无其他运行会话时重启,PM2、HTTP 和线上资源正常 |
|
||||||
|
|
||||||
|
## 关键决策
|
||||||
|
|
||||||
|
- 继续复用 `ccweb_mcp_child_agent_update`,不新增 WebSocket 消息类型。
|
||||||
|
- 子代理只向父卡片暴露 `{ completed, total }` 与当前 `in_progress` 步骤,不复制完整计划项。
|
||||||
|
- 计划事件解析集中在 `codex-app-runtime`,避免父、子线程各维护一套协议兼容逻辑。
|
||||||
|
- 保留工作区中用户和上一任务的既有改动,仅在相关区域增量修改。
|
||||||
|
|
||||||
|
## 错误记录
|
||||||
|
|
||||||
|
| 错误 | 尝试 | 处理 |
|
||||||
|
|---|---|---|
|
||||||
|
| Plan List 回归仍从 wrapper 提取 DOM 契约,抽取 helper 后断言失败 | 1 | 契约改为检查 `createPlanProgressElementFromProgress` |
|
||||||
Binary file not shown.
@@ -211,7 +211,7 @@ function createAgentRuntime(deps) {
|
|||||||
// cc-web UI supports "gpt-5.4(high)" style selection, but Codex CLI expects:
|
// cc-web UI supports "gpt-5.4(high)" style selection, but Codex CLI expects:
|
||||||
// - model: "gpt-5.4"
|
// - model: "gpt-5.4"
|
||||||
// - reasoning effort: config key `model_reasoning_effort = "high"`
|
// - reasoning effort: config key `model_reasoning_effort = "high"`
|
||||||
const m = raw.match(/^(.*)\((low|medium|high|xhigh)\)\s*$/i);
|
const m = raw.match(/^(.*)\((low|medium|high|xhigh|ultra)\)\s*$/i);
|
||||||
if (m) {
|
if (m) {
|
||||||
const base = String(m[1] || '').trim();
|
const base = String(m[1] || '').trim();
|
||||||
const lvl = String(m[2] || '').trim().toLowerCase();
|
const lvl = String(m[2] || '').trim().toLowerCase();
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ const RUNTIME_TRUNCATED_HEAD = '[cc-web: 前文过长,已保留尾部以保护
|
|||||||
const RUNTIME_TRUNCATED_TAIL = '\n[cc-web: 内容过长,已截断以保护服务稳定性]';
|
const RUNTIME_TRUNCATED_TAIL = '\n[cc-web: 内容过长,已截断以保护服务稳定性]';
|
||||||
const CODEX_APP_PLAN_ITEM_TYPES = new Set(['plan', 'plan_list', 'planlist', 'todo', 'todo_list', 'todolist', 'task_list']);
|
const CODEX_APP_PLAN_ITEM_TYPES = new Set(['plan', 'plan_list', 'planlist', 'todo', 'todo_list', 'todolist', 'task_list']);
|
||||||
const CODEX_APP_PLAN_TOOL_NAMES = new Set(['update_plan', 'plan', 'plan_list', 'todo_list', 'updateplan', 'todolist']);
|
const CODEX_APP_PLAN_TOOL_NAMES = new Set(['update_plan', 'plan', 'plan_list', 'todo_list', 'updateplan', 'todolist']);
|
||||||
|
const CODEX_APP_PLAN_UPDATE_METHODS = new Set([
|
||||||
|
'plan/updated',
|
||||||
|
'turn/plan/updated',
|
||||||
|
'item/plan/updated',
|
||||||
|
'item/todoList/updated',
|
||||||
|
]);
|
||||||
|
|
||||||
function createCodexAppRuntime(deps = {}) {
|
function createCodexAppRuntime(deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -208,8 +214,7 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTodoListFromPlanItem(item) {
|
function planEntriesFromItem(item) {
|
||||||
if (!isPlanLikeItem(item)) return null;
|
|
||||||
const candidates = [
|
const candidates = [
|
||||||
item.arguments,
|
item.arguments,
|
||||||
item.input,
|
item.input,
|
||||||
@@ -225,7 +230,13 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
entries = extractPlanEntries(candidate);
|
entries = extractPlanEntries(candidate);
|
||||||
if (entries) break;
|
if (entries) break;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(entries)) return null;
|
return Array.isArray(entries) ? entries : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTodoListFromPlanItem(item) {
|
||||||
|
if (!isPlanLikeItem(item)) return null;
|
||||||
|
const entries = planEntriesFromItem(item);
|
||||||
|
if (!entries) return null;
|
||||||
const items = entries
|
const items = entries
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const text = truncateEnd(planEntryText(entry), RUNTIME_TOOL_INPUT_MAX_CHARS);
|
const text = truncateEnd(planEntryText(entry), RUNTIME_TOOL_INPUT_MAX_CHARS);
|
||||||
@@ -244,6 +255,15 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function currentPlanStep(entries) {
|
||||||
|
const activeEntry = (Array.isArray(entries) ? entries : []).find((entry) => {
|
||||||
|
if (!entry || typeof entry !== 'object') return false;
|
||||||
|
const status = normalizeIdentifier(entry.status || entry.state);
|
||||||
|
return ['inprogress', 'in_progress', 'active', 'running', 'working'].includes(status);
|
||||||
|
});
|
||||||
|
return truncateEnd(planEntryText(activeEntry), RUNTIME_TOOL_INPUT_MAX_CHARS);
|
||||||
|
}
|
||||||
|
|
||||||
function isTodoListPlanDone(item, todoList) {
|
function isTodoListPlanDone(item, todoList) {
|
||||||
const status = normalizeIdentifier(item?.status || item?.state);
|
const status = normalizeIdentifier(item?.status || item?.state);
|
||||||
if (['completed', 'complete', 'done', 'success', 'succeeded', 'failed', 'error', 'cancelled', 'canceled'].includes(status)) {
|
if (['completed', 'complete', 'done', 'success', 'succeeded', 'failed', 'error', 'cancelled', 'canceled'].includes(status)) {
|
||||||
@@ -272,6 +292,20 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function planUpdateFromNotification(notification = {}) {
|
||||||
|
if (!CODEX_APP_PLAN_UPDATE_METHODS.has(notification?.method)) return null;
|
||||||
|
const item = planUpdateItemFromParams(notification.params || {});
|
||||||
|
const todoList = normalizeTodoListFromPlanItem(item);
|
||||||
|
if (!todoList) return null;
|
||||||
|
return {
|
||||||
|
item,
|
||||||
|
todoList,
|
||||||
|
progress: todoList.progress,
|
||||||
|
currentStep: currentPlanStep(planEntriesFromItem(item)),
|
||||||
|
done: isTodoListPlanDone(item, todoList),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function cleanRuntimeText(value) {
|
function cleanRuntimeText(value) {
|
||||||
return value == null ? '' : String(value).trim();
|
return value == null ? '' : String(value).trim();
|
||||||
}
|
}
|
||||||
@@ -891,10 +925,10 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
case 'turn/plan/updated':
|
case 'turn/plan/updated':
|
||||||
case 'item/plan/updated':
|
case 'item/plan/updated':
|
||||||
case 'item/todoList/updated': {
|
case 'item/todoList/updated': {
|
||||||
const item = planUpdateItemFromParams(params);
|
const planUpdate = planUpdateFromNotification(notification);
|
||||||
const todoList = normalizeTodoListFromPlanItem(item);
|
if (!planUpdate) return { done: false };
|
||||||
if (!todoList) return { done: false };
|
const { item, todoList } = planUpdate;
|
||||||
updateToolResult(entry, sessionId, todoList.id, JSON.stringify(todoList, null, 2), isTodoListPlanDone(item, todoList), {
|
updateToolResult(entry, sessionId, todoList.id, JSON.stringify(todoList, null, 2), planUpdate.done, {
|
||||||
name: 'PlanList',
|
name: 'PlanList',
|
||||||
kind: 'todo_list',
|
kind: 'todo_list',
|
||||||
input: todoList,
|
input: todoList,
|
||||||
@@ -1000,6 +1034,7 @@ function createCodexAppRuntime(deps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
planUpdateFromNotification,
|
||||||
processCodexAppNotification,
|
processCodexAppNotification,
|
||||||
updateUsage,
|
updateUsage,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6528,24 +6528,24 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPlanProgressElement(tool) {
|
function createPlanProgressElementFromProgress(progress, options = {}) {
|
||||||
const progress = resolveToolPlanProgress(tool);
|
const normalizedProgress = normalizePlanProgress(progress);
|
||||||
if (!progress) return null;
|
if (!normalizedProgress) return null;
|
||||||
|
|
||||||
const meter = document.createElement('span');
|
const meter = document.createElement('span');
|
||||||
meter.className = 'plan-progress';
|
meter.className = 'plan-progress';
|
||||||
meter.setAttribute('role', 'img');
|
meter.setAttribute('role', 'img');
|
||||||
const progressLabel = `计划进度:已完成 ${progress.completed} 项,共 ${progress.total} 项`;
|
const progressLabel = `计划进度:已完成 ${normalizedProgress.completed} 项,共 ${normalizedProgress.total} 项`;
|
||||||
meter.setAttribute('aria-label', progressLabel);
|
meter.setAttribute('aria-label', progressLabel);
|
||||||
meter.title = progressLabel;
|
meter.title = progressLabel;
|
||||||
|
|
||||||
const dots = document.createElement('span');
|
const dots = document.createElement('span');
|
||||||
dots.className = 'plan-progress-dots';
|
dots.className = 'plan-progress-dots';
|
||||||
dots.setAttribute('aria-hidden', 'true');
|
dots.setAttribute('aria-hidden', 'true');
|
||||||
const visibleDotCount = Math.min(progress.total, 12);
|
const visibleDotCount = Math.min(normalizedProgress.total, 12);
|
||||||
const completedDotCount = progress.total <= visibleDotCount
|
const completedDotCount = normalizedProgress.total <= visibleDotCount
|
||||||
? progress.completed
|
? normalizedProgress.completed
|
||||||
: Math.round((progress.completed / progress.total) * visibleDotCount);
|
: Math.round((normalizedProgress.completed / normalizedProgress.total) * visibleDotCount);
|
||||||
for (let index = 0; index < visibleDotCount; index += 1) {
|
for (let index = 0; index < visibleDotCount; index += 1) {
|
||||||
const dot = document.createElement('span');
|
const dot = document.createElement('span');
|
||||||
dot.className = `plan-progress-dot ${index < completedDotCount ? 'is-complete' : 'is-remaining'}`;
|
dot.className = `plan-progress-dot ${index < completedDotCount ? 'is-complete' : 'is-remaining'}`;
|
||||||
@@ -6553,16 +6553,20 @@
|
|||||||
}
|
}
|
||||||
meter.appendChild(dots);
|
meter.appendChild(dots);
|
||||||
|
|
||||||
if (progress.total > visibleDotCount) {
|
if (options.alwaysShowCount || normalizedProgress.total > visibleDotCount) {
|
||||||
const count = document.createElement('span');
|
const count = document.createElement('span');
|
||||||
count.className = 'plan-progress-count';
|
count.className = 'plan-progress-count';
|
||||||
count.setAttribute('aria-hidden', 'true');
|
count.setAttribute('aria-hidden', 'true');
|
||||||
count.textContent = `${progress.completed}/${progress.total}`;
|
count.textContent = `${normalizedProgress.completed}/${normalizedProgress.total}`;
|
||||||
meter.appendChild(count);
|
meter.appendChild(count);
|
||||||
}
|
}
|
||||||
return meter;
|
return meter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPlanProgressElement(tool) {
|
||||||
|
return createPlanProgressElementFromProgress(resolveToolPlanProgress(tool));
|
||||||
|
}
|
||||||
|
|
||||||
function toolSubtitle(tool) {
|
function toolSubtitle(tool) {
|
||||||
if (toolKind(tool) === 'file_change') {
|
if (toolKind(tool) === 'file_change') {
|
||||||
return '';
|
return '';
|
||||||
@@ -6863,6 +6867,7 @@
|
|||||||
if (/^(returned|return)$/.test(normalized)) return 'returned';
|
if (/^(returned|return)$/.test(normalized)) return 'returned';
|
||||||
if (/^(completed|complete|done|finished|finish|success|succeeded)$/.test(normalized)) return 'completed';
|
if (/^(completed|complete|done|finished|finish|success|succeeded)$/.test(normalized)) return 'completed';
|
||||||
if (/^(closed|close|closing|stopped|stop)$/.test(normalized)) return 'closed';
|
if (/^(closed|close|closing|stopped|stop)$/.test(normalized)) return 'closed';
|
||||||
|
if (normalized === 'interrupted') return 'interrupted';
|
||||||
if (/^(failed|fail|error|errored|cancelled|canceled|aborted|rejected)$/.test(normalized)) return 'failed';
|
if (/^(failed|fail|error|errored|cancelled|canceled|aborted|rejected)$/.test(normalized)) return 'failed';
|
||||||
return done ? 'completed' : 'running';
|
return done ? 'completed' : 'running';
|
||||||
}
|
}
|
||||||
@@ -6971,8 +6976,12 @@
|
|||||||
nickname: cleanCollabAgentText(state.nickname || ''),
|
nickname: cleanCollabAgentText(state.nickname || ''),
|
||||||
name: cleanCollabAgentText(state.name || ''),
|
name: cleanCollabAgentText(state.name || ''),
|
||||||
role,
|
role,
|
||||||
|
agentPath: cleanCollabAgentText(state.agentPath || state.agent_path || ''),
|
||||||
status,
|
status,
|
||||||
detail,
|
detail,
|
||||||
|
planProgress: normalizePlanProgress(state.planProgress || state.plan_progress),
|
||||||
|
planCurrentStep: cleanCollabAgentText(state.planCurrentStep || state.plan_current_step || ''),
|
||||||
|
planUpdatedAt: state.planUpdatedAt || state.plan_updated_at || null,
|
||||||
taskDescription,
|
taskDescription,
|
||||||
hasReadableSourceTitle,
|
hasReadableSourceTitle,
|
||||||
};
|
};
|
||||||
@@ -7346,7 +7355,7 @@
|
|||||||
function collabStateTone(statusText) {
|
function collabStateTone(statusText) {
|
||||||
const normalized = String(statusText || '').toLowerCase();
|
const normalized = String(statusText || '').toLowerCase();
|
||||||
if (!normalized) return 'pending';
|
if (!normalized) return 'pending';
|
||||||
if (/(closed|close)/.test(normalized)) return 'closed';
|
if (/(closed|close|interrupted)/.test(normalized)) return 'closed';
|
||||||
if (/(returned|done|completed|success|finished|idle)/.test(normalized)) return 'done';
|
if (/(returned|done|completed|success|finished|idle)/.test(normalized)) return 'done';
|
||||||
if (/(fail|error|cancel|aborted|rejected)/.test(normalized)) return 'error';
|
if (/(fail|error|cancel|aborted|rejected)/.test(normalized)) return 'error';
|
||||||
if (/(running|working|active|inprogress|in_progress|executing)/.test(normalized)) return 'running';
|
if (/(running|working|active|inprogress|in_progress|executing)/.test(normalized)) return 'running';
|
||||||
@@ -7357,6 +7366,7 @@
|
|||||||
const normalized = String(statusText || '').trim();
|
const normalized = String(statusText || '').trim();
|
||||||
if (!normalized) return '等待中';
|
if (!normalized) return '等待中';
|
||||||
const lower = normalized.toLowerCase();
|
const lower = normalized.toLowerCase();
|
||||||
|
if (/interrupted/.test(lower)) return '已中断';
|
||||||
if (/(closed|close)/.test(lower)) return '已关闭';
|
if (/(closed|close)/.test(lower)) return '已关闭';
|
||||||
if (/(returned)/.test(lower)) return '已返回';
|
if (/(returned)/.test(lower)) return '已返回';
|
||||||
if (/(done|completed|success|finished)/.test(lower)) return '已返回';
|
if (/(done|completed|success|finished)/.test(lower)) return '已返回';
|
||||||
@@ -7431,6 +7441,7 @@
|
|||||||
displayTitle,
|
displayTitle,
|
||||||
descriptionTitle,
|
descriptionTitle,
|
||||||
entry.role ? `角色: ${entry.role}` : '',
|
entry.role ? `角色: ${entry.role}` : '',
|
||||||
|
entry.agentPath ? `路径: ${entry.agentPath}` : '',
|
||||||
entry.detail ? `结果: ${entry.detail}` : '',
|
entry.detail ? `结果: ${entry.detail}` : '',
|
||||||
entry.id ? `ID: ${entry.id}` : '',
|
entry.id ? `ID: ${entry.id}` : '',
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
@@ -7465,12 +7476,12 @@
|
|||||||
const closeBtn = document.createElement('button');
|
const closeBtn = document.createElement('button');
|
||||||
closeBtn.type = 'button';
|
closeBtn.type = 'button';
|
||||||
closeBtn.className = 'collab-agent-close-btn';
|
closeBtn.className = 'collab-agent-close-btn';
|
||||||
closeBtn.textContent = '关闭';
|
closeBtn.textContent = '中断';
|
||||||
closeBtn.title = `关闭子代理\n${entry.id}`;
|
closeBtn.title = `中断当前子代理任务\n${entry.id}`;
|
||||||
closeBtn.addEventListener('click', (event) => {
|
closeBtn.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
closeBtn.disabled = true;
|
closeBtn.disabled = true;
|
||||||
closeBtn.textContent = '关闭中';
|
closeBtn.textContent = '中断中';
|
||||||
send({
|
send({
|
||||||
type: 'ccweb_mcp_child_agent_close',
|
type: 'ccweb_mcp_child_agent_close',
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
@@ -7487,10 +7498,28 @@
|
|||||||
description.title = descriptionTitle;
|
description.title = descriptionTitle;
|
||||||
item.appendChild(description);
|
item.appendChild(description);
|
||||||
|
|
||||||
if (entry.id || entry.role) {
|
const planProgress = normalizePlanProgress(entry.planProgress);
|
||||||
|
if (planProgress) {
|
||||||
|
const plan = document.createElement('div');
|
||||||
|
plan.className = 'collab-agent-item-plan';
|
||||||
|
const meter = createPlanProgressElementFromProgress(planProgress, { alwaysShowCount: true });
|
||||||
|
if (meter) plan.appendChild(meter);
|
||||||
|
item.appendChild(plan);
|
||||||
|
|
||||||
|
const currentStepText = cleanCollabAgentText(entry.planCurrentStep);
|
||||||
|
if (currentStepText) {
|
||||||
|
const currentStep = document.createElement('div');
|
||||||
|
currentStep.className = 'collab-agent-item-plan-current';
|
||||||
|
currentStep.textContent = `当前:${currentStepText}`;
|
||||||
|
currentStep.title = currentStepText;
|
||||||
|
item.appendChild(currentStep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.id || entry.role || entry.agentPath) {
|
||||||
const footer = document.createElement('div');
|
const footer = document.createElement('div');
|
||||||
footer.className = 'collab-agent-item-footer';
|
footer.className = 'collab-agent-item-footer';
|
||||||
footer.textContent = entry.role || '';
|
footer.textContent = [entry.role, entry.agentPath].filter(Boolean).join(' · ');
|
||||||
if (!footer.textContent) footer.hidden = true;
|
if (!footer.textContent) footer.hidden = true;
|
||||||
item.appendChild(footer);
|
item.appendChild(footer);
|
||||||
}
|
}
|
||||||
@@ -9019,11 +9048,12 @@
|
|||||||
showOptionPicker(`选择 ${isCodexAppAgent(currentAgent) ? 'Codex App' : 'Codex'} 模型`, baseOptions, current.base || '', (baseValue) => {
|
showOptionPicker(`选择 ${isCodexAppAgent(currentAgent) ? 'Codex App' : 'Codex'} 模型`, baseOptions, current.base || '', (baseValue) => {
|
||||||
const base = String(baseValue || '').trim();
|
const base = String(baseValue || '').trim();
|
||||||
const thinkingOptions = [
|
const thinkingOptions = [
|
||||||
{ value: '', label: '无 (默认)', desc: '不附加 (low/medium/high/xhigh) 后缀' },
|
{ value: '', label: '无 (默认)', desc: '不附加推理强度后缀' },
|
||||||
{ value: 'low', label: 'low', desc: '较轻 thinking' },
|
{ value: 'low', label: 'low', desc: '较轻 thinking' },
|
||||||
{ value: 'medium', label: 'medium', desc: '中等 thinking' },
|
{ value: 'medium', label: 'medium', desc: '中等 thinking' },
|
||||||
{ value: 'high', label: 'high', desc: '更强 thinking' },
|
{ value: 'high', label: 'high', desc: '更强 thinking' },
|
||||||
{ value: 'xhigh', label: 'xhigh', desc: '最强 thinking' },
|
{ value: 'xhigh', label: 'xhigh', desc: '高强度 thinking' },
|
||||||
|
{ value: 'ultra', label: 'ultra', desc: '最高强度 thinking' },
|
||||||
];
|
];
|
||||||
showOptionPicker('选择 Thinking 强度', thinkingOptions, current.level || '', (lvl) => {
|
showOptionPicker('选择 Thinking 强度', thinkingOptions, current.level || '', (lvl) => {
|
||||||
const level = String(lvl || '').trim().toLowerCase();
|
const level = String(lvl || '').trim().toLowerCase();
|
||||||
|
|||||||
@@ -5899,6 +5899,38 @@ html[data-theme='coolvibe'] .settings-back:hover {
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
.collab-agent-item-plan {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.collab-agent-item-plan .plan-progress {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.collab-agent-item-plan .plan-progress-dots {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.collab-agent-item-plan .plan-progress-dot {
|
||||||
|
flex: 0 0 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-left: 0;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
.collab-agent-item-plan .plan-progress-count {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.collab-agent-item-plan-current {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.collab-agent-item-footer {
|
.collab-agent-item-footer {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
|
|||||||
@@ -74,12 +74,15 @@ function retryScenarioKey(text, marker) {
|
|||||||
function collaborationSummary(params = {}) {
|
function collaborationSummary(params = {}) {
|
||||||
const collaborationMode = params.collaborationMode;
|
const collaborationMode = params.collaborationMode;
|
||||||
const settings = collaborationMode?.settings || {};
|
const settings = collaborationMode?.settings || {};
|
||||||
|
const developerInstructions = String(settings.developer_instructions || '');
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
mode: collaborationMode?.mode || null,
|
mode: collaborationMode?.mode || null,
|
||||||
hasModel: Boolean(settings.model),
|
hasModel: Boolean(settings.model),
|
||||||
hasDeveloperInstructions: /Codex sub-agent spawning rules/.test(String(settings.developer_instructions || '')),
|
hasDeveloperInstructions: /Codex sub-agent runtime rules/.test(developerInstructions),
|
||||||
hasWaitAgentRetryGuidance: /wait_agent[\s\S]*timeout_ms[\s\S]*additional wait_agent rounds/.test(String(settings.developer_instructions || '')),
|
hasSchemaDrivenSubagents: /current runtime tool schema/.test(developerInstructions),
|
||||||
|
hasLegacyV1Guidance: /fork_context|additional wait_agent rounds/.test(developerInstructions),
|
||||||
hasReasoningEffort: Object.prototype.hasOwnProperty.call(settings, 'reasoning_effort'),
|
hasReasoningEffort: Object.prototype.hasOwnProperty.call(settings, 'reasoning_effort'),
|
||||||
|
reasoningEffort: settings.reasoning_effort || null,
|
||||||
hasTopLevelModel: Object.prototype.hasOwnProperty.call(params, 'model'),
|
hasTopLevelModel: Object.prototype.hasOwnProperty.call(params, 'model'),
|
||||||
hasTopLevelEffort: Object.prototype.hasOwnProperty.call(params, 'effort'),
|
hasTopLevelEffort: Object.prototype.hasOwnProperty.call(params, 'effort'),
|
||||||
});
|
});
|
||||||
@@ -167,6 +170,85 @@ function emitChildCollabTurn(threadId, turnId, finalMessage) {
|
|||||||
childThread.activeTurnId = null;
|
childThread.activeTurnId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emitNestedChildCollabTurn() {
|
||||||
|
const threadId = 'child-thread-v2';
|
||||||
|
const turnId = 'child-turn-v2';
|
||||||
|
const grandchildThreadId = 'grandchild-thread-v2';
|
||||||
|
const childThread = ensureThread(threadId);
|
||||||
|
childThread.activeTurnId = turnId;
|
||||||
|
send({
|
||||||
|
method: 'turn/started',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turn: { id: turnId, status: 'running', items: [] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'turn/plan/updated',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turnId,
|
||||||
|
plan: [
|
||||||
|
{ step: '定位子线程路由', status: 'completed' },
|
||||||
|
{ step: '同步父卡片进度', status: 'in_progress' },
|
||||||
|
{ step: '完成集成回归', status: 'pending' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'item/agentMessage/delta',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turnId,
|
||||||
|
itemId: 'child-agent-msg-v2',
|
||||||
|
delta: 'V2 子代理最终消息:父级子代理路由正常。',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'item/completed',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turnId,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'grandchild-activity-v2',
|
||||||
|
type: 'subAgentActivity',
|
||||||
|
kind: 'started',
|
||||||
|
agentThreadId: grandchildThreadId,
|
||||||
|
agentPath: '/root/v2_parent/v2_grandchild',
|
||||||
|
prompt: '验证 Multi-agent V2 嵌套子代理路由。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
emitChildCollabTurn(
|
||||||
|
grandchildThreadId,
|
||||||
|
'grandchild-turn-v2',
|
||||||
|
'V2 孙代理最终消息:嵌套路由正常。'
|
||||||
|
);
|
||||||
|
send({
|
||||||
|
method: 'item/completed',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turnId,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'child-agent-msg-v2',
|
||||||
|
type: 'agentMessage',
|
||||||
|
content: [{ type: 'text', text: 'V2 子代理最终消息:父级子代理路由正常。' }],
|
||||||
|
status: 'completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'turn/completed',
|
||||||
|
params: {
|
||||||
|
threadId,
|
||||||
|
turn: { id: turnId, status: 'completed', items: [] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
childThread.activeTurnId = null;
|
||||||
|
}
|
||||||
|
|
||||||
function completeTurn(thread, turnId, text, status = 'completed') {
|
function completeTurn(thread, turnId, text, status = 'completed') {
|
||||||
if (thread.activeTurnId !== turnId) return;
|
if (thread.activeTurnId !== turnId) return;
|
||||||
const suffix = thread.steers.length > 0 ? ` | steer: ${thread.steers.join(' | ')}` : '';
|
const suffix = thread.steers.length > 0 ? ` | steer: ${thread.steers.join(' | ')}` : '';
|
||||||
@@ -267,7 +349,57 @@ function completeTurn(thread, turnId, text, status = 'completed') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/subagent/i.test(text)) {
|
if (/subagent v2/i.test(text)) {
|
||||||
|
send({
|
||||||
|
method: 'item/completed',
|
||||||
|
params: {
|
||||||
|
threadId: thread.id,
|
||||||
|
turnId,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'child-activity-v2',
|
||||||
|
type: 'subAgentActivity',
|
||||||
|
kind: 'started',
|
||||||
|
agentThreadId: 'child-thread-v2',
|
||||||
|
agentPath: '/root/v2_parent',
|
||||||
|
prompt: '验证 Multi-agent V2 子代理路由。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
emitNestedChildCollabTurn();
|
||||||
|
send({
|
||||||
|
method: 'item/started',
|
||||||
|
params: {
|
||||||
|
threadId: thread.id,
|
||||||
|
turnId,
|
||||||
|
startedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'wait-empty-v2',
|
||||||
|
type: 'collabAgentToolCall',
|
||||||
|
tool: 'wait_agent',
|
||||||
|
receiverThreadIds: [],
|
||||||
|
agentsStates: {},
|
||||||
|
status: 'inProgress',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
send({
|
||||||
|
method: 'item/completed',
|
||||||
|
params: {
|
||||||
|
threadId: thread.id,
|
||||||
|
turnId,
|
||||||
|
completedAtMs: Date.now(),
|
||||||
|
item: {
|
||||||
|
id: 'wait-empty-v2',
|
||||||
|
type: 'collabAgentToolCall',
|
||||||
|
tool: 'wait_agent',
|
||||||
|
receiverThreadIds: [],
|
||||||
|
agentsStates: {},
|
||||||
|
status: 'completed',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (/subagent/i.test(text)) {
|
||||||
send({
|
send({
|
||||||
method: 'item/started',
|
method: 'item/started',
|
||||||
params: {
|
params: {
|
||||||
@@ -331,7 +463,7 @@ function completeTurn(thread, turnId, text, status = 'completed') {
|
|||||||
emitChildCollabTurn(
|
emitChildCollabTurn(
|
||||||
'child-thread-a',
|
'child-thread-a',
|
||||||
'child-turn-a',
|
'child-turn-a',
|
||||||
'子代理最终消息:结构化渲染和关闭按钮链路已完成。'
|
'子代理最终消息:结构化渲染和中断按钮链路已完成。'
|
||||||
);
|
);
|
||||||
emitChildCollabTurn(
|
emitChildCollabTurn(
|
||||||
'child-thread-b',
|
'child-thread-b',
|
||||||
|
|||||||
@@ -882,12 +882,12 @@ function assertPlanListProgressContract() {
|
|||||||
assert(result.progress?.completed === 3 && result.progress?.total === 5, 'Persisted todo result should expose 3/5 progress');
|
assert(result.progress?.completed === 3 && result.progress?.total === 5, 'Persisted todo result should expose 3/5 progress');
|
||||||
|
|
||||||
const summarySource = extractFunctionSource(source, 'applyToolSummary');
|
const summarySource = extractFunctionSource(source, 'applyToolSummary');
|
||||||
const progressElementSource = extractFunctionSource(source, 'createPlanProgressElement');
|
const progressElementSource = extractFunctionSource(source, 'createPlanProgressElementFromProgress');
|
||||||
assert(summarySource.includes('createPlanProgressElement(tool)'), 'Tool summaries should append the plan progress element beside the title');
|
assert(summarySource.includes('createPlanProgressElement(tool)'), 'Tool summaries should append the plan progress element beside the title');
|
||||||
assert(progressElementSource.includes("meter.setAttribute('role', 'img')"), 'Plan progress should expose an accessible image role');
|
assert(progressElementSource.includes("meter.setAttribute('role', 'img')"), 'Plan progress should expose an accessible image role');
|
||||||
assert(progressElementSource.includes("meter.setAttribute('aria-label', progressLabel)"), 'Plan progress should announce completed and total counts');
|
assert(progressElementSource.includes("meter.setAttribute('aria-label', progressLabel)"), 'Plan progress should announce completed and total counts');
|
||||||
assert(progressElementSource.includes('Math.min(progress.total, 12)'), 'Long plans should cap visible dots to protect the header layout');
|
assert(progressElementSource.includes('Math.min(normalizedProgress.total, 12)'), 'Long plans should cap visible dots to protect the header layout');
|
||||||
assert(progressElementSource.includes("count.textContent = `${progress.completed}/${progress.total}`"), 'Compacted long plans should keep an exact numeric count');
|
assert(progressElementSource.includes("count.textContent = `${normalizedProgress.completed}/${normalizedProgress.total}`"), 'Compacted long plans should keep an exact numeric count');
|
||||||
|
|
||||||
assert(styleSource.includes('--plan-progress-complete: var(--success);'), 'Plan progress should inherit the active theme success color');
|
assert(styleSource.includes('--plan-progress-complete: var(--success);'), 'Plan progress should inherit the active theme success color');
|
||||||
assert(styleSource.includes('--plan-progress-remaining: var(--accent);'), 'Remaining plan progress should inherit the active theme accent color');
|
assert(styleSource.includes('--plan-progress-remaining: var(--accent);'), 'Remaining plan progress should inherit the active theme accent color');
|
||||||
@@ -1508,9 +1508,15 @@ function assertFrontendSubagentCardMetadataContract() {
|
|||||||
assert(/mergeCollabAgentTaskState\(\s*states\[id\]/.test(source), 'Receiver-only child states should use the tested metadata merge helper');
|
assert(/mergeCollabAgentTaskState\(\s*states\[id\]/.test(source), 'Receiver-only child states should use the tested metadata merge helper');
|
||||||
assert(source.includes('entry.detail ? `结果: ${entry.detail}` :'), 'Card title should keep runtime result in the container title');
|
assert(source.includes('entry.detail ? `结果: ${entry.detail}` :'), 'Card title should keep runtime result in the container title');
|
||||||
assert(source.includes("description.className = 'collab-agent-item-description'"), 'Sub-agent cards should render a visible task intro node');
|
assert(source.includes("description.className = 'collab-agent-item-description'"), 'Sub-agent cards should render a visible task intro node');
|
||||||
|
assert(source.includes('function createPlanProgressElementFromProgress(progress'), 'Frontend should expose reusable plan progress rendering for sub-agent cards');
|
||||||
|
assert(source.includes("plan.className = 'collab-agent-item-plan'"), 'Sub-agent cards should render a compact plan progress row');
|
||||||
|
assert(source.includes("currentStep.className = 'collab-agent-item-plan-current'"), 'Sub-agent cards should render the current plan step');
|
||||||
assert(source.includes('description.title = descriptionTitle'), 'Task intro node should expose the full task intro or fallback in its title attribute');
|
assert(source.includes('description.title = descriptionTitle'), 'Task intro node should expose the full task intro or fallback in its title attribute');
|
||||||
assert(source.includes('label.textContent = displayTitle;'), 'Rendered card label should use normalized title selection');
|
assert(source.includes('label.textContent = displayTitle;'), 'Rendered card label should use normalized title selection');
|
||||||
assert(/\.collab-agent-item-description\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/.test(styleSource), 'Sub-agent task intro should use two-line truncation');
|
assert(/\.collab-agent-item-description\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/.test(styleSource), 'Sub-agent task intro should use two-line truncation');
|
||||||
|
assert(/\.collab-agent-item-plan\s*\{[\s\S]*?display:\s*flex;/.test(styleSource), 'Sub-agent plan progress should use a compact flex row');
|
||||||
|
assert(/\.collab-agent-item-plan \.plan-progress-dot\s*\{[\s\S]*?width:\s*8px;[\s\S]*?height:\s*8px;[\s\S]*?margin-left:\s*0;/.test(styleSource), 'Sub-agent plan dots should remain compact inside narrow cards');
|
||||||
|
assert(/\.collab-agent-item-plan-current\s*\{[\s\S]*?text-overflow:\s*ellipsis;/.test(styleSource), 'Sub-agent current plan step should truncate safely');
|
||||||
assert(/\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-direction:\s*column;/.test(styleSource), 'Sub-agent cards should be vertically composed and flex-shrink on narrow screens');
|
assert(/\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-direction:\s*column;/.test(styleSource), 'Sub-agent cards should be vertically composed and flex-shrink on narrow screens');
|
||||||
assert(/@media \(max-width:\s*640px\)[\s\S]*?\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?\}/.test(styleSource), 'Narrow screens should let sub-agent cards shrink without horizontal overflow');
|
assert(/@media \(max-width:\s*640px\)[\s\S]*?\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?\}/.test(styleSource), 'Narrow screens should let sub-agent cards shrink without horizontal overflow');
|
||||||
|
|
||||||
@@ -1666,6 +1672,33 @@ function assertFrontendSubagentCardMetadataContract() {
|
|||||||
assert(spawnedMerge.input.receiverThreadIds.length === 1, 'Spawned child should merge into one visible agent');
|
assert(spawnedMerge.input.receiverThreadIds.length === 1, 'Spawned child should merge into one visible agent');
|
||||||
assert(collabApi.getCachedState('child-thread-a')?.label === '关闭验证代理', 'Structured child state should be cached by thread id');
|
assert(collabApi.getCachedState('child-thread-a')?.label === '关闭验证代理', 'Structured child state should be cached by thread id');
|
||||||
|
|
||||||
|
const planCardTool = {
|
||||||
|
...spawnedTool,
|
||||||
|
id: 'tool-plan-progress',
|
||||||
|
input: {
|
||||||
|
...spawnedTool.input,
|
||||||
|
receiverThreadIds: ['child-thread-plan'],
|
||||||
|
agentsStates: {
|
||||||
|
'child-thread-plan': {
|
||||||
|
title: '进度验证代理',
|
||||||
|
taskDescription: '验证子代理计划简报。',
|
||||||
|
status: 'running',
|
||||||
|
planProgress: { completed: 1, total: 3 },
|
||||||
|
planCurrentStep: '同步父卡片进度',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const planCardElement = collabApi.renderCollabAgentToolElement(planCardTool);
|
||||||
|
const planRows = findNodesByClass(planCardElement, 'collab-agent-item-plan');
|
||||||
|
const planDots = findNodesByClass(planCardElement, 'plan-progress-dot');
|
||||||
|
const planCounts = findNodesByClass(planCardElement, 'plan-progress-count');
|
||||||
|
const planCurrentSteps = findNodesByClass(planCardElement, 'collab-agent-item-plan-current');
|
||||||
|
assert(planRows.length === 1, 'Sub-agent plan summary should render one compact progress row');
|
||||||
|
assert(planDots.length === 3, 'Sub-agent plan summary should render one progress dot per short plan item');
|
||||||
|
assert(planCounts.length === 1 && planCounts[0].textContent === '1/3', 'Sub-agent plan summary should always render completed/total');
|
||||||
|
assert(planCurrentSteps.length === 1 && planCurrentSteps[0].textContent === '当前:同步父卡片进度', 'Sub-agent plan summary should render the current in-progress step');
|
||||||
|
|
||||||
collabApi.rememberCollabAgentState(
|
collabApi.rememberCollabAgentState(
|
||||||
'unrelated-child',
|
'unrelated-child',
|
||||||
{ title: '无关历史代理', status: 'running' },
|
{ title: '无关历史代理', status: 'running' },
|
||||||
@@ -1974,6 +2007,20 @@ function assertCodexAppRuntimeSubAgentActivityContract() {
|
|||||||
fullText: '',
|
fullText: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert(typeof runtime.planUpdateFromNotification === 'function', 'Runtime should expose shared plan notification parsing');
|
||||||
|
const childPlan = runtime.planUpdateFromNotification({
|
||||||
|
method: 'turn/plan/updated',
|
||||||
|
params: {
|
||||||
|
plan: [
|
||||||
|
{ step: '解析子代理计划', status: 'completed' },
|
||||||
|
{ step: '同步父卡片进度', status: 'in_progress' },
|
||||||
|
{ step: '补充回归验证', status: 'pending' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert(childPlan?.progress?.completed === 1 && childPlan?.progress?.total === 3, 'Shared plan parser should summarize child plan progress');
|
||||||
|
assert(childPlan?.currentStep === '同步父卡片进度', 'Shared plan parser should expose the current in-progress step');
|
||||||
|
|
||||||
runtime.processCodexAppNotification(entry, {
|
runtime.processCodexAppNotification(entry, {
|
||||||
method: 'item/started',
|
method: 'item/started',
|
||||||
params: {
|
params: {
|
||||||
@@ -3423,6 +3470,47 @@ function assertCodexAppUnroutedNotificationRoutingContract() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertMultiAgentV2CompatibilityContract() {
|
||||||
|
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
|
||||||
|
const runtimeSource = fs.readFileSync(path.join(REPO_DIR, 'lib', 'agent-runtime.js'), 'utf8');
|
||||||
|
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/CODEX_REASONING_LEVELS\s*=\s*new Set\(\[[^\]]*'ultra'/.test(serverSource),
|
||||||
|
'Codex config model parsing should accept the ultra reasoning level'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
/low\|medium\|high\|xhigh\|ultra/.test(runtimeSource),
|
||||||
|
'Codex CLI model suffix parsing should accept ultra'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
frontendSource.includes("{ value: 'ultra', label: 'ultra'"),
|
||||||
|
'Codex model picker should expose ultra reasoning'
|
||||||
|
);
|
||||||
|
const instructionsBlock = serverSource.slice(
|
||||||
|
serverSource.indexOf('const CODEX_APP_COLLABORATION_INSTRUCTIONS'),
|
||||||
|
serverSource.indexOf('function getLocalCodexConfigTomlPath')
|
||||||
|
);
|
||||||
|
assert(instructionsBlock.includes('current runtime tool schema'), 'Sub-agent guidance should defer to the current runtime tool schema');
|
||||||
|
assert(!instructionsBlock.includes('fork_context'), 'Sub-agent guidance should not hard-code the V1 fork_context field');
|
||||||
|
assert(!instructionsBlock.includes('additional wait_agent rounds'), 'Sub-agent guidance should not impose obsolete repeated wait_agent calls');
|
||||||
|
|
||||||
|
const syncBlock = extractFunctionSource(serverSource, 'syncCcwebMcpChildAgentsFromCollabItem');
|
||||||
|
assert(syncBlock.includes("itemType === 'subAgentActivity'"), 'Child routing should register canonical V2 subAgentActivity items');
|
||||||
|
const notificationBlock = extractFunctionSource(serverSource, 'handleCodexAppNotification');
|
||||||
|
const activitySyncIndex = notificationBlock.indexOf('syncCcwebMcpChildAgentsFromCollabItem(routed, item)');
|
||||||
|
const childReturnIndex = notificationBlock.indexOf("if (routed.role === 'child')");
|
||||||
|
assert(activitySyncIndex >= 0 && activitySyncIndex < childReturnIndex, 'Nested subAgentActivity registration should happen before child notification routing returns');
|
||||||
|
const childNotificationBlock = extractFunctionSource(serverSource, 'processCcwebMcpChildNotification');
|
||||||
|
assert(childNotificationBlock.includes('codexAppRuntime.planUpdateFromNotification(notification)'), 'Child notification routing should reuse runtime plan parsing');
|
||||||
|
const recoveryBlock = extractFunctionSource(serverSource, 'recoverCcwebMcpChildThreadsFromPersistedToolCalls');
|
||||||
|
assert(recoveryBlock.includes('parseMaybeJsonObject(tool?.result)'), 'Child recovery should read the latest persisted collaboration tool result');
|
||||||
|
assert(recoveryBlock.includes('recoveredState.planProgress'), 'Child recovery should restore persisted plan progress');
|
||||||
|
|
||||||
|
assert(frontendSource.includes("closeBtn.textContent = '中断';"), 'Sub-agent action should use interrupt semantics');
|
||||||
|
assert(frontendSource.includes('entry.agentPath ? `路径: ${entry.agentPath}`'), 'Sub-agent cards should expose the canonical agent path');
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const targetIndex = process.argv.indexOf('--target');
|
const targetIndex = process.argv.indexOf('--target');
|
||||||
const regressionTarget = targetIndex >= 0 ? String(process.argv[targetIndex + 1] || '').trim() : String(process.env.CC_WEB_REGRESSION_TARGET || '').trim();
|
const regressionTarget = targetIndex >= 0 ? String(process.argv[targetIndex + 1] || '').trim() : String(process.env.CC_WEB_REGRESSION_TARGET || '').trim();
|
||||||
@@ -3440,6 +3528,7 @@ async function main() {
|
|||||||
if (regressionTarget === 'subagent-card-metadata') {
|
if (regressionTarget === 'subagent-card-metadata') {
|
||||||
assertFrontendSubagentCardMetadataContract();
|
assertFrontendSubagentCardMetadataContract();
|
||||||
assertCodexAppRuntimeSubAgentActivityContract();
|
assertCodexAppRuntimeSubAgentActivityContract();
|
||||||
|
assertMultiAgentV2CompatibilityContract();
|
||||||
console.log('Subagent card metadata regression checks passed.');
|
console.log('Subagent card metadata regression checks passed.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3503,6 +3592,7 @@ async function main() {
|
|||||||
assertSessionSwitchResilienceContract();
|
assertSessionSwitchResilienceContract();
|
||||||
assertSessionSwitchRaceContract();
|
assertSessionSwitchRaceContract();
|
||||||
assertCodexAppChildToolFallbackContract();
|
assertCodexAppChildToolFallbackContract();
|
||||||
|
assertMultiAgentV2CompatibilityContract();
|
||||||
|
|
||||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
|
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
|
||||||
const configDir = path.join(tempRoot, 'config');
|
const configDir = path.join(tempRoot, 'config');
|
||||||
@@ -3580,7 +3670,7 @@ async function main() {
|
|||||||
}, null, 2));
|
}, null, 2));
|
||||||
|
|
||||||
createFakeClaudeHistory(homeDir);
|
createFakeClaudeHistory(homeDir);
|
||||||
createFakeCodexConfig(homeDir);
|
createFakeCodexConfig(homeDir, { reasoningEffort: 'ultra' });
|
||||||
const codexFixture = createFakeCodexHistory(homeDir);
|
const codexFixture = createFakeCodexHistory(homeDir);
|
||||||
const codexAppImportFixture = createFakeCodexHistory(homeDir, {
|
const codexAppImportFixture = createFakeCodexHistory(homeDir, {
|
||||||
threadId: 'codexapp-import-thread',
|
threadId: 'codexapp-import-thread',
|
||||||
@@ -3730,7 +3820,7 @@ async function main() {
|
|||||||
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: codexInitCwd, mode: 'plan' }));
|
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: codexInitCwd, mode: 'plan' }));
|
||||||
const codexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === codexInitCwd);
|
const codexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === codexInitCwd);
|
||||||
assert(codexSession.mode === 'plan', 'Codex new_session should follow requested mode');
|
assert(codexSession.mode === 'plan', 'Codex new_session should follow requested mode');
|
||||||
assert(codexSession.model === 'gpt-5.5(xhigh)', 'Codex new_session should read default model from ~/.codex/config.toml');
|
assert(codexSession.model === 'gpt-5.5(ultra)', 'Codex new_session should preserve ultra from ~/.codex/config.toml');
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'set_session_pinned', sessionId: codexSession.sessionId, pinned: true }));
|
ws.send(JSON.stringify({ type: 'set_session_pinned', sessionId: codexSession.sessionId, pinned: true }));
|
||||||
const pinnedAck = await nextMessage(messages, ws, (msg) => msg.type === 'session_pinned' && msg.sessionId === codexSession.sessionId);
|
const pinnedAck = await nextMessage(messages, ws, (msg) => msg.type === 'session_pinned' && msg.sessionId === codexSession.sessionId);
|
||||||
@@ -4319,6 +4409,8 @@ async function main() {
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(firstMessageSession.sessionId.slice(0, 8)));
|
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(firstMessageSession.sessionId.slice(0, 8)));
|
||||||
assert(spawnLine && !spawnLine.includes('--search') && spawnLine.includes('--image'), 'Codex exec should attach images and not append unsupported --search flag');
|
assert(spawnLine && !spawnLine.includes('--search') && spawnLine.includes('--image'), 'Codex exec should attach images and not append unsupported --search flag');
|
||||||
|
const parsedSpawnLine = JSON.parse(spawnLine);
|
||||||
|
assert(parsedSpawnLine.args.includes('model_reasoning_effort="ultra"'), 'Codex exec should pass the ultra reasoning level through model_reasoning_effort');
|
||||||
|
|
||||||
const allSpawnsForSession = processLog
|
const allSpawnsForSession = processLog
|
||||||
.trim()
|
.trim()
|
||||||
@@ -4383,7 +4475,7 @@ async function main() {
|
|||||||
].join('\n'));
|
].join('\n'));
|
||||||
ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: codexAppCwd, mode: 'yolo' }));
|
ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: codexAppCwd, mode: 'yolo' }));
|
||||||
const codexAppSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppCwd);
|
const codexAppSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppCwd);
|
||||||
assert(codexAppSession.model === 'gpt-5.5(xhigh)', 'Codex App new_session should read default Codex model');
|
assert(codexAppSession.model === 'gpt-5.5(ultra)', 'Codex App new_session should preserve the ultra default Codex model');
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-skill', trigger: '$', query: 'reg', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-skill', trigger: '$', query: 'reg', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
|
||||||
const codexAppSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-skill');
|
const codexAppSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-skill');
|
||||||
@@ -4397,7 +4489,9 @@ async function main() {
|
|||||||
assert(/"mode":"default"/.test(codexAppDefaultCollab.text || ''), 'Codex App YOLO mode should pass default collaboration mode');
|
assert(/"mode":"default"/.test(codexAppDefaultCollab.text || ''), 'Codex App YOLO mode should pass default collaboration mode');
|
||||||
assert(/"hasModel":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include model');
|
assert(/"hasModel":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include model');
|
||||||
assert(/"hasDeveloperInstructions":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include sub-agent developer instructions');
|
assert(/"hasDeveloperInstructions":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include sub-agent developer instructions');
|
||||||
assert(/"hasWaitAgentRetryGuidance":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include wait_agent retry guidance');
|
assert(/"hasSchemaDrivenSubagents":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should use runtime-schema-driven sub-agent guidance');
|
||||||
|
assert(/"hasLegacyV1Guidance":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should omit legacy V1 fork/wait guidance');
|
||||||
|
assert(/"reasoningEffort":"ultra"/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should pass ultra reasoning_effort');
|
||||||
assert(/"hasTopLevelModel":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate model at top level');
|
assert(/"hasTopLevelModel":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate model at top level');
|
||||||
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
|
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
|
||||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
@@ -4775,13 +4869,13 @@ async function main() {
|
|||||||
assert(/finalMessage/.test(ccwebMcpChildReturned.tool?.result || ''), 'ccweb MCP child final message should be merged into the parent tool result');
|
assert(/finalMessage/.test(ccwebMcpChildReturned.tool?.result || ''), 'ccweb MCP child final message should be merged into the parent tool result');
|
||||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
ws.send(JSON.stringify({ type: 'ccweb_mcp_child_agent_close', sessionId: codexAppSession.sessionId, threadId: 'child-thread-a' }));
|
ws.send(JSON.stringify({ type: 'ccweb_mcp_child_agent_close', sessionId: codexAppSession.sessionId, threadId: 'child-thread-a' }));
|
||||||
const ccwebMcpChildClosed = await nextMessage(messages, ws, (msg) =>
|
const ccwebMcpChildInterrupted = await nextMessage(messages, ws, (msg) =>
|
||||||
msg.type === 'ccweb_mcp_child_agent_update' &&
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
msg.sessionId === codexAppSession.sessionId &&
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
msg.child?.threadId === 'child-thread-a' &&
|
msg.child?.threadId === 'child-thread-a' &&
|
||||||
msg.child?.status === 'closed'
|
msg.child?.status === 'interrupted'
|
||||||
);
|
);
|
||||||
assert(/"status": "closed"/.test(ccwebMcpChildClosed.tool?.result || ''), 'ccweb MCP child close should update the parent collab tool state');
|
assert(/"status": "interrupted"/.test(ccwebMcpChildInterrupted.tool?.result || ''), 'ccweb MCP child interrupt should update the parent collab tool state');
|
||||||
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||||
const hasCollabTool = storedCodexApp.messages
|
const hasCollabTool = storedCodexApp.messages
|
||||||
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
||||||
@@ -4791,13 +4885,76 @@ async function main() {
|
|||||||
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
||||||
.reverse()
|
.reverse()
|
||||||
.find((tool) => tool.id === 'tool-collab');
|
.find((tool) => tool.id === 'tool-collab');
|
||||||
assert(/"status": "closed"/.test(persistedClosedCollabTool?.result || ''), 'ccweb MCP manual child close should persist closed state');
|
assert(/"status": "interrupted"/.test(persistedClosedCollabTool?.result || ''), 'ccweb MCP manual child interrupt should persist interrupted state');
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({ type: 'message', text: 'codexapp subagent v2 prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
|
||||||
|
const codexAppV2ChildStarted = await nextMessage(messages, ws, (msg) =>
|
||||||
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
|
msg.child?.threadId === 'child-thread-v2' &&
|
||||||
|
msg.child?.status === 'running'
|
||||||
|
);
|
||||||
|
assert(codexAppV2ChildStarted.toolUseId === 'child-activity-v2', 'V2 child update should reference its subAgentActivity item');
|
||||||
|
assert(codexAppV2ChildStarted.child.agentPath === '/root/v2_parent', 'V2 child update should preserve its canonical agentPath');
|
||||||
|
const codexAppV2ChildPlan = await nextMessage(messages, ws, (msg) =>
|
||||||
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
|
msg.child?.threadId === 'child-thread-v2' &&
|
||||||
|
msg.child?.planProgress?.completed === 1 &&
|
||||||
|
msg.child?.planProgress?.total === 3
|
||||||
|
);
|
||||||
|
assert(codexAppV2ChildPlan.child.planCurrentStep === '同步父卡片进度', 'V2 child update should expose the current in-progress plan step');
|
||||||
|
const codexAppV2ChildPlanToolResult = JSON.parse(codexAppV2ChildPlan.tool?.result || '{}');
|
||||||
|
assert(
|
||||||
|
codexAppV2ChildPlanToolResult.agentsStates?.['child-thread-v2']?.planProgress?.completed === 1,
|
||||||
|
'V2 child plan progress should merge into the visible parent collaboration tool'
|
||||||
|
);
|
||||||
|
const codexAppV2GrandchildStarted = await nextMessage(messages, ws, (msg) =>
|
||||||
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
|
msg.child?.threadId === 'grandchild-thread-v2' &&
|
||||||
|
msg.child?.status === 'running'
|
||||||
|
);
|
||||||
|
assert(codexAppV2GrandchildStarted.child.parentThreadId === 'child-thread-v2', 'Nested V2 child should retain the immediate child as parentThreadId');
|
||||||
|
assert(codexAppV2GrandchildStarted.child.agentPath === '/root/v2_parent/v2_grandchild', 'Nested V2 child should preserve its canonical agentPath');
|
||||||
|
const codexAppV2GrandchildReturned = await nextMessage(messages, ws, (msg) =>
|
||||||
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
|
msg.child?.threadId === 'grandchild-thread-v2' &&
|
||||||
|
msg.child?.status === 'returned' &&
|
||||||
|
/V2 孙代理最终消息/.test(msg.child?.candidateResult || '')
|
||||||
|
);
|
||||||
|
assert(/grandchild-thread-v2/.test(codexAppV2GrandchildReturned.tool?.result || ''), 'Nested V2 child result should merge into a visible collaboration tool');
|
||||||
|
const codexAppV2ChildReturned = await nextMessage(messages, ws, (msg) =>
|
||||||
|
msg.type === 'ccweb_mcp_child_agent_update' &&
|
||||||
|
msg.sessionId === codexAppSession.sessionId &&
|
||||||
|
msg.child?.threadId === 'child-thread-v2' &&
|
||||||
|
msg.child?.status === 'returned' &&
|
||||||
|
/V2 子代理最终消息/.test(msg.child?.candidateResult || '')
|
||||||
|
);
|
||||||
|
assert(codexAppV2ChildReturned.child.parentThreadId, 'V2 child should retain the root parent thread id');
|
||||||
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
|
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
|
||||||
|
const persistedV2ChildTool = storedCodexApp.messages
|
||||||
|
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
|
||||||
|
.find((tool) => tool.id === 'child-activity-v2');
|
||||||
|
const persistedV2ChildResult = JSON.parse(persistedV2ChildTool?.result || '{}');
|
||||||
|
assert(
|
||||||
|
persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planProgress?.completed === 1
|
||||||
|
&& persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planProgress?.total === 3,
|
||||||
|
'V2 child plan progress should persist in the session collaboration tool for refresh recovery'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planCurrentStep === '同步父卡片进度',
|
||||||
|
'V2 child current plan step should persist for refresh recovery'
|
||||||
|
);
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration plan probe', sessionId: codexAppSession.sessionId, mode: 'plan', agent: 'codexapp' }));
|
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration plan probe', sessionId: codexAppSession.sessionId, mode: 'plan', agent: 'codexapp' }));
|
||||||
const codexAppPlanCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
|
const codexAppPlanCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
|
||||||
assert(/"mode":"plan"/.test(codexAppPlanCollab.text || ''), 'Codex App Plan mode should pass plan collaboration mode');
|
assert(/"mode":"plan"/.test(codexAppPlanCollab.text || ''), 'Codex App Plan mode should pass plan collaboration mode');
|
||||||
assert(/"hasDeveloperInstructions":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep sub-agent developer instructions');
|
assert(/"hasDeveloperInstructions":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep sub-agent developer instructions');
|
||||||
assert(/"hasWaitAgentRetryGuidance":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep wait_agent retry guidance');
|
assert(/"hasSchemaDrivenSubagents":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep runtime-schema-driven guidance');
|
||||||
|
assert(/"hasLegacyV1Guidance":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should omit legacy V1 guidance');
|
||||||
assert(/"hasTopLevelModel":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate model at top level');
|
assert(/"hasTopLevelModel":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate model at top level');
|
||||||
assert(/"hasTopLevelEffort":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate effort at top level');
|
assert(/"hasTopLevelEffort":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate effort at top level');
|
||||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
|
||||||
|
|||||||
147
server.js
147
server.js
@@ -725,20 +725,16 @@ const MCP_PROMPT_RESPONSE_MAX_CHARS = 20000;
|
|||||||
|
|
||||||
// Codex 默认模型优先读取 ~/.codex/config.toml,缺失时再回退到旧默认值。
|
// Codex 默认模型优先读取 ~/.codex/config.toml,缺失时再回退到旧默认值。
|
||||||
const FALLBACK_CODEX_MODEL = 'gpt-5.4';
|
const FALLBACK_CODEX_MODEL = 'gpt-5.4';
|
||||||
const CODEX_REASONING_LEVELS = new Set(['low', 'medium', 'high', 'xhigh']);
|
const CODEX_REASONING_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'ultra']);
|
||||||
const CCWEB_TITLE_TOOL_INSTRUCTIONS = [
|
const CCWEB_TITLE_TOOL_INSTRUCTIONS = [
|
||||||
'Use the ccweb title tool sparingly. For a new chat, call "mcp__ccweb__ccweb_set_title" or "ccweb_set_title" once after the user\'s initial request is clear, and set a concise task title.',
|
'Use the ccweb title tool sparingly. For a new chat, call "mcp__ccweb__ccweb_set_title" or "ccweb_set_title" once after the user\'s initial request is clear, and set a concise task title.',
|
||||||
'Do not rename the chat for routine progress, substeps, implementation details, or slightly better wording. Rename only when the user\'s primary objective changes substantially and the existing title would be misleading.',
|
'Do not rename the chat for routine progress, substeps, implementation details, or slightly better wording. Rename only when the user\'s primary objective changes substantially and the existing title would be misleading.',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
const CODEX_APP_COLLABORATION_INSTRUCTIONS = [
|
const CODEX_APP_COLLABORATION_INSTRUCTIONS = [
|
||||||
CCWEB_TITLE_TOOL_INSTRUCTIONS,
|
CCWEB_TITLE_TOOL_INSTRUCTIONS,
|
||||||
'Codex sub-agent spawning rules:',
|
'Codex sub-agent runtime rules:',
|
||||||
'- Treat omitted fork_context the same as fork_context: true: a full-history fork inherits the parent agent type, model, and reasoning effort.',
|
'- Follow the current runtime tool schema and tool descriptions; do not assume optional fields exist.',
|
||||||
'- If you call spawn_agent with fork_context omitted or true, do not set agent_type, model, or reasoning_effort.',
|
'- Do not hard-code version-specific spawn, wait, or completion behavior in prompts.',
|
||||||
'- If you need a specific agent_type, model, or reasoning_effort, set fork_context: false and include only the necessary context in the message.',
|
|
||||||
'- Do not rely on parent turn reasoning settings for spawned agents; only set reasoning_effort on spawn_agent when the chosen child model supports it.',
|
|
||||||
'- When calling wait_agent, always pass timeout_ms explicitly. Use 300000ms for normal waits, and use a longer value when the child task is expected to run longer.',
|
|
||||||
'- If wait_agent returns without a final child-agent status, keep waiting on the same target agents in additional wait_agent rounds until they return, the user interrupts, or the result is no longer needed.',
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
function getLocalCodexConfigTomlPath() {
|
function getLocalCodexConfigTomlPath() {
|
||||||
@@ -7963,14 +7959,14 @@ function closeCcwebMcpChildAgent(sessionId, childThreadId, options = {}) {
|
|||||||
}
|
}
|
||||||
const child = ccwebMcpChildThreads.get(normalizedThreadId);
|
const child = ccwebMcpChildThreads.get(normalizedThreadId);
|
||||||
if (!child || child.parentSessionId !== normalizedSessionId) {
|
if (!child || child.parentSessionId !== normalizedSessionId) {
|
||||||
return { ok: false, code: 'child_agent_not_found', message: '未找到可关闭的 ccweb MCP 子代理。' };
|
return { ok: false, code: 'child_agent_not_found', message: '未找到可中断的 ccweb MCP 子代理。' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
child.status = 'closed';
|
child.status = 'interrupted';
|
||||||
child.closedAt = now;
|
child.interruptedAt = now;
|
||||||
child.updatedAt = now;
|
child.updatedAt = now;
|
||||||
child.closeReason = options.reason || 'manual';
|
child.interruptReason = options.reason || 'manual';
|
||||||
ccwebMcpChildThreads.set(normalizedThreadId, child);
|
ccwebMcpChildThreads.set(normalizedThreadId, child);
|
||||||
|
|
||||||
if (child.turnId && codexAppClient?.isRunning()) {
|
if (child.turnId && codexAppClient?.isRunning()) {
|
||||||
@@ -8010,7 +8006,7 @@ function handleCcwebMcpChildAgentClose(ws, msg = {}) {
|
|||||||
tone: 'info',
|
tone: 'info',
|
||||||
transient: true,
|
transient: true,
|
||||||
autoDismissMs: 4000,
|
autoDismissMs: 4000,
|
||||||
message: `已关闭子代理 ${result.child.label || result.child.threadId}。`,
|
message: `已中断子代理 ${result.child.label || result.child.threadId}。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8562,10 +8558,11 @@ function codexAppCollabToolName(value) {
|
|||||||
function ccwebMcpChildStatus(value, fallback = 'running') {
|
function ccwebMcpChildStatus(value, fallback = 'running') {
|
||||||
const normalized = String(value || '').trim().toLowerCase().replace(/[\s_-]/g, '');
|
const normalized = String(value || '').trim().toLowerCase().replace(/[\s_-]/g, '');
|
||||||
if (!normalized) return fallback;
|
if (!normalized) return fallback;
|
||||||
|
if (normalized === 'interrupted') return 'interrupted';
|
||||||
if (/^(closed|close|cleanup|cleaned)$/.test(normalized)) return 'closed';
|
if (/^(closed|close|cleanup|cleaned)$/.test(normalized)) return 'closed';
|
||||||
if (/^(returned|completed|complete|done|success|succeeded|finished)$/.test(normalized)) return 'returned';
|
if (/^(returned|completed|complete|done|success|succeeded|finished)$/.test(normalized)) return 'returned';
|
||||||
if (/^(failed|failure|error|errored)$/.test(normalized)) return 'failed';
|
if (/^(failed|failure|error|errored)$/.test(normalized)) return 'failed';
|
||||||
if (/^(cancelled|canceled|aborted|interrupted)$/.test(normalized)) return 'closed';
|
if (/^(cancelled|canceled|aborted)$/.test(normalized)) return 'closed';
|
||||||
if (/^(pending|pendinginit|queued|waiting|running|working|active|inprogress|started)$/.test(normalized)) return 'running';
|
if (/^(pending|pendinginit|queued|waiting|running|working|active|inprogress|started)$/.test(normalized)) return 'running';
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
@@ -8633,6 +8630,17 @@ function ccwebMcpRecoveredChildStatus(input = {}, tool = {}) {
|
|||||||
return 'running';
|
return 'running';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCcwebMcpChildPlanProgress(value) {
|
||||||
|
if (!value || typeof value !== 'object') return null;
|
||||||
|
const total = Number.parseInt(value.total, 10);
|
||||||
|
const completed = Number.parseInt(value.completed, 10);
|
||||||
|
if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(completed)) return null;
|
||||||
|
return {
|
||||||
|
completed: Math.max(0, Math.min(completed, total)),
|
||||||
|
total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function isSubAgentActivityTool(tool = {}, input = {}) {
|
function isSubAgentActivityTool(tool = {}, input = {}) {
|
||||||
return tool.name === 'subAgentActivity'
|
return tool.name === 'subAgentActivity'
|
||||||
|| tool.kind === 'subAgentActivity'
|
|| tool.kind === 'subAgentActivity'
|
||||||
@@ -8650,10 +8658,16 @@ function recoverCcwebMcpChildThreadsFromPersistedToolCalls(sessionId, state = {}
|
|||||||
for (const tool of toolCalls) {
|
for (const tool of toolCalls) {
|
||||||
const input = parseMaybeJsonObject(tool?.input) || (tool?.input && typeof tool.input === 'object' ? tool.input : {});
|
const input = parseMaybeJsonObject(tool?.input) || (tool?.input && typeof tool.input === 'object' ? tool.input : {});
|
||||||
if (!isSubAgentActivityTool(tool, input)) continue;
|
if (!isSubAgentActivityTool(tool, input)) continue;
|
||||||
|
const result = parseMaybeJsonObject(tool?.result) || (tool?.result && typeof tool.result === 'object' ? tool.result : {});
|
||||||
const threadId = normalizeCodexAppThreadId(
|
const threadId = normalizeCodexAppThreadId(
|
||||||
input.agentThreadId || input.agent_thread_id || input.threadId || input.thread_id
|
input.agentThreadId || input.agent_thread_id || input.threadId || input.thread_id
|
||||||
);
|
);
|
||||||
if (!threadId) continue;
|
if (!threadId) continue;
|
||||||
|
const resultStates = result.agentsStates || result.agents_states || {};
|
||||||
|
const inputStates = input.agentsStates || input.agents_states || {};
|
||||||
|
const persistedState = resultStates[threadId] || inputStates[threadId] || {};
|
||||||
|
const recoveredState = { ...input, ...persistedState };
|
||||||
|
const recoveredPlanProgress = normalizeCcwebMcpChildPlanProgress(recoveredState.planProgress || recoveredState.plan_progress);
|
||||||
|
|
||||||
const existing = ccwebMcpChildThreads.get(threadId);
|
const existing = ccwebMcpChildThreads.get(threadId);
|
||||||
const child = existing || {
|
const child = existing || {
|
||||||
@@ -8662,12 +8676,17 @@ function recoverCcwebMcpChildThreadsFromPersistedToolCalls(sessionId, state = {}
|
|||||||
parentSessionId,
|
parentSessionId,
|
||||||
parentThreadId,
|
parentThreadId,
|
||||||
spawnToolId: tool.id || '',
|
spawnToolId: tool.id || '',
|
||||||
label: ccwebMcpRecoveredChildLabel(input, threadId),
|
label: ccwebMcpRecoveredChildLabel(recoveredState, threadId),
|
||||||
role: String(input.role || input.agentRole || input.agent_role || '').trim(),
|
role: String(recoveredState.role || recoveredState.agentRole || recoveredState.agent_role || '').trim(),
|
||||||
|
agentPath: String(recoveredState.agentPath || recoveredState.agent_path || '').trim(),
|
||||||
|
taskDescription: String(recoveredState.taskDescription || recoveredState.task_description || input.prompt || '').trim(),
|
||||||
lastAssistantMessage: '',
|
lastAssistantMessage: '',
|
||||||
candidateResult: '',
|
candidateResult: '',
|
||||||
finalMessage: '',
|
finalMessage: '',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
planProgress: recoveredPlanProgress,
|
||||||
|
planCurrentStep: String(recoveredState.planCurrentStep || recoveredState.plan_current_step || '').trim(),
|
||||||
|
planUpdatedAt: recoveredState.planUpdatedAt || recoveredState.plan_updated_at || null,
|
||||||
summaryAttempts: 0,
|
summaryAttempts: 0,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -8675,9 +8694,18 @@ function recoverCcwebMcpChildThreadsFromPersistedToolCalls(sessionId, state = {}
|
|||||||
child.parentSessionId = child.parentSessionId || parentSessionId;
|
child.parentSessionId = child.parentSessionId || parentSessionId;
|
||||||
child.parentThreadId = child.parentThreadId || parentThreadId;
|
child.parentThreadId = child.parentThreadId || parentThreadId;
|
||||||
child.spawnToolId = child.spawnToolId || tool.id || '';
|
child.spawnToolId = child.spawnToolId || tool.id || '';
|
||||||
child.label = ccwebMcpRecoveredChildLabel(input, child.label || threadId);
|
child.label = ccwebMcpRecoveredChildLabel(recoveredState, child.label || threadId);
|
||||||
child.role = String(input.role || input.agentRole || input.agent_role || child.role || '').trim();
|
child.role = String(recoveredState.role || recoveredState.agentRole || recoveredState.agent_role || child.role || '').trim();
|
||||||
if (child.status !== 'closed') child.status = ccwebMcpRecoveredChildStatus(input, tool);
|
child.agentPath = String(recoveredState.agentPath || recoveredState.agent_path || child.agentPath || '').trim();
|
||||||
|
child.taskDescription = String(recoveredState.taskDescription || recoveredState.task_description || input.prompt || child.taskDescription || '').trim();
|
||||||
|
if (child.status !== 'closed') child.status = ccwebMcpRecoveredChildStatus(recoveredState, tool);
|
||||||
|
if (recoveredPlanProgress) child.planProgress = recoveredPlanProgress;
|
||||||
|
if (recoveredState.planCurrentStep !== undefined || recoveredState.plan_current_step !== undefined) {
|
||||||
|
child.planCurrentStep = String(recoveredState.planCurrentStep || recoveredState.plan_current_step || '').trim();
|
||||||
|
}
|
||||||
|
if (recoveredState.planUpdatedAt || recoveredState.plan_updated_at) {
|
||||||
|
child.planUpdatedAt = recoveredState.planUpdatedAt || recoveredState.plan_updated_at;
|
||||||
|
}
|
||||||
child.updatedAt = child.updatedAt || now;
|
child.updatedAt = child.updatedAt || now;
|
||||||
child.recoveredFromState = true;
|
child.recoveredFromState = true;
|
||||||
ccwebMcpChildThreads.set(threadId, child);
|
ccwebMcpChildThreads.set(threadId, child);
|
||||||
@@ -8705,7 +8733,12 @@ function ccwebMcpChildPublicState(child = {}) {
|
|||||||
threadId: child.threadId || '',
|
threadId: child.threadId || '',
|
||||||
label: child.label || child.threadId || '子代理',
|
label: child.label || child.threadId || '子代理',
|
||||||
role: child.role || '',
|
role: child.role || '',
|
||||||
|
agentPath: child.agentPath || '',
|
||||||
|
taskDescription: child.taskDescription || '',
|
||||||
status: child.status || 'running',
|
status: child.status || 'running',
|
||||||
|
planProgress: normalizeCcwebMcpChildPlanProgress(child.planProgress),
|
||||||
|
planCurrentStep: child.planCurrentStep || '',
|
||||||
|
planUpdatedAt: child.planUpdatedAt || null,
|
||||||
detail: ccwebMcpChildSummary(child),
|
detail: ccwebMcpChildSummary(child),
|
||||||
candidateResult,
|
candidateResult,
|
||||||
finalMessage: child.finalMessage || '',
|
finalMessage: child.finalMessage || '',
|
||||||
@@ -8714,6 +8747,7 @@ function ccwebMcpChildPublicState(child = {}) {
|
|||||||
createdAt: child.createdAt || null,
|
createdAt: child.createdAt || null,
|
||||||
updatedAt: child.updatedAt || null,
|
updatedAt: child.updatedAt || null,
|
||||||
returnedAt: child.returnedAt || null,
|
returnedAt: child.returnedAt || null,
|
||||||
|
interruptedAt: child.interruptedAt || null,
|
||||||
closedAt: child.closedAt || null,
|
closedAt: child.closedAt || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -8792,11 +8826,16 @@ function mergeCcwebMcpChildIntoTool(tool, child) {
|
|||||||
title: child.title || child.label || previousState.title || '',
|
title: child.title || child.label || previousState.title || '',
|
||||||
name: child.label || agentsStates[child.threadId]?.name || child.threadId,
|
name: child.label || agentsStates[child.threadId]?.name || child.threadId,
|
||||||
role: child.role || agentsStates[child.threadId]?.role || '',
|
role: child.role || agentsStates[child.threadId]?.role || '',
|
||||||
|
agentPath: child.agentPath || previousState.agentPath || previousState.agent_path || '',
|
||||||
status: child.status || 'running',
|
status: child.status || 'running',
|
||||||
|
planProgress: normalizeCcwebMcpChildPlanProgress(child.planProgress),
|
||||||
|
planCurrentStep: child.planCurrentStep || '',
|
||||||
|
planUpdatedAt: child.planUpdatedAt || null,
|
||||||
taskDescription,
|
taskDescription,
|
||||||
summary: ccwebMcpChildSummary(child),
|
summary: ccwebMcpChildSummary(child),
|
||||||
candidateResult,
|
candidateResult,
|
||||||
finalMessage: child.finalMessage || '',
|
finalMessage: child.finalMessage || '',
|
||||||
|
interruptedAt: child.interruptedAt || null,
|
||||||
closedAt: child.closedAt || null,
|
closedAt: child.closedAt || null,
|
||||||
returnedAt: child.returnedAt || null,
|
returnedAt: child.returnedAt || null,
|
||||||
};
|
};
|
||||||
@@ -8869,31 +8908,57 @@ function sendCcwebMcpChildAgentUpdate(sessionId, child) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function syncCcwebMcpChildAgentsFromCollabItem(routed, item = {}) {
|
function syncCcwebMcpChildAgentsFromCollabItem(routed, item = {}) {
|
||||||
if (!routed?.sessionId || item?.type !== 'collabAgentToolCall') return;
|
const itemType = String(item?.type || '').trim();
|
||||||
const toolName = codexAppCollabToolName(item.tool || item.name);
|
const isSubAgentActivity = itemType === 'subAgentActivity';
|
||||||
const receiverThreadIds = extractCcwebMcpStringArray(item.receiverThreadIds, item.receiver_thread_ids, item.targets);
|
if (!routed?.sessionId || (!isSubAgentActivity && itemType !== 'collabAgentToolCall')) return;
|
||||||
|
const toolName = isSubAgentActivity ? 'spawn_agent' : codexAppCollabToolName(item.tool || item.name);
|
||||||
|
const activityThreadId = isSubAgentActivity
|
||||||
|
? normalizeCodexAppThreadId(item.agentThreadId || item.agent_thread_id || item.threadId || item.thread_id)
|
||||||
|
: '';
|
||||||
|
const receiverThreadIds = activityThreadId
|
||||||
|
? [activityThreadId]
|
||||||
|
: extractCcwebMcpStringArray(item.receiverThreadIds, item.receiver_thread_ids, item.targets);
|
||||||
if (receiverThreadIds.length === 0) return;
|
if (receiverThreadIds.length === 0) return;
|
||||||
const states = item.agentsStates && typeof item.agentsStates === 'object'
|
const activityAgentPath = String(item.agentPath || item.agent_path || '').trim();
|
||||||
|
const activityPrompt = String(item.prompt || item.taskDescription || item.task_description || '').trim();
|
||||||
|
const activityState = isSubAgentActivity ? {
|
||||||
|
label: activityAgentPath ? path.basename(activityAgentPath) : '',
|
||||||
|
role: String(item.role || item.agentRole || item.agent_role || '').trim(),
|
||||||
|
status: item.kind || item.status || 'started',
|
||||||
|
agentPath: activityAgentPath,
|
||||||
|
taskDescription: activityPrompt,
|
||||||
|
} : null;
|
||||||
|
const states = activityState
|
||||||
|
? { [activityThreadId]: activityState }
|
||||||
|
: (item.agentsStates && typeof item.agentsStates === 'object'
|
||||||
? item.agentsStates
|
? item.agentsStates
|
||||||
: (item.agents_states && typeof item.agents_states === 'object' ? item.agents_states : {});
|
: (item.agents_states && typeof item.agents_states === 'object' ? item.agents_states : {}));
|
||||||
|
|
||||||
for (const threadId of receiverThreadIds) {
|
for (const threadId of receiverThreadIds) {
|
||||||
const state = states[threadId] && typeof states[threadId] === 'object' ? states[threadId] : {};
|
const state = states[threadId] && typeof states[threadId] === 'object' ? states[threadId] : {};
|
||||||
const existing = ccwebMcpChildThreads.get(threadId);
|
const existing = ccwebMcpChildThreads.get(threadId);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const isSpawn = toolName === 'spawn_agent' || !existing;
|
const isSpawn = toolName === 'spawn_agent' || !existing;
|
||||||
|
const parentThreadId = routed.role === 'child'
|
||||||
|
? routed.child?.threadId || ''
|
||||||
|
: routed.entry?.threadId || item.senderThreadId || item.sender_thread_id || '';
|
||||||
const child = existing || {
|
const child = existing || {
|
||||||
threadId,
|
threadId,
|
||||||
turnId: null,
|
turnId: null,
|
||||||
parentSessionId: routed.sessionId,
|
parentSessionId: routed.sessionId,
|
||||||
parentThreadId: routed.entry?.threadId || item.senderThreadId || item.sender_thread_id || '',
|
parentThreadId,
|
||||||
spawnToolId: isSpawn ? item.id : '',
|
spawnToolId: isSpawn ? item.id : '',
|
||||||
label: ccwebMcpChildLabel(state, threadId),
|
label: ccwebMcpChildLabel(state, threadId),
|
||||||
role: String(state.role || state.agent || state.agentType || state.agent_type || '').trim(),
|
role: String(state.role || state.agent || state.agentType || state.agent_type || '').trim(),
|
||||||
|
agentPath: String(state.agentPath || state.agent_path || '').trim(),
|
||||||
|
taskDescription: String(state.taskDescription || state.task_description || item.prompt || '').trim(),
|
||||||
lastAssistantMessage: '',
|
lastAssistantMessage: '',
|
||||||
candidateResult: '',
|
candidateResult: '',
|
||||||
finalMessage: '',
|
finalMessage: '',
|
||||||
status: 'running',
|
status: 'running',
|
||||||
|
planProgress: null,
|
||||||
|
planCurrentStep: '',
|
||||||
|
planUpdatedAt: null,
|
||||||
summaryAttempts: 0,
|
summaryAttempts: 0,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
@@ -8901,16 +8966,24 @@ function syncCcwebMcpChildAgentsFromCollabItem(routed, item = {}) {
|
|||||||
if (!child.spawnToolId && isSpawn) child.spawnToolId = item.id;
|
if (!child.spawnToolId && isSpawn) child.spawnToolId = item.id;
|
||||||
if (!child.spawnToolId && item.id) child.spawnToolId = item.id;
|
if (!child.spawnToolId && item.id) child.spawnToolId = item.id;
|
||||||
child.parentSessionId = child.parentSessionId || routed.sessionId;
|
child.parentSessionId = child.parentSessionId || routed.sessionId;
|
||||||
child.parentThreadId = child.parentThreadId || routed.entry?.threadId || item.senderThreadId || item.sender_thread_id || '';
|
child.parentThreadId = child.parentThreadId || parentThreadId;
|
||||||
child.label = ccwebMcpChildLabel(state, child.label || threadId);
|
child.label = ccwebMcpChildLabel(state, child.label || threadId);
|
||||||
child.role = String(state.role || state.agent || state.agentType || state.agent_type || child.role || '').trim();
|
child.role = String(state.role || state.agent || state.agentType || state.agent_type || child.role || '').trim();
|
||||||
|
child.agentPath = String(state.agentPath || state.agent_path || child.agentPath || '').trim();
|
||||||
|
child.taskDescription = String(state.taskDescription || state.task_description || item.prompt || child.taskDescription || '').trim();
|
||||||
|
const planProgress = normalizeCcwebMcpChildPlanProgress(state.planProgress || state.plan_progress);
|
||||||
|
if (planProgress) child.planProgress = planProgress;
|
||||||
|
if (state.planCurrentStep !== undefined || state.plan_current_step !== undefined) {
|
||||||
|
child.planCurrentStep = String(state.planCurrentStep || state.plan_current_step || '').trim();
|
||||||
|
}
|
||||||
|
if (state.planUpdatedAt || state.plan_updated_at) child.planUpdatedAt = state.planUpdatedAt || state.plan_updated_at;
|
||||||
if (child.status !== 'closed') {
|
if (child.status !== 'closed') {
|
||||||
const candidate = extractCcwebMcpChildCandidate(state);
|
const candidate = extractCcwebMcpChildCandidate(state);
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
child.candidateResult = truncateTextValue(candidate, SESSION_MESSAGE_CONTENT_MAX_CHARS);
|
child.candidateResult = truncateTextValue(candidate, SESSION_MESSAGE_CONTENT_MAX_CHARS);
|
||||||
child.lastAssistantMessage = child.candidateResult;
|
child.lastAssistantMessage = child.candidateResult;
|
||||||
}
|
}
|
||||||
const rawStatus = state.status || state.state || item.status;
|
const rawStatus = state.status || state.state || item.kind || item.status;
|
||||||
const fallback = child.status || 'running';
|
const fallback = child.status || 'running';
|
||||||
const nextStatus = ccwebMcpChildStatus(rawStatus, fallback);
|
const nextStatus = ccwebMcpChildStatus(rawStatus, fallback);
|
||||||
child.status = nextStatus === 'returned' && !child.candidateResult && !candidate ? fallback : nextStatus;
|
child.status = nextStatus === 'returned' && !child.candidateResult && !candidate ? fallback : nextStatus;
|
||||||
@@ -8933,6 +9006,15 @@ function processCcwebMcpChildNotification(child, notification) {
|
|||||||
return { changed: false, done: false };
|
return { changed: false, done: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const planUpdate = codexAppRuntime.planUpdateFromNotification(notification);
|
||||||
|
if (planUpdate) {
|
||||||
|
child.planProgress = normalizeCcwebMcpChildPlanProgress(planUpdate.progress);
|
||||||
|
child.planCurrentStep = planUpdate.currentStep || '';
|
||||||
|
child.planUpdatedAt = now;
|
||||||
|
child.updatedAt = now;
|
||||||
|
return { changed: true, done: false };
|
||||||
|
}
|
||||||
|
|
||||||
if (method === 'turn/started') {
|
if (method === 'turn/started') {
|
||||||
child.status = 'running';
|
child.status = 'running';
|
||||||
child.updatedAt = now;
|
child.updatedAt = now;
|
||||||
@@ -9027,6 +9109,12 @@ function handleCodexAppNotification(notification) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const item = notification?.params?.item || null;
|
||||||
|
const isChildActivityItem = item?.type === 'collabAgentToolCall' || item?.type === 'subAgentActivity';
|
||||||
|
if (routed.role === 'child' && isChildActivityItem) {
|
||||||
|
syncCcwebMcpChildAgentsFromCollabItem(routed, item);
|
||||||
|
}
|
||||||
|
|
||||||
if (routed.role === 'child') {
|
if (routed.role === 'child') {
|
||||||
const result = processCcwebMcpChildNotification(routed.child, notification);
|
const result = processCcwebMcpChildNotification(routed.child, notification);
|
||||||
if (result.changed) sendCcwebMcpChildAgentUpdate(routed.sessionId, routed.child);
|
if (result.changed) sendCcwebMcpChildAgentUpdate(routed.sessionId, routed.child);
|
||||||
@@ -9034,8 +9122,7 @@ function handleCodexAppNotification(notification) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = codexAppRuntime.processCodexAppNotification(routed.entry, notification, routed.sessionId);
|
const result = codexAppRuntime.processCodexAppNotification(routed.entry, notification, routed.sessionId);
|
||||||
const item = notification?.params?.item || null;
|
if (item?.type === 'collabAgentToolCall' || item?.type === 'subAgentActivity') {
|
||||||
if (item?.type === 'collabAgentToolCall') {
|
|
||||||
syncCcwebMcpChildAgentsFromCollabItem(routed, item);
|
syncCcwebMcpChildAgentsFromCollabItem(routed, item);
|
||||||
}
|
}
|
||||||
persistCodexAppTurnState(routed.sessionId, routed.entry, { immediate: !!result?.done });
|
persistCodexAppTurnState(routed.sessionId, routed.entry, { immediate: !!result?.done });
|
||||||
@@ -9677,7 +9764,7 @@ function handleCodexAppServerExit(signature, info = {}) {
|
|||||||
|
|
||||||
function codexAppModelSettings(session) {
|
function codexAppModelSettings(session) {
|
||||||
const raw = String(session?.model || getDefaultCodexModel() || '').trim();
|
const raw = String(session?.model || getDefaultCodexModel() || '').trim();
|
||||||
const match = raw.match(/^(.*)\((low|medium|high|xhigh)\)\s*$/i);
|
const match = raw.match(/^(.*)\((low|medium|high|xhigh|ultra)\)\s*$/i);
|
||||||
if (!match) return { model: raw || null, effort: null };
|
if (!match) return { model: raw || null, effort: null };
|
||||||
return {
|
return {
|
||||||
model: String(match[1] || '').trim() || null,
|
model: String(match[1] || '').trim() || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user