AIエージェントの端末画面スクレイピングによる状態判定とは、tmux や PTY(疑似端末)経由で対話型 CLI エージェントの画面バッファを走査し、特定のプロンプト記号やスピナー文字列の有無から応答中(Busy)か入力待ち(Idle)かを外部から推定する監視手法です。

Claude Code や Codex CLI などの対話型コーディングエージェントを tmux 上で並列稼働させ、自作スクリプトでオーケストレーションを組もうとするとき、多くの開発者が最初に思いつくのは画面キャプチャ(tmux capture-pane)による状態判定でしょう1。エージェント本体に手を加えることなく、非侵入で手軽に外部から監視できるため、プロトタイピングにはうってつけの手段に見えます。

しかし、画面全体を単純な文字列走査で監視する設計は、思わぬ罠を抱えています。エージェント自身が完了報告の本文中で判定ルールを解説した瞬間に、監視ロジックが自爆してしまうのです。入力待ちになっているにもかかわらず、画面内の判定語に引っかかって「応答中」と誤認し、3600 秒のタイムアウト停止を引き起こしました。

この失敗を経て、私たちは探索範囲を入力欄周辺の「見出し行スライス」に絞り込み、さらにステータス行から非同期待ちを識別する設計へと改修しました。しかし根本的には、画面スクレイピングという対症療法から、エージェントのフックや通知イベントを用いた構造的シグナルへの移行が必要です。本記事では、実際に遭遇した障害ログとコード差分をもとに、画面監視の落とし穴と堅牢化の境界線を解き明かします。

完了報告の本文に判定語が載って3600秒待った日

私たちが開発・運用している自作エージェントオーケストレータでは、並列に起動したワーカーエージェントの状態監視において、画面内に esc to interrupt という文字列が存在するかどうかで「応答中」か「入力待ち」かを判定していました。Claude Code や Codex CLI などの対話型エージェントは、推論中やコマンド実行中に画面下部やスピナー行へ割り込み用キー案内を表示するため、この語の有無を見るのは自然な発想に思えました。

ところが、ある日の並列実行タスクにおいて、ワーカーが正常に作業を終えて完了報告を出力し、入力待ちプロンプト(❯)に戻ったにもかかわらず、監視側が「応答中」と判定し続けました。オーケストレータ側が設定していた監視タイムアウト(3600 秒)に達するまで待機し、再試行時にも同様に本文の判定語を拾った結果、3600 秒のタイムアウトによる強制停止(STOP)が 2 回連続で発生したのです。

画面全行走査による「自己言及インジェクション」のメカニズム

原因を突き止めるため、障害発生時の画面バッファを採取して解析しました。入力欄の上端位置は input_box_top=21 であり、プロンプト行の上には実行中スピナー(Levitating… 等)は一切存在していませんでした。

それにもかかわらず判定が「応答中」に倒れていた理由は、ワーカーエージェントが出力した完了報告の 本文そのもの にありました。ワーカーが丁寧にも、自分が実行した判定ルールの説明として次のような行を出力していたのです。

- esc to interrupt: 画面にこの語を含む行があれば応答中

監視側は画面全体の全行を対象に esc to interrupt を検索していたため、エージェント自身が出力した解説テキストを「画面に割り込み案内が出ている=応答中である」と誤読してしまいました。エージェントが判定ルールに言及したテキストを出力した瞬間に監視窓が汚染される、いわば「自己言及インジェクション」とも呼ぶべき事態が発生していたのです。

古典的 expect におけるコマンドエコー問題との構造的類似性

この現象は、端末自動化における古典的なツールである Pexpect や Expect において、出力ストリームを順次走査する expect() が2、送信したコマンド文字列の疑似端末(PTY)ローカルエコーによって送り返され、意図せず自分自身の入力にマッチしてしまう問題と構造が酷似しています3。

Pexpect では疑似端末のデフォルトエコーが有効になっているため、子プロセスからの出力だけでなく自分が送ったコマンド文字列もバッファに入り込みます3。これを防ぐには setecho(False) や waitnoecho() を用いてエコーバックを明示的に制御する必要があります。

今回の画面スクレイピングでも同様に、「外部から観察しているつもりの画面バッファの中に、被監視対象自身の言葉として判定語が混入する」という観測窓の汚染が起きていました。

以下は、この障害を引き起こした旧版の判定コード(skills/dispatch/scripts/dispatch.py の screen_idle 関数)の抜粋です。判定の起点となる入力欄上端 top は、box_top 関数によって Claude Code 等の TUI 入力ボックスを囲む罫線(╭ や ─)の行位置を検出して特定しています。旧版では、この入力欄位置とは無関係に、画面の全行に対して単純な走査を行っていました。

