Files
codex-app-server/TEST-REPORT.md
T

488 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实测记录(2026-10-25,VM,codex-cli 0.164.0-alpha.9)
## 环境
- 本机已有官方 codex:`/opt/hatch-image/bin/codex`,`codex-cli 0.164.0-alpha.9`
- 无 cargo/rustc,2 核、可用内存约 0.5GB:从 Rust 源码现编不现实,
因此“照搬”走的是官方二进制自带的 app-server(即开源 core 编译产物),
而不是在本机重新编译一遍源码。协议 schema 同样由该二进制导出。
- `codex doctor`:除“active model provider auth env var is missing”外
其余健康;MCP 有可选配置问题(缺 env var 的 server)。
## 协议导出
- `codex app-server generate-json-schema --out …` 成功,导出 2.6MB,
本仓库只收了两个合并文件:`protocol/codex_app_server_protocol.schemas.json`
与 `protocol/codex_app_server_protocol.v2.schemas.json`。
- 从 v2 schema 确认的方法:initialize、initialized、model/list、
thread/start、thread/resume、thread/fork、thread/list、thread/archive、
thread/delete、turn/start、turn/steer、turn/interrupt、turn/completed。
- ThreadStartParams:cwd、model、modelProvider、sandbox、approvalPolicy、
approvalsReviewer、baseInstructions、developerInstructions 等。
- TurnStartParams:threadId、input(text)、model、effort、cwd、
parentTurnId、rootTurnId(委派链路用)、sandboxPolicy、approvalPolicy 等。
- features:`multi_agent` 为 stable/true,`multi_agent_v2` 为 stable/false。
## 客户端链路实测
隔离 CODEX_HOME 跑 client.py(见 README):
- initialize:成功。
- thread/start:成功,thread id 如 `01a17394-…`,cwd 正确落到 work/job-a。
- turn/start:成功,收到 turn/started 与 userMessage 事件。
- 模型调用:失败,`401 Unauthorized: Missing bearer token`,
重试 5 次后 turn/completed 状态 failed,耗时约 17 秒。
- 结论:协议、线程、回合、事件流、错误回推全部走通;
唯一缺的是模型登录,这一步在隔离目录里本就没有,
在已登录的机器上不需要改任何代码。
## 宿主编排实测(2026-10-25,host.py,中转站 gpt-6.1-sol,low 档位)
测试仓:`work/host-repo`,git 初始化,`sample.py` 只有 `hello()`。
隔离 CODEX_HOME(/tmp/codex-host-test,配了中转站 config),
配对二进制 `bin/codex`。
- **并行**:`parallel` 一次派 host-a(加 add2)和 host-b(加 mul2),
单个 app-server 进程内两个 thread 并发,约 70 秒双双 completed,
各自 worktree 里的 `sample.py` 正确,互不污染。
首跑曾因隔离目录缺中转站 config 而 401 到 api.openai.com,
补 config 后重跑通过——这是测试配置问题,不是 host 代码问题。
- **评审**:`review host-a` 给出 `M sample.py` + 完整 diff。
- **委派**:`delegate --parent-task host-a` 子任务只读检查 add2,
结论列了 NaN/inf、字符串拼接、TypeError 等边界,未改文件;
`--report-to-parent` 后主 thread 汇总了该结论。
- **fork / 续跑**:`fork host-a -> host-a-v2` 拿到新 thread id;
`run host-a` 续跑一回合“一句话说 add2 是干嘛的”成功。
- **目标循环**:`goal goal1` 目标是创建 `result.txt`(内容 goal-ok),
第 1 轮验收 `test -f result.txt` 通过,状态 goal-met。
- **合并**:`merge host-b` 先在 worktree 提交、再 `--no-ff` 合并,
主仓 `sample.py` 已含 mul2,worktree 已清理。
注意:merge 前必须先提交 worktree 改动,否则分支本身还是
旧 HEAD,合不到东西——这个 bug 在实测中发现并已修。
- **自动化**:`automations --add daily-check` + `run-due`,
到期任务自动派发并 completed,进评审队列。
- **状态**:`host-state.json` 当前任务:host-a completed、
host-b merged、host-a-review completed、host-a-v2 forked、
goal1 goal-met、daily-check-* completed。
## 宿主编排第二批对齐试验(2026-10-25 晚,中转站 gpt-6.1-sol,low 档位)
上一批测的是“能跑起来”,这一批测的是“边界和控制对不对齐”,
全部通过,只有一个协议坑被挖出来并已修成 host.py 的命令:
- **沙箱边界**:`--sandbox read-only` 的任务让 agent 建文件,
agent 回答“当前工作目录是只读文件系统”,文件确实没建出来。
边界不是摆设,是真拦住了。
- **审批 deny**:`--approval-policy untrusted --approval deny`,
agent 要跑 `echo approval-test-123`,宿主 deny 后 agent 明确说
“自动审批拒绝了”,没拿到输出。注意 host.py 里审批回答的
payload 是按实测反推的 `{"decision": "deny"}`,这条路是通的。
- **模型内子代理**:3 个子代理并行检查 sample.py,安全 / 质量 /
测试覆盖三份视角汇总回来,未改任何文件,结论还点名了主仓
已合并的 mul2 和 0% 测试覆盖——子代理不是走形式。
- **中断**:长诗任务跑了 5 秒后发 `turn/interrupt`,turn/completed
状态直接是 `interrupted`,不用等它写完。
- **compact**:直接对旧 thread 调 `thread/compact/start` 会报
`thread not found`,先 `thread/resume` 再 compact 就通了。
这是协议层面的先后顺序,host.py 已加 `compact` 命令,
内部就是 resume + compact,不让用户踩这个坑。
- **合并冲突**:conflict-a(hello-a)和 conflict-b(hello-b)改同一行,
先合 a 成功,主仓变 hello-a;再合 b 时 host 打印“合并冲突,
已 abort”,主仓仍是 hello-a 且干净,b 的 worktree 留着等人工解。
- **自动化幂等**:给 skip-test 设 interval 86400、last_run 为现在,
`run-due` 没派发它。同一轮里到期的 daily-check 被派发了,
但那次调用我没传中转站 key,turn 401 失败,已清理掉那个
误建任务和 worktree——这是测试调用失误,不是 host 逻辑问题。
## 宿主编排第三批对齐试验(2026-10-25 晚,ChatGPT 宿主能力)
这一批测的是 ChatGPT 宿主最有区分度的几件:宿主给工具、
宿主换模型、运行中改向、多任务并行上限、评审的未追踪文件、
以及失败时宿主是否稳得住。
- **宿主提供 MCP 工具(最重要的一条,带一个真修)**:
新写了 `examples/mcp_echo.py`(零依赖 stdio MCP,两个工具),
在隔离 CODEX_HOME 里注册为 `[mcp_servers.echo]`。
工具确实被 agent 发现了(工具名带 mcp__echo__ 前缀),但前两轮
都被审批拦住:第一轮 approval-policy=never 时直接报
“MCP tool call requires approval”;第二轮 untrusted + host 自动
放行时仍报 “user rejected MCP tool call”。
查 rollout 才找到根因:MCP 审批走的不是通用 approval,
而是 `mcpServer/elicitation/request`(mode=form),必须按
MCP elicitation 协议回 `action=accept/decline + content`,
回通用 `{"decision": ...}` 会被当成拒绝。
host.py 已按这个协议分流回答,第三轮 `mcp-test3` 成功:
agent 拿到了固定事实和 20+22=42,且未改文件。
结论:宿主给工具这条路是通的,但审批 payload 不能一刀切。
- **模型切换**:同一 thread host-a(原 gpt-6.1-sol)用
`--model gpt-6-sol` 续跑一回合,turn 的 model 字段确实是
gpt-6-sol,回答正确——宿主可以在 thread 内换模型。
- **运行中改向(steer)**:长诗任务跑 5 秒后发 `turn/steer`
(expectedTurnId 校验),turn 完成时已按新指令只剩 4 行短诗,
不是等旧任务跑完再开新回合。
- **3 并行**:host-c/div2、host-d/pow2、host-e/mod2 三个
worktree 同时跑,全部 completed,文件各自正确、互不污染。
- **未追踪文件评审**:`review goal1` 时 diff 为空,但 status
和 untracked 栏都列出了 result.txt——评审不能只看 diff,
未追踪文件要单独看,这一点 host.py 的 review 是对的。
- **失败不崩宿主**:用不存在的模型派发,task 状态为 failed,
错误是中转站 404(模型不可用),host.py 没崩、没卡住,
该任务的 worktree 和分支已清理。
- **未完成项**:另有一项仓库级上下文试验,其工具输出未通过
安全检查,本批报告不采用它的细节、不做结论;上面六项
不依赖它,结论独立成立。
## 生产级试验(2026-10-25 晚,真实项目仓,不是玩具 sample.py)
试验仓 `work/prod-repo`:core.py(add/sub)+ features/ 独立模块 +
tests/ 标准库 unittest,验收命令就是 `python3 -m unittest discover -s tests`。
起点 2 个测试,终点主仓 5 个功能、17 个测试全部通过。
- **并行生产**:prod-pow 和 prod-mod 各自建功能文件 + 测试文件,
不碰共享文件,并行完成,各自 worktree 4 个测试通过,互不污染。
- **目标循环(真实测试验收)**:prod-safe-div 1 轮通过,验收是
整个测试套件,不是“文件存在”。
- **评审闭环**:prod-parse 先实现了一个 naive 版本,委派只读评审
找出 5 条边界问题,汇总回主任务后,主任务修复为严格校验
(只接受 str、只接受 ASCII 十进制、空串抛错等),测试从 5 个
涨到 8 个,全部通过。评审任务本身没改任何文件。
- **合并链**:pow、mod、parse、safe-div、clamp 依次 `--no-ff`
合并进主仓,每次合并后主仓测试都跑过,最终 17 个测试 OK。
合并前评审时发现一个缺口并已修:新文件默认 untracked,
git diff 里看不到内容,review 现在会把未追踪文本文件内容
也打印出来,否则评审等于没看新文件。
- **自动化**:prod-nightly-tests(只读跑测试)run-due 派发完成,
紧接着再 run-due 一次全部跳过,幂等正确。
- **MCP(生产仓)**:prod-mcp 在生产仓 worktree 里调用宿主 MCP,
拿到固定事实和 7+8=15,未改文件。
- **失败与恢复(生产级真正挖出来的东西)**:
1. 委派评审第一轮因 shell 超时 300 秒被杀,turn_aborted,
且任务登记只在 turn 完成后才落盘,等于任务丢了。
已修:dispatch / delegate / goal / run-due 都先落盘再跑 turn,
中断后可用 run 续跑。
2. fork 换 worktree 后沙箱被服务端重置为 read-only,fork 时传
sandbox 参数不认;同 cwd fork 则保留 workspace-write。
已修:turn 级 sandboxPolicy 会覆盖 thread 级设置,run 时按
任务沙箱传,fork 隔离 worktree + 写操作已验证通过。
3. fork 拷工作区文件时把 .git 文件也拷过去了,新 worktree
的指针指回旧 worktree,git worktree 校验失败。
已修:拷文件时跳过 .git,已坏的 v2 清理、v3 指针已修复。
4. 目标循环第一版只说“验收未通过”,不告诉 agent 错在哪,
prod-retry 3 轮原地不动(测试数一直是 1)。
已修:目标循环把验收命令和失败输出喂给下一轮,且验收命令
本身要打印诊断信息(如 test_count=1, need >=3),
prod-retry2 第 1 轮失败、第 2 轮补到 3 个测试、目标达成。
- **生产级结论**:本地、无 GUI 的宿主编排,ChatGPT 宿主在
本地能做的那部分,基本对齐了——并行隔离、测试验收、评审门、
委派修复闭环、合并链、自动化、MCP、模型切换、改向/中断、
失败恢复,都在真实项目上跑通了。
没对齐的仍然是云端和界面那部分:云端派发、推送通知、
GitHub/Slack 触发、CI/PR 集成、GUI inline 评论、Computer Use,
以及一个已知局限:host-state.json 是单文件,多 host.py
进程同时写会有 read-modify-write 竞态,同一进程内的并行
是安全的,跨进程并行没有锁,不要两个 host.py 进程同时跑
同一个 state 文件。
## 生产加固(2026-10-26,state 锁 / 项目配置 / 定时自动化)
- **state 锁**:`host-state.json.lock` + fcntl 锁,锁只包住读/写
短临界区,不包整个模型 turn;保存时在锁里重读磁盘,按任务名
覆盖、按自动化名合并,tmp 文件带 pid。
实测:两个进程同时加载、睡 0.5 秒、各自加一个任务再保存,
最终两个任务都在,没有合并逻辑的话后保存的会把先保存的覆盖掉。
- **项目配置**:`host.py init` 生成 `host-config.json`(repo、
model、effort、沙箱、审批、codex_home、state,无密钥),
`host.py config` 打印有效配置,优先级 CLI > 配置 > 内置默认;
dispatch/goal 不传 --repo 时用配置里的 repo,没配则明确报错。
实测:/tmp 测试配置 init、config、repo 缺省路径都通;
项目里默认没有 host-config.json,旧调用方式不受影响。
- **定时自动化**:run-due 现在写 `host-run.log`(派发和跳过都记),
`automations --cron-line` 只打印 crontab 行,不装机;
`automations --install-cron` 才真正写 crontab 且幂等。
实测:未到期的自动化 run-due 输出为空,但日志里有一条 skip;
cron 行已验证格式正确,本次没有实际装机——装机后模型任务
会自动跑、花中转站额度,所以装机这一步留给用户显式下令。
另注意:VM 会被平台回收,crontab 不一定活过回收,常驻机器
用 crontab,VM 上更稳的是 Muse cron 定时调 run-due。
- **未修 / 已知局限**:锁只能防同一台机器上的多进程竞态,
不能防两台机器同时写同一个共享 state 文件;定时任务本身
仍需外部调度器(crontab 或 Muse cron)触发,host.py 自己
不常驻。
## 真实项目一轮(2026-10-26 凌晨,用户选“先在真实项目上跑一轮再打包”)
- **选项目**:没动生产敏感的 wechat-bridge(带 session 凭证),
选 codex-app-server 本身做 dogfooding——这是真实代码,不是
玩具仓。为此先把本项目初始化成 git 仓(commit b49b9cf、
d1e842b):.gitignore 排除 bin/(367M 本机补丁)、work/、
host-state*.json、host-run.log、本地 host-config*.json;
真实任务用独立 state(host-state-real.json),与玩具任务的
host-state.json 分开。
- **并行任务**:real-tests(新建 tests/test_host.py,标准库
unittest 覆盖 load_state/save_state 合并、task_diff 未追踪
文件、_load_config_file 缺失文件,3 个测试全过,不改
host.py);real-backup(save_state 锁内加备份轮转,保留
.bak/.bak.1/.bak.2,py_compile 通过,临时 state 两次保存
后 .bak 存在)。
- **评审**:两个 diff 都干净,无无关改动;real-backup 的只读
评审指出轮转多步非原子、复制中断可能留截断 .bak、无
fsync、无 fcntl 平台上锁失效——前两条做了小修复(见下),
后两条是本地单机工具的已知边界,未为此上重型方案。
- **修复轮**:real-backup 追加一回合,把 _read_state_file 和
save_state 的 open 改成 with open(修掉真实测试跑出的
ResourceWarning 未关闭文件),备份复制改成先拷临时文件再
os.replace 到 .bak;轮转层数和锁逻辑未改,未新建 tests/。
- **合并验收**:real-tests、real-backup 依次 merge 到主仓
(commit 7f4e2ed、73fdead,主仓 clean,只剩 work/ 下的
本地 job 定义,未进版本控制);主仓 `python3 -m unittest
discover -s tests -v` 3 个测试全过,且无 ResourceWarning;
主仓手动保存两次,.bak 存的是前一版 state,主文件是
合并后版本,备份逻辑正确。
- **玩具仓没暴露、真实轮暴露的**:项目本身不是 git 仓时 host
的 worktree/merge 门跑不起来,必须先 git init;.gitignore
缺失时 367M 的 bin/ 会进版本控制;state 文件和本地配置
必须与玩具任务分开,否则真实任务和测试任务混在一个
host-state.json 里;save_state 的未关闭文件警告只有在真实
测试跑起来后才被看见。
- **结论**:host 编排在真实项目上跑通了完整链路(并行 →
评审 → 只读评审 → 修复 → 合并 → 主仓测试验收),下一步
才是打包:.gitignore 已有、tests/ 已有雏形、备份已落地,
打包缺的仍是安装说明、host 快速开始、版本号/changelog、
以及 merge 后自动跑主仓测试这一步(本次是手动跑的)。
- **state 锁**:`host-state.json.lock` + fcntl 锁,锁只包住读/写
短临界区,不包整个模型 turn;保存时在锁里重读磁盘,按任务名
覆盖、按自动化名合并,tmp 文件带 pid。
实测:两个进程同时加载、睡 0.5 秒、各自加一个任务再保存,
最终两个任务都在,没有合并逻辑的话后保存的会把先保存的覆盖掉。
- **项目配置**:`host.py init` 生成 `host-config.json`(repo、
model、effort、沙箱、审批、codex_home、state,无密钥),
`host.py config` 打印有效配置,优先级 CLI > 配置 > 内置默认;
dispatch/goal 不传 --repo 时用配置里的 repo,没配则明确报错。
实测:/tmp 测试配置 init、config、repo 缺省路径都通;
项目里默认没有 host-config.json,旧调用方式不受影响。
- **定时自动化**:run-due 现在写 `host-run.log`(派发和跳过都记),
`automations --cron-line` 只打印 crontab 行,不装机;
`automations --install-cron` 才真正写 crontab 且幂等。
实测:未到期的自动化 run-due 输出为空,但日志里有一条 skip;
cron 行已验证格式正确,本次没有实际装机——装机后模型任务
会自动跑、花中转站额度,所以装机这一步留给用户显式下令。
另注意:VM 会被平台回收,crontab 不一定活过回收,常驻机器
用 crontab,VM 上更稳的是 Muse cron 定时调 run-due。
- **未修 / 已知局限**:锁只能防同一台机器上的多进程竞态,
不能防两台机器同时写同一个共享 state 文件;定时任务本身
仍需外部调度器(crontab 或 Muse cron)触发,host.py 自己
不常驻。
## 补充:原始协议探针(同一晚,隔离目录)
- `model/list` 成功:返回 `gpt-6.1-sol`,推理档位 low/medium/high/
xhigh/max/ultra,`multiAgentVersion` 为 `v2`。
- `thread/list` 在隔离目录下为空,符合预期(不污染真实线程)。
- 附带警告(不影响核心链路):本机缺 bubblewrap 时用内置兜底;
Code Mode 因缺 `codex-code-mode-host` 可执行文件而 fail closed,
需该功能时要另装该组件。
## 工具执行修复(2026-10-25,用户选“先修好工具执行问题”后)
根因是两层缺件,不是中转站问题,rollout 日志里有明文证据:
1. **缺 code-mode host**:系统里的 codex 是裸二进制,
`/opt/hatch-image/bin` 是只读挂载,旁边没有
`codex-code-mode-host`,app-server 报
“Code Mode is unavailable … host executable was not found”,
工具 fail closed,agent 称“写入工具不可用”。
修复:从官方 npm 平台包
`@openai/codex@0.164.0-alpha.9-linux-x64` 里取出同版本
`codex-code-mode-host`(73MB),与 codex 放到同一目录。
注意:host 只认真实可执行文件旁边的路径,放 PATH 里没用;
系统目录只读,所以配对目录只能放 workspace,不能直接补系统目录。
2. **缺 bwrap**:配对后工具能启动了,但写文件仍 3 次 instant failed,
rollout 里 panic 明文是
“bubblewrap is unavailable: no system bwrap was found on PATH
and no bundled codex-resources/bwrap binary was found
next to the Codex executable”。
修复:把系统目录里的 `codex-resources/bwrap` 也复制到配对目录。
修复后配对目录:`bin/codex`(293MB)+
`bin/codex-code-mode-host`(74MB)+
`bin/codex-resources/bwrap`(502KB),合计约 366MB。
这是本机 VM 的补丁,Mac 上 npm 正常安装自带这三件套,不需要它;
`start.sh` 已改成优先用 `bin/codex`,`client.py` 也支持
`CODEX_BIN` 环境变量指定配对二进制。
修复后实测(中转站,gpt-6.1-sol,effort low,workspace-write):
- 回合 10.3 秒完成,状态 completed,不是 failed。
- `commandExecution` 状态 completed,命令是
`printf '%s\n' 'codex-ok' > test-result.txt && cat test-result.txt`。
- 落盘验证:`work/xcm-test/test-result.txt` 内容确为 `codex-ok`。
- 此前失败的两个版本(max 60 秒、配对后无 bwrap 113 秒)均未建文件,
对照说明修的是执行环境,不是 prompt 或模型问题。
## 未实测
- 真实模型出结果的一回合(缺登录,VM 里不放密钥)。——已过期:
上面中转站实测已覆盖,见“工具执行修复”一节。
- ws / unix 传输的多客户端并发(start.sh 已把参数透传给官方
app-server,传输层本身是官方实现的,未在本机多客户端压测)。
- 审批请求的完整回包字段:各版本间字段有差异,client.py 默认
用 never 绕开审批;on-request + auto/deny 的回包是尽力而为,
生产交互客户端应以本版 protocol schema 为准再对齐一次。
## 第二类缺口补齐(2026-10-26 凌晨,用户选“继续补齐第二类所有缺口”)
- **合并后自动验测 + 回滚**:merge_task 抽成可单测函数,
默认仓库有 tests/ 就跑 unittest,验测失败 reset 回合并前
HEAD,分支/worktree 保留,任务标 merge-failed 并走失败通知。
单测覆盖验测通过清 worktree、验测失败回滚主仓 intact。
- **任务依赖 + 优先级 + 重试**:jobs 里 depends_on/priority/
max_retries,topo_sort_jobs 拓扑排序(缺依赖/成环抛错),
parallel 按波次跑,依赖失败时依赖任务标 skipped 且落盘
(修过一次 skipped 未落盘导致 discard KeyError 的 bug)。
依赖任务现在从依赖分支建 worktree(dispatch_from_branch:
先把依赖 worktree 提交到依赖分支,再从该分支建新分支),
这样依赖分支是依赖任务分支的祖先,合并时不再重复加同一段。
重试:run_task_turn_with_retry,默认 0 次行为不变,配了
max_retries 时失败后同 prompt 再跑,重试前查预算。
- **评审评论回流**:review-comment --file --line --comment
存进任务,review 时列出 open 评论,fix-review 按 open
评论逐条修,修完标 resolved。模型实测:gap2-b 的
add2_mul2 docstring 评论,第一次 fix-review 因我给的
300 秒 shell 超时被杀(turn 本身没完成,评论正确地保持
open),第二次 900 秒超时下 51 秒完成并标 resolved。
- **server 崩溃重连**:Server 抽出 _start/ensure_alive,
进程死了重拉并 resume thread(thread 在 CODEX_HOME 里),
request 遇 BrokenPipe 重试一次。单测 kill 进程后重拉 +
list 能通。
- **成本预算 + 失败通知**:配置 max_daily_turns(按 turn 数,
协议不暴露真实 token 数,这是诚实边界),超预算不派发/
不重试;失败时写 host-run.log + 跑 notify_command(本地
shell,模板 {task} {status});failures 列失败任务,doctor
列 version/备份/失败/缺失 worktree/open 评论。模型实测:
不存在的模型 + max_retries=1,跑了 2 个 turn(retry_count=1,
均为 404),status failed,notify 日志里有 gap3-fail failed。
- **多机器 state 冲突**:state 加 version/last_writer/
updated_at,保存时磁盘版本比加载时新就记 STATE-CONFLICT
日志并合并保留双方任务。单测:磁盘版本被另一写者改到 5
后再保存,合并后版本 6,双方任务都在,日志里有冲突行。
诚实边界:这只是“检测 + 不丢”,不是 ChatGPT 云端那种
中心同步,两台机器共享一个 state 文件仍不推荐。
- **模型实测 gap2/gap3(依赖 + 合并,教训最多的一轮)**:
第一轮 gap(/tmp)因 VM 回收丢了 /tmp 里的 repo 和 state,
但 home 下的 worktree 还在,证明依赖拷贝已生效(gap-b 里
有 gap-a 的 add2 且未重复定义);第二轮 gap2(state 放
home)先因 codex_home 新建后没有中转站配置 401 失败两次,
靠失败通知和依赖 skipped 门正确地没浪费后续调用,修配置
(model_provider + [model_providers.xcmapi] 两部分都要有,
只给其一时要么走 api.openai.com 401,要么报 provider not found)
后第三轮并行全通:gap2-a(优先级 5)先跑,gap2-b 依赖它且
工作区里有 add2,gap2-c 独立;评审评论 + fix-review 通;
合并时暴露两个真问题:① 依赖任务文件拷贝法建的分支没有
血缘关系,合并依赖任务时在同一位置重复加 add2,-X ours
自动解会把 mul2 整段丢掉——幸好新增行核对把它拦下来回滚
了(这是安全网的价值,不是 bug);② merge 的 git add -A
会把 __pycache__/*.pyc 提交进主仓,下次合并被 .pyc 改动
挡住。① 用“从依赖分支建 worktree”修了,② 用“合并前清
worktree 的 __pycache__/*.pyc,主仓 .pyc 改动先还原”修了;
gap3 用新方法跑依赖对 + 合并全通(add2/mul2/add2_mul2,
测试通过,值 3/6/8),gap2 的遗留分支用手动取目标文件内容
的方式解完(主仓最终 add2/mul2/add2_mul2/sub2 全有,测试通过)。
独立任务在同一文件同一位置追加时仍会冲突,这是 git 本身
的行为,host 正确的做法是 abort/回滚 + 人工解,不是自动猜。
- **回归**:tests/test_host.py 从 3 个涨到 15 个,全部通过,
包括 topo 排序、merge 验测通过/失败回滚/重复加自动解/
pyc 不入库、评审评论、重试成功/预算拦截/通知落盘、
state 版本冲突、server kill 重拉。
## 第二阶段:守护进程 + 健康检查 + 自动恢复(2026-10-26 凌晨,用户选“先做第二阶段”)
- **先堵的洞**:旧 `_read_state_file` 会把 JSON 解析错误
吞掉、当成空 state 返回——state 损坏一次就等于全部任务
静默丢失。新加 `load_state_strict`(文件存在但解析失败
时返回错误,不当空 state),health/recover 都用严格版。
- **health(不跑模型)**:查 codex 二进制存在、配置可读、
state 可解析、备份存在、daemon pid 存活/过期、server
能 initialize + list、缺失 worktree。实测:完好时 ok=true、
server_threads=7、exit 0;故意写坏 state 后 ok=false、
state_valid=false、带解析错误信息、exit 1。
- **restore-state**:state 损坏或丢失时按 .bak、.bak.1、
.bak.2 顺序找第一个能解析的备份,先把坏文件改名成
.corrupt.<时间戳> 留底,再把备份拷回。实测:坏 state +
version=9 的 .bak,恢复后 version=9,health 回到 ok=true。
- **recover(只标记,不自动重跑模型)**:status=running
且最后回合超过 --older-than(默认 3600 秒)未更新的
任务标 interrupted 并记日志,用户随后可用 run 续跑
(thread 在 CODEX_HOME 里不丢)。不自动重跑是刻意的
安全边界:自动重跑会不经用户同意花中转站额度。
实测:2 小时前的 stuck -> interrupted,1 分钟前的
fresh 保持 running;单测覆盖只标旧任务。
- **daemon(双 fork)**:daemonize 用 fork -> setsid ->
再 fork,孙进程 reparent 到 PID 1、写 pidfile、stdout/
stderr 重定向到 host-daemon.log,循环每 interval(默认
300 秒)跑 recover + run-due,每轮都是独立子进程调
host.py CLI,一轮崩了不带走 daemon 本体。实测(interval
5 秒):daemon pid 10776、PPID=1(活过 launcher 会话),
daemon-status alive=true,日志每 5 秒一个 tick;
daemon-stop 后 pid 消失、status alive=false、pidfile
删除、日志有 host daemon stopped。
- **VM 边界(诚实说)**:daemon 本身活不过平台回收,
和微信桥 serve 同理——回收后要重拉,Muse cron 定时调
run-due/health 才是 VM 上更稳的定时。本轮没有自动装
Muse cron 或 crontab,因为 run-due 到期会跑模型花额度,
装定时这一步留给用户显式下令(见下一条待办)。
- **回归**:tests 从 15 个涨到 19 个,全部通过,新增
corrupt 检测 + 从 .bak 恢复、无备份时不覆盖、recover
只标旧任务、health 用假 server 全通过。
- **待办(不在本轮自动做)**:Muse cron 定时调
`host.py --config <config> health`(无模型成本)和 /
或 `run-due`(到期才跑模型),需用户定间隔和是否允许
自动跑模型后才装。
## GitHub PR/CI 一条线(2026-10-26,用户选“先做 GitHub PR/CI 这一条线”)
- **只做代码和本地可测部分,真实 PR 本轮没跑**:本仓没有
git remote origin(`git remote -v` 为空),gh 未登录
(`gh auth status`:未登录)。按任务要求,不登录 GitHub、
不向真实 GitHub push 或建 PR,真实 PR 那一步留给用户以后
给仓库远端和 PAT 之后再跑;PAT 走 Secure Vault,绝不写进
文件、state、日志或聊天记录。
- **新增命令(都在 Server 创建之前,不跑模型)**:
- `github-workflow`:生成 `.github/workflows/host-verify.yml`
(checkout + setup-python 3.12 + `python3 -m unittest
discover -s tests -v`),幂等:已存在且内容相同则不改写。
临时仓库实测:第一次 changed=true,第二次 changed=false。
- `pr --task <名>`:校验任务(无 branch 或状态不是
completed/merged 就报错退出)→ 查 remote origin(无 remote
就明确报错,不静默失败)→ `git push origin <branch>` →
`gh pr create --title/--body/--head`(body 含任务名、提示摘要、
未解决评审评论)→ 成功后 pr_url/pr_number 写回任务记录并
保存 state;失败打印 gh/git 错误原文,不编造 URL。
- `pr-checks --task <名>`:调 `gh pr checks <number>`,解析
输出返回 {pr_url, checks: [{name, state}], all_pass},
all_pass 仅当 checks 非空且全部为 pass 时为真(空 checks
不算过,否则空 PR 也能合)。解析兼容制表符格式和
人类可读格式(✓ build pass / ✗ test fail)。
- `pr-merge --task <名>`:先调 pr-checks,未全过拒绝且不调
merge,并打印未过项;全过才 `gh pr merge --merge`,成功
后任务状态标 merged。**不动本地主分支**:合并由 GitHub
完成,和本地 merge_task()(直接合进本地主仓 + 自动验测
回滚)是两条线,一个任务不要两边都合。
- **gh 可注入**:全部 gh 调用走 `run_gh(args, cwd, runner)`,
单测注入假 gh,不依赖真实网络和登录;真实路径走 gh CLI。
- **单测**:tests 从 19 个涨到 27 个,全部通过。新增 8 个:
workflow 内容正确且幂等(含被改坏后恢复);pr-checks
解析 pass / fail / mixed / 空;pr-merge 未全过时拒绝且未
调 merge、全过时才调 merge 并标 merged;无 branch /
非 completed 状态被拒;无 remote 时 pr 报错含 remote;
本地 bare 远端 push 成功后假 gh 返回 PR URL(pr_number
42 写回任务,bare 远端里确有 host/t1 分支);CLI 层
`pr --task` 在无 remote 时退出码非零且输出含 remote。
- **未动的部分**:本地 merge/goal/review 行为未改;没有动
~/.codex/config.toml;没有动 wechat-bridge;没有提交
git commit,改动留在工作区。
- **真实 PR 仍缺什么**:仓库远端(`git remote add origin <url>`)
和 gh 登录(PAT 走 Secure Vault)。两者齐了之后的顺序:
github-workflow 生成并提交 workflow → 任务 completed 后
pr → pr-checks → pr-merge。