审批弹窗不再自己消失了:CLI、TUI 和桌面会一直等你回答

你让 agent 清一下几个月没动的构建缓存,它弹出提示问你「这条命令要执行吗」,你顺手按了回车去接杯水。回来的时候弹窗已经没了,命令没跑,聊天里只留一行字:审批超时。你没有答错,只是墙比你先到。
更新(2026-10-03): 这个改动在合并当晚就被官方撤回了(PR #130826:无限等待的方向此前已被否决,而且它让
hermes chat -q在没有应答者时挂死)。现在 CLI / TUI / 桌面的审批和 clarify 提示重新受approvals.timeout与agent.clarify_timeout约束。完整原因和替代做法见撤回说明。
这条「墙」以前被写死成 approvals.timeout,默认 300 秒。现在它按一个很朴素的标准分了家:人在不在屏幕前面。命令行、TUI 和桌面 App 的提示会一直挂着,直到你回答、打断这一轮、或者关掉会话;消息平台继续保留超时。下面把这套新规则的来龙去脉讲清楚。
先看那道 5 分钟的墙
审批设计上有一个必须守住的约定:超时等于没批准。它不是「等不到人就算了,放手执行」,而是「等不到人就别执行」。也就是说,超时是安全的默认,但代价是你会莫名其妙地丢一次操作——而且失败在界面上看起来跟「命令执行失败」没什么区别。
为什么是 5 分钟?配置文件里的注释写得很实在:审批在消息平台上是以推送通知的形式到达的,60 秒被证明太紧——在 Telegram 和 Discord 上,提示常常在用户摸到手机之前就过期了。于是默认值被提到 300 秒。
问题在于,这 300 秒对消息平台合理,对「人已经坐在屏幕前面」的场景却完全不必要:你只是起身接了杯水,而你回来时得到的不是「还在等你」,是「你已经错过了」。
现在的规则:按界面分家
| 界面 | 提示能活多久 |
|---|---|
交互式命令行(hermes chat) |
一直等到你回答或打断这一轮 |
| TUI | 同上 |
| 桌面 App | 同上 |
| Telegram / Discord / Slack 等消息平台 | 仍然是 approvals.timeout(默认 300 秒) |
| ACP 编辑器、审批传输插件 | 仍然是 approvals.timeout |
判断依据不是「哪个界面更高端」,而是用户能不能立刻看到提示。消息平台的审批是一条推送,可能几小时没人点,而且平台上的按钮本身会过期——这类界面必须有一个截止时间。而命令行、TUI 和桌面里,提示就画在你正看着的那块屏幕上,等多久都不会「错过」。
还有一条容易被忽略的边界:等的时候什么也没在跑。协议里没变,这仍然是 fail-closed——等待期间没有任何工具被放行,你打断它就等于拒绝。
代码层面是怎么接上的
改动分散在几个地方,但核心是一个每轮绑定的开关:
| 位置 | 做了什么 |
|---|---|
tools/approval_context.py |
新增 set_prompts_wait_for_answer() / reset_prompts_wait_for_answer(),用一个 per-turn 的 context 变量记录「本轮提示无截止时间」;approval_wait_seconds() 成为唯一的解析入口 |
agent/deadline.py |
MAX_SAFE_TIMEOUT_S:非 Windows 平台取 365 天,Windows 取 DWORD 毫秒的上限(约 49.7 天)——两者在语义上都是「无上限」,只是要保证底层 Lock.acquire / Thread.join 不会算术溢出 |
tools/approval_human_wait.py |
human_wait_ceiling() 返回「审批窗口 + 60 秒余量」,让批处理截止时间和授权闸门的串行锁用同一个上限,不会各算各的 |
hermes_cli/cli_chat_turn_mixin.py |
在每一轮 run_conversation 前后绑定与解绑这个开关 |
tui_gateway/prompt_turn.py |
TUI 与桌面共用这个后端,在准备回合输入时绑定、在清理时还原 |
顺带修掉的一个隐患:批处理截止时间的循环可能在「看不到这个绑定」的上下文里读取等待上限,那样一个没有上限的提示会被当成卡死的窗口,把整批任务的截止时间一直往后拖。现在它读到的是真实上限,读到无上限的情况会被整段排除而不是被误判。消息平台那边的窗口照旧被夹在上限内,行为和以前一致。
有一个例外要记住:单次问答模式
hermes chat -q "..." 这类单次问答没有人在旁边回答,所以它不绑定这个开关,仍然走 approvals.timeout。而且它默认根本不会干等:approvals.single_query_mode 的默认值是 deny,遇到危险命令直接拒绝,让 agent 自己找别的路。
如果你在脚本或 CI 里跑单次问答,别指望「等久一点就好了」——那里从来没有人会来按那个键。
你该做的检查
- 交互式场景不用再改超时了。 如果你以前为了给自己留时间,把
approvals.timeout调到了一两千秒,现在可以调回去——它只对消息平台、ACP 和插件传输还有意义。 - 消息平台想留更多时间就单独调。
hermes config set approvals.timeout 600只影响那类界面,配置项的说明见 hermes config 命令参考。 - 别再指望「超时等于同意」。 想少被打断,用智能审批模式;想彻底放开有 yolo 档位;想永久禁止某类命令用拒绝清单。整体思路见 无人值守审批指南。
- 动手改之前先空跑一遍。 审批的 dry-run 与自检可以在不真的执行命令的前提下,看清哪条规则会命中什么判定。
- cron 和无人值守平台仍然是拒绝。 那两类场景的默认值没变,这也正是它们该有的样子。
小结
这次改动表面上是「弹窗不再消失了」,实质上是把超时这一个参数,拆成了两种场景:有人看着屏幕,就没有截止时间;没人看着,就必须有截止时间。 前者省掉了一次被自己错过的操作,后者保住了推送通知那条链路的可靠性。
判断标准简单到可以背下来:提示画在你正看着的屏幕上,它就会一直等你;提示是发到你手机上的,它就必须过期。