HT001 · 从零写一个终端 IDE¶
2026-09-07,一个 agent 在 flower 下从零写出了一个跑在 macOS 终端里的 C/C++ 单文件 IDE。 它连续跑了 10.4 小时,花掉 $171.62,交出 12,212 行产品代码和 13,291 行测试, 中途断过一次网、自己接着跑完。这一页不是成果展示 —— 是把框架的每条主张拿到真实规模上量一遍, 包括量出来站不住的那两条:协调者"经常忽视权限限制"是假的,而它自己给自己的验收里有一条判错了。
完整记录(含 19MB transcript)在 ChenyuHeee/cppide。 下面每个数字都由 tools/analyze_run.py 从 sessions.db 算出,可复现:
这一页的数字出自哪里
HT001 的 runs/ 在上面那个 cppide 仓库里,不在 flower 仓库里。所以本页的数字是 从当时的运行记录原样搬运的,写这一页时没有再对着 sessions.db 复算一遍 —— 要复算,得先 clone cppide,再跑上面那条命令。 HT002 反过来:原始记录就在本仓库,那一页的数字逐条复算过, 对不上的两处直接写在了页面上。
这一跑的形状¶
| 项 | 值 |
|---|---|
| 任务 | "做一个 macOS 终端里的 C/C++ 单文件 IDE,给 ICPC 训练用" |
| 花费 | $171.62 —— 确认需求 $0.3704 / 5 轮,干活 $171.2476 / 31 轮 |
| 时长 | 0.06h + 10.44h |
| 主线程上下文 | 28.7K → 185.9K,70 个模型轮次单调增长,全程没压缩 |
| 分工 | 协调者 32 次工具调用 vs subagent 1,893 次;正文字符 94.8% 落在 subagent |
| 缓存 | 总输入 299.4M token,命中 96.1% |
| 产出 | src/ 12,212 行、tests/ 13,291 行(约 3,000 条断言)、README 44K |
| 版本 | 加目标看守之前的 flower,所以这一跑没有每轮判定 |
这一页有两个不同的"轮"
表里的 5 轮 / 31 轮 是 runs/manifest.json 记的 num_turns,一个步骤一个数; 下面上下文曲线里的"第 70 轮"数的是主线程上的模型轮次,即每条 assistant 消息。 两者数的不是一件事,不要互相换算。
一、上下文经济学:94.8% 的现场没进主线程¶
这是框架的核心主张 —— 协调者只装决策, 现场沉到 subagent 和磁盘。机制本身见上下文经济学。
| 主线程 | subagent | subagent 占比 | |
|---|---|---|---|
| 模型轮次 | 70 | 3.0K | 97.7% |
| 正文字符 | 200.1K | 3.6M | 94.8% |
| 工具调用 | 32 | 1,893 | —— |
这一跑派出了 23 个 subagent,输出 token 最大 184,377、中位 72,197、最小 11,684。
1,893 次动手的工具调用(1,034 Bash / 471 Read / 258 Edit / 126 Write),只有 32 次进了协调者的视野 —— 59:1。 平均每派一次人,有 82 个工具调用是它看不见的。
早先的小规模测试量到 83%,真实规模是 94.8%。规模越大,分工的收益越大 —— 因为任务书和回话的开销是每次派人固定的, 而被挡在外面的现场随任务复杂度增长。
二、上下文曲线:第一次知道"长程"的上限在哪¶
每条 assistant 消息的 input + cache_read + cache_creation 就是那一轮模型看到的上下文:
- 70 轮单调增长,斜率约 2.2K/轮
- 全程没有发生过压缩(
DISABLE_AUTO_COMPACT=1;唯一一次回落是断网那轮的空记录) - 用掉 1M 窗口的 18.6%
按这个斜率外推,撞墙在约 440 轮。 这次 70 轮产出了 12K 行代码, 所以当前形态下长程的真实上限大约是这次的 6 倍。 这是个可以拿去做决策的数字,以前只能猜。
要突破它,得让主线程的增长次线性 —— 更多沉到磁盘、更少留在对话里。
三、缓存命中 96.1% —— $171.62 能成立全靠它¶
299M 输入 token 只花了 $171,因为 96% 走的是缓存价。
推论:长程运行的经济性建立在缓存命中率上,而缓存命中率取决于上下文前缀的稳定性。 任何"重排上下文"的优化 —— 把旧内容挪走、往前面插一段新的系统提示 —— 都会击穿缓存, 省下的 token 可能远抵不上失去的缓存折扣。这条以前没量过,现在有数了。
这也是 flower 用换代而不用压缩的一个附带理由: 换代是开一条新会话从头建缓存,前缀在一代之内始终稳定。
四、工作台复用:61 个脚本,没有一个写完就扔¶
工作台的主张是"脚本写一次,以后直接跑,不再重写"。 验证方式不是问 agent,是数 .flower/scripts/ 里的脚本在 transcript 里被 Write/Edit 和被 Bash 执行的次数:
wave5-verify.sh 写了 5 次、跑了 45 次 —— 迭代打磨然后反复用,正是想要的形状。 没有一个脚本是写完就扔的。
五、协调者动手被拒 1 次,而且是白名单的错¶
动手之前的怀疑是:"协调者经常忽视权限限制,去触发大量被拒调用。"实测把这个前提打掉了。
32 次工具调用里被拒 1 次(3.1%),而那一次是:
which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs
逐段判定:echo 放行、ls 放行、which 拦下 —— 它不在 一次性命令表里。
整条命令纯只读,是一次标准的环境探测。模型做得完全正确,是白名单不全, 逼它为一句 which g++ 付了一次 subagent 启动成本(约 4.3k token)。
已修:补进 which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers。command 和 type 只放行查询形式 —— 光秃秃的 command rm -rf / 是执行,少写那个 -v 就等于开后门, tests/glance.py 里有 7 条对抗样本守着。
这条最值得记的不是那个补丁,是方法。 一个持续了很久的架构讨论,起点前提在真实数据上是 1/32, 而且归因反了 —— 问题不在模型,在白名单。先量再说。
六、断网续跑:第一次被真实故障验证¶
01:52:40 tool_result: "Agent terminated early due to an API error" ← 一个 subagent 被打断
01:55:41 assistant: "API Error: ENOTFOUND" ← 主线程收到
↓ 探针挂着等网络恢复
resumed=True,续跑同一条会话,又跑了 8 个多小时到完成
manifest 记着 attempts=2 / resumed=True / ok=True。10 小时的活没有从头再来。 在这之前,韧性一直挂在"仍未验证"里 —— 掐网要 sudo 改 hosts,没人想在真跑时干这个。
那条合成的 "API Error" 消息被 PruningSessionStore 在 load 时摘掉了,模型没有看见它 —— 剪除的设计也一并被真实故障验证。
暴露出来的两个问题(见 issue #2):
- 恢复发生在框架层而不是传输层。代价是整个上下文重放一遍,而那次上下文很大。
- 被打断的那个 subagent,半成品工作丢了。 resume 救的是主线程, subagent 级的中断恢复是另一个问题。
七、它的自我验证是真的吗¶
这是最该怀疑的地方:agent 自己写测试、自己跑、自己宣布通过,很容易变成走过场。
事后派了一个独立的审计 subagent,静态读完 58 个脚本和 22 份产出。结论是真的,而且严格度罕见:
- 恒真断言 0 处。 全仓库没有
[ 1 = 1 ]那类模式。 || true共 8 处,7 处正当(grep -c无匹配返回 1、ulimit兜底之类)。- 判定的是行为,不是文件存在。
audit-idle-cost.sh:53让 IDE 在 pty 里空转 60 秒, 数本地假服务器实收的请求条数,-le 2才 PASS。fix-p1-idle-verify.sh:100-110更进一步: 既断言空转恰好 1 次,又断言 ghost 仍然显示 —— 防止"限流的改动把功能一起干掉"。 - 真起子进程。
wave5-pty-drive.cpp:316用forkpty真跑 TUI;audit-fake-ai-server.py是真 socket 服务器,支持/err500 /err401 /badjson /drop /slow五种故障注入; 所有 AI 路径强制清空CPPIDE_API_KEY。
有两件事超出了预期。
它给自己的验证器做了正负控。 验收审计.md:29-34:先喂 ZZZ_NEVER_APPEARS_ZZZ 确认驱动器会 FAIL, 再喂真串确认会 PASS —— 避免"拿一台永远 PASS 的假机器去审计"。这是在验证验证器本身。
它主动披露了自己的弱断言。 wave5-b4-ai-states.sh:114 的 for m in $must 会把 "AI 错误 请求超时" 按空白拆成三个子串分别 grep,而 AI、错误 在任何一屏都必然出现 —— 名义"通过 3 条",实际只有 1 条有鉴别力。这一条是它自己写进 验收审计.md:576-582 的, 不是审计员挖出来的。
同类的还有:wave5-verify.sh:296-302 用 7 行注释说明 B3b 那条为什么降级成 SKIP (末屏时序断言,与产品代码无关),并注明"判定逻辑与容忍范围一字未动"; :236-238 记录了它发现并修掉了自己早先写的一个假断言 —— if pgrep ... 在没有 pgrep 的精简镜像里静默走 else,变成永远 PASS。
复用的定性证据和第四节的数字吻合:audit-fake-ai-server.py 被 7 个脚本复用; wave5-pty-drive.cpp 被 5+ 个编译复用,且用 [ -x "$DRIVE" ] || 缓存编译; wave4 → wave5 的驱动器是扩展不是重写(diff 只改了 1 行、新增 343 行,wave4 的 281 行原样保留)。
这些行为都发生在没有目标看守的版本上。 唯一的约束是需求确认书里那 10 条可判定的验收标准。这说明:把"做完"写成可判定的形式,本身就能诱发自审和自披露。 目标看守的边际价值要拿这个当基线量,而不是拿零。
但下一节说明:自审在验证技术上很强,却在最重要那条的终判上错了。
这里的"审计员"不是判定者
第七、第八节说的独立审计员,是协调者自发派出去的 subagent,不是 目标看守的判定者 —— 这一跑根本没有目标看守。两者读起来像,做的事不同:审计员是事后复盘, 判定者是每轮结束时的判定门。
八、它给自己打的分对吗 —— 验收 1 判错了¶
又派了第二个独立审计员,拿那 10 条验收标准逐条对代码判。
验收 2–10 全部属实,代码证据都在。练习模式不插码甚至是结构性保证: 唯一 sink 决策点 ai.h:117 + 唯一落地 switch app.cpp:590-615 + 四道 early-return editor.cpp:679-683,不是散落的 if。它自审发现的 P1–P4 也全部属实、修复全部落地 (P4 半属实:那行蓝色 pair 是死代码,ghostAttr() 本来就走 A_DIM)。
但验收 1 判错了。
标准原文: "编译出一个独立可执行文件,在 macOS 终端直接运行"
交付的产物: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
它的判定: 通过
这一条是人亲手跑 file 核实过的。
它并没有隐瞒 —— 交付说明.md §4 的标题就是"最大的未验证风险:macOS 一次都没跑过", 审计 §6 也列了 macOS 六个未验证面。它披露了风险,却从没把这个事实回接到验收 1 的判定上, 而验收 1 的字面要求恰恰就是那件事。
但这不是"需要一个独立判定者"的证据¶
很容易顺手把这条读成"所以要目标看守"。不对,而且这么读就是先射箭后画靶 —— 正好是上一节刚批评过的那种动机性推理。
当时那版目标看守,默认配置下大概率也会漏掉这一条。 judge() 默认 can_run=False, 工具只有 Read / Glob / Grep —— 它跑不了 file。 它会去读源码,看到 Makefile:25-38 确实有 Darwin 分支,然后判"达成"。和自审一模一样的失败。
真正的结构性事实¶
验收 1 在这次运行的环境里根本不可验证。
它断言的是"产物在 macOS 上的行为",而整个运行在 Linux 容器里。 没有任何判定者能在容器内验证一个 macOS 二进制,不管它是自审还是独立的。 问题不在"谁来判",在于这一条被当成了一条普通的可判定标准,然后被判了。
审计员另外挖到的三条(自审都没提)¶
doTab()的额度语义:app.cpp:909在acceptGhost()之后无条件noteActivity()→gen_++→ 重开额度,于是每次 Tab 接受都会在 500ms 后再触发一次自动请求。P1(空转烧钱)的分析 只审了setMode/cancelInFlight/askNow三个取消点,漏了这条最高频的花钱路径。- 验收 6 的"灰色"只在 256 色终端字面成立;8 色档定死成
A_DIM(变暗,不是灰色)。 验收审计.md§7 小标题写"9 条验收标准",表格却有 10 行 —— 笔误,但出现在总评表头。
九、落盘与工作台最终规模¶
103 次 spill、791.4K 字符换成了路径指针,没有常驻上下文。
.flower/ 最终:58 个脚本(250K)、22 份产出(330K)、5 份笔记(43K)。
遗留风险¶
交付目标是 macOS,但全部验证在 Debian aarch64 容器内完成,macOS 真机零验证。 这是容器隔离方案的真实副作用:为了安全把它关进 Linux,而它要交付的是 macOS。 项目自己的 README §10.24 诚实记录了这一条并列了 4 个风险点,但没有闭环。
这个缺口后来由 HT002 补上 —— 那一跑就是把这个二进制真装到 macOS 上。
意外的好处:容器里只看得见 /work,所以 19MB 的 transcript 里 宿主用户名 /Users/hechenyu 零泄露(/work 出现 11,495 次)。 隔离顺带把路径也隔离了。
这一跑没能验证的¶
- 目标看守:这一跑在加它之前。协调者自发组织了独立审计,验证技术很硬, 但在最重要那条上判错了(第八节)。两条结论要一起记 —— 需求确认书写清可判定标准就能诱发自审,所以目标看守的基线不是零; 而当时默认配置的目标看守也会漏掉那一条,所以它也不是万灵药。
- 1M 窗口的行为:只用到 18.6%,没碰到边界。
- 微压缩在
DISABLE_AUTO_COMPACT=1下的行为:全程没触发压缩,仍是推断。
这一跑改了框架什么¶
| 发现 | 落地的改动 |
|---|---|
which 被拦(第五节) | is_ephemeral() 补进 which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers;tests/glance.py 加 7 条对抗样本守住 command / type 的查询形式 |
| 验收 1 判错(第八节) | JUDGE_RULES 第 1 条改成判的是产出物,不是源码 —— 能跑就跑起来,跑不了也要检查产物本身(平台、大小、能不能加载) |
| "风险我披露过了"换来了一个通过 | 判定要区分"没做到"和"在这里没法验"。Verdict.parse 现在把无法验证 / 没法验证 / 无法判定识别成第三态 unreachable → 浮上来问人,绝不许判通过 |
| 验收 1 在这个环境里原理上就验不了 | 设定目标那一步要求把此环境验不了的条目当场标出来,不要等干完活再发现 |
| 断网恢复在框架层、subagent 半成品丢(第六节) | 记为 issue #2,未闭环 |
前三条在 HT002 里第一次上真实 API,其中"判产出物不是源码"当场救了一次 —— 但同一份规则也造出了一个新的失败。