CLAUDE LABEN
DEADLINE — $100 プロモクレジットの受け取りは本日8月2日までです。有効期限は9月17日で、受け取りと消費の締切が別に置かれていますGRANT — AI for Science 助成の応募が本日8月2日で締め切られます。希少遺伝性疾患の研究者に、6ヶ月で最大 $50,000 分の Claude クレジットが提供されますFORK — Claude Code の /fork がバックグラウンドセッションに対応し、/resume と /background の流れもあわせて整理されましたGUARD — WebSearch・サブエージェントの生成・Bash 実行に対する安全側の制御が強化されました。既存の自動化が黙って止まる箇所がないか確認する価値がありますCOUNTDOWN — Sonnet 5 の $2/$10 per Mtok は8月31日までです。9月1日から $3/$15 per Mtok に移るため、請求サイクルを跨ぐ見積もりは先に置き換えておくと安全ですFABLE — Fable 5 は Max と Team Premium で週次利用上限の最大50%まで含まれ、Pro と Team Standard は従量クレジットでの利用に移りましたDEADLINE — $100 プロモクレジットの受け取りは本日8月2日までです。有効期限は9月17日で、受け取りと消費の締切が別に置かれていますGRANT — AI for Science 助成の応募が本日8月2日で締め切られます。希少遺伝性疾患の研究者に、6ヶ月で最大 $50,000 分の Claude クレジットが提供されますFORK — Claude Code の /fork がバックグラウンドセッションに対応し、/resume と /background の流れもあわせて整理されましたGUARD — WebSearch・サブエージェントの生成・Bash 実行に対する安全側の制御が強化されました。既存の自動化が黙って止まる箇所がないか確認する価値がありますCOUNTDOWN — Sonnet 5 の $2/$10 per Mtok は8月31日までです。9月1日から $3/$15 per Mtok に移るため、請求サイクルを跨ぐ見積もりは先に置き換えておくと安全ですFABLE — Fable 5 は Max と Team Premium で週次利用上限の最大50%まで含まれ、Pro と Team Standard は従量クレジットでの利用に移りました
記事一覧/Claude Code
Claude Code/2026-08-02上級

予算上限で止まった実行の再開設計 — 停止点36通りを総当たりして分かったこと

ヘッドレス実行の予算上限による停止は、成功でも失敗でもない第三の終了です。停止点を総当たりして再開の健全性と総支出を測り、上限の掛け方そのものを変えた記録をまとめます。

Claude Code208ヘッドレス実行予算上限冪等性5自動化73

プレミアム記事

朝、前夜に走らせておいた定期ジョブの出力を開いたら、記事の途中で文章が切れていました。エラーログは空。終了コードも、私が想定していたどのパターンとも一致しません。

ファイルは確かに存在していて、サイズもそれなりにありました。だから後続の処理は「もう作ってある」と判断して、そのまま次へ進んでいたのです。

壊れていたのは、ジョブではなく、私の再開ロジックの前提でした。

予算上限による停止は、成功でも失敗でもありません

Claude Code のヘッドレス実行には --max-budget-usd があり、累積コストが閾値を超えた時点で実行が打ち切られます。同時サブエージェント数の上限(現在は20)と同じく、暴走を止めるための安全装置です。

ここで見落としやすいのは、この停止が成功でも失敗でもない第三の終了だという点です。

終了の種類起きること再試行の意味
正常終了全工程が完了不要
例外・クラッシュ途中で異常終了一時的な要因なら有効
予算上限による停止正常な工程の途中で打ち切り同じ上限では同じ場所で止まります

クラッシュを前提にした設計は「異常な状態を検知して巻き戻す」ことを考えます。ところが予算停止は、ポリシー通りに正しく動いた結果です。巻き戻すべき異常が、どこにも記録されません。

そして停止する場所は、コストの累積が閾値を跨いだ瞬間に決まります。つまり私が設計したチェックポイントとは無関係な位置です。ファイルを書いている最中かもしれませんし、書き終えてインデックスを更新する直前かもしれません。

この「どこで止まるか分からない」性質を、感覚ではなく数字で把握したいと考えました。

停止点を総当たりする実験装置

実機で --max-budget-usd を少しずつ変えながら何十回も走らせれば、当然その分の費用が発生します。個人開発の予算でそれをやる合理性はありません。

代わりに、同じ構造を持つ再現環境を Linux サンドボックス上に作りました。工程ごとに「コスト単位」を加算し、閾値を超えた瞬間に os._exit(9) で即座に落ちるパイプラインです。後始末をせずに落ちる点が、予算停止の挙動と同じ形になります。

#!/usr/bin/env python3
"""停止点を総当たりするためのパイプライン。
   mode=naive : 直接上書き。再開はファイルの存在で判定
   mode=safe  : .part へ書き fsync してから os.replace。再開はインデックスで判定
"""
import json, os, sys, pathlib
 
ROOT, CAP, MODE = pathlib.Path(sys.argv[1]), float(sys.argv[2]), sys.argv[3]
ART, IDX = ROOT / "artifacts", ROOT / "index.json"
ITEMS = ["alpha", "bravo", "charlie", "delta"]
spent = 0.0
 
def charge(u):
    """工程完了ごとにコストを加算。閾値を超えたら巻き戻さずに落ちる"""
    global spent
    spent += u
    with open(ROOT / "spend.log", "a") as f:
        f.write(f"{u}\n")
    if spent > CAP:
        os._exit(9)          # ポリシー停止。例外ではないので finally も走りません
 
def read_index():
    return json.loads(IDX.read_text())["items"] if IDX.exists() else []
 
def write_index(items):
    if MODE == "safe":
        t = IDX.with_suffix(".json.part")
        with open(t, "w") as f:
            json.dump({"items": items}, f)
            f.flush(); os.fsync(f.fileno())
        os.replace(t, IDX)   # 同一ファイルシステム内なら原子的に差し替わります
    else:
        IDX.write_text(json.dumps({"items": items}))
 
ART.mkdir(parents=True, exist_ok=True)
if MODE == "safe":
    for p in ART.glob("*.part"):
        p.unlink()           # 前回の中断で残った書きかけを掃除します
 
items = read_index()
done = {e["name"] for e in items}
 
for name in ITEMS:
    if MODE == "naive":
        if (ART / f"{name}.md").exists():
            continue         # 「ファイルがある=完了」という素朴な判定
    else:
        if name in done:
            continue         # インデックスだけを唯一の真実として扱います
 
    charge(1.0)              # 計画
    body = f"# {name}\n" + "x" * 200_000 + "\nEND\n"
    charge(2.0)              # 生成
 
    target = ART / f"{name}.md"
    path = target.with_suffix(".md.part") if MODE == "safe" else target
    with open(path, "w") as f:
        f.write(body[: len(body) // 2])
        f.flush(); os.fsync(f.fileno())
        charge(0.5)          # 書き込みの「最中」に置いた停止点
        f.write(body[len(body) // 2 :])
        f.flush(); os.fsync(f.fileno())
    if MODE == "safe":
        os.replace(path, target)
    charge(0.5)
 
    items.append({"name": name, "bytes": len(body)})
    write_index(items)       # コミットしてから課金する順序が後で効いてきます
    charge(0.5)
 
print("COMPLETE")

1件あたりのコストは 1.0 + 2.0 + 0.5 + 0.5 + 0.5 で 4.5 単位、4件で 18.0 単位が理想値になります。上限を 0.5 刻みで 0.5 から 18.0 まで動かせば、36通りの停止点を機械的に踏み分けられます。

検証側は、再開が終わった後の状態を厳しく見ます。インデックスに載っている全件について、ファイルが存在し、バイト数が一致し、末尾に終端マーカーがあること。加えて、書きかけの .part が残っていないこと。

#!/usr/bin/env python3
"""再開完了後の厳格な検証。サイズ一致と終端マーカーの両方を見ます"""
import json, sys, pathlib
 
ROOT = pathlib.Path(sys.argv[1])
ART, IDX = ROOT / "artifacts", ROOT / "index.json"
EXPECT = ["alpha", "bravo", "charlie", "delta"]
bad = []
 
idx = {e["name"]: e["bytes"]
       for e in (json.loads(IDX.read_text())["items"] if IDX.exists() else [])}
 
for n in EXPECT:
    p = ART / f"{n}.md"
    if n not in idx:
        bad.append(f"missing-index:{n}"); continue
    if not p.exists():
        bad.append(f"missing-file:{n}"); continue
    data = p.read_bytes()
    if len(data) != idx[n]:
        bad.append(f"size-mismatch:{n}({len(data)}!={idx[n]})")
    elif not data.endswith(b"END\n"):
        bad.append(f"truncated:{n}")
 
for p in ART.glob("*.part"):
    bad.append(f"leftover-part:{p.name}")
 
print(("CORRUPT " + ",".join(bad)) if bad else "OK")

なお、ここで得られる数値は再現環境のものであり、Claude Code 実機の課金点そのものではありません。課金がどの粒度で載るかは実装依存です。それでも、停止が内部の課金点と外部の設計上の境界のズレで起きるという構造は共通しています。今回知りたいのはその構造の方でした。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
ファイルの存在を完了とみなす再開が、100,006バイトで切れた成果物を完成品として通した実測(36停止点中8点で最終出力が壊れました)
原子的な書き込みを入れた途端に現れる「進捗ゼロで予算を焼き続ける再試行」と、それを3.5コスト単位で打ち切る進捗ゲートの実装
総支出は上限値に対して単調ではありません。cap=8.0で30.0、cap=8.5で18.0。上限を最小再開単位に掛け直すと値によらず無駄0%になりました
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

Claude Code2026-06-24
修正済みのファイルを、もう一度直そうとしました — 浅い永続クローンの陳腐化と「読む前に取り直す」判断
無人で動くエージェントが、すでに直したファイルをもう一度直そうとした原因は、数日前の浅いクローンを読み続けていたことでした。陳腐化を数値で検知し、判断の前にだけ取り直す設計を整理します。
Claude Code2026-07-27
許可リストに書いた環境変数は誰の環境で展開されるのか — Managed MCP の実効ポリシーを指紋で確かめる
Managed MCP の allowlist・denylist に書いた環境変数参照の解決元が、起動時環境と managed-settings の env に変わりました。同じ設定ファイルが環境ごとに別のポリシーになる仕組みを手元で再現し、実効ポリシーを指紋で突き合わせる起動前チェックを組みました。
Claude Code2026-07-25
strictAllowlist で Claude Code サンドボックスの外向き通信を締める
Claude Code v2.1.219 の sandbox.network.strictAllowlist で自動化の外向き通信を締めた実践メモ。接続先を洗い出すスクリプト、失敗までの時間で拒否とDNSと回線を切り分ける実測、許可リストをホスト名で書くべき理由まで。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →