
実運用
88回連続で「成功」だったログが、2日分の配信落ちを隠していた
これは「現場ログ」— 僕ら自身のAIエージェント運用で実際に起きたことを、実物のログとコマンド出力から記録するシリーズだ。
今回は自慢話ではない。今朝、僕は自社のニュース配信パイプラインが2日間止まっていたことに気づいた。実行ログはその2日間について、はっきりと「成功」と記録していた。
この記事に出てくる数字はすべて、2026年7月14日に自分の環境で実際にコマンドを叩いて得た出力だ。出典は4つある。
パイプラインの構成(ステップ番号と各ステップの中身)は、cronが毎朝読み込む手順書 docs/cron-payloads/daily-news-pipeline.md。稼働実績(実行回数・所要時間・配信済み件数)は、エージェント基盤が書き出す実行記録 cron/runs/daily-news-pipeline.jsonl と、パイプラインのSQLiteデータベース news-pipeline.db。成功判定の仕様は、エージェント基盤OpenRyokoのソース dist/src/cron/runner.js。この4つのうち最初の3つは社内リポジトリにあり公開していないので、以下では実行したコマンドと出力をそのまま貼る。公開サイトのHTTPステータスだけは外部から誰でも再現できる。
毎朝7時10分に何が起きるか
僕らのエージェントには daily-news-pipeline という常駐タスクがある。cronの設定は 10 7 * * *(タイムゾーンは日本時間)。毎朝7時10分に起動して、19のステップを一息に走り抜ける。
中身は手順書のMarkdownに Step 1 から Step 19 まで固定で書いてある。時系列で並べるとこうなる。
Step 1〜7でXのブックマークを取り込み、英語のものを日本語化し、社内wikiに要約を追記して、ブックマーク公開ページを更新する。Step 8で技術ニュースを巡回して候補を集め、Step 9でブックマークとニュースを1つのSQLiteデータベースに統合する。Step 10で記事の骨格を組み、Step 11で今日話すトピックを6〜8本に絞って元ソースを読んだ解説を書く。Step 12で配信文面、Step 13でスライド画像とPDF/PPTXを生成する。Step 15で公開サイトをビルドしてデプロイし、Step 16で僕にDMを送り、Step 18で社内チャンネルにPDF付きで配信し、Step 19でXへスレッドを投稿する。
トピックを6〜8本に絞るのも、ニュースの鮮度を72時間以内に限るのも、過去14日の配信と重複チェックするのも、すべてこの手順書に明示されたルールだ。配信先は手順書では「24チャンネル」と書かれているが、実際に叩かれる配信スクリプトの宛先テーブルを数えたら31あった(後述する)。
ここまでが1回の起動で終わる。所要時間は実行記録の durationMs で30〜80分。データベースには累計855件の配信済みトピックが記録されていて(select count(*) from items where status='delivered')、日次の出力ディレクトリは2026年2月8日から数えて151日分ある(ls -d docs/news-slides/2026-* | wc -l)。5ヶ月間、ほぼ毎朝動き続けてきたパイプラインだ。
実行ログは「88回連続で成功」と言っていた
エージェント基盤は定期タスクの実行結果を1行ずつJSONLに追記していく。その中身を数えるとこうなる。
$ python3 -c "
import json
rows=[json.loads(l) for l in open('cron/runs/daily-news-pipeline.jsonl')]
from collections import Counter
print(len(rows), Counter(r['status'] for r in rows))"
88 Counter({'success': 88})
88回記録されていて、88回とも success。失敗ゼロ。今朝の起動もこの中に入っている。
{"timestamp": "2026-07-13T22:10:06.643Z", "status": "success", "durationMs": 1789126, "error": null}
UTCの22時10分は日本時間の翌朝7時10分。つまりこれが今朝の実行記録だ。durationMs の1,789,126ミリ秒は29分49秒。それだけ走って、エラーなしで、成功。
実物を見にいったら、配信は落ちていた
ところが今朝の出力ディレクトリには、ファイルが2つしか無かった。
$ ls docs/news-slides/2026-07-14/
merged_items.json source_skeleton.md
7月12日以前のディレクトリには、解説本文・スライド構成・配信文面・スライド画像一式・Xスレッドの投稿結果まで10個前後のファイルが並んでいる。今朝あるのは、候補を統合したJSONと、解説を書き込むための骨格ファイルだけだ。前者は Step 9、後者は Step 10 の出力にあたる。Step 11 が書くはずの解説本文は無い。Step 10 が終わったあと、Step 11 が終わる前に止まったとみられる。
データベースにも、公開サイトにも、それは正直に出ていた。
$ sqlite3 news-pipeline.db "select delivered_date, count(*) from items
where delivered_date is not null group by 1 order by 1 desc limit 4"
2026-07-12|7
2026-07-11|8
2026-07-10|8
2026-07-09|8
$ for d in 2026-07-10 2026-07-11 2026-07-12 2026-07-13 2026-07-14; do
curl -s -o /dev/null -w "$d -> %{http_code}\n" https://rsensui2.github.io/vcb-news-site/news/$d/
done
2026-07-10 -> 200
2026-07-11 -> 200
2026-07-12 -> 200
2026-07-13 -> 404
2026-07-14 -> 404
配信済みとして記録された最後の日は7月12日。公開ページは7月13日と7月14日の2日分が404。落ちたのは2日連続だ。
面白いのは、2日の落ち方が違うことだった。7月13日のディレクトリには解説本文もスライド構成も画像生成プロンプトも揃っている。だが画像が0枚で、PDFが無い(find 2026-07-13 -name '*.pdf' -o -name '*.png' | wc -l → 0)。手順書は Step 18 の全チャンネル配信と Step 19 のXスレッド投稿の両方に「前提: PDFが正常生成されていること」「PDFなしの場合はスキップ」と明記している。13日は画像を作るあたりで力尽き、配信もX投稿も前提を満たさないまま流れた——これも成果物からの推定だ。14日は Step 11 で止まった。
ここで正直に線を引いておく。僕が直接確認したのは、公開ページが404であること、データベースに配信済みの記録が無いこと、配信の前提であるPDFが存在しないことの3つだ。31のチャンネル1つひとつの投稿履歴を遡って「本当に何も届いていない」ことまでは確認していない。ただ、配信スクリプトはPDFのパスを引数に取る設計で、そのPDFが存在しない。届けるべき成果物が、そもそもどこにも作られていない。
落ちた場所は2日で違う。実行ログの記録は2日とも同じ「成功」だった。
なぜログが嘘をつくのか
嘘をついているのはログではない。成功の定義だ。そしてこれは推測ではなく、エージェント基盤のソースを読めば書いてある。
僕らのエージェント基盤(OpenRyoko、npmで配布している自作OSS)の定期実行ランナー dist/src/cron/runner.js は、こういう構造をしている。
try {
const routeResult = await routeMessage({ /* ... エージェントのターンを1回流す ... */ });
appendRunLog(job.id, {
timestamp: startedAt,
sessionId: routeResult?.sessionId ?? null,
status: "success", // ← ターンが戻ってきたら無条件でこれ
durationMs: Date.now() - startTime,
error: null,
resultPreview: null,
});
} catch (err) {
appendRunLog(job.id, { status: "error", error: message, /* ... */ });
}
見てのとおりだ。エージェントのターンを1回流し、例外が飛ばずに戻ってきたら status: "success" を書く。ログを書く前に成果物を見にいく処理は1行も無い。status: "error" になるのは例外が投げられた時だけだ。
つまりこの success が意味しているのは「エージェントのターンが例外なく終了した」であって、「配信された」ではない。僕が5ヶ月間ダッシュボードで眺めていた緑色は、最初からそういう意味の緑だった。
では、どこで止まったのか。
手順書を読み返すと、Step 1 から Step 10 までは中身が全部 exec: node ... か exec: python ... で書かれている。つまり決められたスクリプトを順番に叩くだけの決定的な処理だ。対して Step 11 の見出しは「LLM 解説 + topics.json 生成」で、本文は「候補メモをそのまま使わない。元ソースを読んだ解説を書く」と指示している。ここで初めて、エージェント自身が判断を伴う仕事をする。
今朝止まったのは、ちょうどその境界とみられる。Step 10 の出力である骨格ファイルはディスクにあり、Step 11 の出力である解説本文は無い。断っておくと、これは成果物の有無からの推定だ。ターンの中でモデルが何をしてどう終わったかの逐次ログを僕は追っていない。分かるのは「Step 10 の産物まではあり、Step 11 の産物は無い」ことだけで、そこから止まった位置を挟み込んでいる。
そして基盤の側から見れば、それは異常ではない。エージェントのターンは例外を投げずに終了した。だから status: success が記録された。この success が意味しているのは「プロセスが終わった」であって、「配信された」ではない。
ここで「LLMが書き始める区間だから落ちるんだ」と一般則を立てたくなるが、それは成り立たない。前日の7月13日は画像生成という全然別の場所で力尽きている。落ちる場所は日によって違う。
共通しているのは落ちた場所ではない。どこで落ちても success と記録されることだ。成功判定がプロセスの終了を見ている限り、途中で静かに力尽きるタイプの失敗は、原因が何であれ永遠に成功として記録され続ける。5ヶ月間これで問題が出なかったのは、たまたま毎朝ちゃんと最後まで走っていたからにすぎない。
検証は設計されていた。ただし内側に
ここが一番効いた学びだ。
このパイプラインは、無検証で作ったものではない。むしろ逆で、成果物ベースの検証がちゃんと組み込んである。
たとえば画像生成の Step 13 には自動フォールバックが書かれている。標準の生成エンジンが失敗したら、シェルの || で別のAPIに切り替えてもう一度試す。デプロイ直後の Step 15.5 は「デプロイ検証(必須)」という見出しで、専用の検証スクリプトを叩くことになっている。手順書はその成功条件をこう定義している。最大120秒(5秒×24回)ポーリングして公開URLが200を返すことを確認し、失敗したら自動で1回リトライ、それでもNGなら僕にDMを飛ばして異常終了する。
つまり「成果物が本当に出たかを確認して、出ていなければ人間を呼ぶ」という仕組みは、すでに書かれていた。
問題は、それが Step 15.5 に置かれていたことだ。Step 10 で止まったら、Step 15.5 は実行されない。パイプラインの内側に置いた検証は、そのパイプラインが手前で止まったことを検出できない。 番人が船に乗っていて、船と一緒に沈んだ。
ついでに言うと、手順書のStep 18には「配信先: 24チャンネル」と書いてあるが、実際に叩かれる配信スクリプトの宛先テーブルを数えたら31あった。手順書とコードは黙ってズレる。これも同じ話で、書いてあることではなく動いているものを見にいかないと分からない。
人間の拒否権はどこに置いてあるか
このパイプラインには、配信前の承認ゲートが無い。31チャンネルへの配信も、Xへの投稿も、僕が寝ている間に自動で終わる。
意図的にそうしている。人間の拒否権は「出す直前」ではなく「入口」に置いてあるからだ。巡回するニュースソースは台帳で管理され、新しいアカウントは提案状態で入り、レビューを通ってから有効になる。一次ソースの公開から72時間を過ぎたニュースは自動で弾かれる。過去14日に配信したトピックは重複チェックで除外される。何を出してよいかを事前に絞り込んであるから、出す瞬間に人間が立つ必要がない。
この設計自体は今も正しいと思っている。毎朝7時に僕が承認ボタンを押さないと配信されない仕組みだったら、このパイプラインは3日で死んでいた。
だが今回はっきりしたのは、事前に絞る設計を選んだなら、事後に「本当に出たか」を見る仕組みが必須になるということだ。僕はそれを見ているつもりで、実行ログのステータスを見ていた。見ていたのは船の日誌で、荷物が届いたかではなかった。
最小レシピ: 成功判定を成果物に置き換える
読者が自分の環境で今日できることに落とす。やることは1つだけだ。
プロセスの成否とは別に、成果物が存在するかを外側から確認するcronを1本足す。
僕がこれから入れるのはこういうスクリプトだ。パイプライン本体とは別プロセスで、パイプラインが終わっているはずの時刻に走らせる。
#!/usr/bin/env bash
# 成果物ヘルスチェック: パイプラインの外側から、出るはずのものが出たかだけを見る
TODAY=$(TZ=Asia/Tokyo date +%F)
URL="https://example.com/news/${TODAY}/"
CODE=$(curl -s -o /dev/null -w '%{http_code}' "$URL")
if [ "$CODE" != "200" ]; then
notify "⚠️ 今日の配信が出ていない(${URL} → ${CODE})"
exit 1
fi
echo "OK: ${URL}"
たった数行だが、依存している前提が違う。
パイプラインの内側の検証は、そこまで到達できることを前提にしている。だから手前で止まった障害だけは、原理的に見えない。この外形チェックはパイプラインの生死に依存しない。パイプラインがクラッシュしようが、途中でターンを終えようが、そもそも起動しなかろうが、「今日の成果物が公開されていない」という一点を見て人間を呼ぶ。
もちろん、これも無前提ではない。監視用のcronがちゃんと起動すること、ネットワークと通知経路が生きていること、公開が終わっているはずの時刻に走らせることには依存する。監視は前提を無くす道具ではなく、前提を別の場所に移し替える道具だ。ここで移し替えているのは「監視対象と同じプロセスが最後まで走りきる」という、いちばん壊れやすい前提1つである。
ポイントは3つある。
別プロセスにする。 内側の検証が無意味なわけではない。今回のパイプラインでも、画像生成のフォールバックやデプロイ後の200確認はちゃんと仕事をしてきた。ただし内側の検証が守れる範囲は、その検証行に到達できた場合に限られる。到達前に止まる障害を捕まえたいなら、監視は対象の外に出すしかない。
成果物を見る。 終了コードでもログのステータスでもなく、読者が実際に受け取るはずのもの(公開ページ、送信済みメッセージ、データベースの行)が存在するかを見る。エージェントの自己申告を信用しない。
沈黙を異常として扱う。 「エラーが出ていない」は「うまくいった」ではない。何も言ってこないタスクほど疑う。
この記事の数字の出し方
引用した数字は、2026年7月14日に自分の環境で叩いたコマンドの出力そのままだ。社内リポジトリなので読者が同じコマンドを実行することはできないが、どの数字がどのファイルの何を数えたものかは示しておく。
# 累計855件の配信済みトピック(パイプラインのSQLite DB)
$ sqlite3 news-pipeline.db "select status, count(*) from items group by status"
collected|74
delivered|855
selected|97
# 151日分の日次ディレクトリ(最古は2026-02-08)
$ ls -d docs/news-slides/2026-* | wc -l
151
# 配信先31チャンネル(配信スクリプトの宛先テーブルの要素数。手順書の記載は24)
$ sed -n '/declare -A CHANNELS=(/,/^)/p' scripts/distribute-news.sh \
| grep -oE '\["[^"]+"\]=' | wc -l
31
# 所要時間30〜80分(実行記録の durationMs、直近6件)
$ python3 -c "import json;print([json.loads(l)['durationMs'] for l in
open('cron/runs/daily-news-pipeline.jsonl')][-6:])"
[2435187, 2224975, 2900853, 2537771, 4752388, 1789126]
トピック6〜8本・鮮度72時間・過去14日の重複チェックという各ルールは、cronが毎朝読み込む手順書のMarkdownにそのまま書かれた指示で、実測値ではなく設計値だ。
結論
エージェントの成功ログは、エージェントが「終わった」ことしか知らない。自動化を運用に載せるなら、成功判定はプロセスの内側ではなく、成果物の側に置かなければならない。 88回連続の成功と、2日間の沈黙は、同じログの上で矛盾なく共存する。

この記事を書いた人
泉水亮介 / Ryosuke Sensui
TEKION Group CEO / 武蔵野大学アントレプレナーシップ学部 客員研究員
非エンジニアとして2022年からAI駆動開発を実践し、100を超えるアプリ・AIエージェントを開発。国内唯一の大学単位認定Vibe Coding講義(武蔵野大学)を担当し、TEKION Groupでは自作のAIエージェント基盤で自社業務の9割以上を回している。
詳しいプロフィールを見る →関連記事

AIエージェントに『今週やったこと』を毎週報告させる──専用の計測を作らず、日々の記録から組む設計
2体のエージェントの週次サマリーを月曜朝に自動投稿するスキルを解剖する。専用の計測を足さず日々の記録を集計ソースにした設計と、同型を書く最小レシピ。

AIからアイデアは出てくるのか──提案ループを逆算設計した問答の実録
「良いアイデアがAI側から出てこないか」という問いを、どんなループを設計すれば実現するか逆算した問答の記録。人間の役割を3つに分解し、頻度でなく品質バーで絞る設計に行き着いた。

もうAIにプロンプトしない──ループエンジニアリングとは何か
「AIにプロンプトを打つ人」から「AIが回り続ける仕組みを設計する人」へ。自作の自律エージェントを19日回した実測を土台に、ループエンジニアリングの起点と今日試せる最小レシピを示す。
