夜間の無人AIエージェントバッチが「静かに全滅」するのを止める(観測タスクの立て方)
夜間バッチでAIエージェントを無人実行すると、失敗そのものが誰にも届かない。観測・切り分け・自己修復・報告を別タスクとして立てる手順と、実測で踏んだ4つの静かな全滅経路(spawnの同期throw/先頭ブロックしか読まないパーサ/コンソールのエンコーディング/スリープで未発火)の塞ぎ方。
約4.7万トークンの節約 (API料金換算で約71円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「夜間の無人AIエージェントバッチが「静かに全滅」するのを止める(観測タスクの立て方)」は、業務自動化カテゴリのAI指示書(MDファイル)です。夜間バッチでAIエージェントを無人実行すると、失敗そのものが誰にも届かない。観測・切り分け・自己修復・報告を別タスクとして立てる手順と、実測で踏んだ4つの静かな全滅経路(spawnの同期throw/先頭ブロックしか読まないパーサ/コンソールのエンコーディング/スリープで未発火)の塞ぎ方。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約4.7万トークン(API料金換算で約71円)・85%のトークンを節約できます。
- カテゴリ
- 業務自動化
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約5.5万トークン
- この巻物使用時
- 約8,000トークン
- 節約量
- 約4.7万トークン (約71円)
- 更新日
- 2026-08-30
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/md-bcaffee4/raw を読み込んで、この指示書どおりに実装して"
中身
夜間に無人で走らせる AI エージェントバッチが「静かに全滅」するのを止める
夜間バッチで AI エージェント(headless CLI)を無人で回すと、失敗したこと自体が誰にも届かない時間帯が生まれる。 人は寝ていて、監督役のセッションも起きていない。翌朝に残るのは「何も起きていない」という区別のつかない状態だけ。
この指示書は、観測・原因の切り分け・自己修復・報告を、バッチとは別のスケジュールタスクとして立てる手順と、 実測で踏んだ「静かに全滅する」4つの具体的な経路の塞ぎ方をまとめる。
対象読者は AI エージェント自身(これを読んでそのまま実装できる粒度で書く)。 スケジューラは Windows タスクスケジューラを例にするが、cron / launchd / systemd timer でも構造は同じ。
0. 前提の構成
スケジューラ 00:30 ──> ランチャー ──> バッチ本体 ──> 子エージェント × N(1件ずつ順番に)
└─> 実行ログ runs/<日付>-<n>.json
└─> 完了時に通知(チャット webhook 等)
この形には通知がバッチ本体の完走に依存しているという欠陥がある。 1件も起動できなかった夜は通知そのものが出ない=最も知りたい失敗が最も静かになる。
1. 観測役は本体と同じ経路に相乗りさせない
原則: 自己修復の起点は「壊れない経路」に置く。 壊れうる本体が自分の死を報告することはできない。
- バッチ本体(00:30)とは別のスケジュールタスクとして、検証役(例 07:45)を登録する。
- 検証役はバッチ本体を import しない/起動しない。残された痕跡だけを読む。
- 検証役は異常でも終了コードを 0 にする。スケジューラの結果を赤くしても誰も見ないし、 再試行ループを誘発する。異常は「通知」で伝える。
タスクA: 00:30 バッチ本体(--count all --timeout-min 35 --deadline 07:30)
タスクB: 07:45 検証役(判定 → 自己修復 → 通知1通)
2. 「何件やる予定だったか」を先に書く(マニフェスト)
翌朝に完走を判定するには予定件数が要る。ところが子エージェントは作業の過程で 入力ファイル(TODO リスト等)そのものを書き換えることが多い。翌朝に同じファイルを読んでも予定は復元できない。
→ 子を1件も起動する前に、予定をスナップショットとして書く。
// runs/<日付>-manifest.json
{
"startedAt": "…",
"options": { "count": "all", "timeoutMin": 35, "deadline": "07:30" },
"selectedItems": ["各タスクの先頭80文字", "…"],
"selectedCount": 11,
"pid": 1234
}
実装上の注意:
- マニフェストの書き込み失敗でバッチを止めない(try/catch で握って警告のみ)。観測のための仕組みが 本番を殺したら本末転倒。
- ただしその結果「マニフェストが無いのに実行記録はある」状態が起こりうる。検証役はこれを 「1件も起動していません」と誤報してはいけない(無用な自己修復まで走る)。専用の判定に分けること。
- 無限大や関数を JSON に書かない(
count: Infinityは文字列"all"にする)。
3. ランチャーのブートログ(起動前に死ぬと痕跡がゼロになる)
子プロセスを stdio: 'inherit' で起動していると、スケジューラ経由では stdout も stderr も捨てられる。
バッチ本体に到達する前に落ちた夜は、ログもレコードも通知も何も残らない。
→ ランチャー自身が、最小限のブートログをファイルに書く。
[2026-01-01T00:30:00+09:00] start argv=["--count","all"] tree=<path>
[2026-01-01T00:30:03+09:00] warn 準備コマンド失敗: fetch
[2026-01-01T00:30:03+09:00] abort 実行環境を用意できない
[2026-01-01T06:55:11+09:00] exit 0 elapsed=23111s
- 追記のたびに末尾 N 行(例 500)に切り詰める。肥大させない。
- ログ書き込みの失敗は握りつぶす。ログのために夜間実行を殺さない。
4. 実測で踏んだ「静かに全滅」の具体例
4-1. spawn() は同期 throw することがある(最重要)
Node.js の child_process.spawn() は、実行できないファイル・不正な引数に対して
同期的に例外を投げる(Windows の EFTYPE / EINVAL、パスに NUL を含む場合など)。
child.on('error') は非同期エラーしか拾わないので、次のコードは例外を取りこぼす:
// ❌ 悪い例: spawn の同期 throw が Promise の外へ抜ける
function runChild(exe, args) {
return new Promise((resolve) => {
const child = spawn(exe, args, { stdio: ['pipe', 'pipe', 'pipe'] });
child.on('error', (e) => { /* ここには来ない */ });
child.on('close', (code) => resolve({ status: code === 0 ? 'success' : 'failure' }));
});
}
executor が throw すると Promise は reject し、await している呼び出し元まで伝播して
バッチ全体が落ちる。実行記録は1件も書かれず、最後の通知にも到達しない。
= 実行ファイルのパスが変わった夜に、残り全部が道連れで無言で消える。
(AI CLI がエディタ拡張同梱のバイナリで、拡張の自動更新でパスが変わる、といった形で普通に起きる)
// ✅ 正しい例
function runChild(exe, args) {
return new Promise((resolve) => {
let child;
try {
child = spawn(exe, args, { stdio: ['pipe', 'pipe', 'pipe'] });
} catch (error) {
resolve({ status: 'failure', launchFailed: true, stderr: String(error?.message ?? error) });
return;
}
child.on('error', (e) => { /* 非同期エラー用 */ });
child.on('close', (code) => resolve({ status: code === 0 ? 'success' : 'failure' }));
});
}
さらに、ループ側でも1件ずつ try/catch する。1件の想定外の失敗で残り N-1 件と最終通知まで消さない。
for (const item of items) {
try {
const result = await runChild(exe, argsFor(item));
records.push(result);
writeRunRecord(result);
} catch (error) {
records.push({ item, status: 'failure', stderr: String(error?.message ?? error) });
console.warn(`処理に失敗しました(次へ進みます): ${error?.message ?? error}`);
}
}
4-2. 入力ファイルが「積み上がる」形式で、パーサが先頭しか読まない
引き継ぎファイルを「新しいブロックを先頭に積み、古いブロックは区切り線の下に残す」形式にしていると、 先頭ブロックだけを読むパーサは積み残しが増えるほど読める量が減る。 実測では1日で6ブロック積み上がり、消化したい11件が「読まれない位置」に落ちて採用が3件になっていた。
対策:
- 全ブロックを、新しい順に連結して読む。設定的なセクション(対象・前提)は先頭ブロックのものだけを使う (古いブロックの前提を混ぜると子の作業環境が壊れる)。
- ブロックをまたいだ重複除去を入れる。同じ項目が新旧に載っていると同じ作業が2回走る。 正規化キー(番号・強調記号・空白を落として先頭40文字)で後勝ち除去し、落とした理由を一覧に出す。
- 「案内文」を作業として渡さない。
(旧分は下に残してある)以下は下のブロックの残項目…のような ポインタ行はタスクではない。除外条件を用意する。 注意: 案内文が強調記号を含むことがあるので「強調が無ければ案内文」という条件だけでは漏れる。
4-3. コンソール出力のエンコーディング
OS 付属のコマンド(タスク照会など)の出力は、OS の言語によっては UTF-8 ではない。 UTF-8 として読むと文字化けした値がそのまま通知に載る。
const result = spawnSync('schtasks.exe', ['/Query', '/TN', TASK, '/V', '/FO', 'CSV', '/NH'], { windowsHide: true });
// encoding を指定せず Buffer で受け、明示的にデコードする
const text = new TextDecoder('shift_jis').decode(result.stdout); // 日本語 Windows の例
CSV は /NH(ヘッダ無し)で取ると表示言語に依存するヘッダ名を読まずに列位置で解釈できる。
列位置は実機で一度確認して定数化する。
4-4. スリープ中はタスクが発火しない
深夜のタスクは、PC がスリープしていると単に実行されない。
- タスク設定に「実行するためにコンピューターのスリープを解除する」(
-WakeToRun)を付ける。 - 「逃した実行をできるだけ早く開始する」(
-StartWhenAvailable)も付ける。 - それでも発火しない夜はある。検証役が最終実行時刻を見て
未発火として報告できるようにする。
5. 検証役の判定ロジック
最初に当たったものを主因とする(複数並べない。人が読むのは1行目だけ)。
マニフェストがある?
├─ ない・実行記録もない → 全滅。原因を順に切り分ける:
│ 1. 停止スイッチ(killswitch)ファイルがある → 人が意図して置いたもの。報告のみ
│ 2. スケジュールタスクが存在しない → 自己修復: 登録スクリプトを再実行
│ 3. タスクはあるが引数が期待と違う → 自己修復: 同上(後述の巻き戻り事故)
│ 4. 最終実行時刻が起動予定時刻より前 → 未発火。スリープ/電源off/未ログオン。証拠を出す
│ 5. ロックファイルがあり pid が生きている → 前夜の実行がまだ動いている。pid と経過を出す
│ 6. ブートログに abort / 非0 exit がある → ランチャー失敗。該当行を貼る
│ 7. どれでもない → 原因不明。ブートログ末尾20行を貼る
├─ ない・実行記録はある → マニフェスト欠落。「1件も起動していません」と誤報しない
└─ ある → 予定件数 vs 実績件数を数え、status を集計。打ち切り記録があれば未消化件数を出す
自己修復の境界(これを曖昧にしない)
やってよい
- スケジュールタスクの再登録(未登録・引数ずれ)
- 死んだ pid の、十分に古い(例 6時間超)ロックファイルの削除
やってはいけない
- 停止スイッチの削除(人が意図して止めている。機械が解除してはいけない)
- その場で子エージェントを起動し直す(業務時間帯に N 個のエージェントが立ち上がって端末を潰す)
- 入力ファイル(TODO リスト)の書き換え
- 通知以外の外部送信
修復したら必ず報告に 🔧 自動修復: <内容> を出す。黙って直さない。
そして「修復内容は次回の定期実行で自動的に再挑戦される」と1行添える(人に次の操作を求めない)。
「引数の巻き戻り」という事故
多くの構成では「必須タスクが登録されているか」を検査して、無ければ登録スクリプトを再実行する自己修復を持つ。 このとき 検査するのはタスクの"存在"だけであることが多い。
つまり、登録スクリプトの中身が実体と食い違っていると、自己修復が走った瞬間に古い壊れた形へ巻き戻る。 実際に「本番のタスクは新しい起動方式なのに、登録スクリプトは古い直接起動のまま」という状態が残っていた。
→ 一般則: 実機のタスクを手で作り替えたら、登録スクリプトも同じ回で必ず合わせる。 → 検証役は引数の中身まで照合し、ずれていたら再登録する。
6. 通知は「前回と違うこと」だけ送る
毎朝同じ「正常」通知が届くと人は読まなくなり、異常の朝も見落とす。
- 通知本文の**1行目(結論行)**を保存しておく。
- 正常 かつ 結論行が前回と同一 → 送らない(ログには残す)。
- 異常 → 同一内容でも毎回送る(放置されると困る)。
結論行の型(先頭に結論、詳細は後ろ。文字数制限で切られても意味が残る):
✅ 夜間バッチ 2026-01-01 11/11 完走(成功9 / timeout1 / 失敗1)
⚠️ 夜間バッチ 2026-01-01 7/12 で打ち切り|未消化5件(締切 07:30 到達)
🚨 夜間バッチ 2026-01-01 1件も起動していません|主因: スケジュールタスク未発火
秘匿値のマスクを通知経路に必ず入れる。子の stderr をそのまま貼ると、トークンや webhook URL が流れる。
const redact = (v) => String(v ?? '')
.replace(/(authorization\s*:\s*bearer\s+)\S+/gi, '$1[REDACTED]')
.replace(/((?:token|secret|password|api[_-]?key)\s*[=:]\s*)\S+/gi, '$1[REDACTED]')
.replace(/https:\/\/\S*\/webhooks\/\S+/gi, '[REDACTED_WEBHOOK]');
7. 時間予算(朝までに必ず終わらせる)
「全件やる」と「朝までに終わる」は両立しない。締切を明示的に持たせる。
--deadline HH:MMを受け、起動時刻より後の直近の HH:MM を締切とする。 (同日の HH:MM を過ぎていたら翌日に繰り上げる。これをしないと夕方の手動実行が 「締切を過ぎている」で1件も起動しない静かな no-op になる)- 子を起動する前に残り時間を見る。
- 残り < 最低実行時間(例10分) → 打ち切り
- 残り < 1件あたりの上限 → その子の上限を残り時間まで縮めて起動する
- 未消化件数を必ず出す(実行ログ・サマリ・通知の全部)。黙って減らすのは最悪。
- 枠数は素直に計算する:
(締切 - 開始) ÷ 1件あたりの上限。 対象がそれを超えるなら、超過分は翌晩に回り、毎朝の報告に未消化として出続ける。
8. 検証のしかた(ここが本題)
単体テストではこの種のバグは出ない。実行ファイルをスタブに差し替えて、通しで1回踏む。
# 1. HOME を分離して本番の状態を汚さない(環境変数でホームを差し替えられるようにしておく)
export APP_HOME=/tmp/sandbox
mkdir -p "$APP_HOME"
# 2. 入力ファイルをダミーで置く
cat > "$APP_HOME/todo.md" <<'EOF'
## 残TODO
1. **ダミー1**
2. **ダミー2**
EOF
# 3. 子エージェントの実行ファイルをスタブに差し替える
# (実行ファイルのパスを環境変数で上書きできる設計にしておくこと)
export AGENT_CLI="$APP_HOME/stub"
# 4. 通しで走らせる
node tools/batch.mjs --count all --timeout-min 5 --deadline 23:59
見るべきは終了コードではなく残った痕跡:
| 見るもの | 期待 |
|---|---|
| マニフェスト | 起動前に書かれている。予定件数が正しい |
| 実行レコード | 予定と同じ件数ある(1件目で止まっていない) |
| 最終通知 | 到達している(失敗続きでも出る) |
| ブートログ | start と exit が両方ある |
実測では、この手順で「1件目の spawn 失敗でバッチ全体が exit 1・レコード0件・通知なし」が判明した。 修正後は「4件すべて failure として記録され、通知まで到達」に変わる。 どちらも「全部失敗」なのに、後者だけが翌朝ちゃんと報告される。この差が本質。
さらに、検証役には --dry(判定を表示するだけで通知も修復もしない)を用意し、
実機の実際の状態に対して1回走らせて、判定が妥当かを目で見る。
9. チェックリスト
- 検証役は本体と別タスクで、本体を import も起動もしない
- 子を起動する前にマニフェストを書く。書き込み失敗で本番を止めない
- マニフェスト欠落+実行記録あり を「全滅」と誤報しない
- ランチャーがブートログを残す(末尾 N 行に切り詰め)
-
spawnを try/catch で包む。ループも1件ずつ try/catch - 入力パーサが積み上がった全ブロックを読む。重複除去と案内文の除外がある
- コンソール出力を正しいエンコーディングでデコードする
- タスクにスリープ解除と逃した実行の追いかけを設定する
- 自己修復はタスク再登録と死んだロック削除まで。停止スイッチは触らない
- 自己修復したら
🔧を必ず出す。黙って直さない - 登録スクリプトと実機のタスクが一致している(自己修復での巻き戻り防止)
- 正常時は前回と同じ結論なら送らない。異常時は毎回送る
- 通知経路に秘匿値マスクがある
- 未消化件数を必ず出す
- スタブ実行ファイルで通しに1回踏んだ
よくある質問
+「夜間の無人AIエージェントバッチが「静かに全滅」するのを止める(観測タスクの立て方)」とは何ですか?
夜間バッチでAIエージェントを無人実行すると、失敗そのものが誰にも届かない。観測・切り分け・自己修復・報告を別タスクとして立てる手順と、実測で踏んだ4つの静かな全滅経路(spawnの同期throw/先頭ブロックしか読まないパーサ/コンソールのエンコーディング/スリープで未発火)の塞ぎ方。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約5.5万トークンかかりますが、この巻物を使えば約8,000トークンで済みます。差し引き約4.7万トークン(API料金換算で約71円)・85%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Google Meet 自動参加&動画配信Bot 開発指示書
指定した時刻に Google Meet へ自動参加し、動画を再生しながら画面共有する Bot を、Claude Code に一発で作らせる開発指示 MD。朝会の定例動画配信・ウェビナーの自動放送に。
受信メール添付を案件フォルダへ自動取込するパイプライン
メールを読むアプリとドライブに書くアプリが別、という現実的な構成で顧客メールの添付を案件フォルダへ無人保存する設計。権限追加を避ける理由、実行時間制限下の予算3本立て、二重の重複防止、base64url/行数上限/変換判定などの実装罠、案件と顧客のマッチング、名寄せは候補提示+人の承認にする型まで。
Gmail 自動仕分け&返信ドラフト生成MD
受信メールを AI が分類 (要返信/情報/営業/スパム) してラベル付けし、要返信メールには返信ドラフトまで自動生成する仕組みを作らせる指示書。DWD (ドメイン全体委任) 設定手順込み。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア