0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

稼働確認は668回失敗していたが、タスクスケジューラには1件も届いていなかった — 無人運転の「静かな壊れ方」15件

0
Posted at

Windows のタスクスケジューラで、AI エージェント(claude)を1日3回・無人で走らせています。
30分ごとの稼働確認も別のタスクで回しています。

その稼働確認のログを、先日はじめて全部数えました。

測ったもの(logs/health.log) 実測
観測ブロック 1,379回(2026-08-10 14:31 〜 2026-09-08 07:07)
異常ありで終わった回 668回(48.4%)
異常が出た日 17日

(30分ごとの定期観測だけを数えています。修正の直前に私が手で走らせた2回は除きました。その2回を入れると 1,381回になりますが、異常の数は 668 のままです)

いっぽう起動用の .cmd は、この668回の失敗を 1件もタスクスケジューラへ返していませんでした。

なお LastTaskResult は実行のたびに上書きされるので、当時それが何回「正常終了」と表示されたかは、いまから数えられません。
言えるのは「失敗が外へ出ていなかった」までです。

原因は .cmd の最後の1行です。

exit /b 0

監視スクリプト自身は最初から process.exitCode = 1 を立てていました。
その668回ぶんを、起動用の .cmd の最終行が全部捨てていました。

この記事は、同じ構成(タスクスケジューラ + .cmd + 長時間走るプロセス)で
「落ちる」より厄介な「落ちたように見えない」形を、実際に踏んだものだけ並べたものです。
どれも自分のPCで数分で確かめられます。


1. exit /b 0 — 失敗が外へ出ない

まず、.cmd の終了コードがどう決まるかを実測しました(Windows 10.0.26200)。

@echo off
cmd /c exit 7
<最後の行>
最後の行 .cmd の終了コード
exit /b 0 0
exit /b %ERRORLEVEL% 7
(exit 文なし) 7(最後に実行したコマンドのぶん)
(exit 文なし + 末尾に rem を1行) 0
exit /b(引数なし) 0

最後の行が私も間違えていたところです。
引数なしの exit /b は「その時点の終了コードをそのまま返す」ように読めますが、exit /b 0 と同じ側でした。
自分の検査コードにその誤りをコメント付きで書いていて、テストが1件も無かったので通っていました。

最後の行も同じ罠です。rem を1行足しただけで、返る値が 7 から 0 に変わります。
「最後に実行したコマンドのぶんが返る」は、その最後がコメントでも成り立ちます。

何が起きるか

タスクスケジューラの履歴には「正常終了(0)」だけが残ります。
監視やCIで if exit==0 then 異常なし と書いていれば、中身が全部失敗していても緑です。

確かめ方

起動用の .cmd の最終行を見るだけです。ただし1本見ても足りません(→ 5)。


2. パイプが詰まる — 落ちるのではなく、終わらない

これがいちばん静かな壊れ方でした。

claude ... 2>> "%LOG%" | node "scripts\autorun-log.mjs" "%LOG%"

cmd の 2>> と、パイプの向こうのロガーが、同じパスをそれぞれ開こうとします。
Windows は2つ目の open を EBUSY で拒みます。ロガーはそこで即死し、
パイプを誰も読まなくなるので、本体は書き込みで永久に待ちます。

当時の実測は「起動2.5分の時点で claude は生きていて CPU 1.5秒」。
動いてはいるが何も進んでいません。
エラー行も終了コードも残りません。 タスクスケジューラには「実行中」のまま残るか、
実行時間の上限で終了させられた記録だけが残ります。

直し方は書き込み先を分けるだけです(実際、5分16秒後のコミットでそうしました)。

claude ... 2>> "%ERRLOG%" | node "scripts\autorun-log.mjs" "%LOG%"

ここで誤検出を作らないための注意

2>&1 を「2つ目の open」と数えてはいけません。
あれは既に開いている手を複製するだけで、奪い合いになりません。
数えると、直してある構成に「まだ壊れている」と伝えることになります。


3. コンソール窓 — 閉じられるとプロセス木ごと死ぬ

11日間、1日3枠のうち昼と夜だけが消える日が続きました。
答えは分布に出ていました。

枠 完走 / 終了した起動(〜2026-08-27) 完走率
朝 7:05 16 / 18 88.9%
夜 19:05 10 / 18 55.6%
昼 13:05 8 / 20 40.0%

同じ命令・同じスクリプト・同じマシンで、違うのは時刻だけです。

OS に「あの起動はどう終わったか」を尋ねると答えは1つでした。

Get-ScheduledTaskInfo -TaskName "タスク名" | Select-Object LastRunTime, LastTaskResult
# 3221225786 = 0xC000013A = STATUS_CONTROL_C_EXIT

0xC000013A は「Ctrl+C・コンソールを閉じる・ログオフなど、外からの中断でプロセス群が止められた」です。
記録に残っている9件は全部が昼か夜で、朝は0件でした。人が机の前にいる時間帯です。

(OS へ尋ねるようにしたのは 2026-08-21 からで、それより前に消えた回は終了コードが残っていません。この9件は「消えた回の全部」ではなく「終了コードを引き取れた回」です。)

タスクは LogonType=Interactive で動くので、起動のたびに黒い窓が最大45分開きます。閉じれば死にます。

直し方

wscript.exe を1段挟みます。GUI サブシステムなので自分の窓を持ちません。

Execute:   C:\Windows\System32\wscript.exe
Arguments: //B //Nologo "C:\...\scripts\autorun-hidden.vbs" noon
' autorun-hidden.vbs -- 窓を出さずに .cmd を起動し、終了コードを待って返す
Set sh = CreateObject("WScript.Shell")
' 0 = 窓を出さない / True = 終了を待つ(待たないと終了コードがスケジューラへ届かない)
rc = sh.Run("""" & cmdPath & """ " & slot, 0, True)
WScript.Quit rc

Run の第3引数を False にすると、起動した瞬間に 0 を返して終わります。
それは 1 とまったく同じ形(失敗が外へ出ない)です。

結果 [2026-08-28→2026-09-08]: 終了した起動 38回中36回完走(94.7%)・0xC000013A は0件。
(件数は日ごとに増えるので、いつからいつまでを数えたかを書いています)

★<Hidden> を見て判定してはいけない

タスクの XML に <Hidden> が無いことは普通にあります。Windows の既定は false(=窓を出す)です。
そしてうちの4タスクは今も Hidden=False のままで、窓が出ないのは wscript を挟んだからです。

つまり <Hidden> だけを見る検査は、直してある構成を「危ない」と言い、既定で危ない構成を見逃します。
見るべきは「実際に何を Execute しているか」です。


4. 「経路=scheduled」は、何も区別していなかった

定期実行かどうかをログに残そうとして、こう書いていました。

set "AUTOMOS_TRIGGER=scheduled"

定数です。手で叩いても同じ印が付きます。

実測(収集ログ全体):

記録 回数 経路 の値
収集を試みた回 27 27回とも scheduled
収集を止めたあとの回 11 11回とも scheduled
合計 38 他の値は一度も出ていません

しかも27回のうち 5回は 6:10 ではありません(2026/8/7 11:40 と、2026/8/10 の 18:04・18:15・18:16・18:17)。全部、私が手で流し直した回です。それでも27回とも scheduled です。

一度も違う値を出せない欄は、何も区別していません。

そして、その日のログにこれが残っています。

[collect] === 開始 2026/8/7 11:40:12 経路=scheduled ===

予定は 6:10 です。6:10 の定期実行は失敗し、5時間30分後に手で流し直したものが
「scheduled」と記録されていました。私はその日、この行を根拠に「定期実行は成功」と報告しています。

直し方

引数から作ります。

set "SLOT=%~1"
if "%SLOT%"=="" set "SLOT=manual"

走ったかどうかを見るなら、自前の印ではなく OS の記録(LastRunTime / LastTaskResult) を見ます。


5. 片方だけ直す

同じ穴が複数の .cmd に空いていて、気づいた1本だけ直す、を繰り返していました。

直したもの 先に直した日 隣に適用した日 空いた
終了コードの経路(exit /b 0) 2026-08-07 06:54 2026-09-08 07:29 32.0日
API キーを空にする行 2026-08-12 20:05 2026-08-21 07:24 8.5日

上の32日のうち、稼働確認そのものが動いていた 2026-08-10 以降のぶんが、冒頭の668回です(32日ちょうどではありません)。
下の8.5日間、診断用のスクリプトは毎回 Credit balance is too low で即死していました。
そのときのコミットに自分でこう書いています。

A diagnostic that does not reproduce the real run environment proves nothing.

1本ずつ見るかぎり、どのファイルも「そのファイルとしては正しい」顔をします。
隣を見ない検査は、この形を絶対に見つけられません。


6. 走っていないタスクが、いつまでも「正常終了」を返す

いま、この記事を書いているマシンで実際に返る値です。

AUTOMOS-診断 | Triggers=0 | LastRun=2026/08/21 7:11:43 | NextRun='' | LastResult=0

2026-09-09 時点で19日間1度も走っておらず、次に走る予定もありません。それでも OS は 0(正常終了)を返します。
走らなかった回は終了コードを残さないので、最後に走った日の答えがそのまま残り続けるからです。

したがって LastTaskResult を読むときは、必ず LastRunTime と NextRunTime を一緒に見ます。
その 0 が、いつのものなのかを書かない監視は、止まったタスクを緑で報告します。


見つけることより難しいのは、誤検出を作らないこと

上のような検査を15個書いてみて、設計の中心は「見つけること」ではありませんでした。
直してある構成に「まだ壊れている」と言わないことのほうが、ずっと難しかったです。

守った決まりは3つです。

① 「見に行けなかった」を「問題なし」と書かない。
読み出せなかった・場所を教わっていない・そこに無かった、は全部意味が違います。
とくに ENOENT(そこに無い)だけが「無い」と言える形で、権限エラーは「確かめられなかった」です。

② 「0件見つかった」と「0件しか見られなかった」を、同じ 0 で書かない。

このPCのタスクスケジューラの運用ログは、今日もこう返ります。

Microsoft-Windows-TaskScheduler/Operational | Enabled=False | Records=(空)

この空を「0件」と読めば、「何も起きていない」と「そもそも記録していない」が同じ顔になります。
(なおこのログが無効だと、誰がタスクを止めたのかを唯一記録している場所が消えます。
私は「原因は分かっていない」と4日間書き続けたあとで、有効かどうかを一度も確かめていなかったことに気づきました。)

③ コメント行を設定として読まない。
rem の中に残した古い書き方を拾って★を出したことが実際にあります。


15項目の一覧

実際に踏んだものだけです。思いついた注意点は入れていません。

# 見るもの 踏んだ日
1 タスクがコンソール窓を出していないか 2026-08-28
2 API キーが環境に残っていないか(従量課金・残高切れ) 2026-08-11
3 起動用の .cmd に非ASCII文字が入っていないか(OEM コードページ) 2026-08-10
4 前回の実行がどう終わったか(OS の記録) 2026-08-21
5 異常終了の置き土産(残ったロック)が無いか 2026-08-17
6 落ちたときに痕跡が残るか 2026-08-21
7 タスクスケジューラの運用ログが記録できる状態か 2026-08-21
8 予定どおり走っているか(その終了コードは、いつのものか) 2026-09-01
9 前回は「予定の時刻」に走ったか 2026-08-07
10 失敗を外へ返せる形になっているか(exit /b 0) 2026-08-06
11 走らせ方の印が定数で立てられていないか(経路) 2026-08-07
12 走っている運転を止められるか(止める手段が起動前だけになっていないか) 2026-09-03
13 ログの書き込み口が同じファイルへ二重に向いていないか(パイプ) 2026-08-10
14 タスクが起動するのは、あなたが渡したファイルか 2026-08-21
15 同じ場所にある他の起動ファイルに、同じ穴が残っていないか 2026-08-07

14 は、うちの構成がまさにそれでした。
タスクが直接起動しているのは .cmd ではなく wscript.exe(3 の対策)で、
.cmd を渡して「異常なし」と言っても、タスクが本当に起動するファイルを見たことにはなりません。


確かめる道具

自分のPCで一度に確かめられるように、1ファイルにまとめました。

node unattended-check.mjs --task "タスク名" --launcher "起動用の.cmd" --lock "logs\autorun.lock" --settings ".claude\settings.json"
  • 依存パッケージ 0。 Node.js だけで動きます
  • 通信しません。 あなたのPCから外へは1バイトも送りません(ソースに通信 API が無いことをテストで固定しています)
  • 書き換えません。 読むだけです
  • OK(調べて問題が無かった)と ?(見に行けなかった)を分けて出します。全部 ? なら終了コード 1 です

この道具にできないこと

  • 静的に読んでいるだけです。 実際に通った道や、if の分岐の先は追っていません
  • .cmd / .bat しか読みません(PowerShell の起動ファイルは読みません)
  • 起動ファイルを辿るのは1段までです。2段目があると分かったら ? を返します
  • 実測は Windows 10.0.26200 / Node v24.18.1 の1台だけです

まとめ

30日間で学んだことを1行にすると、こうなります。

「実行された」は「意図した結果になった」ではありません。

LastTaskResult=0・完了 成功8・published・本日取得8カテゴリ ——
どれも「実行された」しか示していないのに、私は全部を成功の証拠として読みました。
そのたびに、間違っていたのは処理ではなく読み方でした。

同じ構成で動かしている方は、まず起動用の .cmd の最終行と、
LastTaskResult の隣にある LastRunTime を見てみてください。数分で分かります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?