diff --git a/.planning/codex-0147-audit/findings.md b/.planning/codex-0147-audit/findings.md new file mode 100644 index 0000000..e70ba86 --- /dev/null +++ b/.planning/codex-0147-audit/findings.md @@ -0,0 +1,58 @@ +# 审计发现 + +## 初始关注面 + +- app-server 初始化与能力探测 +- collaborationMode / goals / guided input +- thread/start、turn/start 参数形状 +- MCP server 线程级注入 +- 流式事件与 item 类型 +- CLI 版本检测和降级逻辑 +- mock / regression 覆盖 + +## 已确认基线 + +- 本机实际运行 `codex-cli 0.147.0`,路径为 `/home/hdzx/.local/lib/node_modules/@openai/codex/bin/codex.js`。 +- 官方 0.147.0 发布于 2026-08-07;主要新增 Agent Plugins、会话 sections、`--approve-for-me`、MCP 2026-07-28,并包含多项 app-server 协议调整。 +- 与 cc-web 最相关的上游提交包括:`Box app-server event payloads`、`Update tests for current tool call and app-server event types`、持久化 thread sections、显式 user-input blocking、MCP 非阻塞启动和 paginated discovery。 +- cc-web 当前通过 `thread/start.config["mcp_servers.*"]` 注入 MCP;通过 `collaborationMode.settings` 传 model/reasoning/developer instructions;存在 collaborationMode 不支持时的降级设计。 +- codebase-memory 索引 `home-cc-web` 为 ready(5152 nodes / 10588 edges)。 + +## 初步风险判断 + +- 0.147.0 的新增 CLI/TUI 功能多数不要求 cc-web 立即适配。 +- 最值得核对的是 app-server event payload boxing 与 tool-call item 类型变化;这可能直接影响事件分发与前端状态投影。 +- thread sections、plugin summaries、MCP 2026-07-28 属于可选新能力,若旧协议仍兼容则是功能缺口而非启动阻断。 + +## 协议核验 + +- 0.147.0 `generate-ts --experimental` 仍包含 cc-web 使用的 `thread/start`、`thread/resume`、`turn/start`、`experimentalFeature/enablement/set`、`collaborationMode/list`、`item/tool/requestUserInput` 和 approval server requests。 +- `CollaborationMode` 仍为 `{ mode, settings }`;cc-web 当前参数形状与生成协议一致。 +- 0.147.0 新增 thread sections、plugin APIs、thread status active flags、MCP tool read-only hints 等字段/方法;cc-web 暂未消费,但这些是增量能力。 +- 上游 “Box app-server event payloads” 只改变 Rust 内部 `AppServerEvent` / `InProcessServerEvent` 的内存表示,远程 JSON-RPC 序列化未变。 +- plaintext collaboration tool messages 新增的是 raw response item 的可选 `encrypted_function_args`,cc-web 不依赖该字段,当前容错投影不会因此失败。 + +## 最终差距 + +### P1:`request_user_input.isBlocking` 未适配 + +- 0.147.0 的 `item/tool/requestUserInput` 新增必填 `isBlocking`;`autoResolutionMs` 已弃用。 +- Plan 模式发送 `isBlocking: true`,客户端应持续等待明确回答;Default 模式在实验特性开启时发送 `isBlocking: false`,客户端不应无限阻塞。 +- cc-web 当前无条件创建 10 分钟 timer,并且发给前端的消息不携带 `isBlocking`;因此 Default 模式非阻塞请求会被错误地当作阻塞请求处理。 +- mock 没有发送 `isBlocking`,回归也只断言 questions,导致现有测试无法发现 0.147.0 这项变化。 + +### P2:MCP 2026-07-28 仅需预防性收口 + +- 0.147.0 中 `mcp_2026_07_28` 是 under-development 且默认 false,当前不会影响 cc-web。 +- cc-web MCP server 目前直接回显客户端传来的任意 `protocolVersion`,等于声明支持并未实现的未来协议;启用 2026 协议后存在协商不严谨风险。 +- 建议显式维护支持版本集合,并对未知版本按 MCP 协商规则选定受支持版本;`tools/list` 的 cursor/nextCursor 可作为启用 2026 协议前的兼容门槛。 + +### P3:可选产品能力 + +- thread sections、插件目录/安装、thread status active flags、MCP `readOnlyHint` 都是增量能力;不适配不影响现有聊天、协作、审批和 MCP 主链路。 + +## 验证结果 + +- `npm run regression`:通过。 +- 0.147.0 experimental TS / JSON Schema:生成成功。 +- 本机 0.147.0 的 feature 状态:`goals` stable/on,`plugins` stable/on,`mcp_2026_07_28` under-development/off,`default_mode_request_user_input` under-development/off。 diff --git a/.planning/codex-0147-audit/progress.md b/.planning/codex-0147-audit/progress.md new file mode 100644 index 0000000..5a66d5b --- /dev/null +++ b/.planning/codex-0147-audit/progress.md @@ -0,0 +1,7 @@ +# 审计进度 + +- 2026-08-10:开始 Codex CLI 0.147.0 适配审计;已读取项目约定、OpenAI 文档技能和文件化规划技能。 +- 2026-08-10:Codex manual helper 因 developers.openai.com HEAD 403 失败,已记录并切换官方后备来源。 +- 2026-08-10:已从 OpenAI 官方 GitHub release/compare 获取 0.147.0 变更,并确认本机版本与 cc-web 主要接入函数。 +- 2026-08-10:已生成 0.147.0 experimental TypeScript/JSON Schema 并逐项核对 cc-web 使用的请求、通知和参数形状。 +- 2026-08-10:完整回归通过;确认一个 P1 guided-input 语义差距、一个 P2 MCP 协商风险,其余 0.147.0 变化为兼容或可选能力。 diff --git a/.planning/codex-0147-audit/task_plan.md b/.planning/codex-0147-audit/task_plan.md new file mode 100644 index 0000000..30a7e25 --- /dev/null +++ b/.planning/codex-0147-audit/task_plan.md @@ -0,0 +1,25 @@ +# Codex CLI 0.147.0 适配审计 + +## 目标 + +判断 cc-web 对 Codex CLI / app-server 0.147.0 是否已跟上,并给出有证据的兼容性结论与改造优先级。 + +## 阶段 + +- [x] 获取 0.147.0 官方/上游变更事实 +- [x] 确认 cc-web 当前 Codex 接入架构与版本假设 +- [x] 逐项核对协议、事件、配置和工具兼容性 +- [x] 运行现有相关回归测试与最小协议探测 +- [x] 汇总结论、风险与建议改造清单 + +## 约束 + +- 本轮只审计,不修改业务代码。 +- 代码理解优先使用 codebase-memory-mcp,rg 仅作行号和文本校验。 +- 不使用 graphify。 +- 不覆盖用户现有改动。 + +## 错误记录 + +- 官方 Codex manual helper 的 HEAD 请求返回 HTTP 403;不重复同一路径,按 `openai-docs` 规则降级到官方 Docs MCP / 官方 GitHub 发布源。 +- 首次用 cc-web client 做只读 app-server 探测时进程约 25 秒后无输出退出;不重复该封装路径,改用 schema 生成、现有回归和当前真实会话证据交叉验证。 diff --git a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz index 9bafa07..e1966e9 100644 Binary files a/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz and b/dist-exe/cc-web-bun-linux-x64-baseline.tar.gz differ diff --git a/lib/agent-runtime.js b/lib/agent-runtime.js index bbdb8f9..88bc836 100644 --- a/lib/agent-runtime.js +++ b/lib/agent-runtime.js @@ -211,7 +211,7 @@ function createAgentRuntime(deps) { // cc-web UI supports "gpt-5.4(high)" style selection, but Codex CLI expects: // - model: "gpt-5.4" // - reasoning effort: config key `model_reasoning_effort = "high"` - const m = raw.match(/^(.*)\((low|medium|high|xhigh|ultra)\)\s*$/i); + const m = raw.match(/^(.*)\((low|medium|high|xhigh|max|ultra)\)\s*$/i); if (m) { const base = String(m[1] || '').trim(); const lvl = String(m[2] || '').trim().toLowerCase(); diff --git a/public/app.js b/public/app.js index f07eae8..e2c3731 100644 --- a/public/app.js +++ b/public/app.js @@ -10796,6 +10796,7 @@ { value: 'medium', label: 'medium', desc: '中等 thinking' }, { value: 'high', label: 'high', desc: '更强 thinking' }, { value: 'xhigh', label: 'xhigh', desc: '高强度 thinking' }, + { value: 'max', label: 'max', desc: '最大强度 thinking' }, { value: 'ultra', label: 'ultra', desc: '最高强度 thinking' }, ]; showOptionPicker('选择 Thinking 强度', thinkingOptions, current.level || '', (lvl) => { diff --git a/scripts/regression.js b/scripts/regression.js index 91293ea..6fbe7b1 100644 --- a/scripts/regression.js +++ b/scripts/regression.js @@ -4621,12 +4621,16 @@ function assertMultiAgentV2CompatibilityContract() { 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' + /CODEX_REASONING_LEVELS\s*=\s*new Set\(\[[^\]]*'max'[^\]]*'ultra'/.test(serverSource), + 'Codex config model parsing should accept the max and ultra reasoning levels' ); assert( - /low\|medium\|high\|xhigh\|ultra/.test(runtimeSource), - 'Codex CLI model suffix parsing should accept ultra' + /low\|medium\|high\|xhigh\|max\|ultra/.test(runtimeSource), + 'Codex CLI model suffix parsing should accept max and ultra' + ); + assert( + frontendSource.includes("{ value: 'max', label: 'max'"), + 'Codex model picker should expose max reasoning' ); assert( frontendSource.includes("{ value: 'ultra', label: 'ultra'"), @@ -5665,7 +5669,7 @@ async function main() { }, null, 2)); createFakeClaudeHistory(homeDir); - createFakeCodexConfig(homeDir, { reasoningEffort: 'ultra' }); + createFakeCodexConfig(homeDir, { reasoningEffort: 'max' }); const codexFixture = createFakeCodexHistory(homeDir); const codexAppImportFixture = createFakeCodexHistory(homeDir, { threadId: 'codexapp-import-thread', @@ -5815,7 +5819,7 @@ async function main() { 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); assert(codexSession.mode === 'plan', 'Codex new_session should follow requested mode'); - assert(codexSession.model === 'gpt-5.5(ultra)', 'Codex new_session should preserve ultra from ~/.codex/config.toml'); + assert(codexSession.model === 'gpt-5.5(max)', 'Codex new_session should preserve max from ~/.codex/config.toml'); 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); @@ -6441,7 +6445,7 @@ async function main() { .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'); 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'); + assert(parsedSpawnLine.args.includes('model_reasoning_effort="max"'), 'Codex exec should pass the max reasoning level through model_reasoning_effort'); const allSpawnsForSession = processLog .trim() @@ -6506,7 +6510,7 @@ async function main() { ].join('\n')); 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); - assert(codexAppSession.model === 'gpt-5.5(ultra)', 'Codex App new_session should preserve the ultra default Codex model'); + assert(codexAppSession.model === 'gpt-5.5(max)', 'Codex App new_session should preserve the max default Codex model'); 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'); @@ -6522,7 +6526,7 @@ async function main() { assert(/"hasDeveloperInstructions":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include sub-agent developer instructions'); 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(/"reasoningEffort":"max"/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should pass max reasoning_effort'); 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'); await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId); diff --git a/server.js b/server.js index 0b5924d..ccbf257 100644 --- a/server.js +++ b/server.js @@ -797,7 +797,7 @@ const MCP_PROMPT_RESPONSE_MAX_CHARS = 20000; // Codex 默认模型优先读取 ~/.codex/config.toml,缺失时再回退到旧默认值。 const FALLBACK_CODEX_MODEL = 'gpt-5.4'; -const CODEX_REASONING_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'ultra']); +const CODEX_REASONING_LEVELS = new Set(['low', 'medium', 'high', 'xhigh', 'max', 'ultra']); 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.', '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.', @@ -10347,7 +10347,7 @@ function handleCodexAppServerExit(signature, info = {}) { function codexAppModelSettings(session) { const raw = String(session?.model || getDefaultCodexModel() || '').trim(); - const match = raw.match(/^(.*)\((low|medium|high|xhigh|ultra)\)\s*$/i); + const match = raw.match(/^(.*)\((low|medium|high|xhigh|max|ultra)\)\s*$/i); if (!match) return { model: raw || null, effort: null }; return { model: String(match[1] || '').trim() || null,