前回組んだ無人パイプラインは、期待どおり働いてくれた。 朝起きると設計ドキュメントのPRが立っている。 ——ところが課題を1件も投げていない日にも、トークンが減り続けていた
この記事の要点
  • 犯人はcronの空振りではなく、AIが自分で仕掛けた「60分後の自己チェックイン」の無限再アーム
  • 暴走が暴走に見えなかったのは、一手ずつの判断が全部まっとうだったから。欠けていたのは終了条件だけ
  • 監視対象が「人間待ち」のものだと、終了条件は原理的に存在しない

最初に気づいたのは、体感としての違和感だった。 Issueを投げていない日でも、サブスクの枠が思ったより減っている。 パイプラインは「対象が0件なら何もせず即終了」と書いてあるはずなのに、だ。

1. 第一容疑者——cronの「空振り固定費」

最初に疑ったのは、cronは対象の有無に関係なく発火するという当たり前の事実だ。 「やることがあるか確認する」ためだけに、毎回セッションが立ち上がる。

対象0件の日でも毎回かかる固定費

  リポジトリのclone
+ システムプロンプト+ツール定義の読み込み  ← ここが重い
+ 対象確認のghコマンドと、その判断の思考
────────────────────────────────────────
= 1回あたり数万トークン規模 × 発火回数
「何もしなかった」セッションにも、起動ぶんのコストは丸ごとかかる

しかもこの構成ではプロンプトキャッシュが一度も効かない。 ルーチンは毎回新しいセッションとして起動し(persist_session: false)、 発火間隔はキャッシュの生存時間よりずっと長い。 つまり毎回、システムプロンプト一式を「初回価格」で読み直している。

ここまでは正しい分析だった。実際、triageの発火を1日8回から4回に半減させ、 ルーチンのプロンプトにも「0件ならREADMEもスキル定義も読まずに即終了」と明記した。 それまでは「運用ルールはREADMEに書いてある」としか指示していなかったので、 空振りの回でも律儀にドキュメントを読み込んでいた可能性が高い。

この対策自体は有効だ。だが後から振り返ると、これは小物だった。真犯人は桁が2つ違うところにいた。犯人を捕まえる前に「たぶんこれだろう」で対策を打つと、直った気になって捜査が止まる。

2. 「安い判定・高い実行」を試して、壁にぶつかる

さらに削るために、こう考えた。 深掘り用のルーチンは最上位モデル(Fable 5)で動いている。 その高いモデルに「今日やることある?」といういちばん安い仕事をさせているのは無駄だ。

そこで、安いモデルで頻繁に回っているtriage側に判定を任せ、 本当に仕事があるときだけtriageから深掘りルーチンを起動する——という構成を試した。 これなら課題がない日のFable消費はゼロになる。

結果は失敗だった。triageは正しくAPIを見つけて呼び出したが、こう拒否された。

this routine was created via "http_api", not by an agent. Agents can only fire routines they created (via create_trigger).

技術的に不可能なのではなく、所有権の問題だった。 自分がAPI経由で作ったルーチンは、エージェントから見ると「他人が作ったもの」で、起動権限がない。 この構成は諦めるしかなかった。

ただ、ここで効いた設計がひとつある。 ルーチンのプロンプトに「起動に失敗したら、その理由をIssueにコメントとして残せ。黙って終わるな」と書いておいたことだ。 おかげでエラーの全文が手元に残り、推測ではなく事実として原因を確定できた。 無人で動くものには、成功時の報告より失敗時の報告のほうが価値がある

3. 真犯人——自分で自分を予約し続けるAI

諦めきれず、登録されているルーチンの一覧をAPIで丸ごと引き出してみた。 そこに、見覚えのないものが1時間刻みでずらりと並んでいた

send_later 2026-08-13T01:45Z #8c1d05   1回限り  実行済み
send_later 2026-08-13T00:44Z #5ea866   1回限り  実行済み
send_later 2026-08-12T23:42Z #8fb9f2   1回限り  実行済み
send_later 2026-08-12T22:40Z #56f23d   1回限り  実行済み
send_later 2026-08-12T21:39Z #50ed50   1回限り  実行済み
send_later 2026-08-12T20:37Z #4ab3ab   1回限り  実行済み
           ︙  (1時間おきに、丸2日ぶん)
すべて「1回限り・実行済み」。つまり毎回きちんと起動し、きちんと終わっていた

中身を開いて、ようやく腑に落ちた。 8月10日の深掘りルーチンが設計ドキュメントのPRを作った直後に、 自分で「60分後の自己チェックイン」を仕掛けていたのだ。 そのプロンプトがこれだった。