引用元: skills/dispatch/scripts/dispatch.py(コミット 6fca12b の親コミット)

    top = box_top(rows)
    if top is None:
        return False
    if any("esc to interrupt" in r for r in rows):
        return False
    above = [r.strip() for r in rows[max(0, top - 6):top] if r.strip()][-3:]
    return not any(SPINNER_ROW_RE.match(r) for r in above)

画面全体の走査は典型的なインジェクションの脆弱性を抱えており、エージェントがどのような語を出力するか予測できない以上、直ちに改善が必要でした。

探索スライス設計:直上・直下から「見出し行」への進化

単純な文字列マッチングが危険である以上、探索範囲を絞り込む必要があります。最初に試みたのは、入力欄(プロンプト)の「直上数行および直下」だけに探索範囲を限定する対症療法でした。

初版修正の落とし穴:字下げ行と空行によるスピナー取りこぼし

初版の修正では、入力欄の上端 top から直上 3 行(空行を除く直上 6 行中最後の 3 行)と、入力欄より下のステータス行だけを探索対象としました。会話本文は入力欄よりはるか上方に流れるため、これだけで完了報告本文の誤爆は回避できるように見えました。

しかし、コードレビューにおいて重大な見落としが指摘されました。TUI(テキストユーザーインターフェース)の画面構造において、推論スピナーと入力欄の間に TODO リストの字下げ行(⎿ ☐ a)や空行が複数挟まるケースが存在したのです。

行数を固定で 3 行などに区切ってしまうと、字下げ行が連続した際に入力欄直上のスピナー行が探索ウィンドウから押し出され、応答中であるにもかかわらず「入力待ち」と誤判定(スピナー取りこぼし)を起こしてしまいます。

見出し行スライスアルゴリズム

そこで第 2 版の修正として採用したのが、「見出し行スライス」アルゴリズムです。

入力欄の位置 top から上方向へ向かって空行と字下げ行をスキップしていき、最初に当たる「字下げのない行(見出し行)」を探索起点 head とします。そして、その head からステータス行を含む画面下端までのスライスに対してのみ、判定語が存在するかを走査するように変更しました。

引用元: skills/dispatch/scripts/dispatch.py(コミット d40fd85)

    head = next((i for i in range(top - 1, -1, -1) if rows[i].strip() and not rows[i][0].isspace()), 0)
    if any("esc to interrupt" in r for r in rows[head:]):
        return False

この改修により、以下の 4 つのテストケースがすべて正常にパスするようになりました。

  1. test_esc_to_interrupt_in_body_far_above_input_is_idle: 本文上方に判定語があっても入力待ちと正しく判定。
  2. test_esc_to_interrupt_below_input_is_busy: 入力欄下のステータス行に判定語があれば応答中と判定。
  3. test_esc_to_interrupt_just_above_input_is_busy: 入力欄直上に判定語があれば応答中と判定。
  4. test_esc_to_interrupt_spinner_above_many_indented_rows_is_busy: スピナーと入力欄の間に複数の TODO 字下げ行が挟まっていても確実に応答中と判定。

スライス設計の限界と局所化の意義

ただし、このスライス設計も万能ではありません。もしエージェントが最後に出力した見出し行自体に判定語が含まれていた場合や、Codex CLI のように罫線による明確な入力欄区切りを持たないエージェントでは、依然として誤判定のリスクが残ります。

このスライス設計の本質は、誤爆を 100% 根絶することではなく、「画面全体という広大な地雷原を、入力欄直近の見出しブロックという極小の局所領域に封じ込めた」点にあります。

待ち方の違いによる誤読:Monitor 待機を「停止」と読まないためのステータス行監視

自己言及誤爆を解決した後に直面したもう一つの罠が、「エージェントの待ち方の違いによる誤読」です。

非同期待ちによる「プロンプト空き」

Claude Code などのエージェントは、テスト実行や長時間コマンドを待機する際、Monitor ツールやバックグラウンド Bash ジョブを起動します。このとき、エージェントは外部ツールの完了通知を待つために自身のターンを閉じます。

すると、画面上では実行中スピナーが消え、プロンプト入力欄が空いた状態になります。

外部の監視スクリプトから見ると、これは「エージェントが指示待ちになった」ように見えてしまいます。自作オーケストレータの司令塔は、「ワーカーが報告を出さずに入力待ちになった」と誤読し、2 回の催促メッセージを送信した末に STOP: worker が止まった として強制停止を発生させてしまいました。実際の画面では、ワーカーは止まっていたのではなく、バックグラウンドでテストの完了を待機していただけでした。

