6月15日の朝、私は自動投稿パイプラインの headless 実行部分を新しい課金体系に合わせて切り替える準備をしていました。数日前から「Agent SDK・claude -p の headless・GitHub Actions・サードパーティ製エージェントが、サブスク上限とは別枠の月次クレジットへ移る」と告知されていたためです。ところが当日、この変更は正式に保留(撤回)されました。これらの利用は、引き続き従来どおりサブスクリプションの上限内で扱われます。
もし告知された日付をそのまま信じて、前夜のうちにパイプラインを新体系向けに組み替えていたら、当日は「変更されていない現実」と「変更前提のコード」がぶつかって二重に壊れていたはずです。今回は事なきを得ましたが、この一件は「外部プラットフォームの予告を、いつ・どう運用へ反映するか」という、自動運用では避けて通れない設計課題をはっきり見せてくれました。
ここからは実装に話を絞ります。告知に振り回されず、しかし本当に発効したときには確実に追従する。その両立を、私が Dolice の4サイト運用で使っている「保留できるカットオーバー」のコードとして残しておきます。
告知を真に受けて切り替えると、何が二重に壊れるのか
いちばん素朴な実装は、告知された発効日をコードに直接書くものです。
// アンチパターン:告知日をそのまま信じる
const BILLING_CUTOVER = new Date ( "2026-06-15T00:00:00+09:00" );
function pickRunMode ( now = new Date ()) {
// 6/15 以降は headless を別クレジット前提のモードに切り替える
return now >= BILLING_CUTOVER ? "credit-metered" : "subscription" ;
}
このコードには2つの壊れ方があります。
1つ目は、変更が撤回されたとき です。コードは 6/15 を過ぎた瞬間に credit-metered を返し続けますが、現実の課金はサブスク上限のままです。クレジット残高を見張る監視や、レート上限を低く見積もったスロットリングが、実態と合わない前提で動き出します。最悪なのは「クレジットが尽きた」と誤判定してパイプラインを自分で止めてしまうことです。
2つ目は、変更が予告より遅れたとき です。発効が数日ずれただけでも、その数日間はコードと現実が食い違います。プラットフォーム側の段階ロールアウトでは、こうした数日のずれは珍しくありません。
本番運用では、この食い違いがそのまま落とし穴になります。日付という制御できない要因に切り替えを預けている以上、撤回や遅延を手作業で回避し続けることになるからです。つまり問題の根は「カレンダー上の日付」を切り替えの根拠にしている点にあります。日付は告知の都合で動き、撤回もされます。運用が頼るべきは、変更が実際に発効したことを示す実行時のシグナル です。
設計の核:日付ではなく実行時シグナルで切り替える
考え方はシンプルです。告知を受け取った時点では、コードに「新経路の実装」を入れておくものの、有効化はしません。実際の切り替えは、ランタイムで観測できる事実——たとえば API レスポンスのヘッダー、使用量エンドポイントの返す課金区分、エラーコードの種類——が「新体系になった」と告げたときだけ行います。
私はこれを3つの状態を持つフラグとして管理しています。
announced:告知は受け取ったが、まだ発効を確認していない。旧経路で動く 。
confirmed:実行時シグナルが新体系を確認した。新経路に切り替える 。
reverted:いったん confirmed になったが、その後シグナルが旧体系に戻った。旧経路へ巻き戻す 。
重要なのは、announced の間はコードが旧経路で淡々と動き続けることです。告知は「準備をしておけ」という合図であって、「今すぐ切り替えろ」という命令ではない、という線引きをコードに持たせます。
実装:3状態のカットオーバーフラグ
まずフラグの形を定義します。設定ファイルやKVに置く想定です。
type CutoverPhase = "announced" | "confirmed" | "reverted" ;
interface CutoverFlag {
id : string ; // 例: "billing-credit-metering-2026-06"
phase : CutoverPhase ;
announcedEffectiveAt : string ; // 告知された発効日(参考情報。判断には使わない)
lastProbedAt ?: string ; // 直近で実行時確認をした時刻
confirmStreak : number ; // 連続で「新体系」を観測した回数
revertStreak : number ; // 連続で「旧体系」を観測した回数
}
// 告知を受け取ったら、まず announced で登録するだけ
function registerAnnouncement ( id : string , effectiveAt : string ) : CutoverFlag {
return {
id,
phase: "announced" ,
announcedEffectiveAt: effectiveAt,
confirmStreak: 0 ,
revertStreak: 0 ,
};
}
announcedEffectiveAt は記録しますが、あくまで人間が状況を把握するための参考情報です。pickRunMode 相当の判断は phase だけを見ます。
function resolveRunMode ( flag : CutoverFlag ) : "subscription" | "credit-metered" {
// confirmed のときだけ新経路。それ以外(announced / reverted)は旧経路
return flag.phase === "confirmed" ? "credit-metered" : "subscription" ;
}
このひと工夫だけで、6/15 の保留はノーダメージで通過できました。告知日を過ぎても phase は announced のままで、コードは粛々とサブスク経路を選び続けたからです。
実行時プローブで「本当に発効したか」を確認する
切り替えの判断材料になる実行時シグナルは、変更の種類によって異なります。今回のような課金区分の変更であれば、使用量・課金に関するエンドポイントが返す区分を見るのが素直です。モデルの引退であれば、目的のモデルIDへの軽いリクエストが model_not_found を返すかどうかが確実なシグナルになります。
ここで大切なのは、1回の観測で状態を変えない ことです。プラットフォーム側の段階ロールアウトや一時的な不整合で、シグナルが行ったり来たりすることがあります。私は「2回連続で同じ結果を観測したときだけ状態を遷移させる」ヒステリシスを入れています。
// probe は「いま新体系か?」を boolean で返す関数(変更ごとに差し替える)
async function advanceCutover (
flag : CutoverFlag ,
probe : () => Promise < boolean >,
) : Promise < CutoverFlag > {
const isNewRegime = await probe ();
const now = new Date (). toISOString ();
const next : CutoverFlag = { ... flag, lastProbedAt: now };
if (isNewRegime) {
next.confirmStreak = flag.confirmStreak + 1 ;
next.revertStreak = 0 ;
// 2回連続で新体系を観測したら confirmed へ
if (next.confirmStreak >= 2 && flag.phase !== "confirmed" ) {
next.phase = "confirmed" ;
}
} else {
next.revertStreak = flag.revertStreak + 1 ;
next.confirmStreak = 0 ;
// いったん confirmed になった後、2回連続で旧体系に戻ったら reverted へ
if (next.revertStreak >= 2 && flag.phase === "confirmed" ) {
next.phase = "reverted" ;
}
}
return next;
}
probe の中身は変更の種類ごとに用意します。課金区分の例なら、使用量エンドポイントのレスポンスから「別枠クレジットでメータリングされているか」を読み取って返します。プローブはあくまで観測に徹し、状態遷移のロジックは advanceCutover 側に集約しておくと、変更の種類が増えてもテストしやすくなります。
Before / After:日付ハードコードから保留できる設計へ
冒頭のアンチパターンと、今回の設計を並べてみます。
// Before:告知日を信じて即切り替え。撤回・遅延で即破綻する
const BILLING_CUTOVER = new Date ( "2026-06-15T00:00:00+09:00" );
function pickRunMode ( now = new Date ()) {
return now >= BILLING_CUTOVER ? "credit-metered" : "subscription" ;
}
// After:告知は記録するだけ。実行時シグナルが2回連続で確認したら切り替える
const flag = await loadCutoverFlag ( "billing-credit-metering-2026-06" );
const updated = await advanceCutover (flag, probeBillingRegime);
await saveCutoverFlag (updated);
const mode = resolveRunMode (updated); // announced のうちは subscription のまま
差分は「いつ切り替えるか」を決める主体が、カレンダーから実行時の観測に移ったことです。Before は告知という意図 を信じます。After は発効という事実 を待ちます。自動運用で守るべきは事実のほうだと、今回の保留ではっきりしました。
撤回・遅延を検知して自動で巻き戻す
reverted 状態を用意してあるのは、いったん発効した変更が後から差し戻されるケースに備えるためです。段階ロールアウトの取り下げや、緊急のロールバックは現実に起こります。advanceCutover は confirmed の後に旧体系を2回連続で観測すると phase を reverted に落とし、resolveRunMode は再び旧経路を返します。
巻き戻しのときに私が必ず入れているのは、人間への通知だけは即時に飛ばす ことです。自動で旧経路へ戻すこと自体は安全側の動作ですが、「なぜ戻ったのか」を後から追えないと運用が不透明になります。状態が遷移した瞬間に、フラグID・遷移の方向・直近のプローブ結果を1行ログで残し、自分宛てに通知します。コードは黙って正しく動き、人間には事実だけが届く。この分担が自動運用を長く回すコツだと考えています。
function onPhaseChange ( prev : CutoverFlag , next : CutoverFlag ) {
if (prev.phase === next.phase) return ;
const line = `[cutover] ${ next . id }: ${ prev . phase } -> ${ next . phase } ` +
`(confirm=${ next . confirmStreak }, revert=${ next . revertStreak }, at=${ next . lastProbedAt })` ;
notifySelf (line); // Slack / メール / ログのいずれか1系統で十分
}
個人開発の自動運用へどう落とし込むか
私は Dolice で4つのサイトを自動投稿パイプラインで回しています。各サイトの記事生成には品質ゲートを通す工程があり、そのいくつかは headless 実行です。だからこそ「課金区分が変わる」という告知は、コスト構造に直結する重い話でした。それでも当日に保留されたわけですから、もし告知日で切り替えていたら、撤回のたびに4サイト分の運用を手で戻す羽目になっていたはずです。
実務で意識している判断の順序は、次の3つに整理できます。
告知を受けたら、実装は入れるが有効化はしない。announced で登録して旧経路を維持します。
切り替えの根拠は日付ではなく実行時シグナルに置く。プローブを書けない変更は、そもそも自動で切り替えるべきではないと割り切っています。
状態遷移は2回連続の観測で確定し、遷移したら人間に通知する。
この順序は、私自身が4サイトを回しながら何度か痛い目を見て落ち着いた実体験ベースの結論です。同じ判断に迷っている方には、まず第1項だけでも取り入れることを強く推奨します。
予告された変更が撤回されること自体は、こちらでは制御できません。制御できるのは「告知をどう受け止めるか」というコードの態度だけです。私は、告知には準備で応え、切り替えは事実で決める、という線引きを守っていきたいと考えています。同じように複数の自動運用を抱えている方の、設計の一助になれば幸いです。