从终端控制序列识别智能体待审批状态
文章用 Claude Code 的真实终端捕获说明,光标移动可替代空格和换行,使简单文本匹配失效。它结合测试样本和 Python 监督程序比较三种检测方法,讨论如何识别智能体正在等待用户授权。
AI 深度解读
Claude Code 停在终端里,究竟是在跑一个安静的工具,已经完成任务,还是等你批准命令?osFoundry 给出的答案是:先还原终端屏幕,再识别权限提示;能接入 hook 或协议时,再结合明确的状态报告。
这篇最值得读的地方,是作者用真实终端捕获和模拟实验,解释了为什么「输出停了」「进程睡着了」「出现问号了」都不可靠。对开发 AI 编程面板、终端管理器的人来说,这是一个很具体的状态识别案例。
为什么清理终端输出,反而丢掉了提示
Claude Code 2.1.303 的真实捕获里,Do 和 you 之间没有空格,而是 \x1b[5G:把光标移到第 5 列,空白由屏幕上未写入的格子形成。行与行之间也没有 \n,只有 \r 和向下移动光标的 \x1b[1B。
终端收到的不是普通日志,而是一串绘制指令。
作者收集的 11 份 Claude Code 捕获,每份 2,099–7,672 字节,全部没有换行字节。直接去掉 ANSI 转义序列,会得到 Doyouwanttoproceed?;如果再按常见办法,只保留最后一个 \r 后面的内容,前面的提示也会被扔掉。
实验比较了两种输入:清理后的最后 8 KB 输出,以及还原后的 32×120 终端屏幕。前者只判断对 5/11,后者是 11/11。前者命中的五份全部是 idle,原因还是默认回退到了 idle。
判断终端当前显示什么,必须处理光标移动和擦除,不能只删除控制字符。
osFoundry 为每个会话维护一个 vt100::Parser,分类时读取 screen_text(),有意排除上方保留的 200 行历史。已经处理过的权限提示若进入历史区,继续搜索它,就可能一直报告 blocked。
作者另写了约 100 行的 Python 屏幕模拟器,处理 \r \n \b、光标移动 A B C D E F G H f d 和擦除 J K,跳过颜色、模式及 OSC 标题。它按一个字符一个格子处理,宽字符可能产生列偏差,但足以通过这 11 份样本。生产实现用现成解析器,实验实现只验证思路,两者要分清。
为什么权限规则必须先于工作规则
拿到屏幕文本后,还得识别准确的界面特征。Claude Code 对应的 claude.toml 使用下面这组规则:
[[rules]]
state = "blocked"
any = [
'Do you want to (proceed|continue|make this edit|create)',
'❯\s*1\.\s*Yes',
'Esc to cancel · Tab to amend',
'Enter to select · ↑/↓ to navigate',
'Exit plan mode\?',
]
[[rules]]
state = "working"
any = ['esc to interrupt']
[[rules]]
state = "idle"
any = ['\? for shortcuts', '(?:⏸|⏵⏵) [a-z ]+ mode on']
要看的是第一条 state = "blocked"。CompiledManifest::evaluate 按顺序返回第一个命中的规则,而权限提示打开时,底部仍可能显示 esc to interrupt。working 放在前面,就会盖住等待授权的状态。没有任何匹配时,默认是 idle。
规则也不能只找一句像提问的话。作者删除了 Would you like to:它属于 Codex 的提示措辞,Claude 的普通回答却经常包含类似的 Would you like me to…,容易误报。
osFoundry 按 SWEEP_INTERVAL 每 700 ms 扫描所有面板,没有在每次写入时分类。spinner 每秒重绘多次,逐次运行正则只会重复计算。
作者还做了一个真实 PTY 上的模拟实验,但这里没有运行真实 Claude Code:测试 Mac 未安装 claude,使用的是 fake_agent.py。它模拟 10 Hz 的 spinner,每帧 60 字节;工具会静默运行 0.5–6 s;权限提示通过 CSI n G 定位单词,回答后用光标上移和 \x1b[J 擦除。监督器每 50 ms 记录判断。
seed 7 的一次运行覆盖 30 个任务、10.6 分钟和 25 次权限请求,等待回答共 144 s。机器人在提示出现后 3–8 s 回答,任务结束后 3–10 s 输入新任务。
结果暴露了几种捷径的问题:
- 静默阈值低于 5 s,会撞上工具的安静执行;阈值不超过 3 s,会在全部 30 次任务结束时触发。阈值达到 5 s 后,又开始漏掉已被及时回答的提示。
- 8 s 阈值漏掉全部 25 次请求,与机器人最多等待 8 s 的设定有关,不能直接推广成真实用户的漏报率。
- 行尾问号能覆盖权限提示,也会命中普通回答里的追问,而且文字可能留到下一个任务。
- 清理后的输出尾部在模拟中能识别提示,因为模拟器给
❯ 1. Yes写了真实空格;但提示擦除后,它平均还会误报 77 s,直到旧字节被挤出最后 8 KB。
屏幕模拟器能处理擦除,因此旧提示会及时消失。700 ms 扫描带来的检测延迟,中位数为 0.26 s,最大为 0.70 s;状态解除也可能多等一个扫描周期。
有明确事件,为什么还要读屏幕
进程状态能提供的信息更有限。作者每秒两次采样 ps -o stat=,time=:working 的 603 个样本中,99.5% 为 S;idle 的 357 个和 blocked 的 280 个样本全部为 S。三者 CPU 占用分别约为单核的 0.04%、0.02%、0.02%,wchan 都显示 -。
等待终端输入和 spinner 帧间休眠,都可能显示 S。对基于 Node 的 CLI,event loop 的等待也不能直接解释成「需要人来操作」。这项结果来自 macOS,不能替其他系统下结论。
osFoundry 使用 MasterPty::process_group_leader,即 tcgetpgrp,识别 tty 的前台进程组。这能确认当前运行的是 Claude,或者它已经退出、shell 已回到前台。
若由 osFoundry 启动 Claude Code,则通过 --settings 注入当前启动使用的 hook,同时保留用户原有 hook:
{"hooks": {
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "osf-pane report working >/dev/null 2>&1 || true"}]}],
"PreToolUse": [{"matcher": "*", "hooks": [{"type": "command", "command": "osf-pane report working >/dev/null 2>&1 || true"}]}],
"Notification": [{"hooks": [{"type": "command", "command": "osf-pane notify >/dev/null 2>&1 || true"}]}],
"Stop": [{"hooks": [{"type": "command", "command": "osf-pane report done >/dev/null 2>&1 || true"}]}]
}}
要留意 Notification:权限请求和等待输入一分钟都会触发它。osf-pane notify 读取 payload,把 permission 映射成 blocked,其余映射成 idle。通过 claude-agent-acp 接入 ACP 时,session/request_permission 则直接表达权限请求。
signals::combine 的优先级依次是:屏幕上的 blocked、近期 agent 报告、shell 已回到前台、其他屏幕规则。报告可能过期或因 hook 静默失败而缺失,所以明确的屏幕权限提示优先。
通知按会话和状态设置 30 s 冷却,避免重绘造成 blocked → working → blocked 后重复提醒。工具输入前还会重新执行 classify_session;处于 blocked 的面板只接受用户确认的 runtime_answer,不接受普通 runtime_send。
我的判断
我认同它把「还原屏幕」和「识别状态」拆开。前者解决终端语义,后者处理产品文案;规则放在可独立更新的 TOML 中,也便于跟进 CLI 界面变化。
但 11/11 只证明覆盖了这批捕获。原文没有交代更多版本、语言、终端尺寸的覆盖情况;模拟实验也不能代表真实 Claude Code 的完整行为。具体误报时长和延迟,应当视作该实验条件下的结果。
与静默计时、问号匹配相比,这套方案的机制更充分;与 ACP 或 hook 相比,读屏仍是依赖界面细节的推断。「屏幕 blocked 永远优先」成立的前提,是权限规则足够准确,否则误匹配也会压过明确报告。
作者认为错误提醒比迟到提醒代价更大,这是产品取舍,没有用户对照数据。默认 idle 适合控制提醒噪声,却不能作为自动代答权限请求的依据。
能拿来做什么
- 开发终端会话面板时,为每个 PTY 维护屏幕状态,只分类当前可见区域,并保留原始捕获作为回归样本。
- 给 Node CLI 接状态检测时,分别处理工具静默、任务完成和权限等待,避免用同一个超时阈值解释三种行为。
- 能控制 agent 启动方式时,接入 hook 或 ACP,并明确报告过期、事件缺失和屏幕提示冲突时的优先级。
- 自动向 AI 编程终端输入前重新分类,把权限回答和普通任务输入分成不同入口,避免旧状态导致错误发送。