update-codex-acp
更新内置 Codex ACP agent fork(vendor/codex-acp,自维护 git submodule),合并上游 agentclientprotocol/codex-acp 新提交并重建发布产物。当用户说更新 codex-acp / 合并上游修改 / 同步 codex agent / 升级内置 codex agent 时使用。注意真上游是 agentclientprotocol
Files3
View on GitHubSKILL.md.claude/skills/update-codex-acp/SKILL.md
GitHub更新 codex-acp(合并上游 + 重建产物)
vendor/codex-acp 是我们自维护的 fork,git submodule,不在 pnpm workspace 内,用自带 npm 工具链独立构建(见根 CLAUDE.md)。上游是 https://github.com/agentclientprotocol/codex-acp.git(npm 包 @agentclientprotocol/codex-acp)。我们在某个上游 release 之上叠了若干自定义提交(费用计算、Claude 式 skills/memory、git 工作树会话匹配、跨平台路径比较、AI 会话标题持久化、ESM 标记、rewind/fork、重连失败处理、transcriptPath 暴露、订阅额度、模型注入、request_user_input 回放等)。提交数随上游替旧/我方加减而变,别照抄历史数字:2026-10 升级前是 34 个,其中 3 个旧 shell fallback 补丁被上游 typed 实现取代而有意不再重放(见案例 7),本次 rebase 后 = 31 个重放 + 1 个新适配提交。
⚠️ 真上游踩坑(务必先确认):Codex ACP 有两个同名仓库。老的
zed-industries/codex-acp已废弃(README 明确写着开发已迁移),且其main被回退重写到较旧点、与我们 fork 无共同祖先(git merge-base返回空)。真正的上游是agentclientprotocol/codex-acp(基于新 Codex App Server)。若误把 upstream 设成 zed-industries,会出现「merge-base 为空 / 代码大幅倒退」的假象。判据:正确上游下merge-base命中我们的基线、compare显示 N ahead / M behind 与我方自定义提交数吻合。
核心套路:确认真上游 + 分叉点 → 与用户敲定推送范围 → 备份当前 HEAD → rebase 自定义提交到上游最新 → 按已知套路解决冲突 → 重新装依赖 + 构建 + typecheck + 测试 → 后处理改动 fixup 并入 → push fork → 主仓库 pnpm agent:build + pnpm check → 开分支提交 submodule 指针并 push。
合并方式固定用 rebase(用户已明确要求,不用再问):历史线性、自定义提交清晰留顶、下次再合更省事;代价是重写历史需
--force-with-leasepush fork。只需就“推送范围”征询用户。
⚠️ 第一原则:fork 的自定义提交必须完整保留在历史顶部,且其承载的功能(费用累计上报、Claude 式 skills、AI 标题持久化等)不能被上游同名实现覆盖掉。主仓库 renderer 端依赖这些行为(memory 注入改服务用户项目、本仓库内已空转,见案例 4);合并时优先“两条路并存”,而不是二选一。
流程
0. 摸清现状(只读)
全程用 git ls-remote / gh api / 只读 git 命令探查,不要在 plan/调查阶段改动 submodule:
cd vendor/codex-acp
git remote -v # 通常只有 origin = 我们的 fork(lovebirdsx/codex-acp),无 upstream
git log --oneline -12 # 顶部 N 个是我们的自定义提交(作者 lovebird),其下是某个上游 release(基线)
grep -A3 -i repository package.json # 确认上游仓库 URL(应为 agentclientprotocol,别信 zed-industries)
git rev-parse main HEAD origin/main # ⚠️ submodule 常是 detached HEAD;本地 main 可能已过时,别用它做基线
确认真上游并测分叉(这步决定工作量,也是排除 zed-industries 陷阱的关键):
git remote add upstream https://github.com/agentclientprotocol/codex-acp.git
git fetch upstream
mb=$(git merge-base HEAD upstream/main) && git log --oneline -1 $mb # 应命中我们的基线;空=上游选错了
git log --oneline $mb..HEAD # 我方自定义提交(ahead)
git rev-list --count $mb..upstream/main # 上游新提交数(behind)
git log --oneline $mb..upstream/main # 上游新提交列表,预判影响面(codex 版本升级、SDK 升级等)
git diff --stat $mb..HEAD # 我们改了哪些文件 → 预判冲突面
注:package.json 内部
version与上游 git tag 不是一回事,对不上正常,不影响合并。GitHub 仓库页显示的 “N commits ahead / M behind” 是最快的真上游校验(应与上面 ahead/behind 吻合)。
1. 与用户敲定推送范围(AskUserQuestion)
- 合并方式:固定 rebase,不用再问。
- 推送范围:默认推荐全部推送(push fork + 主仓库提交 submodule 指针)。CLAUDE.md 规定提交/推送只在用户要求时做,必须先确认。选“仅本地不推送”时,跳过 push fork 与提交 submodule 指针,只到本地
main指向合并结果 + 主仓库agent:build/pnpm check验证为止。
2. 备份 + rebase
submodule 常是 detached HEAD,rebase 会重写历史,先落一个备份分支再动手:
cd vendor/codex-acp
git branch backup-before-rebase-<短sha> <当前HEAD sha> # 出事可 git reset --hard 回来
git branch -f main <当前HEAD sha> && git switch main # 让 main 指向我们最新的 8 提交 HEAD
git rebase upstream/main # 把 基线..HEAD 的自定义提交重放到上游最新
逐个提交解决冲突(见下「冲突套路」),每次 git add <file> 后 git -c core.editor=true rebase --continue。
3. 重新装依赖 + 构建 + typecheck + 测试
上游常升级 codex 版本 / ACP SDK / vscode-jsonrpc,rebase 后必须重装依赖再验证:
npm ci # 按 lock 精确安装(不会重生成 lock;manifest 不一致会直接报错,见「冲突套路」)
npx tsc --noEmit # 类型检查(必跑安全网,rebase 零冲突≠语义正确,见案例 5 精神)
node build.mjs # esbuild → dist/index.js(+ dist/package.json 标 ESM)
npm test # vitest;仅截错误。已知 Windows 路径测试会失败(见案例 1),非回归
⚠️ 已知坑(fs mock 只桩了
readFile):file-change-events.test.ts用vi.mock('node:fs/promises')自建mockFilesMap,只桩了readFile——被测代码新增任何 fs 调用(如stat,ReplayFileRead.readFileWithinCap正是 stat 先判)都会在该测试里报is not a function,不是逻辑回归;补桩即可(锚src/__tests__/CodexACPAgent/file-change-events.test.ts)。 把“后处理改动”(更新的快照等)用 fixup + autosquash 并入逻辑所属提交(保持历史干净):
git add <改过的快照/测试文件> && git commit --fixup=<对应功能提交sha>
GIT_SEQUENCE_EDITOR=true git -c core.editor=true rebase -i --autosquash upstream/main
4. push fork
git push --force-with-lease origin main
git branch -d backup-before-rebase-<短sha> # 确认无误后再删备份
# 收尾核对:git rev-parse HEAD main 相等;git rev-list --count upstream/main..main == 我方提交数
5. 主仓库重建产物 + 验证
cd <repoRoot>
# 上游若 bump 了 @openai/codex,同步主仓二进制钉版:
# packages/node-services/src/agentBinary/flavors.ts 的 CODEX_VERSION
# (取 fork lock 里解析出的版本,见 `grep -A2 '"@openai/codex"' vendor/codex-acp/package-lock.json`)
pnpm agent:build # = vendor-install(按 lock 装依赖) + 两个 fork 各自 npm run build;重建 vendor/codex-acp/{dist,node_modules}
pnpm check # lint + typecheck + test,仅截错误
agent:build同时构建 claude-agent-acp 和 codex-acp 两个 fork;vendor-install只在 lock 哈希变化时才npm ci,且不带--omit=dev(2026-10 实测:构建后 fork 的 vitest/tsc 仍在,可直接跑 fork 测试;若某次构建后 devDeps 缺失,先npm ci重装再跑)。pnpm check偶发的 FileWatcher / DiffEditor /Channel closed(IPC) 失败是主仓库既有环境 flake,与本次无关——单独pnpm --filter @universe-editor/editor run test重跑即绿。
三层验证阶梯(由内到外,别只跑最外层):
- fork 自身:在
vendor/codex-acp内npm run typecheck && npm test(Node 24,见.github/workflows/ci.yml的acp-contract作业——它也用npm --prefix vendor/codex-acp run …跑这两条)。先证明 fork 源码自洽。用例入口 / 哪些是真起进程、哪些是桩、哪里仍是缺口 → 见 references/acceptance-matrix.md。- 主仓 wire 契约:
UNIVERSE_FORK_CONTRACT=1 pnpm --filter @universe-editor/editor test:integration acpForkContract(须在pnpm agent:build之后)。证明 editor↔fork 的 ext-method /_meta形状没漂;与 CIacp-contract跑的是同一条。- e2e 按交互影响面:只有改了会话 / agent UI / 渲染路径才需要——
pnpm e2e specs/<相关>.spec.ts定向,再pnpm e2e:smoke。纯 fork 内部实现改动、wire 形状不变时,①② 已足够,不必拉全量 e2e。
6. 提交主仓库 submodule 指针
git switch -c chore/update-codex-acp # 当前多在 main(默认分支),先开分支
git diff --submodule=log vendor/codex-acp # 核对:顶部我方提交 + 其下上游新提交
git add vendor/codex-acp
git commit # chore(agent): 更新 codex-acp 至上游 <版本>(codex <x.y.z> / ACP SDK <a.b>),正文记冲突处理。提交信息不带任何 AI/工具署名(无 Co-Authored-By 之类水印)
git push -u origin chore/update-codex-acp
冲突套路(按文件)
package-lock.json:不要手动解。rebase 中冲突时git checkout --ours package-lock.json && git add(rebase 里--ours=上游侧)。⚠️npm ci只按 lock 精确安装、绝不改写 lock(manifest 与 lock 不一致会直接报错退出),所以它不能用来“重生成”lock;只有 merge 后 manifest 与上游 lock 真有分歧时才用npm install重建 lock,再npm ci复验。本次合并后 fork 的package.json/package-lock.json与上游逐字节相同(git diff ca1d971..main -- package*.json为空),无需重建。package.json:version与 codex/SDK/依赖版本取上游;我方特有改动(如 build 脚本、devDep)保留。⚠️ Edit 解冲突当心重复 key(冲突标记外的公共行别在 new_string 里重写)。src/index.ts:agent 注册入口,双方常同改(见案例 2)。src/CodexEventHandler.ts/src/CodexAcpServer.ts:费用/token 上报,我方改了语义,注意与上游 token-usage 演进的冲突(见案例 3)。src/CodexAcpClient.ts:skills/memory 注入 + session config,我方大改(见案例 4)。- 我方新增、上游无的文件(如
CLAUDE.md、set-session-title 测试)→ 一般无冲突,直接保留。⚠️src/PathUtils.ts不再是 fork 独有:上游 #377 已引入该文件,我方只在其上做了「跨平台路径比较」改动——按普通「双方都改」的文件解冲突(保留我方跨平台语义),别再当「上游无、直接照搬我方版本」处理。
案例库
每条:现象 → 根因 → 解法 → 锚点。新经验往下追加。
案例 1:fork npm test 若干路径测试在 Windows 必失败(非回归)
- 现象:Windows 本地跑
npm test,约 4 个测试失败:CodexAcpClient.test.ts的 skills additional-directories 两条 +should map events from dump(报Invalid request: AbsolutePathBuf deserialized without a base path),以及load-session.test.ts的 history-fallback 一条。diff 全是\test\...vs/test/...(反斜杠 vs 正斜杠)。 - 根因:测试 fixture 用假 cwd
/test/cwd。Windows 上path.join("/test/cwd", ...)产出\test\cwd\...——非盘符绝对路径,真 codex 二进制反序列化AbsolutePathBuf时拒绝(→ 那条 AbsolutePathBuf 错误);其余是快照里分隔符不匹配。Linux/Mac CI 上/test/cwd/...是合法绝对路径,全过。 - 判别:三方验证非回归——① 纯
upstream/main新建 worktree 跑同样测试也失败;② rebase 前的backup-before-rebase-*分支跑也失败。两者都失败 ⇒ 与本次 rebase 无关,是既有 Windows 环境差异。 - 解法:直接忽略,不改测试/不改上游逻辑(改了会破坏 Linux CI)。不影响主仓库
pnpm check(vendor 不在 workspace)。 - 锚点:
vendor/codex-acp/src/__tests__/CodexACPAgent/{CodexAcpClient.test.ts,load-session.test.ts}、src/app-server/AbsolutePathBuf.ts、fixturesrc/__tests__/acp-test-utils.ts(cwd: "/test/cwd")。
案例 2:src/index.ts — 我方表驱动扩展方法注册 撞 上游 prompt 取消支持
- 现象:rebase 到「支持持久化 AI 会话标题」提交时
src/index.ts冲突。HEAD(上游)侧把手写的.onRequest("authentication/status"...)等逐条注册保留、且给 prompt 加了ctx.signal(来自上游取消支持);我方侧把这些手写注册重构成表驱动的EXTENSION_METHOD_REGISTRATIONSfor 循环、但 prompt 还是旧的getAgent().prompt(ctx.params)(无 signal)。 - 根因:两边改同一段 agent builder——上游改控制流(加取消 signal),我方改结构(手写→表驱动)。
- 解法:两者都要。采用我方
const agentBuilder = ...+ 表驱动 for 循环结构,同时把 prompt 改成上游的getAgent().prompt(ctx.params, ctx.signal)。先确认我方EXTENSION_METHOD_REGISTRATIONS(在AcpExtensions.ts)已完整覆盖上游那几个手写方法(authentication/status、authentication/logout、legacy set_model,且多了 set_session_title),确认后删掉上游的逐条.onRequest(...)手写注册与旧z/parser import 残留即可。 - 为什么注册层是承重的(非显然):ACP SDK 只把
index.ts里显式.onRequest(method, parser, …)注册过的方法路由进extMethod——未注册的方法 SDK 直接 methodNotFound 拒掉,根本到不了CodexAcpServer的 switch。所以「server 侧实现了方法」≠「客户端调得到」:曾有一轮单测直连agent.extMethod(...)调用、绕过了注册层,测试全绿而真机黑。验收判据:测试必须断言目标方法在EXTENSION_METHOD_REGISTRATIONS列表内(src/__tests__/CodexACPAgent/set-session-title.test.ts的「注册层」回归例,现 7 例),不能只测 server 方法本身。新增 ext-method 只走两步——AcpExtensions.ts加 method+parser 描述符,index.ts的 for 循环自动注册,别再加手写.onRequest。 - 锚点:
src/index.ts(startAcpServer的 agent builder 链、EXTENSION_METHOD_REGISTRATIONS循环)、src/AcpExtensions.ts(EXTENSION_METHOD_REGISTRATIONS)、src/__tests__/CodexACPAgent/set-session-title.test.ts。
案例 3:费用补丁的 totalTokenUsage 累计语义 撞 上游 token-usage 快照(更新快照)
- 现象:rebase 后
npm test,token-usage-events.test.ts6 个toMatchFileSnapshot失败。diff 显示我方输出totalTokens: 5000而快照期望2500(测试输入里total=5000, last=2500)。 - 根因:我方「优化通知以支持 session 的费用计算」提交刻意把
_meta.quota上报从lastTokenUsage(最后一次调用)改成totalTokenUsage(会话累计),以便客户端从单次快照给整个会话计价。上游后来(#107 等)增强了 token-usage 测试,会断言_meta.quota的具体值 → 与我方语义冲突暴露。旧基线时这些测试还不碰_meta.quota(grep -c _meta 旧快照= 0),所以当年没冲突。 - 判别:确认输出
5000= 测试输入的total= 我方累计语义的正确结果(不是 bug),才更新快照。 - 解法:
npx vitest run -u src/__tests__/CodexACPAgent/token-usage-events.test.ts更新这 6 个data/token-usage-*.json快照,用git commit --fixup=<费用计算提交sha>并入费用补丁(让该提交自洽=行为改动+对应快照在一起)。⚠️-u不带过滤会更新全部失败快照——若它顺手更新了案例 1 的 Windows 路径快照(如load-session-response-item-history-fallback.json),要git checkout还原掉(那些是环境差异,改了破坏 Linux CI)。 - 教训:更新快照前先分清「我方有意的语义差异」(该更新)vs「Windows 环境差异」(该还原)vs「真回归」(该修代码)。
- 锚点:
src/CodexEventHandler.ts(usage_update的_meta.quota)、src/CodexAcpServer.ts(buildQuotaMeta,totalTokenUsagevslastTokenUsage)、src/__tests__/CodexACPAgent/data/token-usage-*.json。
案例 4:skills/memory 注入(Claude 兼容)— 我方大改 CodexAcpClient(一般能自动合并,但注意路径构造)
- 现象:「支持 Claude 兼容的 skills 和 memory」提交改
src/CodexAcpClient.ts:threadStart/threadResume 加...buildMemoryInstructions(cwd)(读cwd/.claude/memory/MEMORY.md作 developerInstructions)、refreshSkills把 skillExtraRoots 扩成.agents/skills+cwd/.claude/skills+additionalRoots/.claude/skills。 - 根因/风险:上游也在演进 skills(
support-skills-from-additionalRoots等分支/PR),两边都动 skills 发现逻辑,可能冲突或语义重叠。 - 解法:合并时保留我方的
.claude/skills+ memory 注入,与上游的.agents/skills处理并存。注意:我方对 skill root 故意不做存在性检查(保持与上游.agents/skills对称、最小化 diff),这会在 Windows 假 cwd 测试下触发案例 1 的 AbsolutePathBuf——那是测试环境问题,不是这里的 bug。 - 现状(2026-10 起,别按旧表述理解):memory 注入服务的是编辑器面向用户项目的功能——「一套
.claude/同时服务 Claude 与 Codex 两个会话」,用户项目 cwd 下照常生效(buildMemoryInstructions(cwd)读cwd/.claude/memory/MEMORY.md作developerInstructions附加层,绝不用baseInstructions——那会替换 codex 自身系统提示;索引缺失/为空则不注入,属零改动优雅降级)。本仓库已下线自建 memory 层(目录已移除,不再是本仓库的知识容器),所以在仓库内该注入是空转的——但这不是重放时跳过该提交的理由:它服务的是用户项目,rebase 仍必须保留。 - 锚点:
src/CodexAcpClient.ts(buildMemoryInstructions定义 + 三个调用点)、src/AcpExtensions.ts(SET_SESSION_TITLE_METHOD)、用户文档docs/user/zh-CN/ai-agent/skills-memory-mcp.md「一套配置服务两个 Agent」。
案例 5:「两者都要」式解冲突丢块 + 语义适配三类(v1.1.7 合并实战)
- 现象:rebase 到上游 v1.1.7(steering #309 / goal control #293 / 可配置 provider #272)时,冲突几乎全是「同一位置双方各加各的」(ext-method 类型 union、
isExtMethodRequest、EXTENSION_METHOD_REGISTRATIONS、import 列表、方法块)。手工「两者都要」合并时把上游 HEAD 侧的SessionSteerRequest/SessionSteeringResponse/SessionSteeringExtRequest三个类型定义整块丢了(new_string 只保留了函数没保留类型),rebase 能完成但npx tsc --noEmit立刻报一串 TS2304。 - 解法:① 丢的块补回去,fixup 进当时解冲突的那个提交(保持每个提交自洽可编译);② 解冲突时优先用「只删标记行」的小步 Edit,别整块重写——整块重写最容易丢内容。
- 语义适配(rebase 零冲突/标记解完 ≠ 完事,tsc + test 抓出来的三类):
- 上游给接口加必填字段:
SessionMetadata新增必填collaborationMode,我方forkSession的返回字面量缺它(TS2741)→ 照上游其他返回点补collaborationMode: this.getCollaborationMode(thread.id),fixup 进 rewind/fork 提交。 - 上游改了测试 helper 签名:
runPromptWithError返回值从unknown变{result, updates}(上游要断言 session updates),我方新增测试fails the turn on a terminal stream disconnect还按旧签名用 → 调用处改const {result: error} = await ...,fixup 进「重连失败当成错误」提交。 - 上游注册方式演进撞我方表驱动:上游把 steering/goal_control 两个新 ext-method 的 parser(
sessionSteerParamsParser/goalControlParamsParser)定义在index.ts并手写.onRequest注册——按案例 2 套路搬进AcpExtensions.ts并加进EXTENSION_METHOD_REGISTRATIONS表,index.ts里删掉(连带import {z} from "zod"变成多余)。
- 上游给接口加必填字段:
- 锚点:
src/AcpExtensions.ts(类型 union / parser / 注册表)、src/CodexAcpClient.ts(forkSession的collaborationMode)、src/__tests__/CodexACPAgent/auth-error-events.test.ts({result: error}解构)。
案例 6:取消通知语义在 fork 内已两次转向 —— 以当前 cancelledPromptResponse 注释与测试为准(2026-10 重写)
- 旧行为(已被取代,别再加回来):上游 #358 删掉
notifyConversationInterrupted后,我方曾一度把它收拢进cancelledPromptResponse,让每次 cancel 都发一条Conversation interrupted(本案例原文的结论)。 - 现状:fork 提交「取消推送中断通知,由编辑器自行渲染」又把语义转回不发:
cancelledPromptResponse只返回stopReason: "cancelled",注释写明理由——编辑器自己渲染取消态(零输出取消 = 撤回 prompt + 恢复草稿;部分输出取消 = 自己追加中断标记),agent 再发一条会孤立或重复。notifyConversationInterrupted函数与调用点不要补回。 - 两条必须同时保住的行为(各自有测试):
- 「修复取消未开始 turn 时缺少中断通知」保的是取消生效不是通知:
cancel()在「无currentTurnId且无pendingTurnStarts」的窗口必须activePrompt.requestCancel()短路到cancelledPromptResponse,否则新会话刚发送就取消会让 prompt 正常跑完。 - 恢复会话的中断痕迹由回放补:
interruptedTurnTailItemIds读turn.status === "interrupted"的 tail item,streamHistoryItem在其上追加[Request interrupted by user];unmaterialized thread 直接跳过(别对它分页查询)。
- 「修复取消未开始 turn 时缺少中断通知」保的是取消生效不是通知:
- 判别:rebase 撞到这类冲突时,以源码注释 +
CodexAcpClient.test.ts的does not notify "Conversation interrupted"测试为真相,不要按本案例的历史版本恢复通知。 - 锚点:
src/CodexAcpServer.ts(cancelledPromptResponse、cancel、interruptedTurnTailItemIds调用、streamHistoryItem)、src/CodexAcpClient.ts(interruptedTurnTailItemIds)、src/__tests__/CodexACPAgent/{CodexAcpClient.test.ts,load-session.test.ts}、上游提交#358。
案例 7:上游 2.x 删掉整块旧回放模块 —— 3 个 shell fallback 补丁不再重放,但 request_user_input 窄回放必须移植(2026-10 实战)
- 现象:rebase 到上游 2.1.1 时,我方 3 个旧补丁(「跳过 JS REPL 内部 wait 调用」「修复 custom tool call 回退丢失 exec/shell 输出」「修复回放时 shell 指令解析不正确」)整块作废——上游删除了
ResponseItemHistoryFallback.ts(及其 shell 解析/回退渲染),改用 typed 流(thread/turns/list+thread/items/list)自带 tool call 渲染。git rebase会提示这些提交为空/需 skip。 - 判别:逐项确认上游 typed 实现覆盖了旧补丁承载的行为(tool call 的 create/update、exec/shell 输出、JS REPL wait 过滤)才 skip;只因为「冲突太乱」而丢不行。核对完在重放结果里
grep旧补丁引入的符号,确认上游确有等价实现。 - 不能丢的例外:
request_user_input的窄 rollout 补充必须保住。live 路径的问答对(tool_call/tool_call_update)不落 typed 流,只有 rollout JSONL 里有模型自己的function_call/function_call_output对;丢掉它 = 恢复会话后所有提问卡片消失。移植方式:保留RequestUserInputReplay.ts(只读那一种 pair,不是恢复整个旧回放模块)+RequestUserInputHistory.ts+ReplayFileRead.ts的字节上限,靠两个 store 共享的锚点(call_id= typed item id、user message 的client_id=clientId、agent message 共享文本)把卡片插回 typed 流;typed 流不是完整 rollout(rewind 截断、resume 边界起点晚),窗口外的卡片按turnAnchorKey丢弃。回放必须永不 fatal(rollout 缺失/超限/不可读只损失卡片)。 - 同源的字节预算:
ReplayBudget.ts(会话历史回放总量上限)+ReplayFileRead.ts(REPLAY_ROLLOUT_READ_CAP_BYTES)保留,别被上游分页实现冲掉。 - 锚点:
src/RequestUserInputReplay.ts、src/RequestUserInputHistory.ts、src/ReplayBudget.ts、src/ReplayFileRead.ts、src/CodexAcpServer.ts(分页streamThreadHistory与 replay 的衔接)、src/__tests__/{RequestUserInputReplay.test.ts,ReplayBudget.test.ts,ReplayFileRead.test.ts}。
案例 8:fork 改 elicitation 字段后缀会波及主仓 renderer —— __other → _note(2026-10)
- 现象:上游 #299 重做 request_user_input 表单:自由文本字段从
<id>__other改名<id>_note(且只在isOther的问题上出现,enum 里多了None of the above选项)。fork 的 fold 语义不变(非 AIR 客户端仍是<选项>(补充:<备注>);AIR 走None of the above+user_note:),但主仓ElicitationCard.tsx靠后缀名配对并排渲染,字段名一改配对即失效(输入框变成独立一行)。 - 解法:主仓
toDisplayFields同时认三种后缀——claude_custom、codex 新旧__other/_note(保留旧的给其他 agent/旧版本),并保证配对只在存在同名 enum 时发生,别把合法的<name>_note独立问题吞掉。acpElicitationForm.ts只透传字段、无需改动,但其头注释的后缀清单要同步。 - 教训:fork 侧任何表单字段命名/
_meta契约的变更都要过一遍主仓消费端(grep -E '_custom|__other|_note' -r apps/editor/src),升级核对表里把「elicitation 字段后缀」列为固定检查项。 - 锚点:
apps/editor/src/renderer/workbench/agents/ElicitationCard.tsx(toDisplayFields)、apps/editor/src/renderer/services/acp/acpElicitationForm.ts(头注释)、apps/editor/src/renderer/workbench/agents/__tests__/ElicitationCard.test.tsx、forksrc/CodexElicitationHandler.ts。
检查清单要点
- 调查阶段全程只读(
git ls-remote/gh api/ 只读 git),别在 plan mode 改 submodule。 - 真上游是
agentclientprotocol/codex-acp,不是已废弃的zed-industries/codex-acp。设错会出现 merge-base 为空 / 代码倒退的假象;用merge-base 命中基线+ahead/behind 与我方提交数吻合校验。 - submodule 是 detached HEAD;本地
main常已过时,基线用merge-base HEAD upstream/main的真实 sha,别信本地 main。rebase 前先落backup-before-rebase-<sha>分支。 - rebase 里
--ours= 被 rebase 到的上游侧、--theirs= 正在重放的我方提交(与平时相反)。package-lock.json取--ours后用npm ci验证;仅 manifest 与 lock 不一致时才用npm install重建 lock。 src/index.ts冲突:我方表驱动注册 + 上游 promptctx.signal两者都要(案例 2);先确认EXTENSION_METHOD_REGISTRATIONS覆盖上游手写方法。ext-method 的验收必须断言注册层(SDK 只路由显式注册过的 method,漏注册=methodNotFound、到不了 server 的 switch;见案例 2)。- 费用补丁是 fork 命脉:
_meta.quota走 totalTokenUsage 累计语义。撞上游 token-usage 快照时更新快照并 fixup 进费用提交(案例 3);别把它改回 lastTokenUsage。 - package.json 解冲突当心重复 key;version/codex/SDK 依赖取上游。
- 后处理改动(更新的快照、适配的测试)用
git commit --fixup=<sha>+GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash并入逻辑所属提交。 - rebase 零冲突 ≠ 语义正确:
npx tsc --noEmit和npm test都是必跑安全网,别因 rebase 顺利就跳过。 - fork 测试已知约 4 个 Windows 路径失败(案例 1)非回归——用「纯 upstream worktree + rebase 前备份分支」两处都失败来判别,别误判为回归,别改测试(会破坏 Linux CI)。
pnpm agent:build同时构建 claude-agent-acp + codex-acp 两个 fork;vendor-install只在 lock 哈希变化时重装(不带--omit=dev,devDeps 保留,可直接跑 fork 测试;若缺失先npm ci)。- 全流程末尾用
git diff --submodule=log vendor/codex-acp核对“我方提交在顶 + 上游新提交在下”,再提交主仓库指针。 - 合并方式固定 rebase,不用再问用户;只需就“推送范围”征询。选“仅本地不推送”时到本地
main指向合并结果 + 主仓库agent:build/pnpm check验证为止,不 push fork、不提交 submodule 指针。 - 自定义提交数别照抄(会变);skip 掉的重放项必须逐个确认上游有等价实现:3 个旧 shell fallback 补丁已被上游 typed 回放取代(案例 7),但
request_user_input窄 rollout 回放 + 回放字节预算(ReplayBudget/ReplayFileRead)是命脉,不能随旧模块一起丢。 - 取消行为以当前源码为准:不发
Conversation interruptedchunk(编辑器自行渲染,恢复时由回放补[Request interrupted by user]);不要按本 skill 的历史案例把通知加回来(案例 6)。 - fork 改 elicitation 字段后缀 → 同步主仓
ElicitationCard.toDisplayFields(_custom/__other/_note三种并存),并确认配对只在同名 enum 存在时发生(案例 8)。 - 上游 bump
@openai/codex时,主仓packages/node-services/src/agentBinary/flavors.ts的CODEX_VERSION必须同步到 lock 解析版本,并跑pnpm --filter @universe-editor/node-services run test。 - 实测真实 codex 行为:fork 内 skill
run-codex(npm run codex-test)的脚本已与上游 2.1.1 脱节——2026-10 实测在 initialize 后抛codexAcpClient.authRequired is not a function(CodexAcpClient已无该方法),别在它上面耗时间。改走 codex CLI 直连:vendor/codex-acp/node_modules/.bin/codex exec -s read-only -C <仓库根>。两个坑:必须重定向 stdin(< /dev/null),否则卡在Reading additional input from stdin不动直到超时;网关不通时表现为stream disconnected+ HTTP 451,与本仓库改动无关。 - 四条版本轴分别验收,不捆绑升级:① adapter:fork
package.json的 npm 包版本及上游基线 SHA;② Codex CLI:@openai/codex的 lock 解析版本,主仓packages/node-services/src/agentBinary/flavors.ts的CODEX_VERSION必须与之相符;③ app-server 协议:由该 CLI 的app-server generate-ts生成的src/app-server/类型(目录v2不是 adapter 版本,也不能据此认定存在独立 semver);④ ACP SDK:fork 与 editor 各自的 lock 解析版本,允许不同,但必须通过跨 SDK 的真实 wire 测试。tag、版本声明、生成类型和运行时能力不能互相替代。 - 删补丁必须保留承载该行为的测试:上游实现取代我方补丁时(案例 7 的 skip),删源码的同时要确认行为测试仍在(对齐到上游等价实现,或保留我方回归测试)。只删代码不留测试,下次 rebase 就无从判别该行为是否还在——不可逆的静默回归。
request_user_input窄回放正是靠RequestUserInputReplay.test.ts/ReplayBudget.test.ts/ReplayFileRead.test.ts守住的。 npm ci不能“重生成”lock(fork 内同样如此):它只按package-lock.json精确安装,manifest 与 lock 不一致会直接报错退出。只有 manifest 与上游 lock 真有分歧时才npm install重建,再npm ci复验(见「冲突套路 / package-lock.json」)。
关键参考路径
- 根
CLAUDE.md「内置 ACP agent」段 +scripts/release/{vendor-install.mjs,runtime-resources.mjs}、package.json的agent:build(含两个 fork) - 行为验收矩阵(每条保留行为挂在哪个测试入口、真进程还是桩、缺口在哪)→ references/acceptance-matrix.md
vendor/codex-acp/src/{index.ts,CodexAcpClient.ts,CodexEventHandler.ts,CodexAcpServer.ts,AcpExtensions.ts,PathUtils.ts}- codex-acp 构建:
vendor/codex-acp/build.mjs(esbuild → dist/index.js,external@openai/codex),非 bun - 主仓库
apps/editor/src/renderer/services/acp/(codex 会话/费用/标题消费端)、scripts/sync-codex-skill-policy.mjs(codex skills policy 同步) - 标题持久化的消费端 + 跨 bucket 回填案例:
apps/editor/src/renderer/services/acp/session/cases-session-ui.md
其它
- 姊妹 skill
update-claude-agent-acp是 claude-agent-acp fork 的同类流程,套路互通、案例可互相参考。 - 后续用本 skill,发现新经验,需同步更新本文件。