ステータス行の正規表現監視

この問題を解決するため、入力欄の下部 4 行(STATUS_ROWS = 4)に表示されるステータス情報を監視対象に加えました。

実測ログにおいて、非同期待ち中のステータス行には 3 monitors still running や ⏵⏵ auto mode on · 1 shell といった表示が出現することが判明していました。そこで、以下の正規表現を用いて、入力欄が空いていてもバックグラウンドジョブが稼働中であれば「応答中」と同等に扱うようにロジックを改修しました。

引用元: skills/dispatch/scripts/dispatch.py(コミット f850628)

BACKGROUND_RE = re.compile(r"(?<![0-9A-Za-z])[1-9][0-9]* (?:monitors?|shells?)(?![0-9A-Za-z])")
STATUS_ROWS = 4

なお、こうしたステータス行の文言表示は CLI のマイナーアップデートによって変更されるリスクがあります。実務で運用する際は、正規表現の検知ルールを定期的にテストで検証できるように保守性を確保しておく必要があります。

画面スクレイピングの限界と 3 つの監視アプローチ比較

ここまで見てきたように、画面スクレイピングによる状態監視は、自己言及誤爆や非同期待ちの誤読など、多くの泥臭いエッジケースに対処し続ける必要があります。

さらに、エージェントごとに入力欄の表現が異なる点も運用の頭痛の種です。Claude Code は ❯、Codex CLI は罫線のない ›、Antigravity は罫線に挟まれた > など、記号も画面レイアウトも異なります。こうした CLI の差異を外部スクレイピングだけで吸収し続けるのは極めて不安定です。

状態監視の方式は、実装の手軽さと信頼性のトレードオフによって以下の 3 つのアプローチに整理できます。

監視アプローチ状態検知の仕組み主なメリット固有のリスク・限界主なユースケース
画面全体スクレイピングcapture-pane 等で画面全行から判定語を文字列検索1外部プロセスから非侵入で手軽に導入可能本文中の判定語を拾う自己言及誤爆、待機誤読簡易スクリプト、プロトタイプ検証
探索スライス局所化入力欄直前の見出し行〜ステータス行のみを抽出判定エージェント未改造のまま自己言及誤爆を大幅抑止CLI ごとの TUI 差分(罫線・記号)対応が泥臭い既存 CLI の外部オーケストレーション
フック・通知イベント連携Notification / Stop 等の CLI 組み込みフック4文字列解釈が不要で自己言及リスクを回避非同期待ち(Monitor 等)の区別に別途工夫が必要本番向けマルチエージェント基盤

状態監視の設計において、通知やフックが利用できる環境であれば、イベント連携を第一選択とすべきです。文字列解釈に依存しない構造化シグナルこそが最も安全だからです。

ただし注意が必要なのは、フック機能(Claude Code の Stop フック等)であっても、Monitor 待機のようにエージェントがターンを閉じた瞬間にイベントが発火する点です4。非同期待ちなのか完全な完了なのかを識別するには、フックの通知内容やステータス行の確認を組み合わせるなど、依然として複合的な判断が求められます。

前提が変われば結論はどう変わるか:フックの進化とオーケストレーション基盤

tmux のイベント対応力や外部自動化の制約次第では、過渡期として画面スクレイピングに頼らざるを得ない場面もあります。

しかし、近年ではエージェントと外部基盤との協調手法が急速に進化しています。たとえば bellmux は、tmux の画面を直接スクレイピングするのではなく、Claude Code のフック(Notification や Stop)を活用して入力待ちペインの ID をキューに積むアプローチをとっています5。また、tmux 自身の display-message と capture-pane を組み合わせて対象ペインの情報をスマートに取り出す手法も実践されています6。

さらに、モデルコンテキストプロトコル(MCP)を介して疑似端末(PTY)の制御を提供する PiloTY のような試みも登場しています7。仮想端末エミュレータである pyte を用いてメモリ上で VT100 エスケープシーケンスをパースするような画面スクレイピング手法も存在しますが8、根本的にはエージェント基盤が構造化シグナルを直接提供する方向へと進化しています。

現状の AI CLI はフックポイントがまだ限られており、セッション終了前の自動振り返り処理をフックで実行できないなど、自動化を進めようとすると後からセッションログを浚う手段に頼らざるを得ないのが実情です。

