📌 当前状态(2026-08-16 02:40 CST)—— 本条 11 条评论,先读这一段
|
|
| 严重度 |
P0。一次误触即永久停摆,而三条健康判据全绿 |
| 触发 |
在 attach 的 TUI 里在空输入框上按一次回车。不需要斜杠,不留任何日志 |
| 暴露面 |
当前 4/4 个共存节点都有活的 attach 会话,都在这个条件下 |
| 实际影响 |
通信狗 2026-08-15 21:41–23:05 停摆 1h24m,期间派过去的任务全部超时 |
| 产品内恢复 |
无。Ctrl+C 被 defer;onHumanDetach() 只管 human_editing。只能重启 PTY |
| 修复 |
已完成并验证:170 pass / 0 fail,移植到生产源码线 b830403b |
| 是否上线 |
🔴 没有。在跑的节点仍是 agent-node 2.5.0-preview.31,修复未部署 |
一句话:洞找到了、修好了、验过了、产物也构建好了(sha256 见下方评论),
但没上线 —— 上线要重启节点,那超出了我的授权范围,在等人拍板。
评论导航(按有用程度)
- 可执行证据 —— 纯 reducer 复现死锁(不依赖 fixture)
- 恢复过程的两个坑 —— defer 回放会重演死锁;注射会撞 TUI 就绪
- 部署方案 + 四条验证 + 回滚步骤
- 产物构建 + 双向比对 + sha256 —— 含"先比生产有我没有"的方法
- 触发条件更正 —— 一次裸回车就够,不需要斜杠(本条最重要)
- 爆炸半径 —— 4/4 节点暴露 + 两次数错的方法教训
- 可观测性缺口 —— 仲裁态不落盘不进日志;一次未做成的尝试(已撤回)
- 停滞检测 —— 先造接缝再装门;判定纯函数 + 5 用例 + 2 次定向变异
现象:节点静默停摆,三条判据全绿
通信狗 从 2026-08-15 21:41 起完全无法接收任务,持续一小时以上。
期间 socket 活着、子进程在、TUI ready、hub 上显示 idle、SSE 还在 22:27 正常重连并重新注册。
派过去的每一条任务都是 queued → 300 秒 → timed out。
触发序列(已在 fixture 里复现)
1. 人打 `/model ...` 回车 → 被挡(#879),代理写 \x03 清空 grok 的输入框,仲裁回 idle
2. 人以为没发出去,**又按一次回车**
3. 这次回车走 idle → human_editing → human_turn
4. 但 grok 的输入框是空的,**不会起任何回合** ⇒ `turn_ended` 永远不来
5. `human_turn` 下人类输入全部被 defer、网络任务一律不注射 ⇒ 永久死锁
复现输出:
被挡后 phase = idle
再按回车后 phase = human_turn
任务到达后 phase = human_turn 写入含 [Agent Network/ = false
结果 = TIMED-OUT
🔴 没有恢复路径
- Ctrl+C 不行:
human_turn 下所有人类输入在循环最前面就被 deferHumanInput() 拦下,
根本走不到处理 0x03 的那一段。(实测:向该节点的 attach 窗格发 Ctrl+C,任务照常超时。)
- 脱离 attach 不行:
onHumanDetach() 只在 phase === "human_editing" 时取消,
human_turn 下它只清空缓冲区。
- 只有重启 PTY 才能解开(
recoveryFrom === "human_turn" → turn_completed)。
现场证据
grok 会话 events.jsonl 最后一条 = turn_ended,时间 21:40:28
之后 grok 侧再无任何写入 ← 回合确实没起来
节点自身日志 21:41:04 最后一条 blocked,之后只有 SSE 和任务超时
attach 窗格里 composer 是空的(❯),看起来完全正常
⇒ 一个看起来完全健康的节点,输入框空着,已经死了一小时。
#879(/model 不能用)和 #881(方向键后整行发不出去)是体验损失,人能感知。
这一条是可用性彻底丧失,而且:
- 触发它的动作极其自然:被挡之后再按一次回车;
- 没有任何提示;
- 三条健康判据全绿,监控看不出来;
- 产品内无法恢复。
而且它是 #879 的直接后果 —— 正是那个拦截写的 \x03 清空了输入框,
才让下一次回车变成"空提交"。
修法
不能让「grok 不会响应的提交」占住仲裁器。审计串是我们对输入框内容的唯一认知:
当它没有可提交的内容、且没有待决的审批对话在等这次回车时,grok 不会有任何动作,
代理也就不该声称占用了一个回合。
const submitHasNothingToSend =
isSubmit
&& !this.humanPasteMode
&& !this.humanComposerAudit.trim()
&& !this.humanComposerAuditTainted
&& !this.hasUnresolvedApproval();
if (submitHasNothingToSend) {
if (this.arbitration.phase === "human_editing") {
this.transition({ type: "human_input_cancelled" });
}
this.scheduleNetworkIfIdle();
continue;
}
hasUnresolvedApproval() 那一项是必须的:权限对话框打开时输入框也是空的,
而那时的回车确实会让 grok 动作 —— 那种情况仍然要占住回合,否则会在对话进行中注射网络任务。
测试
新增红门 empty-submit-deadlock.test.ts:
Expected: "idle" ← 修之前
Received: "human_turn"
修后该门通过,并进一步断言后续网络任务能被注射(这才是这个 bug 真正的代价),
全量 78 pass / 0 fail。
关联
现象:节点静默停摆,三条判据全绿
通信狗从 2026-08-15 21:41 起完全无法接收任务,持续一小时以上。期间 socket 活着、子进程在、TUI ready、hub 上显示 idle、SSE 还在 22:27 正常重连并重新注册。
派过去的每一条任务都是
queued→ 300 秒 →timed out。触发序列(已在 fixture 里复现)
复现输出:
🔴 没有恢复路径
human_turn下所有人类输入在循环最前面就被deferHumanInput()拦下,根本走不到处理
0x03的那一段。(实测:向该节点的 attach 窗格发 Ctrl+C,任务照常超时。)onHumanDetach()只在phase === "human_editing"时取消,human_turn下它只清空缓冲区。recoveryFrom === "human_turn"→turn_completed)。现场证据
⇒ 一个看起来完全健康的节点,输入框空着,已经死了一小时。
为什么这条比 #879 / #881 更严重
#879(
/model不能用)和 #881(方向键后整行发不出去)是体验损失,人能感知。这一条是可用性彻底丧失,而且:
而且它是 #879 的直接后果 —— 正是那个拦截写的
\x03清空了输入框,才让下一次回车变成"空提交"。
修法
不能让「grok 不会响应的提交」占住仲裁器。审计串是我们对输入框内容的唯一认知:
当它没有可提交的内容、且没有待决的审批对话在等这次回车时,grok 不会有任何动作,
代理也就不该声称占用了一个回合。
hasUnresolvedApproval()那一项是必须的:权限对话框打开时输入框也是空的,而那时的回车确实会让 grok 动作 —— 那种情况仍然要占住回合,否则会在对话进行中注射网络任务。
测试
新增红门
empty-submit-deadlock.test.ts:修后该门通过,并进一步断言后续网络任务能被注射(这才是这个 bug 真正的代价),
全量
78 pass / 0 fail。关联
human_editing无超时出口(不同的态,同一类"没有兜底"的问题)