推出 11 小时就撤回:审批弹窗为什么必须保留超时

如果你昨天读到「审批弹窗会一直等你回答」,今天再试可能会发现它又变回去了——弹窗照旧开始倒计时,时间一到就拒绝。这看起来像官方反复无常,其实是一次被迅速纠正的设计越界:改动本身解决了一个真问题,却在一个没有答案的地方造成了死锁,合并 11 小时 30 分钟后被整段回滚。
这篇不站队,只把时间线、原因和现在真正生效的行为讲清楚——因为这直接决定你该怎么配 approvals。
现在到底是什么行为
先把结论钉住,因为这决定了你要不要动配置。今天是 2026 年 10 月 3 日,approvals 的行为是:
| 界面 | 提示能活多久 |
|---|---|
| 交互式命令行、TUI、桌面 App | 倒计时到 approvals.timeout(默认 300 秒),到时按「未批准」处理,命令被拦下 |
| Telegram / Discord / Slack 等消息平台 | 同样走 approvals.timeout,推送通知可能几分钟后才被看到 |
clarify 提问(agent.clarify_timeout) |
默认 3600 秒(1 小时),超时后解锁并回一句「用户未在 X 分钟内回应」 |
关键约定没变,而且很重要:超时等于没批准。它不是「等不到人就放行」,而是「等不到人就不执行」——官方在取消提示的文案里把它写成一句话:沉默不是同意。也就是说,超时是安全默认,代价是你可能莫名其妙丢一次操作。
被撤回的那个改动做了什么
10 月 1 日下午 16:50(CST),PR #130167 合并了。它的主张是「按人在不在屏幕前面分家」:
命令行、TUI 和桌面 App 里的审批和 clarify 提问不再设截止时间,会一直挂着,直到你回答、打断这一轮、或者关掉会话;消息平台、编辑器集成和审批传输插件继续保留 approvals.timeout,因为推送通知本来就可能躺在手机里没人看。
实现方式是一组按轮次绑定的上下文开关:set_prompts_wait_for_answer() / reset_prompts_wait_for_answer() 在每个会话轮次开始时绑定,唯一的时间读取入口 approval_wait_seconds() 在开关生效时返回「平台安全上限」,否则返回 approvals.timeout。命令行、TUI 网关的审批轮询、以及人工等待上限都改成读这个统一入口。
从产品直觉上说,这个方向很讨人喜欢:你只是起身接了杯水,回来提示还在等你,不用重来一遍。
为什么当晚就撤回
10 月 2 日凌晨 04:23(CST),PR #130826 把它整段撤回。撤回说明里给了三个理由,其中两个是硬伤。
第一,方向在合并之前就已经被否掉了。 撤回说明的第一句话很直白:「wait-forever prompts were rejected as a direction before #130167 merged」(无限等待的提示在 #130167 合并之前就已经被否决为方向)。这是流程问题,不是技术问题。
第二,它把 hermes chat -q 弄挂了。 这是最要命的一条:-q 单次查询模式是无人值守的,跑的时候没有人坐在那里回答问题。给审批提示去掉截止时间,等于让这个会话无限期地等一个永远不会来的答案——它就这么挂在那里。官方只好打一个补丁(82c63c505a9)给 chat -q 单独恢复截止时间,而这次撤回把这个补丁一起撤了,因为它存在的唯一理由就是补前一个改动的洞。
第三,端到端测试开始稳定失败。 tests/e2e/core/providers/test_native_codex_app_server_faults.py 在 main 上的每一次 e2e 运行都失败,直到上面那个补丁把其中一个面补上。
回滚的规模:撤回 11 个提交加上那个依赖补丁,涉及 57 个文件,全部回到改动之前的状态、逐字节一致(其中 5 个文件因为之后有无关提交而保留,这些提交没有被回滚)。行为层面恢复的东西包括:命令行和 TUI/桌面的审批面板重新开始倒计时并在配置的超时后拒绝、clarify 请求重新带上截止时间、Bot Mode 群聊里被卡住的轮次重新受 3 小时硬上限约束,以及文档、设置界面文案和各语言字符串。
值得一提的是这一条:那个改动从来没有进过任何正式版本。 当时最新的 tag 是 v2026.9.24(版本号 v0.21.5,9 月 24 日发布),而 #130167 是 10 月 1 日才合并的。所以受影响的只有跟踪 main 的安装,正式版用户从来没有见过那个「无限等待」的弹窗。
这件事真正的教训
把技术细节剥掉,这场 11 小时的反复留下三条对你有用的判断标准。
截止时间是安全机制,不是体验瑕疵。 审批提示的倒计时承担着一个安全职责:在没人确认的情况下,系统必须能自己停下来。去掉它,等于把「没人管」变成「无限期挂起」,而挂起对无人值守的流程来说比拒绝更糟——拒绝会让 agent 换条路走,挂起只会让队列越堆越长。想让它别那么烦人,正确的旋钮是超时时长和审批模式,而不是拆掉超时。
「有没有人能回答」必须在意图层面表达,而不是靠等待时长。 一个 -q 会话、一个 cron 任务、一个 webhook 触发,本质上都和你坐在终端前是不同的场景。Hermes 给这三种场景各留了一个显式开关:approvals.single_query_mode、approvals.cron_mode、approvals.unattended_mode,取值都是 deny 或 approve,默认全部是 deny——因为在这些场景里,立刻拦下并让 agent 换个办法,比让它干等到超时再失败要有用得多。
别指望靠调大超时来「一劳永逸」。 代码里那个上限不是随便写的:approvals.timeout 会被钳到平台安全上限(agent.deadline.MAX_SAFE_TIMEOUT_S,非 Windows 上是 365 天),因为更大的值会在 Thread.join / Lock.acquire 里溢出 time_t,实测会让 macOS 上的并行工具批次直接崩掉。另外,界面上显示的时间窗是按人类语言格式化的——300 秒会显示成「5 minutes」,90 秒显示成「90 seconds」,这样你在 CLI、工具结果和网关卡片上看到的是同一句话。
你现在该怎么做
先把当前值读出来,别猜:
hermes config get approvals.timeout
hermes config get approvals.mode
消息平台觉得 300 秒太紧,就把它调大,而不是关掉:
hermes config set approvals.timeout 1800
无人值守的场景走意图开关,让它在没人应答时立刻做决定,而不是等满一整个超时窗口:
hermes config set approvals.single_query_mode approve # 你信任 -q 场景
hermes config set approvals.unattended_mode deny # webhook / API 默认就该拦
该拦死的命令用 deny 规则——它是 fnmatch 通配符列表,命中的命令即使在 yolo 模式下也会被无条件拦下:
approvals:
deny:
- "rm -rf *"
- "DROP DATABASE *"
配置项的完整语义和 dry-run 验证方法,可以参考我们之前写的无人值守审批指南和审批规则试运行。如果你在用 smart 模式,让评审模型按你团队的习惯判罚,可以用 approvals.smart_policy 追加规则。
小结
一句话:这次撤回不是退化,而是把一个正确的问题交给了错误的机制。 审批超时的本质是安全阀,弹窗太烦人应该去调时长和模式,而不是把阀门拆掉。
顺带一句更正:昨天那篇 「审批弹窗不再自己消失了」描述的是 #130167 当时的行为,我们已经在原文里加了更新说明。官方在一天之内改变方向,是这篇文章本该预料、但没有预料到的——这也是我们为什么不把「官方刚合并」直接当成「官方最终决定」来写。