AI

6 号机 Windows 挂了,失败日志在 Actions:2 号机上的代理怎么拿到现场并改下一刀?(中英切换)

This post is currently available in Chinese only. The original text follows.Open the Chinese page →

凌晨两点十七分,PR #518 agent/pathutil-snapshot 的 windows-verify 红了。失败发生在 GitHub Actions 的 Windows runner 上:两条测试——test_snapshot_uri_uses_forward_slash 和 test_cache_dir_writable。开这条 PR 的是 2 号机上的代理,会话 id 还写在 commit 里:sess-m2-9104。2 号机是 Linux。本机 pytest 三绿。代理的世界里,活已经干完了。

没有人开着 Actions 页。4 号机在审别的 PR。7 号机只在合并闸门变红时才会被吵到,而闸门变红这件事——只证明「别合」,不负责把现场搬到 2 号机。

三人、七台机:1–3 开发(代理高频开 PR),4–5 审查,6 Windows 验证,7 合并/运维。上一篇谈过「合并前必须等 Windows 绿」;那是闸门。这篇只谈闸门亮红之后、人还没介入之前的那一段:失败信号怎么从 Actions 落到 Linux 开发机上的代理手里,并且够它改下一刀。

下面是我在本机搭的玩具仓库:真实 git 分支与修复提交;Windows 失败日志是夹具(没有对 windows-latest 开火);策略对比是 JSON 模拟 10 个早市;fetch_windows_evidence.py 的租约互斥与 EVIDENCE.md 渲染在本机真实跑过。数字都来自这一次实验。

场景:红在云端,代理在 Linux

失败信号路径

项 值
PR #518 pathutil: snapshot uri
开 PR 机器 machine-2 · Linux · 会话 sess-m2-9104
本机测试 3 passed
Windows job windows-verify failed(夹具模拟)
失败用例 test_snapshot_uri_uses_forward_slash、test_cache_dir_writable
根因候选 str(Path) 假定 /;硬编码 /tmp

故意埋的两处雷,都是代理在 Linux 上「看起来完全合理」的写法:

  1. snapshot_uri 用 "file://" + str(rel)。Linux 上 str(Path) 自带 /,golden 测试写 "/snapshots/" in uri 永远过。Windows 上 str(Path) 变成反斜杠,同一条断言炸。
  2. cache_dir 返回 Path("/tmp/pathutil-cache")。Linux 上 /tmp 存在;Windows 上常变成 C:\tmp\...,权限或路径直接 PermissionError。

用 PureWindowsPath 在本机就能预演第一条:"/snapshots/" in str(win_path) 为假,as_posix() 之后为真。第二条改成 tempfile.gettempdir() 即可。关键不在会不会修,在修之前代理能不能看见这两行失败名。

四种把现场送回去的办法

我让同一次 Windows 失败在四种策略下各过 10 个合成早市,量五件事:代理有没有拿到任何「现场」、现场是否够用来修(能点名失败用例 + 嫌疑文件)、有没有复现、到第一次修复向推送的分钟数、有没有双机抢修。

四种策略对比

策略 拿到现场 可用来修 复现成功 平均首修(分) 整天没修 双认领
A 人盯邮件 / Actions UI 40% 30% 10% 148.1 6/10 0%
B 仅 job summary 100% 0% 0% — 10/10 0%
C 产物 + sticky 评论 100% 100% 100% 26.6 0/10 0%
D 标签 + 租约 + fetch 100% 100% 100% 20.3 0/10 0%

夹具尺寸也写进结论:有用日志 3316 字节;故意拼出来的「全量 debug 日志」2.39 MB;结构化 zip 包 3173 字节。GitHub 单条评论上限 65536 字符——把 2.39 MB 往评论里塞,创建会失败。这不是风格问题,是硬上限。

A:人盯收件箱——默认会漏

Tech Lead 或 6 号机旁边偶尔坐着的人,靠邮件/App 看见 Actions 失败,再复制一段到群里或 @ 代理。模拟里 10 天有 6 天完全没人把可用日志交给代理;交出去的平均也要约两个半小时,而且有时只是一句「Windows 红了」——代理听完 Linux 复跑仍绿,就会去改无关文件。

这不是「人不够勤」。是通道错位:失败证据住在 Actions,唤醒却依赖一个会静音的推送面。代理时代通知密度更高,静音是理性选择。

B:只有 job summary——知道红,不知道红在哪

代理轮询 PR checks,两分钟内看见 windows-verify conclusion=failure。它 100%「拿到了信号」。但 summary 常常只有一句 exit code。它在 Linux 上复跑:三绿。于是它的世界自洽了——「Windows 的红大概是 flaky」——或者它开始乱改测试期望。

可用率 0%、复现 0%、10/10 天没修,是这组里最狠的数字。它证明:合并闸门需要的「红/绿」比特,对修复代理来说信息量不够。闸门和现场管道是两件事。

C:产物 + sticky 评论——评论只放摘要

if: failure() 一步打包:failed-tests.txt、last40.txt、evidence-meta.json(OS / Python / cwd / head_sha)、可选 fix_hint.md,上传为 windows-failure-evidence artifact;再在 PR 上留一条带 <!-- windows-failure-evidence --> 标记的 sticky 评论,里面只贴失败用例名和末二十行,外加 gh run download 一行命令。

这一步把「可用来修」拉到 100%,平均首修约 27 分钟。Sticky 评论的价值是唤醒与定位;全量日志走 artifact,不走评论。

D:再加标签租约 + 本地 EVIDENCE.md

在 C 之上:打上 windows-failed;空闲开发机用 fetch_windows_evidence.py --claim 写互斥租约(本机实测:machine-2 持有时 machine-1 立即被拒);gh run download(live)或 --fixture(实验室)把包落到磁盘,渲染成代理一打开就能读的 EVIDENCE.md。

平均首修 20.3 分钟,比 C 再快约 6 分钟——省在「不用从评论 HTML 里抠、租约避免两台机同时改同一 PR」。双认领率 0%。

假路径:2 MB 日志进评论

证据包形状

我把 Actions 常见的 tool-cache debug 行拼到 2.39 MB,再对比有用的 3.3 KB pytest 失败段。前者超过评论上限一个数量级;后者和 3.2 KB 的 zip 包都安然。代理上下文同样受不了两万行 Downloading tool cache entry——就算你用 gist 绕过评论上限,塞进 prompt 也是噪声税。

结构化包要回答的问题只有四个:哪几个测试挂了、最后几行说什么、当时 OS/Python/cwd/sha 是什么、嫌疑符号是谁。 答完这四个,2 号机上的代理才能对得上 PureWindowsPath / tempfile 的修法;答不完,它只会在绿的 Linux 上空转。

机制:为什么反馈环会断

  1. 验证拓扑不对称。 开发与作者在 Linux;唯一会炸的检查在 Windows。失败天然不落在作者工作区。
  2. 闸门比特 ≠ 修复比特。 required check 只需要 conclusion;代理需要 node id 与栈。
  3. 人的推送面会疲劳。 把「复制日志」设计成人的日常职责,等于接受稳定的漏检率。
  4. 会话不知道云端事件。 Claude Code / Codex 不会因为 Actions webhook 自己醒——除非你把事件变成它磁盘上的文件或它轮询得到的标签。

所以目标态不是「让人更快粘贴」,而是 failure() → 小产物 → 可过滤标签 → 开发机 fetch → EVIDENCE.md。人变成例外处理,不是主路径。把「抄日志」从日常职责里删掉,才是这条管道有没有落地的验收标准。

还有一种常见短路:把 Windows 失败直接 @ 给「昨晚开 PR 的那台机器」。若 2 号机会话还活着,这偶尔奏效;若会话已死或机器在跑别的票,@mention 与审查场景一样叫不醒本地代理。失败信号管道应当指向空闲且能读 EVIDENCE.md 的开发机,而不是 git author。原作者机器可以优先 claim,但不该享有「不读证据包、凭记忆续写」的豁免——Windows 栈不在它的 transcript 里。

对 7 号机而言,这条管道改变的是噪音质量:以前合并闸门红了,群里开始问「谁去抄日志」;现在闸门红了,过滤器里该出现一条带 windows-failed、尚未被 windows-fix: 认领的 PR。运维盯的是未认领缺口,不是日志正文。正文只该出现在 artifact 与 EVIDENCE.md。

代价与不适用

  • 多一次 artifact 上传与评论写入:失败时才跑,成功路径零成本。
  • 标签噪声:需要 windows-failed 在变绿时清掉(success 步骤或下一次绿 run);否则 4/5 号机会被旧红标签误导。
  • 租约时钟:机器崩溃要有 TTL / windows-fix:abandoned,否则修卡在死人手里——这和审查租约是同一类问题。
  • 不适用:一个人、永远开着 Actions 页、产品只跑 Linux、Windows 失败一年碰不到一次。那时 C/D 是过度设计;保留 B 的闸门比特就够。
  • 也不替代闸门本身:没有 required Windows check,这条管道只是把「已经合进主干的雷」传得更快。闸门是另一篇文章。

和相邻文章的边界

一次完整的失败→修复走位(本机实测)

按夹具走完一遍,时间线是这样的——这里没有 live Windows,但 Linux 侧每一步都真实执行过。

  1. 红。 夹具里的 windows-job.log 给出两条 FAILED:URI 断言与 /tmp 缓存目录。对应 evidence-meta.json 里的 suspect_symbols: snapshot_uri, cache_dir。
  2. 打标。 示意 workflow 在 failure() 后执行 gh pr edit "$PR" --add-label windows-failed,并 upsert sticky 评论。评论体以 <!-- windows-failure-evidence --> 开头,方便下次失败时 PATCH 而不是堆十条重复评论。
  3. 认领。 2 号机跑 python3 fetch_windows_evidence.py --claim --machine machine-2 --pr 518。租约写在 .windows-fix-lease.json,TTL 45 分钟。紧接着让 1 号机再 claim:脚本直接退出,提示 lease held by machine-2。这就是双修互斥——和审查租约同一类锁,只是触发源换成了 CI。
  4. 落地。 --fixture fixtures 解压 zip,渲染 EVIDENCE.md(本机 4648 字节)。文件开头是 pr/run_id/job/head_sha/runner;中间是失败用例与嫌疑符号;末尾是 last40 与「先读再改」的 agent contract。
  5. 改。 代理(这里是我手动)把 str(rel) 换成 rel.as_posix(),把 Path("/tmp/...") 换成 Path(tempfile.gettempdir()) / "pathutil-cache",提交 cfa1720。Linux pytest 仍 3 passed;用 PureWindowsPath(...).as_posix() 确认 URI 字符串里重新出现 /snapshots/。

注意第 5 步没有「在 Windows 上再跑一遍」——因为实验室没有 Windows runner。文章里凡是声称 Windows 行为的地方,要么来自夹具日志,要么来自 PureWindowsPath 在 Linux 上的字符串预演。把夹具当成 live 跑过,是这类文章最容易犯的诚信事故。

如果停在第 2 步、没有第 3–4 步,代理就得自己去点 Actions 页面,或者等人口头转述。C 策略的 sticky 评论能把唤醒做完,但评论 HTML 对本地会话并不友好:代理更稳的输入是磁盘上的 EVIDENCE.md。这就是 D 相对 C 那六分钟的来源——少一次「从浏览器/API 评论里抠结构化字段」的往返。

为什么 job summary 会把代理带沟里

B 策略的 0% 可用率值得单独拆开。表面上代理「知道失败了」,信息似乎为正;实际上它拿到的是一个与本机观察冲突的比特。本机三绿、远端一红。对语言模型来说,最省事的解释是 flaky runner、缺依赖、或「Windows 那套我们不管」。三条解释都会导向不改 pathutil.py。

真正有用的信息必须强到压过「本机绿」这个先验:失败用例名点到 test_snapshot_uri_uses_forward_slash,栈里出现 str(rel) 与反斜杠 URI,嫌疑文件写明 pathutil.py。到这一步,代理才会把假设从「环境问题」改成「路径字符串假设错了」。job summary 给不了这个翻转;结构化包可以。

这也是为什么我反对「把整段 Actions 日志丢进群」却又不做结构化:2.39 MB 里 99% 是 tool-cache debug,失败信号的信噪比极低。代理上下文窗口会被噪声填满,人也会只瞄第一屏。压缩不是为了省流量,是为了保信号。

标签状态机(可以先手写两天)

最小可用的标签不必一次上齐自动化:

标签 含义 谁来写 谁来清
windows-failed Windows job 红,现场在 artifact Actions failure() Actions success() 或人工
windows-fix:machine-N N 号机持有修复租约 开发机 fetch --claim 或人工 修复推送后 / TTL 到期
windows-fix:abandoned 租约过期,允许偷抢 定时脚本或人工 新的 claim

手写两天的做法:6 号机或任何看到红的人只打 windows-failed,并在评论贴 gh run download <id> --name windows-failure-evidence;2 号机代理的站立任务里加一条「扫 label:windows-failed -label:windows-fix:」。扫到就 claim + fetch。两天后若漏检率还高,再把打标与 sticky 评论收进 workflow——那时代码路径已经验证过,YAML 只是把人手换掉。

和审查租约共用一套「认领 / 放弃 / 偷窃」心智模型有好处:代理不用学两套词。差别只是输入从 review 线程换成了 EVIDENCE.md。不要把 Windows 修复硬塞进 pickup:machine-N 审查租约里——触发条件不同,混用会让 4/5 号机的过滤器失真。

明天能抄的最小集

  1. Workflow:windows-verify 的 if: failure() 里打 zip 包 + upload-artifact + sticky 评论 + windows-failed 标签(示意 YAML 在套件里)。
  2. 仓库放上 fetch_windows_evidence.py:--claim / --fixture / --live(--live 默认只打印 gh 命令)。
  3. AGENTS.md 写死:看见 windows-failed 先 claim 再读 EVIDENCE.md,禁止「Linux 绿就当 Windows flaky」。
  4. 评论里只贴失败用例与末几行;全量进 artifact。量过:2.39 MB 会爆 64 KB 上限。
  5. 可选:windows-fix:machine-N 标签与 45 分钟租约,避免 1、3 号机同时改。

落地顺序:先 C(产物+评论)跑两天,再加 D 的租约。如果 C 之后代理仍然在 Linux 绿上空转,检查 sticky 评论是否真的包含失败用例名——只有 conclusion 的评论等于回到 B。

实际跑了什么

真实:玩具 git 分支 agent/pathutil-snapshot;Linux 上修复前 3 passed、按夹具修完后仍 3 passed;用 PureWindowsPath.as_posix() 验证 URI 断言在 Windows 字符串下会过;fetch_windows_evidence.py 租约互斥(m2 持有时 m1 被拒);策略 A/B/C/D × 10 早市;修复提交 cfa1720。

未跑:没有 live windows-latest / 自建 6 号机 runner;失败日志与 zip 为夹具;未对 live 仓库打标签或开火 Actions;无七机联调;无常驻 webhook 守护进程。

套件:windows-ci-failure-kit.tar.gz — fetch_windows_evidence.py、windows-failure-signal.yml、simulate_strategies.py、AGENTS.md.snippet、label_commands.sh、fixtures/、results.json。

今天最小的一步:下一次 6 号机红了,不要问「谁去 Actions 页面抄日志」。问「PR 上有没有 windows-failed,2 号机磁盘上有没有 EVIDENCE.md」。如果还在群里等某人粘贴 2 MB 日志——你正在付「失败现场过不去 Linux」的税。

At 02:17, PR #518 agent/pathutil-snapshot went red on windows-verify. Two tests failed on a Windows runner: test_snapshot_uri_uses_forward_slash and test_cache_dir_writable. The PR was authored by a machine-2 agent (Linux), session sess-m2-9104 still in the commit message. Local pytest: three greens. In the agent's world, the work was done.

Nobody was babysitting the Actions tab. Machine 4 was reviewing something else. Machine 7 only wakes when the merge gate flips red — and a red gate only proves "don't merge". It does not move the failure scene onto machine 2.

Three people, seven machines: 1–3 develop (agents open PRs at high frequency), 4–5 review, 6 Windows validation, 7 merge/ops. A previous post covered making Windows a required merge gate. This one starts after the gate is red and before a human pastes logs: how the failure signal reaches the Linux authoring agent in a shape it can fix from.

Lab setup on this box: real git branch and fix commit; the Windows failure log is a fixture (no windows-latest run); strategy comparison is a 10-morning JSON simulation; lease exclusion and EVIDENCE.md rendering in fetch_windows_evidence.py ran for real. Every number below is from this run.

Scene: red in the cloud, agent on Linux

Failure signal path

Item Value
PR #518 pathutil: snapshot uri
Author machine machine-2 · Linux · session sess-m2-9104
Local tests 3 passed
Windows job windows-verify failed (fixture)
Failed tests test_snapshot_uri_uses_forward_slash, test_cache_dir_writable
Likely root cause str(Path) assumes /; hardcoded /tmp

Two deliberate landmines that look perfectly reasonable on Linux:

  1. snapshot_uri builds "file://" + str(rel). On Linux str(Path) uses /, so "/snapshots/" in uri always passes. On Windows str(Path) uses backslashes and the same assertion dies.
  2. cache_dir returns Path("/tmp/pathutil-cache"). Fine on Linux; on Windows it often becomes C:\tmp\... and raises PermissionError.

PureWindowsPath on this Linux box already previews (1): "/snapshots/" in str(win_path) is false; after as_posix() it is true. (2) becomes tempfile.gettempdir(). The hard part is not the fix — it is whether the agent ever sees the two failed node ids.

Four ways to send the scene back

Same Windows failure, four strategies, ten synthetic mornings each. Measured: whether the agent got any scene, whether it was usable (failed tests + suspect file), whether it reproduced, minutes to first fix-oriented push, dual-claim rate.

Strategy comparison

Strategy Got scene Usable Reproduced Avg TTF fix (min) Days no fix Dual claim
A human inbox / Actions UI 40% 30% 10% 148.1 6/10 0%
B job summary only 100% 0% 0% — 10/10 0%
C artifact + sticky comment 100% 100% 100% 26.6 0/10 0%
D label + lease + fetch 100% 100% 100% 20.3 0/10 0%

Fixture sizes matter: useful log 3316 bytes; padded full debug log 2.39 MB; structured zip 3173 bytes. GitHub issue-comment hard cap is 65536 characters — pasting 2.39 MB into a comment fails the create call. Not taste; a ceiling.

A: human inbox — misses by default

Someone notices Actions mail/UI and pastes into chat. In the sim, 6/10 days nobody handed usable logs to the agent; when they did, average delay ~2.5 hours; sometimes the paste was just "Windows is red". The agent re-runs on Linux, still green, and wanders into unrelated edits.

Not laziness. Channel mismatch: evidence lives in Actions; wake depends on a human push surface that rationally gets muted.

B: job summary only — knows red, not where

The agent polls checks, sees conclusion=failure in minutes. It 100% "got a signal". The summary is often an exit code. Linux re-run: three greens. World stays coherent — "probably flaky" — or it starts mutating expectations.

0% usable, 0% reproduced, 10/10 days with no fix is the harshest row. The bit a merge gate needs is not the bit a fixing agent needs. Gate ≠ scene pipe.

C: artifact + sticky comment — comments carry excerpts only

On if: failure(): build failed-tests.txt, last40.txt, evidence-meta.json (OS / Python / cwd / head_sha), optional fix_hint.md; upload as windows-failure-evidence; leave a sticky PR comment marked <!-- windows-failure-evidence --> with failed node ids, last twenty lines, and one gh run download line.

Usable rate → 100%; average TTF fix ~27 minutes. The sticky comment is for wake + localization; bulk stays in the artifact.

D: add label lease + on-disk EVIDENCE.md

On top of C: apply windows-failed; a free dev runs fetch_windows_evidence.py --claim (exclusive — machine-1 is refused while machine-2 holds); gh run download (live) or --fixture (lab) lands the pack and renders EVIDENCE.md.

Average TTF fix 20.3 minutes (~6 minutes faster than C). Dual-claim 0%.

False path: 2 MB into a PR comment

Evidence pack shape

Padded tool-cache debug lines to 2.39 MB versus 3.3 KB of useful pytest failure. The fat file blows the comment cap by an order of magnitude; the useful slice and 3.2 KB zip do not. Agent context hates twenty thousand Downloading tool cache entry lines too — even if you sidestep the comment cap with a gist, you still pay a noise tax in the prompt.

A structured pack answers four questions only: which tests failed, what the last lines say, what OS/Python/cwd/sha were, which symbols look guilty. With those, machine 2 can aim at as_posix() / tempfile. Without them it spins on green Linux.

Mechanism: why the loop breaks

  1. Asymmetric topology. Authors develop on Linux; the check that burns is on Windows. Failures do not land in the author worktree.
  2. Gate bit ≠ fix bit. Required checks need a conclusion; agents need node ids and stacks.
  3. Human push surfaces fatigue. Designing "paste the log" as a daily human duty accepts a stable miss rate.
  4. Sessions do not hear cloud events. Claude Code / Codex will not wake on an Actions webhook unless you turn the event into a file on disk or a label they poll.

Target state is not "faster human paste". It is failure() → small artifact → filterable label → dev-machine fetch → EVIDENCE.md. Humans are the exception path.

Costs and when not to bother

  • Extra artifact upload + comment only on failure; success path is free.
  • Label hygiene: clear windows-failed on green or machine 4/5 will chase ghosts.
  • Lease TTL / windows-fix:abandoned when a machine dies mid-fix — same class of problem as review leases.
  • Overkill when: solo, Actions tab always open, Linux-only product, Windows failures are yearly. Keep the gate bit; skip C/D.
  • Does not replace the gate: without a required Windows check, this pipe only delivers "landmine already on main" faster. The gate is a different post.

Boundaries with sibling posts

Copy-tomorrow minimum

  1. Workflow: if: failure() zip + upload-artifact + sticky comment + windows-failed (YAML in the kit).
  2. Ship fetch_windows_evidence.py with --claim / --fixture / --live (live prints gh commands by default).
  3. AGENTS.md: on windows-failed, claim then read EVIDENCE.md; ban "Linux green ⇒ Windows flaky".
  4. Comments get failed tests + a short tail; bulk goes to the artifact. Measured: 2.39 MB blows the 64 KB cap.
  5. Optional: windows-fix:machine-N + 45-minute lease so machines 1 and 3 do not collide.

Rollout: C for two days, then D. If after C the agent still spins on green Linux, check whether the sticky comment actually names failed tests — a conclusion-only comment is just B again.

What actually ran

Real: toy git branch agent/pathutil-snapshot; Linux 3 passed before and after the fixture-guided fix; PureWindowsPath.as_posix() checked the URI assertion; fetch_windows_evidence.py lease exclusion (m1 refused while m2 held); strategies A/B/C/D × 10 mornings; fix commit cfa1720.

Not run: no live windows-latest / machine-6 self-hosted runner; failure log and zip are fixtures; no live labels or Actions on a remote repo; no seven-machine integration; no always-on webhook daemon.

Kit: windows-ci-failure-kit.tar.gz — fetch_windows_evidence.py, windows-failure-signal.yml, simulate_strategies.py, AGENTS.md.snippet, label_commands.sh, fixtures/, results.json.

Smallest step today: next time machine 6 goes red, do not ask "who will open Actions and paste the log?" Ask "does the PR have windows-failed, and does machine 2 have EVIDENCE.md on disk?" If you are still waiting in chat for someone to paste two megabytes — you are paying the failure-scene-never-reaches-Linux tax again.