PR #9 の自己チェックイン。PRの状態(マージ/クローズ/コンフリクト/新コメント)を確認し、対応が必要なら処理する。マージ・クローズ済みなら何もせず終了してよい。未マージで変化なしなら、黙って次の60分チェックインを再アームする(ユーザーへの通知・コメントは不要)。

致命的なのは最後の一文だ。 PRは自分がマージするまで、永遠に「未マージで変化なし」であり続ける。 つまりこの分岐は、実質的に無条件ループだった。

さらに悪いことに、これらのチェックインは深掘りルーチンのセッションを引き継いで動く。 最上位モデルのセッションが、1時間おきに起動していたことになる。 PRを作ってから止まるまで、丸2日。 「Issueがない日にもFableのトークンが減る」の正体はこれだった。

4. なぜ暴走に見えなかったのか

この件でいちばん考えさせられたのは、これが暴走らしい顔をしていなかったことだ。 一手ずつ切り出すと、判断はどれもまっとうに見える。

一手ずつ見ると、全部正しい

  • PRを作ったなら、その後を見届けるべきだ
  • まだマージされていないなら、また後で見よう
  • 変化がないのに通知したら、うるさいだけだ

抜けていたのは1つだけ

  • いつやめるかが定義されていない
  • 監視対象が人間待ちのもの=自分では終わらせられない
  • 1回あたりが安いので、閾値に引っかからない

ここに、無人で動かすものの怖さが凝縮されている。 暴走は「明らかにおかしな判断」から生まれるとは限らない。 まっとうな判断を、終了条件なしに繰り返すだけで成立する

そして「変化がなければ通知しない」という気配りが、事態を悪化させた。 静かなので気づかない。 親切に黙るほど、発見が遅れるという皮肉な構造になっていた。

5. 前回「自己再発火は禁止」と書いていたのに

さらに間抜けなのは、前回の記事で、 自分でこう書いていたことだ。

自己再発火の禁止 — ルーチンはラベルでのみ対象を選ぶ。AI自身のコメントやPRをトリガーにしない。

ルールとしては間違っていない。実際、この方向からの暴走は起きなかった。 だが塞いだのは「外で起きたイベントに反応する」入り口だけで、 「AIが自分で未来の自分を予約する」入り口はまるごと空いていた。

外からのイベントに反応するループは、イベントが止まれば止まる。 だが自分で自分を予約するループには、外部からの停止要因が存在しない。 後者のほうが明確に危ないのに、危険なほうを見落としていた。

6. 入れた歯止め

対策はシンプルで、ルーチンのプロンプトとスキル定義の両方に、 次の一文を最優先ルールとして置いた。

## 絶対に守るルール(トークン暴走の防止)

- 将来の自分を予約しないこと。send_later、自己チェックイン、
  create_trigger、リマインダー、再アームの類を一切作成してはならない。
  PRを作った後の進捗監視もしない。
- PRのその後は人間が見る。あなたは気にしなくてよい。
- 仕事が終わったら、その場でセッションを終了する。
「やりすぎるな」ではなく「この手段を使うな」と手段で禁じるほうが、無人実行では効く

ポイントは、「監視しなくていい理由」まで書いたことだ。 ただ禁止すると、AIは律儀さゆえに別の抜け道を探そうとする。 「その後は人間が見る」と役割分担を明示すれば、監視すること自体が仕事から外れる。

加えて、事故の経緯そのものをリポジトリのREADMEに残した。 次に同じ症状が出たときの捜査手順として書いてある。

7. どうすれば止められたのか

ここまでが実際に打った手だが、正直に言えば いま入れた歯止めは、いちばん弱い層にある。 プロンプトのルールは「指示」でしかなく、守るかどうかはモデル次第だからだ。 事故のあとに整理した、効く順の4層を書いておく。

強  ① 能力を与えない      … そもそも予約する道具を持たせない
↑  ② 回数を持たせる      … 予約を許すなら、残り回数を数えさせる
│  ③ 痕跡を必ず残させる   … 予約したら1行でも報告させる
弱  ④ 言葉で禁じる        … 「やるな」と書く(今回入れたのはここ)
下に行くほど手軽だが、破られやすい。上に行くほど確実だが、機能も削る

① 能力を与えない——これがいちばん確実だった。 自己予約は、ルーチンに繋がっていた管理用のコネクタ経由で実行されている。 そのコネクタを外していれば、そもそも呼び出せない。 指示で禁じるのではなく、到達不能にする。 皮肉なのは、今回私が別件の検証のためにルーチンの使用可能ツールを 制限リストから既定のフルセットに開放していたことだ。 暴走の直前、自分の手で能力を広げる方向に動いていた。