しかし、ワークツリーと仮想端末を統合管理する専用のオーケストレーション基盤(筆者らの環境では Orca を採用)があれば、自作の操縦スキルであっても安定して並列運用を成立させることができます。外部からの素朴な画面スクレイピングから脱却し、エージェント基盤との協調へと舵を切ることが、信頼性の高いマルチエージェント運用の鍵となります。

よくある質問(FAQ)

Q: tmux capture-pane でプロンプト記号(❯ や $)が見えていれば、作業完了・入力待ちと判定して問題ありませんか?

結論から言えば不十分です。エージェントがバックグラウンドでテストや外部プロセス(Monitor ツールや background Bash)を実行している最中、ターンを閉じてプロンプト欄が空く挙動が実測されています。プロンプト記号の有無だけでなく、ステータス行にバックグラウンドジョブの待機表示(N monitors や N shell(s) など)が残っていないかまで確認しなければ、作業中のエージェントを「停止した」と誤認して不要な催促や強制終了を投げてしまいます。

Q: 単純な文字列スクレイピングによるエージェント監視の自己言及誤爆は、正規表現を工夫すれば完全に防げますか?

画面全体を走査している限り完全には防げません。エージェントがどのようなテキスト(プロンプト解説、エラーメッセージ、判定ルールの引用など)を出力するかは非決定論的であり、監視キーワードが本文に含まれる確率をゼロにはできないためです。防ぐためには、探索範囲を入力欄の直前にある見出し行からステータス行までに限定する「画面スライス」を適用して誤爆範囲を狭めるか、CLI の公式フック機能(Notification や Stop イベント)を用いた構造化通知へ切り替える必要があります。

まとめ

本記事で解説したエージェント端末監視の要点は以下の通りです。

  • 画面全体の単純走査は自己言及インジェクションの脆弱性を招く: エージェント自身が判定ルールの解説を出力した瞬間に観測窓が汚染され、3600 秒のタイムアウトを引き起こします。
  • 固定行数のスライスはスピナー取りこぼしを起こす: TUI の字下げ行や空行によってスピナーがウィンドウ外へ押し出されるため、入力欄直前の「見出し行」を起点としたスライスが必要です。
  • プロンプトの空きだけで完了と判定しない: Monitor やバックグラウンドジョブ待機による「意図的なターン終了」をステータス行から検出し、不要な催促や STOP を防止します。
  • 長期的には構造化シグナルへ移行する: 画面スクレイピングは過渡期の対症療法と位置づけ、CLI フック、MCP、端末統合基盤を活用した堅牢なアーキテクチャを目指すべきです。

開発現場でのマルチエージェント並列運用、AI Operations のプロセス設計、および堅牢な監視パイプラインの構築について、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. tmux(1) - Linux manual page Nicholas Marriott、2026年11月1日閲覧。capture-pane の -p(stdout出力)、-S / -E(バッファ範囲指定)オプション仕様。 ↩ ↩2

  2. API Overview — Pexpect 4.8 documentation Noah Spurrier et al.、2026年11月1日閲覧。expect メソッドの順次バッファ走査およびタイムアウト例外仕様。 ↩

  3. Core pexpect components — Pexpect 4.8 documentation Noah Spurrier et al.、2026年11月1日閲覧。疑似端末(PTY)におけるエコーバック仕様と setecho / waitnoecho によるエコー制御。 ↩ ↩2

  4. Hooks reference - Claude Code Docs Anthropic、2026年11月1日閲覧。Notification および Stop イベントの仕様、last_assistant_message によるメッセージ参照。 ↩ ↩2

  5. tmux で Claude Code / Codex CLI の入力待ちペインにジャンプする話 Daiius、Zenn、2026年6月8日。bellmux による Claude Code hooks(Notification/Stop)連携と tmux ペインのキューイング管理。 ↩

  6. Claude Code に別 tmux ペインを覗かせる小ネタ - display-message と capture-pane で「あっち見て」を一行で頼む プランタン、printemps blog、2026年5月26日。tmux display-message と capture-pane を組み合わせた別ペイン内容取得の実務テクニック。 ↩

  7. yiwenlu66/PiloTY: PiloTY: AI pilot for PTY operations via MCP yiwenlu66、GitHub、2026年11月1日閲覧。エージェント向け MCP 経由の PTY 操作基盤と対話型プロンプト制御アプローチ。 ↩

  8. selectel/pyte: Simple VTXXX-compatible linux terminal emulator Selectel、GitHub、2026年11月1日閲覧。インメモリ VT100 エミュレータによる端末画面スクレイピング仕様。 ↩