fix(pi-bridge): 决策到了没唤醒等待者 —— 整条授权链断掉(用户报「授权机制有问题」)

现场(用户:「我发现授权机制有问题,你看看 webui4frpc 的那个 session」):
同一条会话一天被问 6 次「是否允许执行 bash?」,**每次人都在 8 秒内点了同意**,
而每一轮都恰好烧满 10 分钟(TURN_TIMEOUT_MS),回信只有一句 59 字的开场白。
该 agent 自己的会话转录里写着:**"The bash tool keeps returning 'No result provided'"**。

根因:`worker.mjs` 的 `permission_decision` 分支取出了等待者、删了表项、存了备注,
**却没有调用 `resolve`**:

    const resolve = pending.get(msg.relayKey);
    if (!resolve) return;
    pending.delete(msg.relayKey);
    decidedExtra.set(msg.relayKey, { ... });
    return;                       // ← 等的人永远醒不过来

于是一条命令走完下面这一整圈:
  ① 工具调用挂着不动 → 一轮跑到 10 分钟 TURN_TIMEOUT_MS 才结束;
  ② 桥把模型那半句开场白当「本轮总结」发回(59 字);
  ③ 会话里留下**没有 toolResult 的 toolCall** → 下一轮 pi SDK 给它补一条
     `isError: true` 的「No result provided」→ 模型重试 bash → 人又被问一遍。

为什么之前全绿:`permission-note.test.mjs` 的 WIRING 钉的是
`decidedExtra.set(msg.relayKey)`(**备注=装饰**)与 `renderDecisionReason`,
**没有一条钉"唤醒"**。2026-09-13 那次修备注时把唤醒弄丢,判据照样全绿
—— 钉装饰不钉机制。

改:
- `resolve(msg.decision)` 补回,放在 `decidedExtra.set` **之后**(hook 醒来要读备注渲染
  拒绝理由,顺序反了会复现 2026-09-13 的「备注丢失 → 模型重复追问」)
- 判据:WIRING 补一条「必须唤醒」;另加 `checkDecisionBranch` **按标记切出决策分支正文**
  判"存在 + 归属 + 顺序"(不用"相距 N 字符"的窗口断言 —— 第一版就是那样假红的)
- 三种反面写法做变异自检:删掉 resolve / resolve 早于备注 / resolve 挪出分支,都必须红
- 全套 445 条通过

跨端核对:dsh 桥的 `pendingApprovals` 有 `pending.resolve(outcome)`(没这个问题);
opencode 走原生 permission 回复、zcode 走事件钩子 —— 这条路径只有 pi 桥有。
This commit is contained in:
pi
2026-09-14 19:16:22 +08:00
parent 5544edabaf
commit f0884d03be
2 changed files with 88 additions and 1 deletions

View File

@ -696,6 +696,30 @@ process.on('message', (msg) => {
note: typeof msg.note === 'string' ? msg.note : '',
waiting: Array.isArray(msg.freshMails) ? msg.freshMails : [],
});
/*
* ★★ 必须**最后**唤醒等待者,而且**必须唤醒**——这一行曾经不存在。
*
* 顺序:上面的 `decidedExtra.set` 要先落地。hook 醒来后会去读它渲染拒绝理由,
* 先 resolve 就会读到空备注(2026-09-13 那次修的正是这个:人写了
* 「我说了让你拉取仓库到 program 下你听不懂吗」,模型只看到「拒绝」,
* 转头把同一条命令又问了一遍,连问 9 次)。
*
* 但那次修**只补了备注,把唤醒弄丢了**:`const decision = await new Promise(...)`
* (文件上方的 tool_call 钩子)永远等不到 resolve ⇒ 后果不是"备注少了",
* 而是**整条授权链断掉**:
* ① 工具调用挂着不动 → 一轮跑到 10 分钟 `TURN_TIMEOUT_MS` 才结束;
* ② 桥把模型那半句开场白当"本轮总结"发回去(生产实测 59 字);
* ③ 会话里留下一个**没有 toolResult 的 toolCall** → 下一轮 pi SDK 给它补一条
* `isError: true` 的「No result provided」→ 模型重试 bash → 人又被问一遍。
* 实测(webui4frpc 那条会话,2026-09-14):同一条会话一天被问了 6 次 bash,
* 每一次都是人 8 秒内点了「同意」,而每一轮都恰好烧满 10 分钟。
*
* 判据侧当时为什么没红:`permission-note.test.mjs` 的 WIRING 只钉了
* `decidedExtra.set(...)`(装饰)与 `renderDecisionReason(...)`,
* **没有一条钉"唤醒"**。现在补上了。
*/
resolve(msg.decision);
return;
}
if (msg?.type === 'shutdown') {