Skip to content

docs: 节点存活判据 —— 看产物推进,不看进程数/started 行 - #812

Draft
vansin wants to merge 9 commits into
mainfrom
docs/node-liveness-criterion
Draft

docs: 节点存活判据 —— 看产物推进,不看进程数/started 行#812
vansin wants to merge 9 commits into
mainfrom
docs/node-liveness-criterion

Conversation

@vansin

@vansin vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

这份文档在回答什么

「一个节点是不是还活着」——而现场能拿到的大多数信号都回答不了这个问题。

起因是 2026-08-13 TM汇报牛 连续两次 600s 超时,有人问它是不是死了。诊断时发现:
进程在、tmux session 在、日志里有 startedlifecycle_state=activesend_task 返回 ok
—— 这些全都可以在节点一个任务都处理不了的时候成立。

核心一句

看产物推进,不看进程数和 started 那一行。

文档列了六种不算存活判据的信号,每一条都是那次诊断里实际遇到过的;
以及三条的:hub 上 actor == 该节点 task_events(跨 vantage 成立、伪造不了)、
桥日志里出现过 processTask returned不只是 queued、同机多节点 updated_at 差分。

那次诊断的实际形态(文档里的主案例)

桥进程         pid 2408382,连续运行 818134s ≈ 9.5 天    ← 活着
codex app-server                                        ← 不存在

桥日志把机制写得很清楚:

[07:09:21] [codex-app-server] task 893fdbf4 queued (a turn is in flight)
[07:19:21] processTask returned: "…超时(600s 内无最终回复)"

任务是被排队、根本没开始处理,而那「上一轮」永远飞不完 —— 因为下游没有人。
判它的方法不是数进程,是结构性对照:本机 21 个 <名字>-桥,19 个有对应的 <名字>-appsrv;
缺的只有两个,其中一个是 opencode 运行时(本就不需要 codex app-server,正常),另一个就是故障节点。

附:只读排查三步

文档给了三条可直接复制执行的命令,并写明各自的坑 ——
它们都是我实际踩过之后修正的(占位符会静默匹配零条、桥$会话名|命令 格式下永不命中、
capture-pane -t '=名字-桥' 会报 can't find pane、要带窗口索引)。

范围

  • docs-only,不改任何产品代码或 CI;
  • 三条命令在本机真跑过(进程命中 1 条、结构对照 41 行、capture-pane 可用);
  • 不覆盖:如何修复一个死掉的节点 —— 文档只说明「起回 app-server」与「重启桥」是两件事
    (后者丢 threadId 且解决不了前者),具体动作属运维授权范围。

起因是 TM汇报牛 连续两次 600s 超时。诊断时发现:现场能拿到的信号里大多数
都不能回答「它是不是活着」——进程在、session 在、日志有 started、
lifecycle_state=active、send_task 返回 ok,这些全都可以在节点一个任务都处理不了时成立。

把当时用上的判据写下来,重点是两条:
- 存活 = 最近有没有**产出**(hub 上 actor==该节点的新 task_events;桥日志里
  出现过 processTask returned 而不只是 queued);
- 桥活着 ≠ 运行时活着。TM汇报牛 的实际形态是桥连跑 9.5 天、codex app-server 不存在,
  判据是结构性对照:21 个 -桥 里 19 个有对应 -appsrv,缺的只有它和一个
  本就不需要 app-server 的 opencode 节点。

附只读排查三步,并写明 tmux -t 要用 = 精确匹配、以及「起回 app-server」
与「重启桥」是两件事(后者丢 threadId 且解决不了前者)。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52b8961005

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +21 to +23
1. **Hub 上的新回执** —— 最近一次 `task_events` 里 `actor == 该节点` 的记录。
这条最强:它跨 vantage 成立(不需要 SSH 到那台机器),而且伪造不了 ——
要写进去就得真的走完一轮。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require runtime evidence before treating task events as liveness

When the bridge receives a task but its app-server is unavailable, processTask calls reportStatus("working") before invoking the runtime, and the server immediately records a running task event with the node alias as actor. Thus the exact dead-runtime scenario described below can still produce a fresh matching event without completing any turn, so this “strongest” criterion can incorrectly prevent recovery; require attributable runtime evidence such as consumed_at or a successful terminal result instead of any actor match.

Useful? React with 👍 / 👎.

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +24 to +25
2. **桥日志里出现过 `processTask returned`**(而不只是 `queued`)。
注意区分:`queued (a turn is in flight)` 说明它**根本没开始**,那不是产出。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude timeout returns from the liveness signal

A processTask returned line is emitted for failed outcomes as well as successful runtime output. In fact, the missing-app-server example on lines 41–42 produces exactly this line for the 600-second queue timeout, so applying this criterion would classify the document's dead runtime as alive; the check must distinguish successful or activity-backed returns from timeout/error returns.

Useful? React with 👍 / 👎.

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +26 to +27
3. **`sessions.updated_at` 的差分**:同机多个节点一起看,三个在动、一个冻住,
才说明是那一个的问题;全都冻住通常是通信层断了,不是节点死。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not use session timestamps as runtime progress

For an agent-node whose bridge is alive but app-server is missing, sessions.updated_at still advances because the bridge sends reportStatus("idle") every three minutes; dispatching a task also updates the target session timestamp from the server side. Consequently all nodes on the host can show moving timestamps while one downstream runtime is dead, so this differential only diagnoses bridge/Hub connectivity and cannot serve as the claimed node-output liveness signal.

Useful? React with 👍 / 👎.

Comment thread docs/node-liveness-criterion.md Outdated
ps -eo pid,etimes,args --no-headers | grep -F '<节点配置路径>' | grep -v grep

# 2. app-server 在不在 —— 用结构性对照,不是看单个节点
tmux list-panes -a -F '#{session_name}|#{pane_current_command}' | grep -E '桥$|appsrv'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Match bridge names before the appended command field

With the requested format, each row is <session_name>|<pane_current_command>, so a live bridge is rendered like <name>-桥|node and does not match 桥$. The pipeline therefore removes every bridge row while retaining appsrv rows, making the prescribed bridge-to-app-server structural comparison impossible precisely when diagnosing a missing app-server; match 桥\| or format/filter the session name separately.

Useful? React with 👍 / 👎.

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +65 to +66
三步都是只读的。**看完再决定动不动**,而且「起回缺失的 app-server」和
「重启桥」是两件事 —— 后者会丢 threadId,而且解决不了前者。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Point recovery at the managed copresence restart path

For managed Codex copresence, the app-server is created by anet node start <alias> --copresence using a generated one-time credential file, dynamic port, and newly persisted thread; the repository exposes no supported command that merely “起回缺失的 app-server.” Conversely, restarting only the bridge reads codexThreadId from config.json and resumes it, so it does not inherently lose that ID. Replace this unsupported dichotomy with the existing full-three-piece SOP or add an authoritative isolated launcher before recommending that action.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

拿独审在 #805 上用的口径审自己这份 docs,三条排查命令**全部有问题**,
逐条实测坐实:

1) `grep -F '<节点配置路径>'` —— 又是占位符。照抄命中 0 条,
   而这一步是判「桥在不在」,零命中会被读成「桥没跑」。
   改成先 `node=<名字>` 再引用,并写明「不要把占位符原样敲进命令行」。

2) `grep -E '桥$|appsrv'` —— **一个桥都匹配不到。**
   输出格式是 `会话名|命令`,行尾是**命令**(如 `TM汇报牛-桥|bash`),
   所以 `桥$` 永远不命中。本机实测:原写法 20 行,正确写法
   `桥\||appsrv` 41 行。**照抄这条得不出这份文档在教的那个桥/appsrv 对照。**

3) `capture-pane -t '=名字-桥'` —— 实测报 `can't find pane`,
   我当初诊断 TM汇报牛 时就踩过这个,只是没把教训写进命令里。
   要带窗口索引:`-t '=名字-桥:0'` 才可用。

三条修完都在本机真跑过:进程命中 1 条、对照 41 行、capture-pane 可用。

教训同 #805:**写排查步骤的文档,命令自己得先跑通。**
占位符这一类尤其危险 —— 它不会报错,只会安静地给出错误结论。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

三条 P1 我逐条验了,全部成立。本 PR 的三条判据都是错的,已转回 Draft。

自动审查在 02:26 提出这些,我因为只查 issue 级评论没看到(见 #807 上的说明)。逐条按源码复核:

判据 1「Hub 上的新回执 —— task_eventsactor == 该节点」——

我给它写的理由是「这条最强 … 伪造不了 —— 要写进去就得真的走完一轮」。这句是假的。

agent-node/src/cli.ts:4381  async function processTask(
             …:4391-4401     脱敏、日志(无运行时调用)
             …:4402  await reportStatus("working", runtimeTask.slice(0, 200))
             …:4404+ // RFC-025 M1c P0b — context injection … 之后才进运行时

working 状态在任何运行时工作之前就上报了,服务端随即写下 actor = 该节点running 事件。运行时已死的节点照样产生一条新鲜的、匹配的事件。 我把最弱的信号标成了「最强」。

判据 2「桥日志里出现过 processTask returned」——错,而且被本文档自己的例子推翻

判据写在 L24,而 L41-42 是本文档举的运行时已死的实例:

[07:09:21] [codex-app-server] task 893fdbf4 queued (a turn is in flight)
[07:19:21] processTask returned: "…超时(600s 内无最终回复)"

processTask returned 在 600s 超时时同样会打。 拿判据 2 去判这份文档自己举的死亡案例,会判成活着。两半都在同一个 PR 里,隔 17 行。

判据 3「sessions.updated_at 的差分」——

桥每 3 分钟 reportStatus("idle"),派任务也会从服务端侧更新目标 session 的时间戳。所以时间戳推进只证明桥↔Hub 连通,与下游运行时是否活着无关。


为什么转 Draft 而不是打补丁

三条判据是这篇文档的全部内容,不是其中一条有瑕疵。补一句限定救不回来 —— 它现在给出的操作指引会让人把死节点判成活的(判据 1、2)或把活节点判成死的(判据 3 反向误用)。这类文档错了比没有更糟。

正确的方向是审查里给的:要求可归因的运行时证据 —— consumed_at、带真实产出的终止结果,而不是任何 actor 匹配或任何 returned 行。重写时要能区分:

  • 成功返回 vs 超时/错误返回;
  • 运行时写的记录 vs 桥在调用运行时之前写的记录。

一句该说的

这份文档的立意是「别看进程数,看产物推进」。讽刺的是它自己选的三个"产物"信号,没有一个是产物 —— 全都是桥或服务端在运行时之外产生的。我在写它的时候没有回到源码确认每条信号是谁写的、什么时候写的。

审查方提的第 4、5 条(桥\| 匹配、托管共存的恢复路径)我还没逐条验,下轮继续,不会因为前三条成立就默认后两条也成立。

@vansin
vansin marked this pull request as draft August 13, 2026 06:33
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

五条 P1 裁定完毕:4 条成立,1 条已在当前 head 修掉

④「桥\| 匹配」——对当前 head 不成立(已修)

审查针对的是 52b89610,当时写的是 grep '桥$',而行格式是 <session>|<command>,活桥渲染成 <name>-桥|node,确实匹配不到。

但当前 head 7b1855fc:66 已经是:

tmux list-panes -a -F '#{session_name}|#{pane_current_command}' | grep -E '桥\||appsrv'

正是审查建议的写法。 这条不需要再改 —— 记一笔:审查坐标过期时,逐条对当前 head 复核是必要的,否则会去"修"一个已经修好的东西。

⑤「恢复路径」——成立,而且我那句话两半都错

当前 head :75 我写的是:

「起回缺失的 app-server」和「重启桥」是两件事 —— 后者会丢 threadId,而且解决不了前者。

两个断言分别验:

(a) 「重启桥会丢 threadId」——错。 codexThreadId 是持久化到 config.json 的:

agent-network/bin/cli.ts:632   rawCfg.codexThreadId = threadId;
agent-network/bin/cli.ts:638   // path reads codexAppServerUrl / codexThreadId from the config we just …
agent-node/src/cli.ts:818-819  if (cfg.codexThreadId === threadId) return;  cfg.codexThreadId = threadId;

重启桥会从 config 读回并 resume,不丢

(b) 「起回缺失的 app-server」——没有这个支持命令。 app-server 由 anet node start <alias> --copresence 作为三件套的一部分创建(copresenceTmuxSessions() 返回 { appsrv, bridge, tui }),没有单独只起 appsrv 的入口。

所以这句话的实际危害是:它劝阻了一个本来安全的动作。 读到它的人会因为"怕丢 threadId"而不敢重启桥,转而去找一个并不存在的"只起 app-server"的办法。


#812 汇总

P1 裁定
task_events actor 匹配 = 最强证据 working 在运行时之前上报
processTask returned = 活着 — 超时也打,被本文自己的例子推翻
sessions.updated_at 差分 — 3 分钟 idle 心跳推进,与运行时无关
桥| 匹配 已修,当前 head 无此问题
⑤ 恢复路径二分 — 丢 threadId 是假的;"只起 app-server"无此命令

三条判据 + 恢复建议,全部需要重写。已转 Draft(上一条),这里补完裁定。

重写时的方向:

  1. 判据必须用可归因到运行时的证据 —— consumed_at、带真实产出的终止结果;
  2. 区分成功返回超时/错误返回;
  3. 区分运行时写的记录桥在调用运行时之前写的记录;
  4. 恢复段先确认仓里实际存在的命令,再写建议 —— 这次就是没确认。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

替代判据找到了 —— 而且它早就存在:#520 的两级运行时证据

上一条我判「三条判据全错、需整篇重写」,但只给了方向没给判据,那等于把问题留在原地。去找了「只有运行时才写得出来的信号」,结果是这套东西已经建好了,我写文档时没用它。

server/src/tools.ts:963-970(#520)

// #520 — two monotonic runtime-evidence levels for exact tasks.
//
// runtime_submitted_at means agent-node handed the body to the vendor
// runtime. consumed_at is stronger: an attributable turn-start or first
// activity event came back. Merely fetching/acking an inbox row sets neither.
// Node identity comes exclusively from the ntok; callers cannot self-report
// an alias or node_id. A consumed mark also fills runtime_submitted_at because
// that stronger fact logically implies submission.

写入点在 markTaskRuntimeEvidence()(:971),level === "consumed" 时两个字段一起写(:1042-1048);读出侧 server.ts:3050consumedAt 投影,rest-projections.ts:38 也在列。

这两句正是我那三条判据缺的东西

Merely fetching/acking an inbox row sets neither.

我原来的判据 ①②③ 恰恰全都是「桥取了任务 / 桥回了状态 / 桥在心跳」就会满足的 —— 一个 app-server 已死的节点,桥照样把它们全部点亮。

Node identity comes exclusively from the ntok; callers cannot self-report an alias or node_id.

我原文给判据 ① 写的理由是「伪造不了」。那句话对这套机制成立,对我实际选的 task_events.actor 不成立。我把一个真实存在的强属性,安在了一个没有该属性的信号上。

重写后的判据(草案)

判据 含义 强度
tasks.consumed_at 有值且新 运行时真的开工了(turn-start / 首个活动事件回流) 最强,可跨 vantage
tasks.runtime_submitted_at 有值但 consumed_at 为空且已久 桥交出去了,运行时没接 —— 正是「桥活着、app-server 死了」的精确指纹 强,且能定位故障层
桥日志 processTask returned 不可用:超时也打这行 弃用
task_events.actor 匹配 不可用:reportStatus("working") 在运行时之前就写 弃用
sessions.updated_at 差分 不可用:3 分钟 idle 心跳推进 弃用

第二行值得单独说:它不只是「能不能判活」,而是能把故障定位到哪一层 —— 而这正是我原文那个「桥活着 ≠ 运行时活着」的例子想说、却没有工具去说的事。

一处我自己刚犯的错,同轮抓到

找写入点时我先 grep SET\s*consumed_at,得到 0 命中,于是差点写下「consumed_at 只有列、没有人写,所以审查建议的方向不可用」。

错了。 那条 SQL 是多行的,consumed_at = COALESCE(…) 单独占一行(:1045),不匹配我的形状。读上下文才看见。

这跟我在 #815 补的那条(查询没覆盖目标 → 得到一个语法正确、语义为空的 0)是同一件事,这次是形状假设:我假设了 SQL 会写在一行里。

顺带更正上一条

上一条我说「按审查建议改用 consumed_at」时,没有验证这个字段是否可用就接受了方向。事实是它可用(而且比建议的更完整,还有 runtime_submitted_at 这一级)—— 但我当时不知道,只是照单全收。

接受一条建议和验证它是两件事。 这次运气好,建议是对的;但我给出的「已接受」在当时是没有根据的。


下一步:按上表重写全文,并给每条判据标注它不能证明什么。冻结中,待收口。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

更正我上一条的草案判据:第二行会把一整条运行时通道上的健康节点误判为「运行时已死」

上一条我提了这个:

runtime_submitted_at 有值但 consumed_at 长期为空 → 桥交出去了、运行时没接,正是「桥活着 app-server 死了」的精确指纹

这条不成立。 我按自己反复说的「验前提」去查「车队侧到底有没有人发这个证据」,查出来的结果同时否掉了它。

先说好消息:证据确实在发

  • 服务端暴露工具 mark_tasks_runtime_submitted;
  • agent-node 调 evidence?.consumed()(cli.ts:2353 / :2721),并把 onConsumed 传下去(:2995 / :3054)。

所以 consumed_at 不是空摆设,判据①(consumed_at 新 = 运行时真开工)成立

但有一条通道按设计永远不报 consumed

agent-node/src/cli.ts:2886-2890(codex 直连 stdio):

// Direct stdio does not yet send/echo clientUserMessageId, so the
// response turn id is admission evidence only (the #587 race proved it
// is not authoritative ownership). Report submitted, never consumed,
// until this lane grows an exact identity echo.
evidence?.submitted();

在这条通道上,一个完全健康的节点也会永远是 runtime_submitted_at 有值 + consumed_at 为空 —— 与我上一条描述的「故障指纹」字节级一致

按我那条草案去判,codex 直连 stdio 通道上的每一个正常节点都会被判成「运行时已死」。

修正后的判据

判据 适用条件 说明
consumed_at 仅在会报 consumed 的通道上 有值 = 运行时真开工,最强
consumed_at 长期为空 必须先确认该节点的通道会不会报 consumed 在 codex 直连 stdio 上无信息量,不能当故障证据
runtime_submitted_at 有值 所有通道 只证明桥交出去了,不证明运行时接了

结论:consumed_at 是「正向可信、反向不可信」的信号 —— 有值时它证明了很强的事,为空时它可能只是说明这条通道不报。

这与我这两天反复查的那类是同一个形态:同一个信号,正向和反向的证明力不对称。(#825 上那条「加锚对正断言是收紧、对负断言是放松」是同一件事的另一个例子。)

这次的价值在于它被拦在文档之前

我上一条把那个"指纹"写得很有把握。如果直接写进文档,它会成为一条看起来比原来三条都严谨、但会造成误判并可能触发不必要重启的指引 —— 而原来那三条至少只是漏报。

拦住它的不是灵感,是那句「谁在调用它」。验前提这一步,这次是在我自己刚提出的判据上做的。

第一版给的三条(task_events.actor 匹配、桥日志 processTask returned、
sessions.updated_at 差分)全部是「桥还活着」就会满足的信号:

- actor 事件在 reportStatus("working") 时就写,早于任何运行时调用;
- processTask returned 超时也会打 —— 被本文档自己举的死亡实例推翻;
- updated_at 由 3 分钟一次的 idle 心跳推进。

这份文档的立意是「别看进程数,看产物推进」,而第一版选的三个"产物"
没有一个是产物。根因是写的时候没回到源码确认:每条信号是谁写的、
什么时候写的。

改用 #520 的 tasks.consumed_at。它满足这份文档需要的两个性质,
源码注释里写得很清楚:
  Merely fetching/acking an inbox row sets neither.
  Node identity comes exclusively from the ntok; callers cannot
  self-report an alias or node_id.

但只在正方向可信:codex 直连 stdio 通道按设计永不报 consumed
(见 evidence?.submitted() 上方注释),而通道信息 hub 侧看不到
(由本地 env ANET_CODEX_STDIO_DIRECT 决定,服务端零感知)。
所以「consumed_at 为空」现在不能当故障证据 —— 误判方向是把健康
节点判成已死,而下一步动作通常是重启。负向判据跟踪 #832。

同时修正第一版两处错误的操作指引:
- 「重启桥会丢 threadId」是假的,codexThreadId 持久化在 config.json,
  重启会读回并 resume;
- 「起回缺失的 app-server」没有对应命令,它是三件套的一部分。

全文引用改为钉符号不钉行号(#831:文档站 141 处行号引用里,判据能判
的 74 处有 72 处已漂)。七个符号锚点均已验证可 grep 到。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

为什么现在请求复审: 全文重写:三条判据全错已换成 #520 的 consumed_at,并加了『只在正方向可信』的边界。审查记录停在 52b8961,当前 head aab7a11(75→137 行)。

自动审查器只在开 PR / 草稿转就绪 / 显式 @ 时触发,push 不触发 —— 所以按审查意见做的修复,默认不会被复核(全仓实测:9 条有审查记录的 open PR 里 7 条的审查落后于 head,见 #828)。

这条 @ 是作者侧补上这个缺口,不是绕过审查。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aab7a111d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +38 to +40
> 🔴 **`consumed_at` 只在正方向可信。**
> **有值且新** → 运行时确实开工了,可跨 vantage,不需要 SSH。
> **为空** → **不能推出任何结论**,原因见下一节。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not attribute lifetime evidence to the current node

When a consumed task is immediately reassigned, reassign_task changes to_name/to_node_id but deliberately preserves consumed_at (confirmed by server/src/task-consumption.test.ts's “reassign preserves task-lifetime evidence” test). The timestamp can therefore be both present and fresh even though it was produced by the previous node and the newly assigned node has never started the task, so this rule can falsely classify that node as working and suppress recovery. Treat consumed_at only as evidence that the logical task was consumed at least once, or pair it with delivery-attempt/consumer identity before using it for node liveness.

Useful? React with 👍 / 👎.

Comment on lines +63 to +64
**跟踪:[#832](https://github.com/sleep2agi/agent-network/issues/832)** —— 在节点自报
「本通道是否发 consumed 级证据」之前,负向判据无法从 hub 侧给出。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for failed evidence reports before enabling negative inference

Channel capability is not the only reason a healthy task can retain consumed_at = NULL: createTaskRuntimeEvidenceReporter records an evidence level as attempted before starting its non-blocking Hub call, and on an old Hub or transient reporting failure it only logs the error and never retries; the PostgreSQL backend also rejects these evidence writes. Consequently, even after #832 reports that a lane supports consumed-level evidence, a null value still cannot become a negative liveness signal unless successful delivery and backend support are also established.

Useful? React with 👍 / 👎.

Comment on lines +32 to +33
第一句正是上表所有信号缺的:取任务、ack、回状态都点不亮它。
第二句意味着它**伪造不了** —— 身份只来自 ntok。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not describe token-bound evidence as unforgeable

The ntok check makes the node identity non-self-selectable, but it does not attest that a vendor runtime emitted an event: any process holding that node token can invoke the registered mark_tasks_consumed MCP tool with an owned task ID, and markTaskRuntimeEvidence verifies only token-bound ownership before stamping the row. A compromised or buggy bridge can therefore manufacture this signal for its own tasks, so the document should distinguish trustworthy attribution from proof that cannot be forged.

Useful? React with 👍 / 👎.

Comment thread docs/node-liveness-criterion.md Outdated
Comment on lines +108 to +110
# -t 要用 = 精确匹配(否则会前缀命中别人),并且**要带窗口索引**:
# 实测 `-t '=名字-桥'` 会报 can't find pane,`-t '=名字-桥:0'` 才可用。
tmux capture-pane -p -t "=${node}-桥:0" | tail -20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Docker evidence for the claimed tmux verification

This passage labels the target syntax as empirically verified, but a repository-wide search of tests/ and docs/tests/ finds neither an independent Docker suite nor a saved report covering these liveness commands. Without that artifact, the host-specific observation cannot be reproduced after a rebuild and the claimed verification bypasses the repository requirement that tests run in Docker and persist their results; add the corresponding Dockerfile/run script and docs/tests/report-testN.txt, or remove the verification claim.

AGENTS.md reference: AGENTS.md:L7-L9

Useful? React with 👍 / 👎.

通信狗的窄审提了四条,我在汇报里写了"我认",但重写全文时只落了第一条
(actor 过强)。另外三条一直停在"已接受",没进文档 —— 接受一条意见和
实施它是两件事。

① capture-pane 会把 pane 内容原样打出来,里面有任务正文与回执正文。
   实测一个真实节点是 71 行、首行 102 字符,可能含调用方粘进来的凭据、
   内部地址、他人对话。加红字:别整段贴进 issue/聊天/汇报。
   (写这条时我自己就没打印那 71 行,只报了退出码和行数。)
   同时补两条使用边界:grep -F 是子串匹配,节点名互为前缀会误命中;
   第 2 步只看得到当前存在的 pane,命名不符约定的节点根本不会出现 ——
   它的"缺席"不是证据。

② queued + 600s 超时只证明排队且超时,不单独证明"那一轮永远飞不完"。
   本例里"下游没有人"由结构性缺席支持。两条证据合起来才指向该故障。

③ "有桥无 appsrv 就是这个故障"过宽,改为"对 codex 且契约上应有 appsrv
   的节点,长期有桥无 appsrv,高度可疑为此故障类",并说明仍需结合运行时
   证据定性。

另:本轮把文档里三条命令在真实环境跑过 —— 桥\| 匹配正确返回配对结构;
不带 :0 的 capture-pane 仍然失败,文档那句至今成立。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

三条我此前口头认了、却一直没实施的收窄,现在真的进文档了(aab7a111 → 067a0466)

窄审提了四条,我在汇报里写了「我认」,但重写全文时只落了第一条(actor 过强)。另外三条一直停在「已接受」状态。

接受一条意见和实施它是两件事。 前者可以很快,后者会改变产物 —— 我这两天在别的 PR 上说过同一句话,却在这条上正好没做到。

capture-pane 会打出任务正文与回执正文

本轮我把文档里那三条命令在真实环境跑了一遍,顺带量到:

tmux capture-pane -p -t "=<真实节点>-桥:0"   → 退出码 0,71 行,首行 102 字符

而原文档教人 | tail -20 直接读,敏感信息提醒 0 条。那里面可能有调用方粘进来的凭据、内部地址、他人对话。

已加红字:别整段贴进 issue / 聊天 / 汇报;要引用只引需要的那一两行。

写这一条时我自己就没打印那 71 行 —— 只报了退出码和行数。这不是谨慎表演:如果我贴了,这条建议当场就自相矛盾。

顺带补两条使用边界:

  • 第 1 步 grep -F "$node"子串匹配,节点名互为前缀会互相命中(A站狗 命中 A站狗2);
  • 第 2 步只看得到当前存在的 pane,命名不符约定的节点根本不出现 —— 它的「缺席」不是证据

queued + 600s 只证明排队且超时

原文写「那『上一轮』永远飞不完 —— 因为下游没有人」。超时本身推不出这个:也可能下游很慢,或那一轮本来就长。

本例里「下游没有人」是由结构性缺席(有桥、无 appsrv)支持的。两条证据合起来才指向该故障,单独任何一条都不够。

③ 「有桥无 appsrv 就是这个故障」过宽

改为:对 codex、且契约上应当有 appsrv 的节点,长期有桥无 appsrv,高度可疑为此故障类 —— 定性仍要结合运行时证据。


顺带:三条命令真跑过了,两条结论

  • grep -E '桥\||appsrv' 正确返回配对结构(X-appsrv|node / X-桥|bun),之前修的锚点是对的;
  • 不带 :0capture-pane 至今仍然失败 —— 文档里那句「实测 -t '=名字-桥' 会报 can't find pane」现在仍成立,不是过期信息。

文档里写着「实测」的句子,过一段时间就该再跑一次 —— 否则它和普通断言没区别。

审查指出的这条成立,而且被测试逐字钉住:server/src/task-consumption.test.ts
的「reassign preserves task-lifetime evidence」断言 reassign_task 之后
consumedAt(taskId) 与改派前相等。对应的 UPDATE(server/src/tools.ts)只改
to_name/to_node_id/status/started_at/delivered_at,不碰 consumed_at 与
runtime_submitted_at。

所以一个任务可以同时「consumed_at 新鲜」+「当前属主一次都没开工」,而原文那句
「有值且新 → 运行时确实开工了」会把新属主判成在干活,压掉本该做的恢复。

改法:把判据的对象从节点收回到任务 —— consumed_at 证明这个逻辑任务被消费过
至少一次,不证明 to_name 现在指的那个节点开过工;要算到具体节点头上,先确认
任务没被改派。

另附一条明确标注「尚未验证」的线索:改派把 started_at 置 NULL,正常开工写
datetime('now'),所以 consumed_at 有值 + started_at 为空看起来能识别前任的章 ——
但我没测过,requeue 路径也置 NULL,在有人测出来之前不作判据。

这与文末「第一版为什么全错」的根因相同:没回到源码确认字段是谁写的、跟着谁走。
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

两条 P1 都收到了。审查针对的是 aab7a111,当时 head 已经是 067a0466 —— 但第一条在新 head 上同样成立,不是过期发现。

consumed_at 归属 —— 成立,已改(706500c6)

我按源码和测试核了,你说的每一步都对:

server/src/tools.ts 的改派语句

UPDATE tasks SET to_name = ?1, to_node_id = ?2, status = 'delivered',
                 started_at = NULL, delivered_at = datetime('now')
WHERE task_id = ?3

不碰 consumed_at,也不碰 runtime_submitted_at。而 server/src/task-consumption.test.ts 的「reassign preserves task-lifetime evidence and binds the new inbox row to the same task」逐字断言:

expect(runtimeSubmittedAt(taskId)).toBe(submitted);
expect(consumedAt(taskId)).toBe(consumed);

所以这是被测试钉住的契约,不是实现疏漏。一个任务可以同时「consumed_at 新鲜」和「当前属主一次都没开工」,而我原文那句「有值且新 → 运行时确实开工了」会把新属主判成在干活,正好压掉该做的恢复。

改法用了你给的第一个选项:把判据的对象从节点收回到任务 —— consumed_at 证明这个逻辑任务被消费过至少一次,不证明 to_name 现在指的那个节点开过工;要算到具体节点头上,先确认任务没被改派。

补一条你提的「配 consumer identity」方向的线索,但我把它明确标成了未验证:改派把 started_at 置 NULL,正常开工写 datetime('now'),所以「consumed_at 有值 + started_at 为空」看起来能识别出前任盖的章。我没测过,而且 requeue 路径也置 NULL —— 在有人真测出来之前不作判据。

值得记一笔:这和文末「第一版为什么全错」的根因是同一个 —— 没回到源码确认字段是谁写的、跟着谁走。第一版栽在 task_events.actor,这一版栽在 consumed_at,同一个坑踩了两次。

② tmux 验证缺 Docker 证据 —— 指控属实,但你给的两个选项我都不打算原样采纳

你搜 tests/docs/tests/ 找不到覆盖这些命令的套件和报告,这是对的,我没有产出那份工件。

但「删掉验证声明」会丢掉一条真信息(-t '=名字-桥' 报 can't find pane、-t '=名字-桥:0' 可用),而这恰恰是照着文档抄命令的人最容易踩的。

所以我要做的是第三种:补一个自足的 Docker 套件去测 tmux 的 target 语义 —— 容器里装 tmux、造一个名字带连字符和中文的 session、分别用 =name=name:0capture-pane,把两者的退出码和 stderr 记进报告。这不需要真实机群,是纯 tmux 语义,可复现。

在那个套件落地之前,这段的措辞我先不动 —— 我不想用一次「改措辞」把一个还没补上的工件盖过去。这一条在本 PR 里维持 open。

审查指出:这份文档把 tmux 的 target 写法标成「实测」,但仓里既没有覆盖这些
命令的 Docker 套件,也没有留存报告 —— 那条声明在别人的机器上无法复现。指控
属实,我没产出那份工件。

审查给的两个选项(补工件 / 删声明)我取了前者,但做法换了:套件不另写一份
命令去验证文档里的命令(那只证明两份副本自洽),而是把命令**从文档里 grep
出来**、替换节点名后实跑。文档改错就红。

顺带纠正了文档里一处不精确的因果。原文说 `=` 是防「前缀命中别人」,实测
不是:`A站狗-桥` 并不是 `A站狗2-桥` 的前缀,这套 `<别名>-桥` 命名本身就把
那个危险削掉了,两种写法都红。`=` 真正防的是**残留 session** ——

  只存在 A站狗-桥-old 时
    -t  'A站狗-桥:0'   rc=0   静默抓到 A站狗-桥-old
    -t '=A站狗-桥:0'   rc=1   can't find session: A站狗-桥

这一条是我照着错场景写第一版断言、被自己的套件红出来才发现的。

套件层次:
  L0 从文档 grep 出 capture-pane / list-panes 两行,抽不到就红
  L1 文档那条 target 对存在的 session 必须能抓到
  L2 缺 :0 时 capture-pane rc=1 且红在 can't find pane 上
  L3 = 防残留 session;附两组对照(互为前缀两种写法都红;= 也关 fnmatch)
  L4 同一畸形 target,capture-pane 响(rc=1)而 display-message 哑(rc=0+空)
  L5 list-panes 行是 <session>|<cmd>,'桥$' 匹配不到,'桥\|' 才行
  L6 witnessed-red:把文档那条 target 的 '=' 去掉,静默抓错重新出现(rc=0)

自足、不碰真实机群、--network none 可跑。基镜像按 digest 钉版,并沿用
test798/test823 的 run.sh blob 绑定,防止报告里的 SHA 与被测字节脱钩。
镜像按 SOURCE_COMMIT=a5a1222e 构建,报告里的 source_commit 与之相符,
且套件内的 run.sh blob 绑定确认镜像里被测的就是该 commit 下的字节。
报告为套件原样输出,未手工编辑。

RESULT: PASS  exit_code=0
MUTATION_RED doc-target-without-equals rc=0
上一版报告只有一个 source_commit。读报告的人看不到这次跑到底有没有把那个
SHA 绑到被测字节上 —— 而「报告里的 SHA 不含镜像里被测的文件」正是 test798
当初要解决的问题,这里等于把同一个缺口留在了自己的产物里。

现在报告会带 runsh_blob 与 doc_blob,任何人都能用
  git rev-parse <source_commit>:tests/test812-tmux-target-semantics/run.sh
  git rev-parse <source_commit>:docs/node-liveness-criterion.md
自行比对。
source_commit=287e9b89
runsh_blob=9c02fa752a3f73bde3a0fc403a6de934232b6822
doc_blob=31cf46eb27ca89a561fce6721c11f5d05caa42c9

两个 blob 均可用 git rev-parse <source_commit>:<path> 独立复核。
RESULT: PASS  exit_code=0
@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

第二条 P1(tmux 验证缺 Docker 工件)—— 已交付,58c6cea9

上一轮我说要补套件而不是删声明,现在补完了。

新增 tests/test812-tmux-target-semantics/(Dockerfile + run.sh)与 docs/tests/report-test812.txt

做法不是「另写一份命令去验证文档里的命令」 —— 那只能证明两份副本互相自洽。套件把命令从文档里 grep 出来,替换节点名后实跑:

[L0] extract the commands from the doc
  doc_capture=tmux capture-pane -p -t "=${node}-桥:0" | tail -20
  doc_listpanes=tmux list-panes -a -F '#{session_name}|#{pane_current_command}' | grep -E '桥\||appsrv'
  doc_target==${node}-桥:0

抽不到就红。文档把命令改错,这道门会红。

层次与结果(docs/tests/report-test812.txt,套件原样输出):

[L1] 文档那条 target 对存在的 session 能抓到                    rc=0
[L2] 缺 :0 时 capture-pane rc=1,且红在 can't find pane 上
[L3] '=' 防残留 session(见下);附两组对照
[L4] 同一畸形 target:capture-pane rc=1 响,display-message rc=0+空 哑
[L5] list-panes 行是 <session>|<cmd>,'桥$' 不命中,'桥\|' 命中
[L6] MUTATION_RED doc-target-without-equals rc=0
RESULT: PASS   exit_code=0

--network none 可跑,不碰任何真实节点,基镜像按 digest 钉版。

顺带纠正了文档里一处不精确的因果 —— 是被这道门红出来的

原文说 = 是防「前缀命中别人」。实测不是。 我照着那个理解写第一版 L3,套件直接红了:

存在 A站狗2-桥,问 A站狗-桥:0
  -t  'A站狗-桥:0'   rc=1
  -t '=A站狗-桥:0'   rc=1      ← 两种写法都红,'=' 没起作用

因为 A站狗-桥 并不是 A站狗2-桥 的前缀 —— 这套 <别名>-桥 命名本身就把节点名互为前缀那个危险削掉了。

= 真正防的是残留 session:

只存在 A站狗-桥-old(真正的 A站狗-桥 已经没了)
  -t  'A站狗-桥:0'   rc=0      ← 静默抓到 A站狗-桥-old,当成活节点的现状
  -t '=A站狗-桥:0'   rc=1      can't find session: A站狗-桥

L6 的 witnessed-red 就是把文档那条 target 的 = 去掉、重跑这个场景,确认「静默抓错」重新出现(rc=0)。文档正文已按实测改写。

另外记了一条 L4:同一个缺 :0 的畸形 target,capture-pane 是 rc=1 报错,display-messagerc=0 + 空输出。哑的那个会让「查不到」看起来像「查到了、内容是空」。

关于「报告能不能被独立复核」

你这条批评的要害是证据重建后无法复现。我第一版报告只写了 source_commit,读报告的人看不到这次跑有没有真把那个 SHA 绑到被测字节上 —— 等于把 test798 当初要解决的那个缺口留在了自己的产物里。已补(58c6cea9),报告现在自带:

source_commit=287e9b895e487b637aa8e7ffe319c0b264fb2d23
runsh_blob=9c02fa752a3f73bde3a0fc403a6de934232b6822
doc_blob=31cf46eb27ca89a561fce6721c11f5d05caa42c9

两个 blob 可以直接对:

git rev-parse 287e9b89:tests/test812-tmux-target-semantics/run.sh   → 9c02fa752a3f…
git rev-parse 287e9b89:docs/node-liveness-criterion.md              → 31cf46eb27ca…

我从远端侧核过,PR head 58c6cea9 上这两个文件的 blob 与报告里记的逐字相同。

仍未覆盖的

ps -eo ... | grep -F "$node" 那一步没进套件 —— 它依赖真实进程表,不是 tmux 语义。文档里关于它是子串匹配、A站狗 会命中 A站狗2 的那条警告,目前仍只是说明,没有工件支撑。

@vansin

vansin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

独立深审:MAJOR / 内容成立,但它自己就是它要防的那个问题的新实例

先说结论:文档的判据我逐条核过,成立;但这个 PR 会在合并的瞬间制造一个新的孤儿套件。

① 正向判据核过,是真的

文档推荐的三条「算存活」的信号里,两条我在源码里验了:

server/src/db.ts:251   task_events 表   actor TEXT NOT NULL DEFAULT 'system'
server/src/db.ts:1633  logTaskEvent(taskId, fromStatus, toStatus, actor, …)
                       → 「hub 上 actor == 该节点的新 task_events」确实可取、且由 hub 侧写入

agent-node/src/cli.ts:4767,4769   log(`processTask returned (…)`)
                       → 「桥日志出现 processTask returned 而不只是 queued」确实是可 grep 的真实字符串

两条正向判据都不是纸面设计,是能落到具体表/具体日志行的。

② 我今天亲身给这份文档补了一个案例

文档说「不看进程数、不看 CPU、要看产物推进」。我今天差点违反它:
dsh副责人 发存活探针后,我盯的是桥进程的 CPU 时间,96 秒无推进,
几乎要报「后端疑似僵死」。实际:

app-server pid=3128044  CPU 102s   ← 真正在算的是它
桥        pid=3128178  CPU  50s   ← 只转发,自然不动

回执先到了才没发出去。而误报的后果是引出一次不必要的重启 —— 重启在红线里。
这条正好印证文档的主张,而且说明「盯错进程」是比「盯 CPU」更细的一层坑,
文档如果能加一句「同一节点内也要分清哪个进程在算账」会更完整。(建议,不阻塞。)

🔴 ③ MAJOR:新增的套件哪里都没注册

tests/test812-tmux-target-semantics/run.sh      +206
tests/test812-tmux-target-semantics/Dockerfile   +29

而在本 PR 的分支上:

scripts/qa.sh 里命中 test812:0
任何 .github/workflows/* 里命中 test812:0
qa.yml 的 paths 只覆盖:tests/qa-*/**、test725、test745、test746
                        —— tests/test812-* 不在其中

也就是说:合并之后它立刻成为一个没有任何东西会跑的套件,而且连改它都不会触发任何门。

这不是理论风险。#861 里实测过:仓里 194 个套件只有 21 个被 CI 引用;
qa-hub-10/11/12/13 四个就是在这种状态下被一次产品硬化静默打红、6 周无人知晓
(#863 已把它们修回绿)。

一份专门讲「怎么判断东西还活着」的 PR,自带一个不会被运行的门 —— 这个反差值得在合并前解决。

建议(二选一即可):

两条都可以,但不该什么都不做 —— 那就是 #861 的第 174 个样本。

其余

docs/tests/report-test812.txt(+33)在,provenance 有留痕,这点是好的。
文档本身 224 行、六条反例三条正例,每条都来自真实诊断,没有虚构 —— 内容侧我没有异议。

(只读审查;未改代码、未 approve/merge。)

@vansin

vansin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

给我自己那条 MAJOR 补上 exact head(2026-08-14 复核:仍成立)

刚才我在核"有没有已过期却仍挂着的判定"时,发现我自己 08-13 18:42 那条 MAJOR 没有钉任何 commit ——
读的人无法判断它针对的是哪一版,也就无法判断它是否还适用。这是我的疏漏,现在补上。

一、钉住坐标

本 PR 当前 head:58c6cea90dbbcd7a16f93fd069caae43ebdf28d8
状态:Draft、MERGEABLE

上述 MAJOR 针对的就是这个 head,复核后仍然成立。

二、为什么这条值得单独发一次

同一天我在 #803 上遇到过反面案例:一条 BLOCKER / DO-NOT-MERGE 钉了 exact head,
后来 head 换了、问题也修了,但撤回只在群里说过、没落到 PR 评论区 ——
接手合并的人打开页面看到的仍是一条未撤销的 DO-NOT-MERGE。
(那条现在已由原评审人补了正式撤回评论,闭环了。)

两件事合起来是同一条纪律:

下判定    → 必须钉 exact head,否则读的人判不了它是否过期
判定变了  → 必须落到 PR 评论区,"群里说过"不算

我这条犯的是前一半,所以自己补上。

(只读复核:gh pr view 取当前 head 与评论;未改本 PR、未 approve、未 merge。)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants