488 lines
31 KiB
Markdown
488 lines
31 KiB
Markdown
# 实测记录(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。
|