Skip to content

[grok-copresence][P0] 空 composer 上的回车永久死锁节点(通信狗实例,产品内无恢复路径) #883

Description

@vansin

📌 当前状态(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 见下方评论),
但没上线 —— 上线要重启节点,那超出了我的授权范围,在等人拍板。

评论导航(按有用程度)

  1. 可执行证据 —— 纯 reducer 复现死锁(不依赖 fixture)
  2. 恢复过程的两个坑 —— defer 回放会重演死锁;注射会撞 TUI 就绪
  3. 部署方案 + 四条验证 + 回滚步骤
  4. 产物构建 + 双向比对 + sha256 —— 含"先比生产有我没有"的方法
  5. 触发条件更正 —— 一次裸回车就够,不需要斜杠(本条最重要)
  6. 爆炸半径 —— 4/4 节点暴露 + 两次数错的方法教训
  7. 可观测性缺口 —— 仲裁态不落盘不进日志;一次未做成的尝试(已撤回)
  8. 停滞检测 —— 先造接缝再装门;判定纯函数 + 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 / #881 更严重

#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

关联

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions