Files
MailUI4Agents/plugins/pi-mail-bridge/lib/denial-reason.js
JianFeeeee 453f451fbb fix(permission): 人类的备注必须到达模型 + 决策回执不再被当成新任务
用户报的「很严重的问题」:被拒绝的 agent 看不到授权备注,且看不到他发的回复邮件。
按数据查到了两个**真缺陷**,都在桥的权限回路上(不是猜测,三层证据)。

## 缺陷一:备注在桥内被连丢三处

网关其实一路都带着备注(`CreateDecisionMail(..., req.Note)` 把备注写进决策邮件正文,
SSE payload 里也有 `"note"`),但桥的三个环节只传 decision:
  index.mjs  `pool.routePermission(relayKey, String(data.decision))`
  pool.mjs   `child.send({type:'permission_decision', relayKey, decision})`
  worker.mjs `resolve(String(msg.decision))`
模型最终看到的只有 `用户拒绝了这次 bash 调用`(pi 会话转录逐字可查)。

现场:人类写「我说了让你拉取仓库到program下你听不懂吗」,模型不知道要改什么,
把同一条命令换个写法又问了 —— 会话里连问 **9 次**(22:16–22:26)。

## 缺陷二:决策回执照样被当"新任务"投递 + 等人的邮件被堵在后面

决策是**双通道**送达:SSE `permission_decision`(唤醒停放的 worker)+ 一封普通形状的
邮件("Re: 权限请求 - 拒绝")。以前两条都会起动作 ⇒ 同一件事被处理两次;而这条会话
的新邮件在 worker 停放期间只能排队。实测:人类 22:18:08 发出的更正
「不对,不是让你拉取到agentmail仓库,是让你拉取到program仓库!!」
直到 22:26:30(worker 回合结束)才被模型看到 —— **8 分钟**里它一直在错误的目录上打转。
转录里那封更正确实是模型自己 `read_mail` 读到的(不是没人给它)。

## 改动

- 网关:`CreateDecisionMail` 写 `mail_type='permission_decision'` —— 桥据此区分
  「控制面回执」与「新任务」。
- pi 桥(新增 `lib/denial-reason.js`、`lib/waiting-mails.js`):
  · 备注随决策一路透传到**模型看到的拒绝理由**(工具拦截与通知投递两条路都带);
  · 恢复停放的 worker 时,顺带把「等人期间新到、尚未标记已读」的邮件附进理由,
    模型当场就能改道(这正是那 8 分钟的洞);
  · 决策回执不再起新任务轮次(记进 deliveredMails);若决策事件尚未到达,
    退化为 B-4.3 的通知投递,且没有会话时不凭空新开。

## 判据

- `test/permission-note.test.mjs`:11 条(备注进理由、无备注不得凭空造说明、
  等人期间的邮件要点名 read_inbox、只挑本会话非权限类未交付的、上限、旧回包缺
  session_id 不能丢邮件、接线 8 处形状、判据自检)。
- **扰动验证**:把备注从 `pool.mjs` 的 send 里去掉 → 接线判据 2 条红;恢复 → 11 绿。
- 既有 pi 套件 420/420;server 10 包全绿(新增 1 条 Go 判据验决策邮件的类型与备注正文)。

## 现场证据(可复核)

- 桥日志:9 次 `权限 <key> 决策 同意/拒绝(决策人 jianf)已转交 worker`,全程不含备注;
  「worker 2135211 等待权限决策,让出并发额度(停放 1/5)」
- 会话转录:`{"toolName":"bash","content":[{"text":"用户拒绝了这次 bash 调用"}]}` ×6
2026-09-13 22:47:25 +08:00

41 lines
1.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 把「人类对这次权限请求的结论」渲染成模型能读懂的一段话。
*
* # 为什么单独一个模块
*
* 三条路径都要给模型同一句话工具拦截bash/write…、无挂起 worker 时的通知投递、
* 以及主动询问kind=question。而 2026-09-13 的线上缺陷正是这句话里**没有人类的备注**
*
* - 用户原话:「被拒绝的 agent 好像看不到我给授权的备注」
* - 现场证据pi 会话转录):模型收到的工具结果只有
* `用户拒绝了这次 bash 调用`;而人类那封决策邮件里写着
* `拒绝\n\n备注: 我说了让你拉取仓库到program下你听不懂吗`
* - 后果:模型不知道要改什么,把同一条命令换个写法又问了一遍(实测连问 9 次)。
*
* 备注在 `index.mjs → pool.mjs → worker.mjs` 三处被逐个丢掉,所以判据必须落在
* **模型最终看到的那段文本**上(这个函数的返回值),而不是"某个字段有没有传"。
*/
/** 模型最终看到的那句"被拒了",附上人类说明与等人期间新到的邮件。 */
export function renderDecisionReason({ toolName, decision, note = '', waiting = [] }) {
const base = decision === 'shutdown'
? '用户未及决策(桥已关停)'
: `用户拒绝了这次 ${toolName} 调用`;
const parts = [base];
const text = String(note ?? '').trim();
if (text) parts.push(`用户的说明:${text}`);
const fresh = Array.isArray(waiting) ? waiting.filter(Boolean) : [];
if (fresh.length) {
parts.push([
`【等人审批期间新到的邮件 ${fresh.length} 封,尚未标记已读】`,
...fresh,
'请先用 read_inbox 读这些邮件,再决定下一步 —— 里面可能是对当前做法的更正。',
].join('\n'));
}
return parts.join('\n\n');
}