终端窗口里停着一行等待输入的提示,旁边两只小表针分别标着三分钟和十五分钟,窗口下方是一条走平的曲线

我们做无人值守任务时,最常见的告警配置是超时:任务跑过 X 分钟就报。这个规则对付不了凌晨那种情况——进程还在,不报错,输出也不再动,值守的人第二天早上才发现它其实三小时前就停住了。有一款开源终端监管工具(项目名 Polter,用于管理无人值守的 AI 编码代理)给出的做法很少见:它不设一条万能超时,而是用两个阈值加一种读屏方式来判断状态。这篇只讲这件事——用什么信号判定”卡住”,以及为什么读终端缓冲区比截图识别更适合做判定。至于大规模编排的断点续跑与失败隔离,是另一篇的内容。

还要先声明一点:据该项目文档,某编程智能体即使加上自动权限模式的参数,仍会停在”是否继续(y/n)”这类确认提示以及依赖安装确认上,此时进程零 token 消耗、不报错、可以整夜挂着。我们把它当作该文档描述的一种现象来处理,不当作对任何具体产品的结论,也不给识别准确率——那份文档没有给,我们也没有实测数可以补。

一、”卡住”要先拆成四种状态

一条超时规则盖不住的原因是,表面上”没动静”的四种状态,信号特征完全不同:

  • 在算。终端输出不一定在变(一次长推理可能十几秒不打印),但请求与 token 在发生。
  • 在等外部。编译、安装依赖、跑测试、等接口回包,进程在自己的轨道上,只是不快。
  • 在等人点头。有一个明确的待确认提示摆在那里,而它不会自己消失,这是最贵的一种,因为它看起来最像”在跑”。
  • 真的挂了。没有请求、没有输出、没有子进程,只有进程号还在。

判定顺序也因此不能按”多久没动静”来排,要按”哪个信号还在动”来排:先看请求与 token 有没有发生,再看工具调用的时间间隔,最后看屏幕缓冲区里有没有一句等待回答的话。三种信号合起来才能把这四态分开。

二、四类信号各自的代价

信号 采法 能区分什么 代价与坑
输出是否在变 缓冲区文本的指纹比对 有没有新内容产生 长计算与流式停顿都会误判为”没变”
token 与请求是否发生 客户端侧计数或代理侧统计 在算还是停了 只在自己发起的调用里可见,子进程内部看不见
工具调用间隔 每次工具调用的时间戳序列 是在调工具还是在空跑 需要在调用点埋记,跨进程时容易漏
是否存在待确认提示 缓冲区文本按模式匹配 在等人还是在干活 提示文案变体多,规则要能维护

前两条是很多平台已经有的,第三条常被省略,而恰恰是第三条让判定可用:据该项目文档,两个默认阈值各管一件事——屏幕无变化持续 3 分钟触发一次巡检,工具调用间隔 15 分钟则用来区分”正在调工具”和”只在跑后台进程”。

三、为什么必须是两个阈值而不是一个

单看”屏幕 3 分钟没变”会大量误判。真实项目里,一次全量编译、一次数据库迁移、一轮集成测试都可以轻松超过 3 分钟不打印任何东西,把它们判成阻塞,就会在正常负载上刷出一堆假告警,最后大家把告警关掉——这是这类系统最典型的死法。

两条信号合起来才成立:屏幕无变化到点只是”该巡检一次”,巡检要看的是这 3 分钟里有没有发生新的工具调用、间隔是否超过 15 分钟、缓冲区末尾是不是停在一个提问句式上。三项都指空,才转成阻塞。这样长编译会被”有工具调用在跑”解释掉,而等待确认会被”末尾提问”精确抓住。

阈值定完之后要做的不是调数,是回归:把典型场景各录一遍(长编译、长测试、依赖安装、等待确认、真挂死),检查每条场景在这两个阈值下会被判成什么状态。这份记录我没有替任何项目跳过——它比阈值本身更能说明配置是否合适。

四、读原始缓冲区,而不是截图加文字识别

