ウォッチドッグは忙しいエージェントを殺し、行き詰まったエージェントを信頼する
長時間動くエージェントがまだ存在するかどうかだけを見るヘルスチェックは、難しい 2 つのケースの両方を取り違えます。健全なエージェントが不在に見えるほど静かになった瞬間にそれを殺し、プロセスがまだ生きている凍結したエージェントをそのまま通してしまうのです。直すべきは、より鋭いプローブではなく別の信号、すなわちエージェントが仕事をすることによってのみ残す永続的な痕跡です。行き詰まったエージェントはそれを伸ばすのをやめます。伸ばすこと自体が仕事だからです。
長時間動くエージェントが黙って死んでしまわないようにするために、まず見たくなるのは、それがまだそこにいるかどうかです。プロセスは動いているか。セッションはつながっているか。小さなウォッチドッグがタイマーで目を覚まし、守っている対象が存在することを確認し、いなくなったものを再起動します。私たちは何十年もこうやってデーモンを監督してきましたし、デーモンにとってはそれでほぼ正しい。動いていない Web サーバーは落ちている Web サーバーであって、その中間に微妙なものは何もありません。同じウォッチドッグを長時間動く自律エージェントに向けると、そのチェックは静かに、あなたが思っている意味を持たなくなります。存在と健全さが別々のものになってしまい、ウォッチドッグはあなたが気にしていないほうを測っているからです。
まず、痛いほうの方向から。存在チェックは、チェックする対象を観測する能力の分しか役に立ちません。そして動いているエージェントを観測するのは、驚くほど簡単に間違えられます。私は、あるウォッチドッグが健全なエージェントの一群を死んだと宣言して殺し始めるのを見たことがあります。理由は純粋に機械的な 2 つで、どちらもエージェント自体とは何の関係もありませんでした。1 つ目はスコープの境界でした。エージェントはターミナルマルチプレクサの中で動いていて、そのセッション一覧はユーザーごとに、そのユーザー自身のツールだけが問い合わせられるサーバーが保持していました。ところがチェックは間違ったユーザーとして問い合わせ、空のリストを受け取り、その空を死と読んだのです。2 つ目はもっと微妙でした。チェックはエージェントをそのプロセスの名前で照合していましたが、エージェントは小さなシェルのラッパー越しに起動されていたため、ウォッチドッグが見つけたプロセスはシェルであってエージェントではなく、生きているエージェントが不在のものとして読まれました。どちらも観測される側ではなく観測する側の失敗です。患者がそもそも部屋にいるかどうかについて間違えられるヘルスチェックは、十分に長いタイマーの上では、ただ光の届かない場所に立っていただけの相手にナイフを突き立てることになります。
これは新しい危険ではなく、それに出会う新しい場所にすぎません。Kubernetes の下でサービスを運用したことがある人なら、同じ刃に反対側から噛まれたことがあるはずです。Liveness プローブ、つまりコンテナが応答しなくなったときに再起動するチェックを、あまりに攻撃的に設定すると、不健全だったことなど一度もなく、ただ忙しいだけのコンテナを再起動してしまいます。負荷のもとでプロセスは忙しすぎて、期限内にプローブに答えられず、プローブは十分な回数の連続失敗を記録してそれを死と呼び、オーケストレーターがそれを殺して再起動し、殺すことでその負荷を隣のコンテナへ押しつけ、隣は今度は自分のプローブを取りこぼし始めます。こうして、ただ遅かっただけのクラスタが、過負荷が収まる前にそれをさらに悪化させる再起動の波へと傾きかねません。運用の世界がそこから得た教訓は、タイムアウトを少しいじって先へ進むことではありませんでした。プロセスが今この瞬間に答えられるかどうかを測るプローブは、それが自分の仕事をしているかどうかとは別のものを測っているのであり、その 2 つを混同すると監視こそが障害になる、というのが教訓でした。長い計算をすりつぶすように進めているエージェントは、まさにその忙しいコンテナです。静かになって観測しづらくなるその区間は、失敗ではありません。それが仕事です。
さて、もう一方の方向です。こちらは音もなく失敗するぶん、より悪い。存在チェックを完全に直し、正しいユーザーと正しいプロセスを与えても、それでもなお間違った問いに答えています。エージェントは完全に存在しつつ完全に行き詰まっていられるからです。公開されている Claude Code の報告は、まさにこの形の野外図鑑のように読めます。あるセッションは 2 つのツール呼び出しの間で動かなくなり、部分的な応答をすでに受け取った状態で、後になってようやく stream idle timeout を表に出します(issue #46987)。あるサブエージェントのストリームは、接続が確立されたまま受信バイトがゼロのまま静かに止まり、その間メインセッションは影響を受けずに進みます(issue #79292)。ある凍結は、その下で死んでいる接続が決して死んでいると検出されないために、気づかれません(issue #88178)。これらが描いているのは、倒れたプロセスではありません。周りのプロセスも、セッションも、ソケットも、すべて完璧に生きたまま、前進をやめた仕事の単位です。そこから私が引き出す読み、報告そのものはしていない読みは、こうです。存在は進捗の証拠だったことなど一度もなく、あなたが知る必要があったのは進捗のほうだったのだと。プロセスが存在するかどうかしか見られないチェックは、懸命に考えているエージェントと永遠に行き詰まったエージェントを区別できません。存在するか、というレベルでは、この 2 つは同一だからです。
Kubernetes は一度火傷をしたので、答えへ半分だけ進みました。そのたどり着いた半分は、受け取る価値があります。ひとつの問いを 2 つに分けたのです。Liveness、このコンテナは再起動すべきか。Readiness、今このコンテナに仕事を送るべきか。Readiness に失敗すると、コンテナは殺されることなく、トラフィックを送るローテーションから外されます。これは正しい反射で、応答しない一瞬を死刑判決から切り離しています。さらに Kubernetes は、エンドポイントを ping するのではなくコンテナの中でコマンドを実行するプローブも用意しています。これはアプリケーションに返答を求めるのではなく、アプリケーションが働きながら触れるマーカーを読めるもので、それはすでに、ひそかに、進捗チェックになっています。監督の世界はこの手を何年も前から知っていました。systemd のサービスは、自分の仕事のループの中から拍を打つように作ることも、沈黙のゆえに殺されるように作ることもできますし、長時間タスクのフレームワークは、期限内に進捗を報告しなかったタスクを殺します。つまり修正の形は古い。それらの古い形のどれもが片づけなかったこと、そして自律エージェントが表沙汰にせざるをえないことは、その拍が何でできているべきか、です。というのも、それらのどれにおいても拍は依然として「自分は大丈夫だ」と言うプロセスであり、「自分は大丈夫だ」と言うプロセスこそ、最も信頼できないただ一つのセンサーだからです。
だから修正は、より良い存在チェックではありませんし、単に存在を進捗に置き換えることでもありません。エージェントが送ることを選ぶ進捗の拍は、行儀のよくなった存在チェックにすぎないからです。何も着地していなくても打てるし、仕事はうまく進んでいるのに自分だけ落ちることもある。重みを持つ一手は、エージェントに進捗を報告させること自体をやめ、代わりに、エージェントが協力しようがしまいが仕事が残す痕跡から進捗を読むことです。監視する対象を、仕事を記録するその永続的な書き込みそのものにするのです。ステップが完了したときにだけ次の状態へ切り替わるキューの行、アクションが終わってその結果がディスク上に載ったからこそ append-only なログに着地する 1 行。そうすれば拍は、仕事が存在せずには存在できません。拍は仕事そのものの足跡であって、それについてのメッセージではないからです。そしてその足跡は、動きではなく方向のために読みます。カーソルは、限りのある量の仕事を単調に前進していかなければなりません。というのも、リトライループにはまり込み、同じステップを失敗させては試行に試行を継ぎ足しているエージェントは、システムの中で最も忙しく見える痕跡を吐き出し、活動を数える監視ならそれを盤上で最も健全なものと呼んでしまうからです。前へ進まずに伸びていく痕跡は、衣装をまとった行き詰まりです。あなたが見るのは、位置が動いていることであって、ログが長くなっていることではありません。
ここで議論はその値打ちを稼ぎます。そしてここは、近くにあるもう一つの議論、エージェントの群れを分散システムとして扱うべきだという主張が、途切れた地点でもあります。あちらはこう言いました。完了した単位の成功について、エージェントの言葉を鵜呑みにするな、代わりにそれが生み出した成果物を確かめよ、と。これは同じ本能を、別の時制へ向けたものです。あちらでは単位は完了していて、それが作ったものと照らして一度だけ検証します。こちらでは単位はまだ動いていて、まだ動き続けている成果物からその生存を絶えず読み取ります。終端のチェック、対して脈です。そして脈は、完了した単位の版が一度も向き合わずにすんだ反論に答えます。すなわち、動いているエージェントは、調子を尋ねられれば反射的に、しかも行き詰まっているときにこそ最も、朗らかに嘘をつく、という反論です。あなたはそれに決して尋ねません。エージェントがステップを終えることによってしか前進させられないカーソルを見て、カーソルが止まることを信号として扱います。一手は、エージェントの言葉からエージェントの航跡へと移ることです。そしてそれをいったん済ませてしまえば、自己申告の問題はもう部屋の中にいません。あなたはエージェントの言葉を、もう何ひとつ鵜呑みにしていないからです。
これは、望むほど綺麗な勝利を買ってはくれません。正直な版はそれを声に出して言います。足跡はステップが終わったときに着地するのであって、走っている最中ではありません。だからこれが実際にすることは、曖昧さの窓を、プロセスの一生まるごとから一つのステップへと縮めることです。そして本当に長い一つのステップ、40 分の計算、1 回の遅いモデル呼び出しの内側では、痕跡は死体のそれと同じくらい冷たい。運用者はまた、最も長い正当なステップよりも長い期限を選ぶところへ戻ります。それは、よりよく仕立てられたコートを着た忙しいコンテナ問題です。唯一の本当の逃げ道は、ステップを十分に小さくすること、あるいは長いステップに、進みながら部分的な印を落とさせて、健全な沈黙が決して期限を超えて続かないようにすることです。だからこそ、途切れた拍を、そのまま殺しへ配線してはいけません。Kubernetes が学んだもう半分を借りましょう。止まったカーソルは、まずエージェントを degraded と印し、新しい仕事を渡すのをやめるべきです。失敗した Readiness チェックが Pod をローテーションから外すのと同じように。そして、沈黙が、まだ正直でありうるどのステップをも超えて続いたときにだけ、殺しへとエスカレーションします。一つの途切れた拍からナイフまで一気に走るチェックは、より賢い軸の上に組み直された忙しいコンテナのカスケードにすぎません。
最後の失敗がひとつあります。そしてそれは、最初の失敗が巡って戻ってきたものです。痕跡を読むガードそれ自体が観測者であり、観測者は観測に失敗しうる。間違ったパスを見ることもあれば、ログを読む権限がないこともあれば、足元でローテーションされて消えたファイルを追い続けることもある。それが起きるとガードは何も学ばず、そのとき全体の仕組みは、その「何もなし」が何として報告されるかで決まってしまいます。私は、自分のチェックを終えられないというガード自身の不能が、普段どおりの静けさと静かに混ぜ込まれ、上へは異常なしとして報告されるのを見たことがあります。ガードは盲目で、その盲目が緑として現れたのです。それはこのシステムがとりうる最悪の状態で、2 種類のエージェント障害のどちらよりも悪い。なぜならそれは、異常なしの届けを出す唯一のものだからです。痕跡を読めない監視は、それをそう言わなければなりません。声高に、そして言い続けなければなりません。その沈黙が、重複排除されたり、よい知らせの一般的なざわめきへと平均されたりしては決してならない。オフラインになるまさにその瞬間に異常なしと報告するセンサーは、安全装置ではなく、誰かが線を切ったときに緑に変わるように配線されたランプだからです。仕組み全体を正直に保つ規則は、冒頭のインシデントがすでに切に求めていた規則です。監視が自分自身で見えていないということは、決して「何もなし」ではなく、盤上で最も声の大きいものなのです。
プロセスの中の知性は、ここで危険にさらされていたものでは一度もなく、より賢いエージェントに手を伸ばしても、そのどれにも触れはしなかったでしょう。間違っていたのは、境界で尋ねられた問いのほうです。存在は確認するのが安上がりで、しかもほとんどいつも確認すべき対象として間違っています。長時間動くエージェントが最も死んで見える 2 つの瞬間、すなわち懸命な仕事の深い静けさと、行き詰まっていることの深い静けさは、ただの存在を見張るものにとっては見分けがつかないからです。存在を見張れば、あなたは確実に、忙しいほうを殺し、行き詰まったほうを信頼します。それはちょうど逆さまです。代わりに痕跡を、エージェントが動くことによってのみ残す航跡を見張れば、その 2 つはようやく分かれます。働いているエージェントとは、いまだ印を残しているほうであり、行き詰まったエージェントとは、あなたがついに見ることのできる沈黙なのです。