从终端控制序列识别代理权限等待
osFoundry 使用 Claude Code 的真实终端捕获分析权限等待状态:可见空格可能由光标定位形成,屏幕换行也可能完全不含换行字节。文章通过测试样本说明直接匹配原始文本为何容易漏检,并介绍检测实现。
AI 深度解读
判断 Claude Code 是否在等你批准命令,不能只看它多久没输出,也不能把终端内容去掉颜色后搜一句提示词。osFoundry 给出的办法是:先还原终端当前屏幕,再按优先级匹配规则,并结合 hooks、协议事件和前台进程信息判断状态。
这篇值得读的地方,是作者拿真实终端字节和模拟实验拆开了几种常见误判。对做 AI 编程面板、终端集成和多 Agent 管理工具的开发者来说,它讨论的是一个具体问题:程序看起来没动,究竟是在干活、等新任务,还是等你点头?
为什么清理终端输出,还是找不到权限提示
作者展示了 Claude Code 2.1.303 的真实捕获数据。屏幕上明明显示着「Do you want to proceed?」,原始字节里的 Do 和 you 之间却没有空格,而是 \x1b[5G:把光标移到第 5 列。视觉上的空格,只是没有被写入的屏幕单元格。
换行也类似。11 份 Claude Code fixtures,每份 2,099 到 7,672 bytes,全部没有 \n。它们通过 \r 和光标下移指令 \x1b[1B 排出多行。
终端输出是一串绘制指令,当前屏幕才是这些指令执行后的结果。
作者用两种方式处理这些真实捕获:一种取最后 8 KB,移除转义序列,再保留每行最后一个 \r 后的内容;另一种送进 32×120 的屏幕模拟器,再读取行文本。
结果是 5/11 对 11/11。前者碰巧判对的全是 idle,因为 idle 就是默认值;4 个 blocked 和 2 个 working 全漏了。没有换行时,按最后一个回车裁剪会丢掉前面的内容。即便不裁剪,直接删除光标指令也会得到黏在一起的 Doyouwanttoproceed?。
osFoundry 为每个会话维护一个 vt100::Parser,只分类 screen_text(),刻意排除上方保存的 200 行历史。已经批准过的提示如果留在滚动历史里,继续参与匹配就可能让会话一直显示 blocked。
作者的 Python 模拟器约 100 行,处理文字、\r \n \b、光标移动 A B C D E F G H f d 和清除 J K,跳过颜色、模式及 OSC 标题。它足以通过这 11 份样本,但宽字符按单格处理,不能据此当成完整的终端实现。
判断「等批准」,要匹配提示的身份
屏幕还原之后,下一步也不是搜问号,而是识别权限提示特有的措辞和操作提示。下面是 Claude Code 的 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']
要看的是 blocked 排在最前面。CompiledManifest::evaluate 采用首条命中规则,而权限窗口打开时,页脚仍可能包含 esc to interrupt。先检查 working,就会把等待批准误判成工作中;全部不匹配时则返回 idle。
他们还移除了 Would you like to:这是 Codex 提示使用的措辞,Claude 的普通回答也常出现类似表达。规则需要识别界面状态,不能把自然语言里的提问都当成权限请求。
osFoundry 用 SWEEP_INTERVAL 每 700 ms 扫描一次所有面板。spinner 每秒重绘多次,逐次写入都跑正则,可能每秒执行数百次,却得到相同结果。
作者另用模拟 Agent 做了对比,必须和真实 fixtures 区分:测试机器没有 claude,实时实验运行的是 fake_agent.py。它模拟 10 Hz、每帧 60 bytes 的 spinner、0.5 到 6 s 的静默工具调用,以及权限提示和回合结束界面。监督器每 50 ms 记录判断,机器人在提示出现 3 到 8 s 后批准,在回合结束 3 到 10 s 后提交新任务。
种子 7 的一次运行包含 30 个任务、10.6 分钟、25 次权限提示,累计等待批准 144 s。结果暴露了几种办法的局限:
- 静默阈值低于 5 s 会撞上工具调用;不超过 3 s 时,30 次回合结束也都会触发。阈值达到 5 s 后,又开始漏掉及时获批的提示,8 s 阈值漏掉全部 25 次。
- 检查行尾问号能发现权限提示,也会误报普通回答里的「Would you like me to add a regression test?」。
- 清理后的输出尾部在模拟实验中能发现提示,却在批准后平均继续误报 77 s。旧字节要等约 8 KB 新输出才能被挤走,spinner 只有约 600 bytes/s,中间还有静默阶段。
屏幕模拟器收到光标上移和擦除指令,就能立即移除旧提示。700 ms 扫描带来的检测延迟中位数是 0.26 s,最大 0.70 s;批准后的状态更新也可能等待一个扫描周期。
hooks 能直接报状态,为什么还要看屏幕
操作系统能提供的信息也有边界。作者在 macOS 上每秒两次采样 ps -o stat=,time=:working、idle、blocked 分别有 603、357、280 个样本,处于 S 的比例为 99.5%、100%、100%,CPU 占单核 0.04%、0.02%、0.02%。wchan 在所比较的情况里都显示 -。
等终端输入和等下一帧 spinner,都可能处于睡眠状态。Node CLI 的 event loop 同样不能靠一个进程状态字母解释业务进度。
进程信息适合判断「谁占着终端」。osFoundry 通过 MasterPty::process_group_leader,即 tcgetpgrp,识别前台程序;Agent 退出、shell 回到前台时,也能及时纠正残留屏幕造成的判断。
自己启动 Claude Code 时,osFoundry 还通过 --settings 注入 hooks,只影响本次启动,并保留用户自己的 hooks:
{"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 不能直接等于 blocked:权限请求会触发它,等待输入一分钟也会触发。osf-pane notify 要读取 payload,把 permission 映射成 blocked,其他情况映射成 idle。通过 claude-agent-acp 接入 ACP 时,session/request_permission 则直接表达了权限请求。
signals::combine 的优先级是:屏幕 blocked、近期 Agent 报告、shell 回到前台、其余屏幕规则。作者担心 hooks 静默失败或报告过期,因此让可见权限提示保留最高优先级。
通知另有每会话、每状态 30 s 冷却,防止重绘导致重复提醒。真正写入面板之前,runtime/tools.rs 会重新执行 classify_session;blocked 面板只接受用户确认的 runtime_answer,普通 runtime_send 不得写入。
我的判断
最有说服力的是「先还原屏幕」:真实字节和 11 份 fixtures 都直接解释了为什么尾部文本会失败。规则外置到 TOML、把执行器和可更新措辞分开,也适合依赖第三方 CLI 界面的工具。
但 11/11 是这组样本的结果,不是长期准确率。实时对比来自模拟 Agent、单次种子运行,不能当作 Claude Code 各版本、各种终端尺寸下的完整验证。700 ms 对人工看板是否合适,同样属于结合场景的工程取舍。
hooks 和 ACP 比屏幕文案更直接,也更少依赖措辞;屏幕识别则能覆盖从普通 shell 手动启动的会话。两者组合有价值,但屏幕 blocked 永远优先的前提,是规则足够精确。若未来文案变化、普通输出命中规则,这个优先级也会放大误报。作者没有提供长期对照数据,这部分应当作为经验方案评估。
能拿来做什么
- 做终端型 AI 编程面板时,先维护当前屏幕状态,再识别权限提示,并将滚动历史排除在判断之外。
- 为不同 Agent 分别保存规则和真实 fixtures,至少覆盖权限请求、执行中、回合结束及批准后清除提示的过程。
- 能控制启动过程时接入 hooks 或协议事件,同时保留屏幕识别与前台进程信息,并明确各信号的优先级。
- 自动向终端发送内容前重新检查状态;对等待批准的面板使用独立确认入口,避免普通任务文本被当成权限回答。