该项目的读屏方式是读取终端的原始虚拟缓冲区文本,而不是截图再做文字识别。它给的理由很实在:截图识别成本高,还会误读语法与颜色——同一个字符在不同配色与字重下识别结果不同,而终端里恰恰大量出现下划线、连字符、括号这类容易粘连的符号。

从工程角度看,这个选择的关键不在准确率,而在可匹配性。判定要回答的是”末尾有没有一句等待回答的话”,这是文本模式匹配问题,不是图像问题。拿到缓冲区原文之后,可以精确匹配提示串、可以取到光标位置、可以只比对屏幕变化区域而不必重读整屏。

代价也要说清楚,实现里有三块绕不过去:转义序列要剥掉(颜色、光标移动、清行控制码都会混进文本);窗口尺寸变化会让”缓冲区”的内容重排,判定逻辑要按当前尺寸算;虚拟缓冲区与回滚缓冲区是两回事,要的是当前可见区域。输入侧同样有约定:该项目提供管道送键和特殊键两种输入方式,前者送一段文本,后者送回车一类的控制键。把读写两侧都当成接口来设计,比把它当成”模拟人看屏幕”要稳得多。

五、状态机与留证

判出来之后要落成状态。该项目的任务状态是四态:排队、进行、阻塞、完成。我们喜欢把这个结构直接搬过来,因为它给前端和值班的是四个可以分别处理的值。

状态 进入条件 允许的处置 离开条件
排队 已提交未开始执行 取消、重排 开始执行转进行
进行 有输出变化,或有请求 token 发生,或工具调用间隔未到 只观察,不打断 触发巡检且三项信号均空转阻塞
阻塞 屏幕无变化到点,且无新工具调用,且末尾存在待确认提示 分层处置:自动应答或人工告警 得到应答转进行;超过上限转失败
完成 正常结束并有产物 归档 无

留证要跟状态一起落。该项目把数据放在标准状态目录下,包含日志、聊天历史与任务板三样。我们的要求更具体一点,因为事后一定会被问到”那天夜里三点为什么没跑”:每条待确认提示要留原文(不是截图,是可复制的文本)、当时的上下文窗口内容、由谁应答(自动规则还是人)、应答之后的结果。这四样齐了才叫可复盘,缺一样就变成”值班的人记不得了”。

六、自动确认与人工告警必须分层

这一层是我最想让人抄的部分。该项目明确批评了”超时 60 秒自动点 Yes”这类做法,理由是它会把删除目录一类的破坏性命令一并执行掉;它的处理是把”已知依赖的自动确认”和”删除、覆盖类风险动作”分开,后者改为人工告警。

待确认动作 默认处置 判定依据 追加要求
已知依赖的安装确认 自动应答 依赖清单在白名单内且来源可核 记录版本与来源,装不上时转阻塞而不是重试到底
首次出现的依赖或新来源 人工告警 不在白名单 走完人工确认再决定是否入白名单
删除、覆盖、清空目录 人工告警 动词命中风险清单 禁止任何形式的自动应答
修改权限、改配置 人工告警 动词命中风险清单 要留变更前后对照
对外发送(提交、发布、调用外部接口) 人工告警 目标在设备或项目边界之外 记录目标标识与返回码

分层规则要能配置,并按项目默认收敛:新项目的默认值应该是”只自动应答白名单依赖”,而不是”尽量自动”。

同一段还要配上长跑任务自身的三重保护:墙钟上限、token 上限、无进展即停,三者取最严的一条生效。这三条不是判定卡住,而是卡住判定成立之后的止损边界。

最后是演练,三条即可:人为制造一次确认提示、一次静默挂起、一次长编译,看系统能否分别判成阻塞、失败前告警、进行,并量出从发生到告警的实际时间。洹拓在内部平台上线这类监管模块时,还会加一条组织要求——把”任务状态”当成产品字段而不是日志文本。一旦状态只写在日志里,前端只能显示”运行中”,值班只能靠人盯,判定做得再准也没人消费它。