update-codex-acp

更新内置 Codex ACP agent fork(vendor/codex-acp,自维护 git submodule),合并上游 agentclientprotocol/codex-acp 新提交并重建发布产物。当用户说更新 codex-acp / 合并上游修改 / 同步 codex agent / 升级内置 codex agent 时使用。注意真上游是 agentclientprotocol

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-lease push 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') 自建 mockFiles Map,只桩了 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 重跑即绿。

三层验证阶梯(由内到外,别只跑最外层):

  1. 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。
  2. 主仓 wire 契约:UNIVERSE_FORK_CONTRACT=1 pnpm --filter @universe-editor/editor test:integration acpForkContract(须在 pnpm agent:build 之后)。证明 editor↔fork 的 ext-method / _meta 形状没漂;与 CI acp-contract 跑的是同一条。
  3. 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、fixture src/__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_REGISTRATIONS for 循环、但 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.ts 6 个 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,totalTokenUsage vs lastTokenUsage)、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 抓出来的三类):
    1. 上游给接口加必填字段:SessionMetadata 新增必填 collaborationMode,我方 forkSession 的返回字面量缺它(TS2741)→ 照上游其他返回点补 collaborationMode: this.getCollaborationMode(thread.id),fixup 进 rewind/fork 提交。
    2. 上游改了测试 helper 签名:runPromptWithError 返回值从 unknown 变 {result, updates}(上游要断言 session updates),我方新增测试 fails the turn on a terminal stream disconnect 还按旧签名用 → 调用处改 const {result: error} = await ...,fixup 进「重连失败当成错误」提交。
    3. 上游注册方式演进撞我方表驱动:上游把 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 函数与调用点不要补回。
  • 两条必须同时保住的行为(各自有测试):
    1. 「修复取消未开始 turn 时缺少中断通知」保的是取消生效不是通知:cancel() 在「无 currentTurnId 且无 pendingTurnStarts」的窗口必须 activePrompt.requestCancel() 短路到 cancelledPromptResponse,否则新会话刚发送就取消会让 prompt 正常跑完。
    2. 恢复会话的中断痕迹由回放补:interruptedTurnTailItemIds 读 turn.status === "interrupted" 的 tail item,streamHistoryItem 在其上追加 [Request interrupted by user];unmaterialized thread 直接跳过(别对它分页查询)。
  • 判别: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、fork src/CodexElicitationHandler.ts。

检查清单要点

  1. 调查阶段全程只读(git ls-remote / gh api / 只读 git),别在 plan mode 改 submodule。
  2. 真上游是 agentclientprotocol/codex-acp,不是已废弃的 zed-industries/codex-acp。设错会出现 merge-base 为空 / 代码倒退的假象;用 merge-base 命中基线 + ahead/behind 与我方提交数吻合 校验。
  3. submodule 是 detached HEAD;本地 main 常已过时,基线用 merge-base HEAD upstream/main 的真实 sha,别信本地 main。rebase 前先落 backup-before-rebase-<sha> 分支。
  4. rebase 里 --ours = 被 rebase 到的上游侧、--theirs = 正在重放的我方提交(与平时相反)。package-lock.json 取 --ours 后用 npm ci 验证;仅 manifest 与 lock 不一致时才用 npm install 重建 lock。
  5. src/index.ts 冲突:我方表驱动注册 + 上游 prompt ctx.signal 两者都要(案例 2);先确认 EXTENSION_METHOD_REGISTRATIONS 覆盖上游手写方法。ext-method 的验收必须断言注册层(SDK 只路由显式注册过的 method,漏注册=methodNotFound、到不了 server 的 switch;见案例 2)。
  6. 费用补丁是 fork 命脉:_meta.quota 走 totalTokenUsage 累计语义。撞上游 token-usage 快照时更新快照并 fixup 进费用提交(案例 3);别把它改回 lastTokenUsage。
  7. package.json 解冲突当心重复 key;version/codex/SDK 依赖取上游。
  8. 后处理改动(更新的快照、适配的测试)用 git commit --fixup=<sha> + GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash 并入逻辑所属提交。
  9. rebase 零冲突 ≠ 语义正确:npx tsc --noEmit 和 npm test 都是必跑安全网,别因 rebase 顺利就跳过。
  10. fork 测试已知约 4 个 Windows 路径失败(案例 1)非回归——用「纯 upstream worktree + rebase 前备份分支」两处都失败来判别,别误判为回归,别改测试(会破坏 Linux CI)。
  11. pnpm agent:build 同时构建 claude-agent-acp + codex-acp 两个 fork;vendor-install 只在 lock 哈希变化时重装(不带 --omit=dev,devDeps 保留,可直接跑 fork 测试;若缺失先 npm ci)。
  12. 全流程末尾用 git diff --submodule=log vendor/codex-acp 核对“我方提交在顶 + 上游新提交在下”,再提交主仓库指针。
  13. 合并方式固定 rebase,不用再问用户;只需就“推送范围”征询。选“仅本地不推送”时到本地 main 指向合并结果 + 主仓库 agent:build/pnpm check 验证为止,不 push fork、不提交 submodule 指针。
  14. 自定义提交数别照抄(会变);skip 掉的重放项必须逐个确认上游有等价实现:3 个旧 shell fallback 补丁已被上游 typed 回放取代(案例 7),但 request_user_input 窄 rollout 回放 + 回放字节预算(ReplayBudget/ReplayFileRead)是命脉,不能随旧模块一起丢。
  15. 取消行为以当前源码为准:不发 Conversation interrupted chunk(编辑器自行渲染,恢复时由回放补 [Request interrupted by user]);不要按本 skill 的历史案例把通知加回来(案例 6)。
  16. fork 改 elicitation 字段后缀 → 同步主仓 ElicitationCard.toDisplayFields(_custom/__other/_note 三种并存),并确认配对只在同名 enum 存在时发生(案例 8)。
  17. 上游 bump @openai/codex 时,主仓 packages/node-services/src/agentBinary/flavors.ts 的 CODEX_VERSION 必须同步到 lock 解析版本,并跑 pnpm --filter @universe-editor/node-services run test。
  18. 实测真实 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,与本仓库改动无关。
  19. 四条版本轴分别验收,不捆绑升级:① 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、版本声明、生成类型和运行时能力不能互相替代。
  20. 删补丁必须保留承载该行为的测试:上游实现取代我方补丁时(案例 7 的 skip),删源码的同时要确认行为测试仍在(对齐到上游等价实现,或保留我方回归测试)。只删代码不留测试,下次 rebase 就无从判别该行为是否还在——不可逆的静默回归。request_user_input 窄回放正是靠 RequestUserInputReplay.test.ts / ReplayBudget.test.ts / ReplayFileRead.test.ts 守住的。
  21. 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,发现新经验,需同步更新本文件。