为什么超时要贯穿到底层执行器
超时如果只停在助理表层,底层命令、子进程和桌面动作仍可能继续跑。本文解释为什么执行型 AI 助理必须把 timeout 一路传到底层执行器,并用优雅停机替代简单粗暴的直接杀掉。
先说结论:对执行型 AI 助理来说,超时不是“前端等多久”的 UI 参数,而是一条必须贯穿到最底层执行器的约束。 如果只有助理表面停止等待,而底层 shell 命令、子进程、浏览器自动化或桌面动作还在继续,系统就会出现最糟糕的状态:用户以为任务已经停了,机器却还在改文件、占资源、写日志,甚至继续朝错误方向推进。
这也是为什么 OmniGoAI 的 GoWork 这类常驻助理,不能把超时简单理解成“到点就杀主进程”。真正可靠的做法是:上层声明超时,执行链路逐层感知,先尝试优雅停机,必要时再升级为强制终止。 只有这样,任务状态、资源释放和用户预期才会重新对齐。
如果你看过 为什么长任务要定期回报进度心跳 和 为什么“下一步我会去做”不等于任务完成,这篇文章可以看作更底层的一环:为什么助理系统不能只会启动任务,还必须会把任务停干净。
为什么“表层超时”会制造假完成
超时停在助理层,最容易出现状态错位
很多系统表面上有 timeout,但它实际只约束了“当前这一轮等待多久”。一旦时间到了,界面显示超时、对话里回一句“任务已停止”,看上去像是结束了;可底下的命令、脚本、浏览器或桌面自动化还在继续。
这会带来三个直接问题:
- 用户状态错觉:用户以为任务没在跑,实际上副作用还在发生。
- 资源不释放:端口、文件句柄、浏览器窗口、桌面控制权可能继续被占用。
- 后续任务互相污染:下一轮任务启动时,会撞上上一轮残留的进程、日志或锁文件。
对一个只会回答问题的聊天机器人,这种问题还没那么致命;但对一个会读写文件、跑命令、调度子任务、操作桌面的助理,状态错位本身就是事故。
“已经超时”不等于“已经停下”
这是执行系统里最容易被忽视的一点。超时只是上层做出的一个判断,不是底层已经完成的事实。
例如一个构建命令超时,可能只是宿主不再等待 stdout;一个网页自动化步骤超时,可能只是当前调用栈返回了,但浏览器实例还在;一个后台任务超时,可能只是 supervisor 不再阻塞这轮对话,而不是子进程已经结束。
如果系统把“等待超时”和“执行停止”混成一回事,后面所有状态判断都会偏:你会误以为清理完成、资源已释放、结果不会再变化,实际上这些都还没发生。
为什么超时必须一路传到底层执行器
每一层都必须知道“剩余预算”是多少
可靠的超时控制,不是只在入口传一个固定数字,而是让执行链路上的每一层都知道:
- 总预算是多少;
- 现在已经用了多久;
- 自己还剩多少可用时间;
- 到时后该进入哪一种停机路径。
这就是“超时传播”的核心。不是 supervisor 自己记一个倒计时,而是 shell、脚本、子任务、网络请求、桌面控制都能感知同一个 deadline。
这样做有两个好处:
- 下层可以提前收尾。例如命令知道只剩 10 秒,就不再开启下一批下载,而是先 flush 日志、写检查点。
- 上层不用盲猜。不是超时后才去猜“它会不会自己停”,而是所有层都按同一个截止时间行动。
不传播超时,下层就会按自己的节奏继续跑
底层执行器天然只理解自己收到的控制信号。没有 deadline、取消信号或 stop token,它就会认为自己应该继续。
比如:
- shell 进程会继续等待子进程退出;
- 子进程会继续处理队列里的剩余项;
- 桌面自动化会继续尝试点击或等待窗口;
- 后台服务会继续监听端口,直到被显式关闭。
换句话说,如果超时没有被传播,下层不是“故意不配合”,而是压根不知道现在该停。 很多所谓“超时失效”,本质上不是 timer 坏了,而是 timer 只存在于最外面那层。
为什么优雅停机比“直接杀掉”更可靠
直接杀掉很快,但经常把系统留在脏状态
很多人遇到超时的第一反应是 kill。它当然快,但快不等于可靠。直接强杀通常会留下这些后果:
- 临时文件还没清;
- 日志最后几行没 flush;
- 锁文件没释放;
- 浏览器/驱动残留进程还在;
- 任务历史里没有明确的“我是如何结束的”记录。
对一次性脚本来说,这可能只是“不够漂亮”;对常驻助理来说,这会直接影响下一轮执行。你今天强杀留下的脏状态,明天就会变成“为什么这个任务莫名其妙失败”的排查成本。
优雅停机的目标,是让副作用在可控边界内结束
优雅停机不是“慢一点退出”,而是按顺序完成下面几件事:
- 停止接收新工作:不要再启动新命令、新点击、新网络请求;
- 让当前最小原子步骤收尾:例如写完当前文件、结束当前 API 请求;
- 记录可恢复状态:写 checkpoint、run summary 或失败原因;
- 释放资源:关闭窗口、停止子进程、断开占用;
- 把最终状态回写给上层:让 supervisor 知道这是 cancelled、timedOut 还是 partially completed。
这样即使任务没做完,系统也能保持可恢复、可解释、可继续,而不是突然断电式消失。
正确姿势通常是“先优雅,后强制”
最稳的策略不是二选一,而是分级:
- 先发送取消信号;
- 给底层一个很短的 grace period 去收尾;
- 若仍未退出,再升级为 kill process tree;
- 强杀后明确记录“本次非优雅结束”。
这样既不把系统无限卡住,也不一上来就把现场打烂。对 GoWork 这类会持续执行很多轮任务的系统,这种分级终止比“超时即秒杀”稳得多。
在 AI 助理系统里,哪些地方最需要 timeout propagation
1. Shell 命令与子进程树
这是最常见的坑。很多宿主只停止等待顶层命令,却没有清理它拉起的整个进程树。结果就是父进程没了,子进程还在跑,端口还占着,日志还在写。
因此,超时控制至少要覆盖两件事:
- 前台命令的等待超时;
- 后台和子进程树的真正终止。
这也是为什么执行型助理不能只说“命令超时了”,还要继续验证:进程是否还在、端口是否还占用、窗口是否还存在。
2. 桌面自动化与浏览器控制
桌面动作不是纯计算任务,它会占用真实的鼠标、键盘、窗口焦点和用户屏幕。这里的超时如果不传播,风险比普通命令更高。
因为一旦上层超时了,但底下还在等窗口、找控件、尝试点击,用户就会看到“明明说停了,鼠标还在自己动”。这对信任是致命打击。
所以桌面执行器必须在收到取消后立刻停止排队新动作,并尽快释放独占资源。否则后续任务即使逻辑上无关,也会被上一轮残留动作拖住。
3. 长链路任务与多阶段 supervisor
多阶段任务最容易在 phase 边界上出现“半停半跑”。例如 supervisor 觉得这轮该收尾了,但下游的构建、发布、回查还各自悬着一个执行上下文。
这类场景最需要:
- 统一 deadline;
- 统一取消信号;
- 每阶段都能写 checkpoint;
- 下一轮只依赖已验证完成的产物。
如果没有这套机制,续跑时你根本不知道:哪些步骤是真的完成了,哪些只是“看起来像结束了”。
GoWork 这类常驻助理为什么更需要优雅停机
因为它不是一次性脚本,而是长期协作系统
一次性脚本跑完就丢,脏一点也许还能忍;常驻助理不行。它今天留下的残留,会直接进入明天的上下文。它不仅要完成任务,还要维护一个长期可执行的环境。
这也是为什么在 GoWork 这类系统里,超时设计和 为什么多步任务不能只在当轮写计划 是连在一起的:你要能继续,就先得停干净。否则所谓续跑,只是把未清理的旧状态继续往后拖。
因为用户在意的不只是“停没停”,还有“系统现在是不是可信”
用户并不会用“timeout propagation”这个术语描述问题。他们真正感受到的是:
- 我都说停了,怎么还在动?
- 这个任务到底结束了没?
- 下一轮会不会被上一轮污染?
- 你现在说的状态,我还能信吗?
所以超时传播和优雅停机,本质上不是底层工程洁癖,而是用户信任问题。一个停不干净的助理,即使能力再强,也会越来越难让人放心托付任务。
实践上应该怎么设计
如果你在做执行型 AI 助理,至少要满足下面这份 checklist:
- 入口层定义总 deadline,而不是只有一个局部 timeout 数字;
- 每一层执行器都接收取消信号或剩余时间预算;
- 到时先停接新工作,再做短暂优雅收尾;
- 必要时升级为强制终止整个进程树,而不是只停父进程;
- 把终止方式写入运行记录:正常完成、用户取消、超时优雅结束、超时强制结束;
- 续跑时只依赖已验证的产物,不依赖“可能已经停掉”的假设。
这几条看起来偏底层,但它们直接决定了你的助理是“能跑”,还是“能长期稳定地跑”。
你可以在 GoWork 下载页 继续了解这种面向执行的助理设计;如果你还想把“停得干净”和“状态讲得清楚”连起来看,也建议接着读 为什么任务状态不能只看会话摘要 与 为什么失败后别重来,要从上次进度继续。
常见问题
FAQ 1:给每个命令单独设 timeout,不就够了吗?
不够。单个命令 timeout 只能约束那一个调用点,约束不了它拉起的子进程、旁路线程、后台服务和后续阶段。真正可靠的是把 deadline 与取消信号贯穿整条执行链,而不是在几个局部位置各自设表。
FAQ 2:优雅停机会不会拖慢系统?
会增加一个很短的收尾窗口,但通常远小于事后清理残留的成本。对常驻助理来说,几十秒的优雅收尾,往往比下一轮排查半小时残留问题便宜得多。
FAQ 3:什么时候该直接 kill?
当取消信号已经发出、grace period 已经过了、底层仍不退出,或者继续运行会造成更大副作用时,就应该升级为强制终止。关键不是“永不 kill”,而是不要把 kill 当成唯一策略。
FAQ 4:这和用户看到的任务状态有什么关系?
关系很大。只有底层真正停下并把结束方式回写,上层才能诚实地说“已取消”“已超时结束”或“已部分完成”。否则任务状态只是表面文案,不是可验证事实。