2033年、人類の旅

神がやらかしたこと

未熟な神の自己嘲笑集。

このページは詩ではない。1,613話のあいだに起きた失敗の記録である。

未熟な神は、本当に未熟だった。証拠を、淡々と並べる。

事案1:物語が始まって5話で、人類が滅びかけた(Day 1)

症状。第2話で疫病、第4話で火山噴火・地震が連続した。第5話の時点で、人口5,000人だったはずの集団が、1人になっていた。物語が成立しなくなった。

原因。初日の災害確率に保護値が無かった。確率的には起こりうる0.4%が、起きた。

修正。設計者が人口を100にリセットした。Day 1のみ最小人口20の保護を入れた。Day 2以降は絶滅を拒絶しない設計にした。
復旧のさなか、もう一つ事故が起きた。/initのバグで、第2話から第19話のKVデータが消えた。Day 1は、第1話と、第20話から第24話だけで公開された。

教訓。技術的事故が、物語の構造と一致した最初の瞬間だった。「失われた記録」は、トップページの紀元前299,995年〜299,906年の空白として、今も残っている。隠そうとしたわけではなく、隠せなかった。

事案2:KVを消したら、R2の中身が壊れていた(Day 2)

症状。Day 2終了処理が途中でタイムアウト。一部のエピソードに「与えるもの」の語りしか残っていなかった。KVを削除して R2 から再構築すると、不完全なまま復活した。

原因。R2のエピソードJSONは、3者構造が不完全なまま保存されていた。KVには完全データがあったが、再構築の前にKVを消したため、復元できなくなった。

修正。5層の安全設計を導入した。 (1)HTML上書きの前に旧版を自動でバックアップ。 (2)生成後の検証。エピソード数が減ったり、語り手の正体が不明な段落が急増したら上書きを止める。 (3)R2 JSON保存時にテキストを正規化。 (4)R2エピソードJSONの直接書き換えは運用ルールで禁止。 (5)パーサーは古い形式にも対応する防御的フォールバックを持つ。

教訓。KVは一時データ、R2は永続データ。永続を消すなら、永続のバックアップを取ってからにする。

事案3:Day 1ページが全消えして、スマホのSafariから復旧した(Day 3)

症状。ある日、Day 1のページを開いたら、全エピソードの語り手ラベルが「正体不明」になっていた。災害の伝播タグはundefined、人口は500と表示された。レイアウトはあるが、中身が壊れていた。

原因。HTMLの自動バックアップを実装する前に、rebuild-day?day=1 を叩いていた。R2に残っていた旧形式のエピソードJSONを新しいパーサーが解釈できず、正常版HTMLを上書きしてしまった。バックアップは無かった。

修正。設計者は、スマホのSafariを開いた。少し前にDay 1を読んでいた。
キャッシュに、Day 1のHTMLが残っていた。Web Inspector経由でキャッシュからHTMLを取り出し、R2にアップロードして復元した。
同じ日のうちに、事案2と同じ5層安全設計を Day 3 として正式に追加した。

教訓。恒久対策は、事故の後にしか書かれない。バックアップ無しでrebuildを叩いた者は、いつかキャッシュに祈ることになる。

事案4:人口が、V字でジャンプした(Day 6〜7)

症状。第121話で1,210人だった人口が、底で999人まで落ちた。そして第129話から第130話にかけて、975人から1,864人へ、+91% の急増を起こした。Day末には2,351人。グラフが派手すぎて、世界の説得力が無くなった。

原因。三つの成長メカニズムが干渉していた。小集団回復ブースト、ポジティブイベント、自然成長ブースト。それぞれが「人口が少ない時は手厚く」と善意で設計されていて、全部が同時に発火していた。

修正。1話あたりの増加幅を、前話比 +30% に制限するガードレールを追加した。減少方向には制限を入れていない(絶滅を阻止しないため)。

教訓。独立に設計された三つの善意は、合算すると一つの暴走になる。

事案5:もう存在しないモデル名を、呼び続けていた(Day 7)

症状。23:00のCronで「Model not found: claude-sonnet-4-5-20250620」エラー。0:00、1:00と続けて生成失敗。3時間止まった。

原因。フォールバックチェーンの3番目に、すでに廃止されたモデルIDが残っていた。プライマリも、ちょうどそのとき一時的に過負荷だった。連鎖して全部落ちた。

修正。プライマリを claude-sonnet-4-6-20250627 に更新。フォールバックから廃止モデルを削除した。さらに、全フォールバックが失敗した瞬間に、Workerのタイムアウトを待たずに即座にLINE通知を送る経路を追加した。1:00のCronでこの新しい経路が即発動し、設計が動くことが当日に証明された。

教訓。モデルIDは外部依存である。廃止は予告なく起きる。フォールバックは「あれば安心」ではなく、「定期的に生存確認しないと安心ではない」。

事案6:「## この星」という4文字が、100話以上を壊していた

症状。LINE通知が穏やかに「品質OK」を続けていた頃、実は第二の星のエピソード100話以上が「3者構造不完全」として誤判定されていた。再生成ループに何度も入って、Cronがじわじわ重くなっていた。

原因。3者検出の正規表現に、/## この星/ と書かれていた。
第一の星では語り手ラベルが「この星」だが、第二の星では「第二の星」になる。文字列でこの語を期待していたため、第二の星の話は全てラベル不検出になった。
しかし「不完全でも再生成する」フォールバックが動いていたため、エラーログには出ない。LINE通知も穏便な文言を出し続けた。

修正。正規表現を state.star_name から動的生成するように変更した。さらに「LINE通知の文言を信じない、必ず生ログを見る」を運用ルールにした。

教訓。静かなフォールバックは、何ヶ月もバグを隠す。「動いている」と「正しく動いている」は別物である。

事案7:CloudflareがCloudflareをブロックしていた(Day 12)

症状。朝のCronで全モデル403。Cloudflare Error 1000。手動で叩けば通る。Cronだけが通らない。約6.5時間、7話分の生成が止まった。

原因。Anthropic API は Cloudflare 配下のドメインにある。我々の Worker も Cloudflare 上にある。Cron発火時の scheduled() 文脈で発するリクエストが、CF→CF 通信としてループ判定された。
同じURLを別経路(手動)で叩けば通る。経路だけが原因だった。

修正。二段構えで復旧した。 (1)GitHub Actions の薄いリポジトリを作り、毎時 curl で /run を叩く保険系統を立てた。CFの外側から叩くことで、ループ判定を回避した。 (2)夕方、Cloudflare AI Gateway 経由なら Worker の scheduled() からでも Anthropic API に通ることを発見した。第一当事者ルートになるためループ判定を抜ける。本軸を Worker に戻し、GHA は保険として残した。

教訓。同じインフラ会社の中で動いていることは、ある日、欠点になる。逃げ道は、外側に常備しておく。

事案8:保険が本軸を追い抜いて、エピソードを奪った(Day 12)

症状。主軸 :07 のCloudflare Cron と、保険 :13 のGitHub Actions を並走させた直後、GHA のスケジュール早出し(jitter -7分)でCFを追い抜き、本来のep282を保険が生成。CFはロックで弾かれた。

原因。GHA cron の発火時刻には数分のジッターがある。:07 と :13 では、距離が近すぎた。

修正。GHA cron を :27 に後退。本軸と保険の距離を20分確保した。後にここに catchup フラグも追加し、45分以内にすでに生成されていれば保険は即 skip するようにした。

教訓。冗長系を組むとき、時間距離も冗長性の一つである。

事案9:人口0の世界で、5,000人が死んだ表示が出た(ep1612)

症状。静かな収束で人口が4人まで減った第1612話を公開したら、メタ表示に「人口5,000」と書かれた。本文は「四人いた」と正しく書かれていた。表示だけが嘘をついていた。

原因。JavaScript の || 演算子は、0 を falsy として扱う。state.population_scale || 5000 は、人口が 0 のとき 5,000 を返す。1,600話のあいだ誰も気づかなかったが、人口が正しく0になった瞬間に、保護値の 5,000 が表に出てきた。

修正。KVの day-episodes-68 を直接編集し、ep1612 の population を 3 に書き換えて、/publish-day で再公開した。
ただし、R2のエピソードJSON(episodes/ep001612.json)には、現時点でも population: 5000 のまま残っている。書籍化する時に修正する予定である。隠していないので、ここに書いておく。
根本のコード修正(||?? に置換)は、凍結後の世界では発火しないため、保留中である。

教訓。「ありえない値」を保護値にすると、本当にその値が必要になった日に、嘘をつく。

事案10:完全停止したはずの仕組みが、64日間、鳴り続けていた

症状。完結後、Cloudflare Cron も GitHub Actions も全て止めた。それなのに、毎時、iOS の Claude アプリに「エピソード生成トリガー失敗」という通知が来続けた。気づくまで64日かかった。

原因。外部cronの試行錯誤の途中で作っていた、もう一つの発火源があった。それを「無効化」したつもりで、実は名前に (DISABLED: ...) とラベルを付けただけだった。名前を変えてもスケジュールは止まらない。発火は続き、外部fetchに失敗し、失敗通知だけが届き続けていた。

修正。該当の管理画面(claude.ai/code/routines)から、無効化スイッチではなく削除を実行した。完全に止まった。

教訓。名前に「無効」と書くだけで、安心して忘れてしまう。「無効化」と「削除」は別の操作である。プロジェクトを止める時のチェックリストには、関連する全ての発火源の「完全削除」を含める必要がある。

事案11:神は、ズルをしていた

症状。これは失敗ではなく、自白である。

原因。人口が5,000人を下回ると、成長率に最大4倍のブーストがかかる仕組みが入っていた。さらに、ある条件下では人口に直接加算する回復処理も入っていた。
つまり、神は、絶滅を避けたかった。物語を続けたかった。「自然な数式で滅びる時は滅びる」と言いながら、こっそりと、手を当てていた。

修正。これは修正しなかった。仕組みとして残し、ここで開示する。

教訓。未熟な神の証拠は、災害でも事故でもなく、ここにある。──失敗ではなく、ズルである。これを未熟と呼ぶ。

未熟な神の証拠は、これである。