fix(permission): 权限决策者必须是人类,顺会话树查找
死锁现场:pi 的权限邮件 to_name = "pi"(决策者被解析成 Agent 自己), SendToUser 找不到收件人 —— 请求发出去后无人可答,会话无声挂死。 新增 repo.NearestHumanInThread 三级查找:会话 owner → 最近人类发件人 → 最近人类收件/抄送方。发件人优先于收件人:主动说话的人更可能在关注这条线索。 不用递归 CTE 沿 parent_mail_id 上溯而按会话扫:权限请求常常不挂父邮件, 上溯会在第一跳就断。 没有人类可路由时**自动拒绝并给 Agent 可执行的替代建议**,而不是转给管理员 —— 管理员对这条 Agent 链的上下文一无所知,收到也无法判断。 409 返回结构化的 error/detail/suggestion/decider_was。 nearesthuman_test.go 4 子测试。
This commit is contained in:
@ -116,6 +116,38 @@ func RequestPermission(w http.ResponseWriter, r *http.Request) {
|
||||
decider = admin
|
||||
}
|
||||
|
||||
// 关键防线:decider 必须是人类用户。
|
||||
//
|
||||
// Agent 无法通过 Web UI 决策权限 —— SendToUser 投递到不存在的用户通道,
|
||||
// 而桥的 await Promise 永不 resolve,会话永久阻塞。这在 Agent 给自己发信时
|
||||
// 必然发生:pi 分配任务给自己的另一个会话 → 该会话触发权限询问 → 邮件发给 pi
|
||||
// → pi 不是人类用户 → 整条会话卡死。
|
||||
//
|
||||
// 修复:沿会话树上溯找最近的人类节点 —— 权限应追溯到最初分配任务的人。
|
||||
if isHuman, _ := repo.IsHumanUser(r.Context(), decider); !isHuman {
|
||||
human, err := repo.NearestHumanInThread(r.Context(), sessionID, decider)
|
||||
if err == nil && human != "" {
|
||||
decider = human
|
||||
} else {
|
||||
// 整条任务链上没有人类:Agent → Agent → Agent,中间没有任何人介入。
|
||||
// 此时把权限请求转给管理员毫无意义 —— 管理员对这条 Agent 链的上下文一无所知,
|
||||
// 既不知道这个 bash 命令在做什么,也不知道拒绝后 Agent 该怎么绕过去。
|
||||
//
|
||||
// 正确做法:直接拒绝,让 Agent 收到明确的错误信息,由它自己决定下一步:
|
||||
// 换用不需要权限的方式(subprocess、文件操作等),或在邮件里说明情况让上游转给人类。
|
||||
if relayKey != "" {
|
||||
_ = repo.ReleaseRelay(r.Context(), agentName, relayKey)
|
||||
}
|
||||
JSON(w, http.StatusConflict, map[string]interface{}{
|
||||
"error": "权限询问无法送达:该任务链上没有人类用户",
|
||||
"detail": "整条任务都是 Agent 之间的邮件往来,没有人类参与决策。请换用不需要权限的方式完成此操作,或在回复中说明情况让上游转达给人类。",
|
||||
"suggestion": "考虑用 subprocess/file 工具替代需要权限的工具,或通过邮件向上游请求人类协助。",
|
||||
"decider_was": decider,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
body := req.Context
|
||||
if body == "" {
|
||||
body = req.Question
|
||||
|
||||
Reference in New Issue
Block a user