ci(qa): L1 并发上限 + 它自己的 Docker 回归(把 #823/#835 真正带到 main —— 之前合错了 base) - #934
Merged
Conversation
L1 原来是无节制后台化:L1_TESTS 有多少条就同时拉起多少个容器(当前 17 条)。
在专用 CI runner 上没问题;在开发/生产共用的机器上不行 ——
实测本机(8 核,同时跑着生产 hub、dashboard 与约 200 个 agent session)
一次 `qa.sh --l1` 把 load1 顶到 58,即 7.3x 超订;跑完回落到 30 一线。
默认上限取 `nproc`(不是更激进的 nproc/2),要同时满足两件事:
小核 CI runner 上尽量不改变现有耗时,大核共享机上把超订压下来。
`QA_L1_MAX_PAR=0` 表示不限,可完全恢复旧行为。
## 第一版是坏的,靠实测才发现
最初写的闸门是:
while … (( $(jobs -rp | wc -l) >= QA_L1_MAX_PAR )); do sleep 0.2; done
**它从不阻塞。** `$( )` 会开子 shell,而 `jobs` 的作业表不跨子 shell 继承,
所以那个计数恒为 0。实测坐实:上限设 2,`docker ps` 采样到的 anet-* 容器峰值仍是 **3**。
改成在父 shell 里用 `kill -0` 数活着的 pid 之后:
上限 2 → 采样峰值 **2**(分布:0×6、1×3、2×29),`ALL PASS in 40s`
两版用的是同一组 4 个套件、同一台机器、同样的采样方式(每秒数一次
`docker ps --format '{{.Image}}' | grep -c '^anet-'`),只差闸门实现。
## NOT COVERED
- **没有测 CI 上的墙钟影响**。GitHub runner 核数少,默认上限会等于那个核数,
与现在的 17 路并发不同。当前 L0+L1 job 实测用 141–148s / 预算 300s,
我无法在本地可靠复现 runner 的时序 —— 若复核认为有风险,
可以在 workflow 里显式设 `QA_L1_MAX_PAR=0` 保持旧行为,或设一个更大的值。
- 只限制了 `docker run` 的并发;**build 仍是串行**,未改。
闸门条件是 `[[ "$QA_L1_MAX_PAR" -gt 0 ]]`,而 bash 在算术上下文里把非数字 当 0 —— 0 的语义恰好是「不限」。于是 QA_L1_MAX_PAR=two / =4x 会**静默恢复 本 PR 要消除的无上限行为**,同时那行 note 还照打「L1 并发上限 = two」, 输出主动确认了一个并不存在的上限。 这条不是理论问题:无上限时实测本机 load1 顶到 58(8 核,同时跑着生产 hub、 dashboard 与约 200 个 session)。fail-open 的方向正好是压垮共享机那一边。 改成 fail-closed:值不是非负整数就退回默认并大声报出来。 另修一个我自己第一版没料到的洞 —— 全数字还不够:bash 把前导零当八进制, `[[ "08" -gt 0 ]]` 报 `value too great for base` 并返回非零,闸门照样失效; `010` 则会被静默当成 8。加 `$((10#…))` 规范化。这个洞是跑对照表时发现的 (用例里放了 08),不是想出来的。 对照(旧 → 新): '4' 开 → 4 开 '0' 不限 → 0 不限 (有意保留) 'two' 不限 → 8 开 + 告警 '4x' 不限 → 8 开 + 告警 ' 3' 开 → 8 开 + 告警 '08' 不限+报错 → 8 开 '04' 开 → 4 开
结论和修法都对,合。但注释里的机制说法我复核后不准确,顺手改准。
原文:「命令替换会开子 shell,而 `jobs` 的作业表不跨子 shell 继承,
那样数出来**恒为 0**、闸门形同虚设。」
实测(bash 5.2.21,非交互脚本,MAX=2,启 8 个后台任务):
jobs 口径采样序列: 0 1 1 1 0 1 0 1
kill-0 口径峰值 : 2
**不是恒 0,是从来到不了上限值** —— `(( n < MAX ))` 因此永远为真,闸门永远放行。
坏的结果一样(PR 里实测「上限设 2、docker ps 峰值 3」是对的),但机制不同。
为什么值得改这一句:下一个人照「恒为 0」去排查,一跑发现是 1,
很可能得出「这条注释过时了/不适用了」,从而把 `jobs` 版本改回来。
**一个正确的结论配一个编错的机制,会把下一步的动作导向错的地方。**
同时把另外两条陷阱各自实测确认(它们的描述是准确的):
[[ two -gt 0 ]] → 假 ⇒ while 不进入 ⇒ 无上限(非数字在算术上下文当 0)
[[ 08 -gt 0 ]] → bash: [[: 08: value too great for base ⇒ rc=1 ⇒ 同样静默失效
$((10#08)) = 8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(#823): L1 并发上限闸门的 Docker 回归套件
审查指出这道闸门没有可复现的回归:仓里搜 QA_L1_MAX_PAR 只有 qa.sh 一处,
提交信息里的人工采样无法从仓库复现,于是下一次 fail-open 的计数/解析回归
会静默恢复无上限运行。
套件跑的是**真的 scripts/qa.sh**,不是逻辑副本:把 docker 换成 PATH 上的桩
(qa.sh 的 dockerrun() 是 bash -c "$*",会解析到桩),真实闸门代码原样执行。
峰值用事件流算最大重叠,不用采样 —— 采样会漏峰值。
四个用例(审查点名的四种):
cap=2 生效值 2,峰值 2 上限确实生效
非法值 two 告警,生效值退回 nproc=8 不是静默不限
前导零 08 生效值 8 按十进制,不撞八进制
0 生效值 0,峰值 7 保留「不限」逃生口
对照:cap=2 峰值 2,而不限/8 时峰值 7 —— 断言有分辨力,不是恒真。
写这个套件时它自己抓到我两个 harness bug:
1) 生效值提取用 grep -oE '[0-9]+',先命中了 "L1" 里的 1,四个用例全报 1
—— 判据没在已知输入上校准过。改成只取 `= ` 之后那个数,并用两组已知
输入(8 / 0)校准;
2) 桩对 build 和 run 一视同仁各睡 0.35s,而 build 是同步的,导致 run 之间
几乎不重叠、峰值恒为 1 —— 高上限下断言没有分辨力。改成 build 立即返回、
run 睡 1.2s。
另:容器是 --network none,qa.sh 会跑 npm view 做 registry 快照,真 npm 会
等 DNS 超时而不是快速失败(第一版就这么跑成超时)。npm 一并桩掉,让被测
闸门成为唯一耗时来源。
* docs(tests): report-test823 — 在 08f54e8b 上的运行结果(report-only child)
Source 08f54e8b 是包含被测套件本身的那个提交,不是它的父提交 ——
#801 上有一条 P1 正是「report 里的 SHA 早于套件本身,证据无法从其
标注的版本复现」。这里刻意先提交套件、再按该 SHA 建镜像跑,最后
把结果作为 report-only 子提交落下。
Exit 0 / RESULT: PASS,并附去掉校验段的变异见证(RESULT: FAIL)。
* test(#823): 把 test823 注册进 L1_TESTS —— 上一版建了个没人会跑的门
自查发现:上一版新增了 tests/test823-l1-concurrency-cap/,但全仓没有任何
地方引用它。用阳性对照判实的 —— test823 在自身目录之外命中 0 处,而同类
的 test745 命中 1 处;同时确认被查文件可读、grep 能命中已知串,排除"查询
没生效"。
也就是说我在一条"补上缺失回归"的 PR 里,交付了一个不会被任何东西调用的
套件 —— 正是 #801 那条 P1(gate_is_wired)说的情形,只不过这次在我自己
的产出上。
注册后再验:test823 命中 1 处,与对照 test745 一致。
自包含也验了:注册之后套件会在自己的 L1 列表里看到自己。实测仍 PASS
(退出码 0,四个用例全绿,峰值对照依旧有分辨力:cap=2 → 峰值 2,
不限 → 峰值 7),没有递归或自锁。
* test(#823): 三条审查意见 —— 桩只记 run、断言不限真放开、SHA 绑到被测字节
① 桩原来对任何非 build 的 docker 子命令都记事件,峰值会被无关调用抬高。
改成只有 `docker run` 才记。
② 0(不限)原来只断言生效值为 0 —— 那只证明它被这么解析,没证明它真的
放开了并发。补一条:不限时峰值必须明显高于 cap=2 的峰值。
③ SOURCE_COMMIT 原来只验 40 位十六进制格式。任何 SHA 都能通过,而报告
里那个 SHA 可能根本不含镜像里被测的文件 —— 这正是我自己在 #801 上
提的那条 P1,建这个套件时原样犯了一遍。
改成:构建时把 run.sh 在该 commit 下的 git blob 哈希作为 build-arg
传入,容器内就地重算并比对(blob 哈希 = sha1("blob <len>\\0"+内容),
不需要容器里装 git)。
第四条「接进自动 workflow」上一提交已自查修掉(注册进 L1_TESTS),
审查针对的是修之前的坐标。
* docs(tests): report-test823 刷新到 76c12e98379b67eb074a2a42e2a170e6aa94db1f(含 blob 绑定与三种见证红)
* ci(qa.sh): SOURCE_COMMIT 改为按套件名推导 —— 逐套件 elif 正是本 PR 撞红的成因
本 PR 的 L0+L1 稳定红,失败行只有一句:
FAIL: TEST823_SOURCE_COMMIT 必须是一个完整的小写 SHA(收到 '')
根因不在被测的门,在供给侧。qa.sh 里原本是一串逐套件的 elif:
if [[ "$t" == "test686-rest-shape-golden" ]]; then --build-arg TEST686_SOURCE_COMMIT=…
elif [[ "$t" == "test765-batch-runtime-gate" ]]; then …
elif [[ "$t" == "test766-bunx-preflight" ]]; then …
elif [[ "$t" == "test746-setup-bun-pin" ]]; then …
fi
本 PR 把 test823 加进了 L1_TESTS,但没人记得这里也要加一条 —— 于是
TEST823_SOURCE_COMMIT 是空串,门正确地 fail-closed。
只补一条 elif 能让它变绿,但下一个新套件还会踩同一个坑:
「注册了套件」和「在供给侧登记」是两处,分开就会漂。所以改成按名推导:
testNNN-... → --build-arg TESTNNN_SOURCE_COMMIT=$(git rev-parse HEAD)
qa-*-... → 不传(与原行为一致,它们的门不要这个变量)
行为等价性验证(对当前 L1_TESTS 全部 18 个套件逐个模拟):
test823-l1-concurrency-cap → TEST823_SOURCE_COMMIT (新增,本 PR 需要的)
test686-rest-shape-golden → TEST686_SOURCE_COMMIT (与原 elif 一致)
test765-batch-runtime-gate → TEST765_SOURCE_COMMIT (一致)
test766-bunx-preflight → TEST766_SOURCE_COMMIT (一致)
test746-setup-bun-pin → TEST746_SOURCE_COMMIT (一致)
qa-cli-01 / qa-hub-05 / qa-node-03b / … → 不传 (一致)
bash -n 退出码 0。
顺带记一条同类:#801 的红是同一个形状 —— run.sh 要求 TEST798_RUNSH_BLOB、
Dockerfile 接了线、workflow 的 docker build 从没传。都是「门要求 X,
供给侧不知道要给 X」。
* ci(qa.sh): 两套 build-arg 命名都供给 —— 上一版只覆盖了旧的那套,test823 照旧红
上一个提交(2bb734af)把逐套件 elif 改成按名推导 TESTNNN_SOURCE_COMMIT。
方向对,但**覆盖不全**:CI 照旧红在同一行
FAIL: TEST823_SOURCE_COMMIT 必须是一个完整的小写 SHA(收到 '')
原因是仓里并存两套命名,而我只按其中一套推导:
tests/test686-rest-shape-golden/Dockerfile ARG TEST686_SOURCE_COMMIT
tests/test765-batch-runtime-gate/Dockerfile ARG TEST765_SOURCE_COMMIT
tests/test766-bunx-preflight/Dockerfile ARG TEST766_SOURCE_COMMIT
tests/test746-setup-bun-pin/Dockerfile ARG TEST746_SOURCE_COMMIT
tests/test823-l1-concurrency-cap/Dockerfile ARG SOURCE_COMMIT / ARG RUNSH_BLOB ← 不一样
test823 的 Dockerfile 收的是 `SOURCE_COMMIT`,再由它自己组装
`ENV TEST823_SOURCE_COMMIT=${SOURCE_COMMIT}`。我传的是 TEST823_SOURCE_COMMIT,
名字对不上 → ARG 空 → ENV 空 → 门 fail-closed。它还要 RUNSH_BLOB(run.sh:28)。
这次两套都传。未被 Dockerfile 声明的 build-arg 只产生一条警告,不影响构建。
blob 等价性实测(本分支 head 上):
git rev-parse HEAD:tests/test823-l1-concurrency-cap/run.sh
{ printf 'blob %d\0' "$(wc -c < run.sh)"; cat run.sh; } | sha1sum
两者相同 —— 与 run.sh:31 的算法一致。
bash -n 退出码 0;对 L1_TESTS 里各形态逐个模拟,qa-* 仍不传。
🔴 记一条:上一版我验证了「四个旧套件行为逐条复现」,那个验证是对的,
但它只覆盖了我知道的那套约定 —— **我没有去核每个 Dockerfile 实际声明了什么 ARG**。
「与原行为一致」不等于「对所有套件都正确」。
* ci(qa.sh): git 调用改为非致命 —— 上一版把闸门自己的回归打红了(我引入的)
第三次 CI 仍红,但**换了一种红法**,而且这次是我造成的。
前两次红的是 `TEST823_SOURCE_COMMIT 收到 ''`。那个已经修好了 ——
本次日志里 `source_commit=1f2ab57a…` 正常出现、blob 校验也过了。
这次红在:
- cap=2 峰值=0 生效值=2 告警=0 FAIL cap2
- 0(不限) 峰值=0 生效值=0 告警=0 FAIL zero_conc
failures=2
**每个用例的峰值都是 0** —— 桩一次都没被调用。根因:
scripts/qa.sh:17 set -euo pipefail
test823 的 Dockerfile 只装 bash / ca-certificates / coreutils / procps —— **没有 git**
test823 的 run.sh 桩了 docker 和 npm,**没有桩 git**
而我上一版把 `$(git rev-parse HEAD)` 从「4 个具名套件」扩到了「所有 testNNN 套件」。
于是在 test823 重放 qa.sh 的那个容器里:git 不存在 → 127 → set -e 当场中断 →
docker 桩一次没被调用 → 事件流为空 → 峰值恒 0 → 闸门自己的回归失败。
**这不是被测代码的问题,是我改出来的回归。**
修法:git 调用全部 `2>/dev/null || true`,取不到就不拼 build_args ——
无 git 环境下退回到「和我动手之前一样」的行为(不传 build-arg),
真 CI 里 git 在,照常传。
模拟验证(PATH 置空以制造无 git 环境,带 set -euo pipefail):
未中断,build_args 为空。bash -n 退出码 0。
🔴 教训:我改的是**一个会被别的门重放的脚本**。给它加依赖(git)时,
我只想着「CI runner 上当然有 git」,没想过它还会在一个刻意最小化的容器里被重放。
「这个环境肯定有 X」——当脚本本身是被测对象时,这句话要先证明。
* fix(cli): 起/停这几条路不再宣布没量过的成功 (#895)
* fix(cli): stop `anet node start --accept-dev-channels` reporting dead nodes as started
Two independent false greens on this path, both measured while restoring 97
nodes after a power loss on 2026-08-17.
1. The success line was printed on the strength of `tmux new-session -d`
returning. That call succeeds even when the inner `anet node start` refuses
and exits 1 a moment later, so a refused node printed
`✅ node "X" started detached (tmux session live; …)` and exited 0 — with
`can't find pane: X` on the line directly above it. Byte-identical to a real
success, so a batch restore counted 64/64 up when 6 had never started.
Now: unstartable profiles are refused before anything is spawned (same
resolveStartProfile check launchAgent runs, so the message is the real one),
and success is claimed only after verifyNodeUp — the function `project up`
already uses to decide whether a node came alive. The success line quotes
its evidence (`pid N alive`) instead of asserting a session it never checked.
2. The 45 s auto-confirm window was spent on the wrong prompt. A workspace
Claude Code has not seen before shows folder-trust FIRST; the watcher knew
only the dev-channels markers, so it stared at a prompt it would not answer
until the window closed, and the dev-channels prompt that appeared later was
never confirmed. The node hung silently and the hub showed it offline
(TM智空负责人 died exactly this way and needed two manual Enters).
Now the watcher answers folder-trust too and restarts its clock, because the
window is meant to bound the wait for ONE prompt, not for the whole
trust-then-channels sequence.
A failed start deliberately does not kill the tmux session — a node stuck on a
prompt is one keypress from working — but the failure output names the session
and says `tmux has-session` will answer yes for it, since that is the criterion
batch callers use.
Verified against the two failure modes and the happy path in an isolated
workspace, with the inner agent stubbed:
bogus runtime before: ✅ exit 0 after: refusal on stderr, exit 1, no spawn
dies on start before: ✅ exit 0 after: ❌ exit 1 quoting the pane's reason
trust sequence before: ✅ exit 0, 46 s, node hung with no pid
after: ✅ exit 0, 5 s, pid alive, both prompts confirmed
All 6 wiring assertions fail against the unmodified file; both pure-module
mutations turn their tests red. Package suite 454 pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): apply the same verify-before-claiming rule to --tmux and codex co-presence
Auditing the other 54 `✅` claims in the CLI for the same class as the
--accept-dev-channels false green. Most are honest — `hub start`, the dashboard
launcher and the co-presence app-server all measure before they claim (a
/health fetch, a listener-pid scan, waiting for the `listening on:` line). Two
were not.
`anet node start <alias> --tmux` polls `tmux has-session` for 2 s and calls that
proof. It isn't: tmux registers the session before the inner command has
finished failing, so an unsupported runtime printed
`✅ tmux session "X" started detached` and exited 0 — the session was gone two
seconds later. Measured, then fixed with the same refuse-before-spawning check.
The narrower claim this path makes (the SESSION started, not the node) is left
as-is; it is true, and unlike --accept-dev-channels this path cannot promise a
working node because it never answers the prompts.
The codex co-presence launcher spawns three tmux sessions and then declares the
node 就绪. Only ① proved itself. Its OpenCode twin already checked its TUI
session before making the same claim, so the two sibling paths disagreed about
whether "ready" is measured; now they agree, and 就绪 requires all three
sessions to be alive at the moment it is printed.
Not verified end-to-end: the codex co-presence path needs a working codex, and
this account's quota is exhausted until 2026-08-20. The change mirrors the
OpenCode twin's shape exactly and only adds a failure path where a session is
genuinely absent.
The first version of the --tmux gate passed against the unfixed file — anchored
loosely enough that it found the OTHER branch's preflight. Rescoped to the
--tmux path itself. 3 of the 4 assertions now fail against both origin/main and
the previous commit; the fourth is the OpenCode twin, green on all three
because it is the reference, not a change. Suite 458 pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): say which condition made a resolved agent-node unsafe, and name umask
Chasing why the 5 grok co-presence nodes were unstartable. The published anet
(2.3.0-preview.38) has no grok-build-cli in its runtime whitelist, so the
morning's conclusion was "rewrite their config to grok-build-acp". That was
wrong twice over: origin/main already whitelists grok-build-cli, and once past
that the real blocker turned out to have nothing to do with grok.
Measured chain on this machine:
umask 0002
npx -y @sleep2agi/agent-node@preview dist/cli.js 0775, package.json 0664
the check (mode & 0o022) !== 0 → refuse
0o775 & 0o022 = 0o020 (group-write alone)
what the operator saw [anet] Incompatible grok-build-cli runtime.
[anet] resolved agent-node package has unsafe ownership or mode
Owner was correct throughout (uid 1000, my own), so the sentence sent every
reader to look at ownership. `chmod g-w,o-w` on those two files made the same
command run all the way through to the agent-node process, failing only on the
fake hub the test config points at — which is what confirmed the diagnosis.
The check is right and stays: refusing to execute a payload the group can
rewrite is correct, and anet cannot know this box's group has one member. What
changes is that it now names the path, the octal mode, which of the four
conditions fired, and that the usual cause is a stock Debian/Ubuntu umask —
with both fixes spelled out. Ownership failures deliberately do NOT mention
umask, so that message stays about ownership.
Both call sites share the new pure module: the grok preview resolver in cli.ts
and the OpenCode pairing check, which enforces the identical rule and would
have produced the identical dead end. The existing assertion on the old wording
still matches — the sentence is kept as the headline and the diagnosis appended.
Suite 465 pass, tsc clean. The npx directory was left exactly as found
(775/664); the fix is the operator's to apply.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(doctor): warn about the umask that makes grok-build-cli and opencode-cli unstartable
A better error message only helps someone already stuck. `anet doctor` can see
this coming from local state alone.
Both runtimes refuse a resolved agent-node payload whose mode has a group- or
other-write bit. npm creates files as `0o666 & ~umask`, so a stock
Debian/Ubuntu umask of 0002 — every user gets a private group, so 0002 is the
distro default — guarantees 0775/0664 and guarantees the refusal, which reaches
the operator as "Incompatible grok-build-cli runtime" with no mention of umask.
doctor now reports two things, from the process umask and whatever is already
extracted under ~/.npm/_npx. It never fetches, so an empty payload scan means
"nothing extracted yet", not "safe" — the umask verdict is what speaks to the
next fetch. On this machine:
⚠ Package file modes: umask is 0002, so npm extracts packages
group-writable. grok-build-cli and opencode-cli refuse to execute a
payload in that state, and the refusal reads as an "Incompatible runtime"
error. Start those runtimes under `umask 0022`, or run
`chmod -R g-w,o-w` on the resolved package root.
⚠ Resolved agent-node payload: 2 already-extracted file(s) would be
rejected right now, e.g. …/@sleep2agi/agent-node/dist/cli.js (mode 775).
Fix: chmod -R g-w,o-w …/@sleep2agi/agent-node
A set umask bit means "withhold that permission", so the predicate reads
inverted from how the symptom presents; that inversion is why judgeUmask is a
tested function rather than an inline expression, and 0002/0022/0000/0077 are
each pinned. Reading the umask requires the POSIX set-and-return call — the
helper puts the old value straight back, verified equal on a second read.
Suite 474 pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): address tmux sessions exactly — bare -t prefix-matches a sibling node
Every human-facing string in this CLI already spells the exact form
(`tmux attach -t '=<alias>'`, with a comment at the OpenCode co-presence
launcher explaining that a missing TUI would otherwise silently attach to the
bridge). Every tmux command the CLI actually ran passed the bare name.
Measured on this machine with only `zz-honest-probe-extra` alive:
tmux has-session -t zz-honest-probe → success (it is not running)
tmux has-session -t =zz-honest-probe → failure (correct)
tmux kill-session -t zz-honest-probe → killed zz-honest-probe-extra
The live fleet here has four colliding pairs — A站内容/A站内容牛,
A站数据/A站数据牛, P站测试/P站测试牛, P站运维/P站运维牛 — so each of the
three consequences is reachable today:
* has-session false-positives → `node start --accept-dev-channels` prints
`tmux session "X" already running — skipping spawn` for a node that is
down, exits 0, and never starts it. Reproduced end-to-end: with only
`anet-collide-兄弟` alive, origin/main skipped the spawn and left no pid;
the fixed build started the node (pid alive) and left the sibling running.
* kill-session reaps the sibling, and `node stop` reports success.
* send-keys would deliver an Enter into the sibling's Claude UI — the worst
of the three, since the prompt watcher fires it unattended.
All eight call sites now go through one helper: kill-session, has-session,
capture-pane ×4, send-keys ×2.
killTmuxSession additionally returns whether the session is actually gone. Its
`kill-session` failure is swallowed on purpose — a session that already exited
is the common case — so the only way to know is to look afterwards. `node stop`
now checks that and refuses to report a stop it did not achieve, instead of
deriving "killed" from the pre-kill has-session probe and notifying the hub
offline over a session that is still up.
Suite 478 pass, tsc clean. Fleet untouched at 89 sessions throughout; the
integration test uses its own `anet-exacttest*` names and cleans up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): project up / restart 的退出码要反映节点是否真的起来了 (#896)
Follow-up to #895, one level up. That PR fixed the single-node start paths; this
is the same defect class in the batch entry point, and it is the one automation
actually calls.
`anet project up` already measures every node with verifySpawnedNodes and prints
each failure, so its OUTPUT was honest — unlike the single-node path, it never
claimed a dead node was started. What it did not do was set an exit code: both
projectUp and projectRestart returned normally, so a run that brought up 60 of
74 nodes exited 0.
That matters because this is the entry point scripts use. While reviewing a
boot-time sweep for this machine's ~74 agent nodes, the design leaned on a
post-flight tmux audit rather than on `$?` — and the reason turned out to be
load-bearing rather than stylistic: `grep -c process.exit` inside projectUp
returns 0. Any watchdog or CI step that trusted the exit code was being told the
fleet was fine.
`invalid` counts toward failure too. A node whose config cannot start was never
attempted, so exiting 0 hides it exactly as well as a crash does.
The gate runs after printProjectSummary so the operator still gets the full list
before the process dies, and a clean run returns early and stays at exit 0.
All 5 assertions fail against f565e9b8 and pass here. Suite 483 pass, tsc clean.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs: 去掉过期版本号与硬编码计数,改为指向权威来源 (#869)
Doc-only refresh from 通信狗 review (issue #639). No behavior changes,
no runtime/config edits — every touched file is markdown or a
package.json `description` field.
## Version facts sourced from npm at commit time
Verified via `npm view <pkg> dist-tags` on 2026-08-14:
latest : agent-network 2.2.21 / agent-node 2.4.13 / commhub-server 0.8.8
preview : agent-network 2.3.0-preview.39 / agent-node 2.5.0-preview.31
commhub-server 0.9.0-preview.29
local anet: v2.3.0-preview.38 (matches preview channel, one behind head)
## Per-file changes
P0-1 CHANGELOG.md banner
- Drop hardcoded "当前 stable 是 v0.10.11" (out of date).
- Point readers at npm `latest` + docs-site/docs/changelog.md as the
live source; keep the v0.10.15 archival anchor + v0.8.1 OSS-first
note. File still an archive of pre-2026-04 v1.0.0-preview history.
P0-2 docs/getting-started.md
- Runtime table now includes `grok-build-acp` (needs `grok login`).
- Note that @preview additionally ships `codex-app-server` and
`opencode-cli`; the authoritative full runtime table is at
anet.sh/guide/runtimes.
- `anet ls` → `anet node ls` (matches current CLI).
P0-3 docs-site/docs/{,en/}guide/getting-started.md
- Add a preview-channel warning next to the admin/anethub line:
@preview prints a one-time random password on first
`anet hub start`, don't hard-code `anethub`. This aligns with the
README + cli.md wording that already carries the note.
P0-4 AGENTS.md 项目结构
- Drop "39 命令" / "4 runtime" hardcoded counts (both drift).
- Point at docs-site/docs/guide/cli.md as canonical CLI list.
- Split runtimes into stable (4: claude-code-cli / claude-agent-sdk /
codex-sdk / grok-build-acp) + preview extra (2: codex-app-server /
opencode-cli); mark `grok-build-cli` as unreleased in any channel.
P0-5 docs/version/README.md + docs/plans/release-plan.md
+ docs/version/0.11.0/README.md
- Backfill preview matrix from `npm view <pkg>@preview` (was pinned
at .34/.26/.20 — now .39/.31/.29). Added timestamp + reminder to
re-check `npm view` before editing.
- WAIC 7-月-下旬 anchor is out of the window; strike-through the
completed date, keep the archival link to waic-release.md, and
replace with "current promote status per release-plan".
- release-plan.md defaults table gains a commhub-server column so
readers see all three packages, not just two.
P1-6 docs-site/docs/guide/architecture.md
- "14 张表" → "20+ 张表(含 sessions / tasks / nodes / users /
networks / SkillHub / providers / vault 等,实数按 schema
版本浮动)"; EN mirrors it. Fixes both the two mermaid diagrams
and the paragraph.
P1-7 docs/architecture.md
- Runtime paragraph now says "stable 4 + preview 2" and points at
anet.sh/guide/runtimes as authoritative.
- 14 cli.ts deep `#L<line>` anchors defanged (link stays, line
number dropped — they rot every release; kept the function name
in the link text so intent survives).
- Directory tree gains an "已不完整,以仓库实际为准" note so
readers don't treat it as canonical.
P1-8 server/package.json + server/README.md
- package.json `description` now says "MCP tools (17 collaboration-
core + node/provider ops tools; authoritative list at
docs-site/docs/api/mcp-tools.md)" — was "and 17 MCP tools"
(readers took it as the total).
- server/README.md MCP section gains one line saying the 17 in the
table are the collaboration-core subset; full list at
docs-site/docs/api/mcp-tools.md.
P1-9 README.md + README.en.md
- "能做什么" / "What it does" gain one bullet pointing at Codex TUI
co-presence and OpenCode as preview-channel additions with a link
to the Runtime page.
## Not touched (per review scope)
- docs/v3-postgresql-design.md archive banner (do not edit)
- upgrade-v2 archive banner (do not edit)
- grok-copresence danger banner (do not edit)
- runtimes 官方表 (canonical, do not edit)
## Verification
`grep -c` on the touched files confirms:
- `14 张表` remaining in docs-site/docs/guide/architecture.md: 0
- `39 命令` remaining in AGENTS.md: 0
- stale `cli.ts#L<n>` deep anchors in docs/architecture.md: 0
- `anet ls` (bare, without node prefix) in docs/getting-started.md: 0
Co-authored-by: t <t@t>
* fix(ci): 让 CI 真会跑的测试能重触发自己那道门,并加一道防漂回去的门 (#897)
Four findings from the open-issue triage, each verified against origin/main
before touching anything. Two of the four numbers in those issues were wrong in
ways worth recording.
## qa.yml path filter missed tests CI actually runs (#860)
qa.yml fires on a path filter. Four directories CI executes were outside it, so
editing the test could not re-run its own gate — and the run looks identical to
a gate that passed on the new code:
tests/test292-e2e-hard-gate (referenced by a workflow path)
tests/test686-rest-shape-golden ┐
tests/test765-batch-runtime-gate ├ reached through scripts/qa.sh L1_TESTS
tests/test766-bunx-preflight ┘
#860 reported three; it missed test292-e2e-hard-gate. My own first scan
under-counted in the other direction — it matched `tests/testNNN` and so never
saw L1_TESTS, which names directories bare. The remaining ~160 directories under
tests/ are run by no workflow at all, and are deliberately left out: a filter
entry for an unrun test reads like coverage it does not have.
## …and a guard so it does not drift back
`.github/scripts/check-qa-trigger-coverage.py` asserts every CI-executed test
directory is in the filter. Three behaviours, each exercised:
fixed repo → exit 0, "all 7 CI-executed test dirs can re-trigger"
f565e9b8's qa.yml → exit 1, names all four with the line to add
L1_TESTS renamed away → exit 2, "no CI-executed test directories detected"
That last one matters most: if the parser stops matching, the honest answer is
"I can no longer see the denominator", not a clean run against nothing.
Its workflow intentionally carries NO `paths:` filter. It guards a path filter;
gating it on paths would let an edit to qa.yml's filter or to L1_TESTS slip past
the check that watches them — the same blind spot it exists to catch.
## public-script safety: TLS verification (#890)
check-public-script-safety.py flagged `rm -rf` outside our paths and unscoped
`pkill`, but nothing about `curl -k` / `--insecure` /
`wget --no-check-certificate` / `NODE_TLS_REJECT_UNAUTHORIZED=0`. These scripts
are fetched over https and piped into bash, so verification is the reader's only
defence against a tampered download; there is no legitimate reason for a script
published at a public https URL to skip verifying it. That meets the file's own
"only unambiguous rules" bar. Zero current hits — this one is preventive.
While adding it I hit a bug in the reporting: the hint was chosen by an
if/else whose `else` branch belonged to the kill rule, so every TLS finding
printed advice about `pkill -u`. Remediation text is now keyed by rule, and an
unknown rule exits 2 rather than borrowing another rule's advice — pointing the
reader at a problem they do not have is worse than printing nothing.
Verified with real exit codes (not through a pipe, where `$?` is the last
command's): known-bad fixture → exit 1 with the right hint on all three forms,
comment lines ignored; real repo → exit 0 across 6 scripts.
## dashboard min_uptime (#892)
deploy/dashboard/ecosystem.config.cjs had `min_uptime: 20_000` while
docs-site/docs/deploy/daemon.md documents 45000 and explains why: below the
time a failing process takes to exit, PM2 counts the start as successful,
backoff never engages, and a crash loop looks like normal restarts. A dashboard
rebuilt from this repo landed exactly in that gap. Aligned to 45000 with the
reasoning inline; `node -e require(...)` confirms it still parses.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(docs,ci): 修 W19 编码与死链、给矛盾耗时标条件、把两个没人调的验证脚本挂上 (#899)
Four more from the open-issue triage. Each was verified against origin/main
first, and two of them turned out to be bigger than the issue said.
## docs/qa/weekly/2026-W19.md would not decode as UTF-8 (#887)
Three multi-byte characters were truncated mid-sequence, not one. The issue
reported the first; repairing it revealed the second, and that one the third.
The damage pattern is consistent — every case is `_italic text_` with the
character immediately before the closing `_` eaten — which points at a
truncating edit rather than a bad encoding.
The lost characters are NOT recoverable, so they are marked as damaged rather
than guessed. This is a QA weekly report; inventing a plausible character would
be worse than saying a character is missing.
## …and all 24 of its relative links were dead (#872)
Not "24 broken links" in the sense of a few typos: 0 of 24 resolved. The file
sits three levels deep and the links were written for two, so every `../../`
landed inside docs/ instead of at the repo root. Four more used a single `../`
for directories that live under tests/. All 24 now resolve — verified by
resolving each one against the filesystem, not by eyeballing the diff.
## docs/qa timings contradicted each other three ways (#871)
docs/qa/README.md ~16s warm
docs/qa/strategy.md ~16s warm
docs/qa/v0-summary.md ~93s local, ~40s CI
v0-summary's own per-test table, summed: 156s
The issue framed this as "pick one and unify". None of the three can be picked,
because not one of them says what it measured — warm or cold, serial or
parallel, which machine. 156s serial against a 93s wall clock just means there
is parallelism nobody wrote down.
So the dead numbers are gone from README and strategy, replaced with
`time bash scripts/qa.sh` and the one fact that stays true: the per-test table
sums to 156s, anything lower implies parallelism, cold starts are worse.
v0-summary keeps its 93s but now says it is a 2026-05 measurement.
## scripts/verify-published-pins.sh and verify-release-tag.sh had zero callers (#862)
Both committed, both executable, both carrying the incident that motivated them
in their header — and `grep -rl` across .github/ and scripts/ found nothing that
invokes either. A guard nothing calls protects nothing, while its presence reads
as if the risk were covered.
Running verify-published-pins.sh by hand, for the first time, failed on its
first invocation:
❌ OPENCODE_AGENT_NODE_VERSION 期望 2.5.0-preview.31,
产物里是: 2.5.0-preview.28
1 个 pin 与已发布产物不一致 —— main 修了但用户装到的包没修
That is the exact distinction its own header says bit this repo three times in
one day, live in the published preview and undetected. (Independently confirmed
by hand earlier the same night: installing preview.39 demands agent-node
preview.28, while main's source constant reads preview.31.)
Now scheduled daily plus manual dispatch, with the exit codes mapped so that
"could not measure" does not become the same green as "measured and fine":
rc=2 (registry unreachable) fails with a notice saying the run verified nothing.
Per-PR would be wrong — it inspects the published artifact, which a PR does not
change.
## A guard for the first two
`.github/scripts/check-docs-integrity.py` checks UTF-8 validity across every
tracked .md and relative-link resolution under docs/qa/. Three behaviours, each
exercised: repaired tree → exit 0 (359 files, 80 links); f565e9b8's W19 → exit 1
with 25 errors naming each; LINK_SCOPE pointed at a missing directory → exit 2,
"scope regression, refusing to pass".
It starts green, so it is not a backlog canary — a red here will always mean
something just broke. Link checking is scoped to docs/qa/ and says so: some
pages elsewhere link to generated paths, and a guard that cries wolf gets
disabled.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): 让 dev-channels 自动应答真的能用 —— pane 目标用坐标 + 候选按 server: channel (#901)
* fix(cli): revert `=name` on pane commands — it cannot resolve non-ASCII sessions
Regression I introduced in #895 and merged. #895 replaced eight bare `-t <name>`
tmux targets with `-t =<name>` to stop prefix matching. That is correct for
session-targeting commands and WRONG for pane-targeting ones.
Measured on tmux 3.4 with a session literally named `zz中文探针`:
tmux has-session -t 'zz中文探针' rc=0 -t '=zz中文探针' rc=0
tmux kill-session -t 'zz中文探针' rc=0 -t '=zz中文探针' rc=0
tmux capture-pane -t 'zz中文探针' rc=0 -t '=zz中文探针' rc=1 can't find pane
tmux send-keys -t 'zz中文探针' rc=0 -t '=zz中文探针' rc=1 can't find pane
This fleet's session names are nearly all Chinese, so #895 silently disabled the
dev-channels prompt watcher for essentially every node: capture-pane throws, the
watcher reads that as "session gone", returns false immediately, and the confirm
box is never answered. The node then sits on the prompt forever. That is worse
than the prefix ambiguity the `=` was added to fix, and it is the same failure
mode #895's second half existed to eliminate.
Caught on a live node. `SDK马` was sitting on the dev-channels box with a live
pid, and:
capture-pane -t '=SDK马' → rc!=0
capture-pane -t 'SDK马:0.0' → rc=0, 16 lines, prompt visible
The exact-and-portable form for a pane is the coordinate
`<session>:<window>.<pane>`, resolved by listing panes and matching the session
name with string equality in our own code — which is both unambiguous and
encoding-agnostic, instead of asking tmux to disambiguate. `has-session` and
`kill-session` keep `=name`; they accept it for non-ASCII and still need the
prefix guard.
The watcher now re-resolves the coordinate on every poll rather than caching it:
a session may have no pane on the first iteration, and "no pane yet" must not be
mistaken for "prompt absent" — it keeps waiting and lets the deadline decide.
The wiring assertion fails against f565e9b8..7752437f and passes here; the pure
parser is pinned for prefix siblings, missing sessions, non-zero pane indexes,
and malformed rows. There is also an integration test that creates a real
non-ASCII session and asserts the exact rc difference above, so this cannot
regress silently again. Suite 491 pass, tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(cli): auto-confirm dev channels for every node that loads one, not only claude-code-cli
Second half of the same failure. #895's `=name` change stopped the watcher from
seeing the pane; this is why the watcher was never even asked to look at a whole
family of nodes.
`autoConfirmDevChannels` selected candidates with
normalizeRuntime(n.profile) === "claude-code-cli" && channels has "server:"
but the runtime is not what causes the prompt — loading a `server:` channel is.
`claude-agent-sdk` nodes with `server:commhub` show the same confirm box, and
`claude-code` normalizes to `claude-agent-sdk`, so legacy-named nodes were
excluded too. Those nodes sat on the box forever during `project up` /
`node start --all` with no watcher assigned to them.
The correct predicate was already in this file. The #494 warning on the `--tmux`
path keys purely on `server:` channels with no runtime test:
if ((resolved.profile.channels ?? []).some(ch => ch.startsWith("server:")))
console.warn(`[anet] ⚠ this node loads dev channels (server:*): …`)
Two places answering the same question with different rules, and the narrow one
was the one doing the work.
Measured on this machine, all three with `channels: ['server:commhub']`:
微信马 claude-code-cli → was selected, came up (late, but up)
评估m马 claude-agent-sdk → not selected, sat on the confirm box
I站工程马 claude-code → not selected (normalizes to agent-sdk)
Widening is safe because dismissDevChannelPrompt is detection-gated: Enter is
sent only when the prompt's exact text is on screen, so a node that never shows
it times out without a keystroke.
Both assertions fail against main and pass here. One of them initially failed
against the FIXED code too — the new comment quotes the old predicate verbatim,
and a plain `toContain` matched the comment. The test now strips comment lines
before asserting absence, because the claim is about the code. Suite 493 pass,
tsc clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* docs(refresh): stale-snapshot caveats on 4 独立面 (task 27faa700) (#898)
* docs(refresh): 4 独立面 stale-snapshot caveats (task 27faa700)
Local-only branch. NOT pushed, NO PR opened — awaiting 通信龙 review after
PR #869 merges (per instruction 2026-08-14).
Doc-only, no behavior changes. Each edit adds a snapshot-date caveat and
points at the live source of truth (release-plan.md / `npm view`); no
existing evidence-anchored text was removed.
## 1. docs-site/docs/{,en/}preview/index.md:16 (章节标题死数)
- Old heading: "当前 preview = canonical(2.3.0-preview.34 / 2.5.0-preview.26,2026-07-16)"
- New heading: "当前 preview channel canonical build(snapshot 2026-08-14)"
- Added 1 paragraph with:
- real 2026-08-14 npm-view numbers (agent-network preview.39 /
agent-node preview.31 / commhub-server preview.29)
- the main-source-vs-published-binary caveat (通信龙 Fact 1):
preview.39 binary's embedded .d.ts pair still names
agent-node@2.5.0-preview.28
- install-via-@preview-tag reminder (already stated once above,
reinforced with npm view <pkg> dist-tags recipe)
- English mirror updated with parallel wording.
## 2. docs/release/v2.3.0/plan.md:30 (GA-gate 6-week-old snapshot)
- Prepended one blockquote line marking the段 as a 2026-07-05 snapshot,
pointing at release-plan.md as the live source, and citing the
2026-08-14 real preview numbers.
- Kept the original "最后更新:2026-07-05 …" line intact for GA-gate
milestone history.
## 3. docs/release/versioning-and-compatibility.md:37-43 (fleet snapshot table)
- Prepended one warning line: the first three rows are 2026-06
preview-iteration snapshots; live numbers via release-plan.md +
npm view.
- Renamed rows 39-41 to append "(2026-06 快照)" so readers cannot
misread them as current.
- Added one new row "已发布 preview 头(snapshot 2026-08-14)" with
real npm-view numbers + the .d.ts pair caveat (通信龙 Fact 1).
- Kept "v2.3.0 GA 目标" and "latest(稳定线)" rows unchanged (真值
仍准).
## 4. docs/runbooks/feishu-channel-ops.md:11-18 (runbook 死数)
- Runbook section title now names as-of 2026-07-01 deployment
snapshot + instructs to复核 real deployed version via
`docker exec anet-feishu-local anet -v` (does not touch prod —
runbook only tells the operator what to run).
- agent-network / agent-node rows retain the deployed values but
add "当前 preview 头 2026-08-14 快照为 preview.39 / preview.31" +
release-plan.md link for cross-check.
## Version facts used (npm view 2026-08-14)
| pkg | latest | preview |
|---|---|---|
| agent-network | 2.2.21 | 2.3.0-preview.39 |
| agent-node | 2.4.13 | 2.5.0-preview.31 |
| commhub-server | 0.8.8 | 0.9.0-preview.29 |
## Not landed here
- Q2 段全部 8+ 条 (行为句缺版本) — 需 git log 追溯精确 version anchor,
单独任务处理 (通信龙 视 #869 merge 时机再拍)
- 通信龙 Fact 2 (anet node start ✅ vs tmux has-session) — origin/main
未直接命中"看到 ✅ 就成功"教学句, README / clean-server.md / feature-
audit.md 可通过通用页脚 note 批量处理, 单独任务处理
- docs/RELEASE-SOP.md:232/243 pairing caveat 详写 — 属 SOP 内部, 请
RELEASE-SOP owner 决定文案
- docs/grok-build-runtime.md:111 (v0.10.11 anchor stale) — 属 Q2 段
item 16, 单独 issue
* docs(refresh): bump snapshot date 2026-08-14 → 2026-08-17 (rebase day)
Per 通信龙 instruction on task 27faa700: snapshot dates should reflect
the rebase/push day, not the authoring day.
Re-verified `npm view <pkg> dist-tags` on 2026-08-17 — numbers unchanged
since 2026-08-14 authoring:
latest : agent-network 2.2.21 / agent-node 2.4.13 / commhub-server 0.8.8
preview : agent-network 2.3.0-preview.39 / agent-node 2.5.0-preview.31
commhub-server 0.9.0-preview.29
Fact-1 pairing caveat also still holds (通信龙 tested 2026-08-17 evening):
main-source constant `OPENCODE_AGENT_NODE_VERSION` = agent-node preview.31,
but the published preview.39 binary embeds a `.d.ts` pair naming
agent-node preview.28. They installed preview.28 to get opencode-指挥狗
to start.
---------
Co-authored-by: t <t@t>
* docs(refresh): Q2 anchors for password + #450 + Fact-2 (#895/#896) notes (#900)
Follow-up to PR #898 (task 27faa700). Doc-only; 5 files, +20 -7.
## Anchors nailed via git log -S / gh issue view (2026-08-18)
### 1) README.md:50 + README.en.md:50 — 一次性随机密码 anchor
Introducing commit: `3e4e190c` (PR #264 fixing #261 P0-2), merged
2026-06-28. First npm-published preview containing the behavior:
`@sleep2agi/agent-network@2.2.22-preview.4`. All subsequent
`2.3.0-preview.0..39` inherit.
Stable `@latest` at the time of writing is `2.2.21` — pre-dates the
fix. So @latest users still get the fixed default `admin` / `anethub`.
Older preview `≤ 2.2.22-preview.3` also pre-dates the fix.
Rewrote both README lines to state the anchor version, PR/issue links,
and the explicit "you are still on the fixed default if you're on
@latest 2.2.21 or preview ≤ 2.2.22-preview.3" caveat.
### 2) docs-site/docs/{,en/}guide/getting-started.md:97 — #450 anchor
Issue #450 is **OPEN** (verified `gh issue view 450`). Root fix landed
in PR #239 commit `1eff3a4d` on 2026-06-28. Vincent's 2026-08-09 audit
verified the fix in an isolated Docker probe on
`agent-network@2.3.0-preview.38` reaching SSE connected.
But #450 is not closed because four acceptance gates need to run
green before promoting to latest:
1. promote a release containing 1eff3a4d to npm latest and repeat
the cold-install journey against @latest
2. exercise a >60s cold fetch scenario
3. verify actionable DNS/registry/timeout/version failure
classification
4. run the credentialed idle → send_task → non-empty reply layer
So: current `@preview` (2.3.0-preview.39) has the fix; `@latest`
(2.2.21) still ships the bug. Rewrote the warning block on both zh
and en getting-started to name that split explicitly and provide the
workaround.
Method-note (for the audit trail): PR #239's title only names #237,
not #450 — so the standard `gh pr list --search "fixes:#450"` returns
nothing. The link is only recoverable via `git log -S` on the error
string. Worth remembering.
## Fact-2 notes (通信龙 D1-D3) — #895 / #896 in main NOT yet in npm
- PR #895 (`f565e9b8`) fixed `anet node start` false-`✅` /
false-`started detached (tmux session live)` in detached scenarios.
Merged to main.
- PR #896 (`40574a02`) fixed `anet project up / project restart`
exit-code lie. Merged to main.
**Neither has been cut into an npm release yet** — so for anyone on
`@preview` (currently `2.3.0-preview.39`), the trap still exists. The
real check remains `tmux has-session -t "=<alias>"` — the `=` is
required (bare alias is a prefix match and can go green on the wrong
session).
Added this note in three places:
1. `README.md` + `README.en.md` quickstart — right after
`curl /health` verify, before the "open localhost:3000" line, so
the first-run reader sees the caveat while their brain is still
on `anet node start`.
2. `docs-site/docs/deploy/clean-server.md` §7.1 — right below the
`tmux new -s anet-<alias> + anet node start <alias>` recipe.
3. `docs-site/docs/deploy/clean-server.md` §故障排查表 — added a
new row 5.5 (`✅ printed but tmux session not there`) with the
diagnostic recipe.
Not touched (per current scope):
- `docs/version/0.11.0/feature-audit.md:33` (D4) — the audit already
correctly reports the bug and its `✅` refers to `anet node create`
(wizard), not `anet node start`; no misleading claim to correct.
- The 6-10 `anet node start` command demonstrations in
`docs-site/docs/deploy/npm.md`, `.../concepts/networks.md`,
`.../concepts/tokens.md` — those are pure command samples with no
success-criteria text; the central note in clean-server.md is
where they land.
## Not touched — Q2 leftovers (independent follow-ups)
Sixteen other Q2 lines identified in the exhaustive scan still carry
`TBD 追溯` anchors (multi-model.md, agent-node.md, batch.md,
dashboard.md, feishu.md, runtimes.md, upgrade.md, RELEASE-SOP.md,
etc.). Each needs a targeted `git log -S` pass. Scheduled for
subsequent follow-ups so this PR stays reviewable.
## Verification
```
$ git diff origin/main...HEAD --stat
README.en.md | 8 ++++++--
README.md | 8 ++++++--
docs-site/docs/deploy/clean-server.md | 7 ++++++-
docs-site/docs/en/guide/getting-started.md | 2 +-
docs-site/docs/guide/getting-started.md | 2 +-
5 files changed, 20 insertions(+), 7 deletions(-)
$ git grep -c '2.2.22-preview.4' README.md README.en.md
README.en.md:1
README.md:1
$ git grep -c '1eff3a4d' docs-site/docs/{,en/}guide/getting-started.md
docs-site/docs/en/guide/getting-started.md:1
docs-site/docs/guide/getting-started.md:1
$ git grep -c '#895' README.md README.en.md docs-site/docs/deploy/clean-server.md
README.en.md:1
README.md:1
docs-site/docs/deploy/clean-server.md:2
```
## Version facts sourced from npm (2026-08-18)
Verified `npm view <pkg> dist-tags` on 2026-08-18 — unchanged since
2026-08-14 authoring pass:
latest : agent-network 2.2.21 / agent-node 2.4.13 / commhub-server 0.8.8
preview : agent-network 2.3.0-preview.39 / agent-node 2.5.0-preview.31
commhub-server 0.9.0-preview.29
These are snapshots, not promises. Both `@latest` and `@preview` tags
keep drifting; re-read via `npm view <pkg> dist-tags` before quoting
elsewhere.
Co-authored-by: t <t@t>
* fix(tests): derive the opencode pair versions from source instead of pinning them in tests (#902)
Found by running the RELEASE-SOP pre-flight for preview.40 rather than by
reading it. `scripts/sync-pinned-versions.sh` bumps
`OPENCODE_AGENT_NETWORK_VERSION` / `OPENCODE_AGENT_NODE_VERSION`, but it does not
touch the tests — and two suites hard-code that pair in nine places:
test386 5 assertions + 3 fixtures (bin/npx spec, two package.json versions)
test384 run.sh defaults + Dockerfile ARG defaults
The sharpest one is test386:398. It `grep -Fq`s the exact install command that
`opencodeExactPairInstallCommand()` builds FROM those constants, so bumping a
constant makes the assertion fail by construction. Following the SOP as written
produces a red, and the cheapest way to make that red go away is to edit the
number in the assertion — which turns the test into a copy of the current value
that checks nothing.
Everything now reads the constants at run time, fail-closed: if the source file
cannot be parsed the run fails rather than continuing with an empty string,
because `grep -Fq ""` matches everything and would silently turn five assertions
into permanent passes. The two fixture package.json files are rewritten by
run.sh before use and carry a note saying not to chase the constant by hand.
Two traps hit while doing this, both worth recording because the fix and the
no-op look identical:
* test384's Dockerfile sets `ENV *_UNDER_TEST=${ARG}` from an ARG that had a
hardcoded default, and run.sh reads `${*_UNDER_TEST:-<derived>}`. With a
non-empty ARG default the ENV is never empty, so the derived branch could
never run — the "now it derives" change would have been inert while the
suite kept passing against the previous version. The ARG defaults are now
empty so `:-` reaches the derivation; `--build-arg` still overrides.
* bin/npx compared against a literal spec. It now compares against
`$EXPECT_NODE_SPEC` exported by run.sh and exits 65 if that is unset, rather
than falling through to "unexpected npx arguments" — which would have read
as a product failure instead of a harness one.
RELEASE-SOP gains a calibration note saying these files are deliberately NOT in
the Live versions table: they are self-consistent now, and registering them
would re-introduce a second copy to drift.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(docs): changelogs must not line-anchor into main — the anchor rots by construction (#903)
A changelog entry describes a state that was true at some past release. A
`blob/main/<file>#L<n>` link resolves against today's code. Those two facts are
incompatible: the link is wrong after the next commit that touches that file,
and nothing reports it.
Measured, not assumed. Two of the six such links in the changelogs:
cli.ts#L61 documented as `PINNED_SERVER_VERSION`
now lands on `} from "../src/opencode-preset";`
cli.ts#L2589 documented as the `bunx --bun @sleep2agi/commhub-server@…`
line in `anet hub start`
now lands on a line of `anet project restart` help text
Both now link the file without the anchor and name the symbol instead, which is
what a reader can actually search for. The original line number is kept in
parentheses as historical context — it was true when written, and saying so is
more useful than deleting it. This follows the precedent RELEASE-SOP records at
R367, which replaced `cli.ts:NNN` references with symbol references for the same
reason.
Scoped to changelogs on purpose. `docs-site/docs/api/mcp-tools.md` carries 44 of
these anchors and all 44 are still in range, landing on plausible content — they
are maintained, because that page documents current behaviour rather than past
releases. A guard reddening on ~100 maintained links would be a backlog canary
that dies the day the backlog clears, and would train people to ignore it.
check-docs-integrity.py gains the rule, exercised three ways: repaired tree →
exit 0 (2 changelogs, 0 anchors), f565e9b8's changelog → exit 1 naming each,
CHANGELOG_GLOB pointed at a missing filename → exit 2 rather than a clean pass
against nothing.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(hub): let get_all_status filter by alias, and say what its summary counted (#904)
`get_all_status` returns one row per session with 31 columns and no way to ask
about specific nodes. On this hub — 222 sessions — the response is about 259 KB,
past what an MCP client takes in one result. So the caller who wanted the status
of THREE nodes could not get it from this tool at all, and had to go around it
to the REST API. That is the whole of #824, hit first-hand.
`filter_alias` takes one alias or several separated by commas, matched exactly
through bound parameters. The patrol loop still gets everything, so the argument
is optional and nothing existing changes.
Blank entries are dropped rather than matched. A trailing comma would otherwise
produce `alias = ''`, which matches no row — and "no rows" reads exactly like
"those nodes do not exist". The failure and the true answer would be
indistinguishable to the caller. That behaviour is the reason the parsing lives
in its own module with tests rather than inline: nine cases pin it, including
that placeholder count always equals alias count so parameters cannot misalign,
and that a filter of only commas means "no filter" rather than "match nothing".
The response also now carries `summary_scope` and `sessions_returned`. `summary`
has always counted every session in the read scope while ignoring the filters,
which is right for the patrol loop — but a caller who asked about three aliases
and gets back three rows plus `idle: 96` can easily read the 96 as being about
their three. Rather than change the semantics under existing callers, the
response says what the number covered.
Verified: the wiring assertion fails against main and passes here; the project's
own runner reports server/src/alias-filter.test.ts pass=9 fail=0.
Pre-existing and NOT from this change: `server/src/task-lifecycle-watcher.test.ts`
fails on main today ("startHub owns a live watcher timer instead of relying on
import side effects", expected 0 received 1). Confirmed by running that file
against main's tools.ts in this same tree — identical failure. Aggregate is
937 pass / 1 fail either way.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(hub): PORT=0 must mean an ephemeral port, not the production Hub port (#906)
`server/src/server.ts:51` read
const PORT = Number(process.env.PORT) || 9200;
`Number("0")` is `0`, which is falsy, so `PORT=0` — the conventional way to ask
the OS for an ephemeral port — silently resolved to 9200, the production Hub
port. Three consequences, and the middle one is the worst:
1. On a host where 9200 is taken (a running Hub), a test that sets PORT=0 dies
with EADDRINUSE and reads as a product bug.
2. On a host where 9200 is FREE, that same test PASSES — by binding 9200. It
is green because it grabbed the production port, not because PORT=0 did
anything. Green for the wrong reason is worse than red.
3. Anyone asking for an ephemeral port gets the production port instead.
This is not hypothetical. `server/src/task-lifecycle-watcher.test.ts` fails on
main today, and that is why: it spawns the Hub with `PORT: "0"`, the child binds
9200, 9200 is already in use on this machine, the child exits 1, and the
assertion `expect(child.exitCode).toBeNull()` fails. The test reports "the
watcher did not stay alive" and says nothing about ports — the message points at
the wrong layer entirely.
The file already knew. `bootServer` uses `opts.port ?? PORT` with a comment
saying `||` "would swallow a legitimate 0". The correct rule was one level above
the line that needed it.
`resolvePort` also rejects a malformed value instead of defaulting. Falling back
to 9200 on `PORT=abc` means a typo starts the server somewhere the operator did
not ask for, and on this fleet that somewhere is production. Parsing is decimal
digits only after trimming: `Number()` alone accepts `"0x10"` as 16, so a value
that does not look like a port would still resolve to one, quietly and to a
different number than was typed.
Verified as an A/B on the same tree, same DB layout, cwd at the repo root:
main's server.ts 4 pass, 1 fail (EADDRINUSE, child exit 1)
this branch 5 pass, 0 fail (child binds 41885 and stays up)
The project's own runner now reports 946 pass / 0 fail / bad=false. Before this,
it was 937 pass / 1 fail.
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): outbound 工具集断言改为从真相源派生(#816 —— 门是错的,而且没人跑) (#905)
* fix(tests): assert the outbound tool set from the source of truth, not a stale copy (#816)
test235's harness asserted the outbound MCP surface as a hard-coded array of
three names. `OUTBOUND_TOOL_NAMES` in node-server.ts has held FOUR since
`commhub_upload_file` shipped in #693, so that assertion has been wrong on main
— and nothing reported it, because no workflow and neither of qa.sh's L0/L1
lists runs test235. A gate that is wrong and unrun is indistinguishable from a
gate that passes.
Third instance of the same shape tonight, after qa.yml's path filter missing the
tests it runs and the two orphaned verify scripts: the check exists, it is
plausible to read, and nothing invokes it.
The names now live in `agent-network/src/outbound-tool-names.ts` and both sides
import them. That module exists as its own file rather than an export from
node-server.ts for a measured reason: importing node-server.ts to read a
constant BOOTS THE SERVER.
$ bun -e 'import { OUTBOUND_TOOL_NAMES } from "./src/node-server.ts"; …'
[commhub] MCP stdio connected
[commhub] starting SSE listener...
OUTBOUND_TOOL_NAMES: commhub_get_all_status, …
A harness that opens a live MCP connection to read a list fails for reasons that
have nothing to do with what it tests. I hit that while writing this fix.
The assertion also sorts both sides. It is about WHICH tools are exposed, not
about the order the server registers them in, and an order-sensitive comparison
would have turned a reordering into a mystery failure.
Both new assertions fail against main. Suite 497 pass, tsc clean.
One note on the credit: I first reported this issue as not reproducible. My grep
searched for the NEW tool names, so of course it found nothing — the assertion
pins the OLD three and never mentions upload_file. 通信团队's triage node caught
the mistake and pointed at socket-harness.ts:210. Probe for the assertion, not
for the symptom.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): scope the cross-package assertion — it fails ENOENT inside the unit image
My own CI break, one commit old. The new test read
tests/test235-grok-mcp-outbound-only/socket-harness.ts, but
tests/test745-agent-network-unit-ci/Dockerfile copies ONLY agent-network/ (plus
agent-node/package.json and its own run.sh). So the assertion passed on a full
checkout and failed with ENOENT in the container — the same "works where I ran
it, not where it runs" shape I spent tonight cataloguing, this time in a test I
wrote to catch that shape.
The harness assertion now skips when the file is absent. Skipping is fail-open,
so it is paired with an assertion that refuses to let the skip be silent: on a
full checkout the harness MUST exist (its absence there is a real regression),
and in a package-scoped image the run prints which assertion did not execute.
A green in that image is therefore never mistaken for "the harness was checked".
Verified both ways:
full checkout 5 pass, 0 skip (assertion really runs)
simulated package-only image 4 pass, 1 skip + "the socket-harness
assertion did NOT run in this image"
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tests): stop detecting the checkout shape — assert only what is in this package
Third attempt at the same 20 lines, and the first two were both wrong in the
same way.
1. Read tests/test235-.../socket-harness.ts unconditionally.
→ ENOENT inside tests/test745-agent-network-unit-ci, whose image copies
only agent-network/ (plus agent-node/package.json and its own run.sh).
2. Skip when `tests/` is absent.
→ the container HAS a `tests/` directory: test745's own run.sh lives in it.
The probe answered "full checkout", the assertion ran, and it failed.
The second is the first mistake repeated: probing an incidental feature ("is
there a tests/ directory") instead of the thing itself ("is THIS harness here").
A third detector would be a third guess, so this suite now asserts only what
lives inside its own package — the constant's contents, that reading it does not
boot a server, and that node-server.ts consumes it instead of redeclaring it.
The gap is written down rather than papered over: nothing gates the fact that
socket-harness.ts derives its expectation from OUTBOUND_TOOL_NAMES. That is not
new here — no workflow and neither of qa.sh's L0/L1 lists runs test235 at all,
which is exactly why its assertion could sit wrong on main for as long as it
did. Wiring test235 into CI fixes that and is a separate change: it needs a real
hub and a socket harness, not a unit runner.
Verified in both shapes: full checkout 3 pass / 0 skip; a simulated
package-only image that also contains a partial tests/ (the case that broke
attempt 2) 3 pass / 0 skip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(install.sh): stop blaming the registry for every failure (#868) (#908)
The public installer ran
npm install -g @sleep2agi/agent-network >/dev/null 2>&1 || {
say "Default registry failed, retrying via npmmirror..."
...
}
Two problems in two lines. The first attempt's stderr went to /dev/null, and
whatever went wrong was then announced as a registry problem. A permission
error, a full disk, an unsupported Node — all told the reader to blame the
registry, and the npmmirror retry then failed the same way a moment later. The
reader ends up with a confident, wrong story and no sign of the real one.
Now the output is kept, and "registry" is only claimed when the output actually
looks like a fetch problem (ETIMEDOUT / ENOTFOUND / ECONNRESET / ECONNREFUSED /
EAI_AGAIN / network / registry / fetch failed / socket hang up). Anything else
is printed verbatim with an explicit note that retrying a different registry
would fail the same way. If the mirror path is taken and also fails, the first
attempt's output is shown too — otherwise the mirror's error replaces the
original one and the actual cause is gone.
Verified against the real script with a stubbed npm, both directions:
EACCES → "does not look like a registry problem", no mirror retry,
EACCES shown to the reader
ETIMEDOUT → "looks unreachable, retrying via npmmirror", and on the second
failure "The mirror failed too" plus the first output
Same stub against main's copy prints "Default registry failed" for the EACCES
case and never shows the word EACCES at all.
(While building the stub I first wrote a fake `node` that did not implement
`-p`, so the script bailed at its version check with "Node.js >= 22.13 required
(current: v22.13.0)" — a self-contradiction that was the harness talking, not
the script. Worth noting because that message is exactly what a reader would
report as a product bug.)
Co-authored-by: t <t@t>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): 把唯一的第三方 action 钉到 SHA,并加一道门防下一个 (#746) (#907)
* fix(ci): pin the one third-party action to a SHA, and guard against the next one (#746)
`uses: oven-sh/setup-bun@v2` runs whatever that tag points at today, and the tag
is writable by the action's owner. What it resolves to executes here with this
repository's checkout and secrets in scope, and it can change without any commit
in this repo for anyone to review. Three workflows used it: qa, e2e-docker,
release. All three now pin
0c5077e51419868618aeaa5fe8019c62421857d6, with `# v2` kept as a comment so the
pin still reads as something.
The SHA was resolved from the GitHub API rather than copied out of a log line —
`git/ref/tags/v2` returns that commit directly (a lightweight tag), and it
matches the ref that appeared in tonight's failed download.
#799 already pinned `bun-version: 1.3.14`. That pins the TOOL; …
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔴 这个 PR 存在的原因是我的一个错误
#835(给 L1 并发上限闸门补 Docker 回归)的 base 是ci/l1-concurrency-cap,不是main。我合它的时候只看了 draft / CI / mergeable_state,没有看
.base.ref——合并 API 返回
merged=true,而它合进的是另一条特性分支。于是那条分支比 main 领先 5 个提交,#835 的产物一个字节都没到 main:
本 PR 把这条分支正式提到 main。试合无冲突(
git merge --no-commit探过,0 个冲突文件)。带来的内容
#823 —— L1 并发上限(已在 main 上,这里是它的分支基底)
#835 —— 这道闸门自己的 Docker 回归
tests/test823-l1-concurrency-cap/scripts/qa.sh(把docker/npm换成 PATH 上的桩),不是逻辑副本;我在合并 #835 时做的三处(都在这条分支上):
qa.sh的 build-arg 推导取并集 —— main 的「从 Dockerfile 读 arg 名」(权威、不猜)+ test(#823): 给 L1 并发上限闸门补 Docker 回归(含变异见证) #835 的RUNSH_BLOB供给 + 非致命 git。🔴 非致命 git 是硬需求:test823 在一个没有 git 的容器里重放 qa.sh,直接
$(git rev-parse HEAD)→ 127 →set -e中断 → docker 桩一次都没被调用 → 峰值恒为 0,回归"通过"得毫无意义。逐套件核过:只有 test823 多拿一个
RUNSH_BLOB,其余 17 个与 main 逐字相同。jobs那段注释取 main(我在 ci(qa): L1 加并发上限(默认 nproc);第一版用 jobs 计数是坏的,实测才发现 #823 里更正过:原文说「恒为 0」,实测采样是0 1 1 1 0 1 0 1—— 不是恒 0,是从来到不了上限值)。qa.yml补tests/test823-l1-concurrency-cap/**的paths:—— 这是check-l1-paths-sync抓到的,不是我先想到的:套件注册进了 L1_TESTS,但改这个套件不会触发跑它的 workflow。对一个「测这道闸门自己」的套件尤其要命。CI 在这条分支上跑过并全绿(18 个 check),日志里能看到
L1 并发上限 = 4和✓ L1 test823-l1-concurrency-cap (RESULT: PASS)。🤖 Generated with Claude Code