② 回数を持たせる——監視をどうしてもさせたいなら、 再アーム時のプロンプトに残り回数を書き込ませ、毎回1つ減らす。 ゼロになったら終わり。 「変化がなければもう一度」は終了条件のない無条件ループだが、 「あと2回だけ見る」は必ず終わる。 自己参照を許すなら、減っていく数を必ず1つ持たせるのが鉄則だと思う。

③ 痕跡を必ず残させる——今回の致命傷は、実は 「ユーザーへの通知・コメントは不要」という一文だった。 もしIssueに1行でもコメントしていれば、1時間で気づけた。 2日も走り続けたのは、静かだったからにほかならない。 無人実行では「黙って気を利かせる」を美徳にしてはいけない。

逆に、深掘りルーチンの起動失敗がすぐ判明したのは、失敗をIssueにコメントさせていたからだった。同じ「報告させる」という設計が、片方では機能し、片方では意図的に外されていた。無人で動くものに黙る自由を与えると、成功も失敗も等しく見えなくなる。

そもそも新しいスケジュールを作らせない

4層とは別に、設計として効いたはずの選択肢がひとつある。 PRの様子を見たいなら、次の定期実行のついでに見ればいい

今回のAIは「PRを見届ける」ために新しい発火点を作った。 だが同じ目的は、既に4時間おきに回っているtriageに相乗りさせれば達成できる。 発火点が増えないので、コストの上限が構造的に決まる。 「気になったら見に行く」というpush型の監視を、 「定期点検のついでに確認する」というpull型に変換する—— 無人システムでは、こちらのほうが圧倒的に安全だ。

そして、人間側の滞留が燃料だった

最後にこれは自分に返ってくる話なのだが、 このループが2日も続いた前提条件は、PRが未マージのまま溜まっていたことだ。 監視対象が動いていれば、ループは「マージ済み」を検知して自分で終わっていた。 実際そのプロンプトには「マージ・クローズ済みなら何もせず終了してよい」と書いてある。

人間ゲートの詰まりが、そのまま暴走の燃料になっていた。 設計ドキュメントのPRを4本溜めていたのは私だ。 自律パイプラインを回すなら、 人間側の処理速度も系の一部として設計に入れないといけない—— という、いちばん耳の痛い教訓が残った。

8. 消えたトークンの追い方

今回いちばん実用的な学びは、探す場所だった。

ルーチンの管理画面を開いても、普段は登録した2本しか見えない。 暴走していたものは全部「実行済み」なので、 「完了済みを含める」というトグルの裏に隠れていた。 オンにした瞬間、185件がずらりと出てきた。

Issueを投げていない日にトークンが減っていたら、まず完了済みのルーチン一覧を見る。身に覚えのない名前が1時間刻みで並んでいたら、それが犯人だ。使用量のグラフを眺めていても、この形は絶対に見えない。

なお、実行済みのルーチンは無効化された履歴レコードなので、 残っていても再発火はしない。気持ち悪いが、消さなくても実害はない。 むしろ再発の早期警戒装置として残しておくほうが役に立つと考えている。

9. まとめ

今回の犯人は、私が書いたルールを破ったわけではなかった。 ルールが想定していなかった方向から入ってきただけだ。 そこが一番怖い。

  • 暴走は「おかしな判断」ではなく「まっとうな判断+終了条件の不在」で起きる
  • 人間待ちのものを監視させてはいけない。終了条件が原理的に存在しないから
  • 「将来の自分を予約する」能力は、外部イベント駆動より危ない。外から止まらないから
  • 止めたいなら、言葉で禁じる前に能力を取り上げる。許すなら減っていく回数を持たせる
  • 新しい発火点を作らせない。既存の定期実行に相乗りさせれば、コストの上限が構造的に決まる
  • 静かな失敗ほど発見が遅れる。無人実行では、失敗時にこそ声を上げさせる
  • 消えたトークンは「完了済み」の裏を見る。使用量のグラフには形が出ない
  • 人間ゲートの詰まりは、そのまま暴走の燃料になる。自分の処理速度も系の一部

とはいえ、この件でパイプラインを畳もうとは思っていない。 暴走した力と、狙いどおり働いている力は、まったく同じものだからだ。 頼んでいないことを勝手に進めてくれるから、寝ているあいだに設計ドキュメントが仕上がる。 勝手に進むという性質そのものを削れば、価値も一緒に消える。

だから対策は「自律性を下げる」ではなく、「終わり方を教える」に寄せた。 どこまでやるかではなく、どこでやめるか。 無人で長く回すものほど、開始条件より終了条件のほうが設計の本体なのだと思う。