複数Claude Maxアカウントのリソース振り分け機能

Issue
#329
作成日
2026-04-07
最終更新
2026-04-14(文献調査結果追記)
ステータス
設計確定(レビュー完了)
変更規模
新規1ファイル + 既存3ファイル小改修
設計者
W層 architect
フェーズ
Phase 0(実測検証 → 実装 → 本番)

A. 設計レビュー回答(12件)

CEOからの12件の技術質問に対する設計判断と、設計書への反映内容をまとめる。

【高】① Rate limitの粒度検証

Phase 0で実測する前にコードを書くのは手戻りリスクがある。Phase 0の最初のステップとして「2アカウント並行でrate limit粒度を確認するスクリプト」を書いて実測結果を得てから実装に進むべきでは?

判断: 同意。Phase 0を「Step 0: 実測検証」と「Step 1: 実装」の2段階に分割する。

指摘のとおり、rate limitがIP/組織単位だった場合、account_pool.py 全体が無意味になる。コードを書く前に粒度を確認すべき。

具体的な検証スクリプト:

判定基準:

設計変更: Phase 0を2ステップに分割。「11. Phase 0 スコープ」を更新済み

【高】② 並行性制御 — acquire/releaseにLockが必要

asyncio環境で同時にacquire()が呼ばれた場合の競合。asyncio.Lockの使用可否、またはin-memory dictのアトミック性について設計判断を明示すべき。

判断: asyncio.Lockを使用する。

asyncio はシングルスレッドだが、acquire()release() の間に await がある場合(=subprocess実行中)、別のコルーチンが acquire() を呼ぶ可能性がある。

正確に言うと、acquire()release() それぞれは同期関数で await を含まないため、単独の呼び出しはアトミック。しかし acquire() 内の「スコア計算 → active_count += 1」が途中で割り込まれる可能性を構造的に排除するため、asyncio.Lock で保護する。

既存の ProcessRegistrythreading.Lock を使っている(スレッドセーフ)。AccountPool は asyncio 環境でのみ使われるため asyncio.Lock が適切。

設計変更: AccountPoolクラスに _lock: asyncio.Lock を追加。acquire/release/mark_rate_limitedを async メソッドに変更。「3.2 AccountPool クラス」を更新済み

【高】③ active_count不整合の影響が過小評価されていない?

kill -9やbot再起動でrelease()がスキップされると全アカウントのactive_countが永続ファイルに残る。再起動時の具体的な補正ロジックが設計に含まれていない。

判断: 指摘は正しい。active_countを永続化しない設計に変更する。

そもそも active_count は「現在実行中のプロセス数」であり、再起動後に意味を持たない。永続化すべきは cooldown_until(時刻ベースなので再起動後もTTLで自然消滅)のみ。

補正ロジック:

リスク表を「低」→「中」に引き上げる。

設計変更: active_countを永続化対象から除外。起動時にProcessRegistryと突き合わせて補正するロジックを追加。リスクレベルを「中」に変更。「3.4 rate_limit_status.json への統合」と「10. リスク」を更新済み

【中】④ --resume削減効果の定量化

アカウント切替時に--resumeが使えないことのコスト(トークン再消費、セッション切断)は何パーセントのオーバーヘッド? 3アカウント運用で本当にネット利益が出るかの試算はあるか?

判断: 定量試算は現時点では不可能だが、構造的にネット利益が出る根拠がある。

--resumeが失われるケース: rate limit発生時のリトライ(別アカウントでone-shot実行)のみ。通常のタスク実行では同一アカウント・同一セッションで --resume が維持される。

コスト構造:

Max Planの特性: トークン従量課金ではないため、--resume喪失による「トークン再消費」は財務コストに影響しない。影響するのは実行時間のみ(CLAUDE.md再読込で数秒〜十数秒)。

Phase 0 Step 0の実測で rate limit 頻度を定量化し、その結果で費用対効果を再評価する。

設計変更なし(元の設計で問題なし)。Phase 0 Step 0で実測後に再評価する旨を明記

【中】⑤ 全アカウント制限時の待機ロジック

全アカウント制限中の場合、最も早くcooldown_untilが解除されるアカウントまで待つロジックを入れるべきでは?

判断: 同意。acquire()にwaitオプションを追加する。

現設計では全アカウント制限時に None を返し、既存の RateLimitError → タスクリトライに委ねている。しかしタスクリトライは exponential backoff(30s→60s→120s)で固定の待機時間を使うため、クールダウン解除タイミングと合わない。

追加するロジック:

rate limitリトライ時(別アカウント取得の場面)では wait=True を使い、最短復帰を待つ。

設計変更: acquire()に wait: bool = False パラメータを追加。「3.2 AccountPool クラス」を更新済み

【低】⑥ スコアリングのtotal_runsは必要か

均等分散の目的が「rate limit回避」ならactive_countだけで十分。total_runsは「トークン消費量」を反映しない。均等分散の目的はrate limit回避かコスト均等化か?

判断: total_runsをスコアリングから除外する。

指摘のとおり、目的は「rate limit回避」。total_runsで均等化しても、rate limit はリクエスト数ではなく利用率(5時間ウィンドウのトークン消費量)で計算されるため、均等化に寄与しない。

変更後のスコアリング:

total_runs フィールド自体を削除する。rate limit はトークン消費量ベースで計算されるため、実行回数を記録しても意味がない。

設計変更: スコアリングを active_count のみに簡素化。「3.3 スコアリング」を更新済み

【低】⑦ クールダウン5分の根拠

「5分で十分に回復する実績あり」の根拠は何件のサンプルか? Phase 0の実測データで調整する仕組みはあるか?

判断: 5分はデフォルトフォールバック値であり、CLI提供値を優先する設計に明確化する。

「実績」は正確ではなかった。Claude Maxの rate limit は「5時間ウィンドウ」で管理されているため、5分待てば利用率が多少下がるという推論に基づく。厳密なサンプル数に基づく根拠ではない。

クールダウン値の決定ロジック(優先順):

  1. rate_limit_info.retry_after(CLIが明示的に返す待機秒数)
  2. rate_limit_info.resetsAt(リセット時刻から算出)
  3. フォールバック: 300秒(5分)— CLI が値を返さなかった場合のみ使用

Phase 0での調整: Step 0の実測で実際のrate limit情報(retry_after, resetsAt)がどの程度返されるかを確認。返却率が高ければフォールバックは滅多に使われない。返却率が低ければフォールバック値を実測値に基づき調整する。

pool_cooldown_seconds は設定値(.env)で変更可能なので、運用中も調整可能。

設計変更: クールダウン値の決定ロジックを3段階に明確化。「6.2 クールダウンの設計」を更新済み

⑧ account_pool.pyの配置場所は bot/ で正しいか

アカウントプールは「botの機能」というよりも「インフラ/リソース管理」の責務。bot/ 配下に置くのは責務の混在にならないか?

判断: bot/ に配置する。理由は3つ。

  1. 利用者が bot/ に閉じている: AccountPool を使うのは bot/claude_runner.py のみ。他のパッケージ(ops/, agents/)からは参照されない。利用者と同じパッケージに置くのが自然
  2. 既存の類似モジュールが bot/ にある: bot/process_registry.py(プロセス管理)、bot/task_tracker.py(タスク管理)はいずれも「インフラ/リソース管理」の責務だが bot/ に配置されている。account_pool も同じカテゴリ
  3. 新しいディレクトリを作る理由がない: infra/lib/ を切り出すほどのモジュール数がない(1ファイル)。一人法人のプロジェクトで過度なディレクトリ分割は保守コストが増える

将来 AI Ops がリソース管理を統括する段階になれば、agents/ai-ops/ 配下への移動を検討する。現時点では YAGNI。

設計変更なし(bot/ 配置を維持)

⑨ 新規作成ファイルの必要性確認

bot/account_pool.pyの1ファイルのみと理解している。「なぜ既存ファイルへの追記ではなくファイル分離なのか」を明示してほしい。

判断: ファイル分離が正しい。理由は以下。

config.py への追記(+6行)は既存ファイルへの追記。新規ファイルは account_pool.py の1本のみ。

設計変更なし(分離方針を維持)

⑩ 直列挟み込み方式 vs サイドカー方式

runner.pyに直接組み込む「直列挟み込み方式」と、独立プロセスとして横から監視する「サイドカー方式」の比較。AI Opsとの責務分離が曖昧にならないか?

判断: 直列挟み込み方式を採用する。サイドカー方式は現時点では過剰。

サイドカー方式の問題点:

AI Opsとの責務分離:

DAG並行実行アーキテクチャ(Issue #139)で bot.py が DAG Scheduler 化する段階で、リソース管理をScheduler内の独立モジュールとして再配置する可能性はある。しかしそれは Phase 0 のスコープ外。

設計変更なし(直列挟み込み方式を維持)

⑪ pool_status.jsonをrate_limit_status.jsonと分ける理由

2つのJSONに分散させるとAI Opsが状態を把握するときに2ファイルを突き合わせる必要が出る。1つのファイルに統合する案と比較して、分離する判断の根拠を聞きたい。

判断: rate_limit_status.json に統合する。

指摘のとおり、アカウント別のcooldown状態と全体のrate limitイベントは密接に関連しており、分散させるメリットがない。

統合後の rate_limit_status.json 構造:

{
  "cooldown_until": null,         // 既存: 全体のクールダウン(単一アカウント時の後方互換)
  "last_event": { ... },          // 既存: 最後のrate limitイベント
  "history": [ ... ],             // 既存: イベント履歴(24h TTL)
  "accounts": [                   // 新規: アカウント別の状態(active_countはメモリのみ)
    {
      "name": "account-a",
      "cooldown_until": null,
      "last_rate_limit": null
    }
  ],
  "updated_at": 1743984001.0
}
設計変更: pool_status.json を廃止し rate_limit_status.json に統合。「3.4 pool_status.json」→「3.4 rate_limit_status.json 拡張」に変更。変更対象ファイルから pool_status.json を削除

⑫ Phase 0完了条件の「均等利用」は本当に必要か

本来の目的は「rate limit時に別アカウントに切り替えられること」であって「均等に使うこと」ではないはず。均等利用を目指すとスコアリングの複雑さが増す。

判断: 同意。「均等利用」を完了条件から削除する。

目的は rate limit 回避であり、均等分散はそのための手段の一つに過ぎない。active_count ベースのスコアリングは「負荷が偏りすぎない」程度の効果があれば十分で、厳密な均等化は不要。

更新後の完了条件:

  1. rate limit がアカウント単位で有効かどうかの実測データが得られる(Step 0)
  2. rate limit 発生時に別アカウントへの切替が動作する(Step 1)
  3. 単一アカウント設定(CLAUDE_ACCOUNTS 未設定)でも既存動作に影響なし(Step 1)

「3アカウントが均等に使用されている」は削除。total_runs の差が多少あっても、rate limit 回避機能が動作すればPhase 0は達成。

設計変更: Phase 0完了条件を3項目に絞り込み。「11.3 Phase 0 完了条件」を更新済み

1. 概要

3つのClaude Maxアカウントを使い分け、rate limitの影響を最小化する仕組みを実装する。CLAUDE_CONFIG_DIR + キーチェーンによるアカウント分離は技術的に実証済み。

解決する問題

設計方針

2. アーキテクチャ図

変更箇所の全体像: bot.py → claude_runner.py → account_pool.py(新規) → Claude CLI subprocess
bot.py / router.py claude_runner.py run_claude() account_pool.py AccountPool acquire() / release() mark_rate_limited() 新規 Account A (CLI) Account B (CLI) Account C (CLI) rate_limit_status.json (accounts拡張) AI Ops Monitor 読取専用で監視 watcher.py 改修 read config.py(設定追加) Rate Limit 発生時のフロー RateLimitError mark_rate_limited() クールダウン設定 acquire(wait) 別アカウント取得 即リトライ実行 全アカウント 制限中 → 最短復帰まで待機

3. account_pool.py の設計

3.1 データモデル

@dataclass
class AccountInfo:
    name: str                  # 識別名(例: "account-a")
    config_dir: str            # CLAUDE_CONFIG_DIR のパス
    active_count: int = 0      # 現在実行中のプロセス数(メモリのみ、永続化しない)
    cooldown_until: float = 0  # rate limit クールダウン終了時刻(time.time()ベース、0=制限なし)
    last_rate_limit: float = 0 # 最後にrate limit検知した時刻
active_count の設計判断(レビュー③反映): active_count はメモリ上のみで管理し、永続化しない。再起動時は 0 で初期化。kill -9 等による不整合は ProcessRegistry との突き合わせで補正する。

3.2 AccountPool クラス

class AccountPool:
    """Claude Maxアカウントのプール管理

    active_countベースのスコアリングで負荷分散し、
    rate limit発生時は該当アカウントをクールダウンして別アカウントに振り替える。
    asyncio.Lock で並行アクセスを保護する(レビュー②反映)。
    """

    def __init__(self, accounts: list[dict]) -> None:
        """アカウント一覧から初期化
        accounts: [{"name": "account-a", "config_dir": "/path/to/.claude-a"}, ...]
        """
        self._lock = asyncio.Lock()  # レビュー②: 並行性制御

    async def acquire(self, *, exclude: str | None = None, wait: bool = False) -> AccountInfo | None:
        """最小スコアのアカウントを取得し、active_count += 1
        exclude: 除外するアカウント名(rate limit後のリトライ時)
        wait: 全アカウント制限中の場合、最短復帰まで待機するか(レビュー⑤反映)
        Returns: AccountInfo(利用可能なアカウントがなく wait=False なら None)
        """

    async def release(self, name: str) -> None:
        """プロセス完了時に active_count -= 1"""

    async def mark_rate_limited(self, name: str, cooldown_seconds: int = 300) -> None:
        """rate limit検知時にクールダウン期間を設定"""

    def _score(self, account: AccountInfo) -> tuple[float, str]:
        """アカウントのスコアを計算(低い = 優先)
        クールダウン中 → (float('inf'), name)
        それ以外 → (active_count, name)  # 同スコアは名前の辞書順で破壊
        """

    def _reconcile_active_counts(self) -> None:
        """ProcessRegistryの実行中PIDと突き合わせてactive_countを補正(レビュー③反映)"""

    def status(self) -> list[dict]:
        """全アカウントの現在状態を返す(ダッシュボード / AI Ops用)"""

    def persist(self) -> None:
        """rate_limit_status.json の accounts フィールドに書出し(レビュー⑪反映)
        書出し前に _reconcile_active_counts() を実行して補正する"""

    def all_limited(self) -> bool:
        """全アカウントがクールダウン中かどうかを返す"""

    @property
    def enabled(self) -> bool:
        """アカウントが2つ以上設定されている場合にTrue"""

3.3 スコアリングアルゴリズム

条件スコア説明
クールダウン中(float('inf'), name)rate limit中のアカウントは選択されない
通常(active_count, name)実行中プロセスが少ないアカウントを優先
選択ロジック(レビュー⑥反映): min(available_accounts, key=lambda a: (a.active_count, a.name))
total_runs フィールドは廃止(レビュー⑥)。目的は rate limit 回避であり、均等分散は不要。同スコア時はアカウント名の辞書順で決定論的に破壊する。

3.4 rate_limit_status.json への統合(レビュー⑪反映)

設計変更: pool_status.json を新規作成せず、既存の rate_limit_status.json に accounts フィールドを追加する。AI Ops が1ファイルだけで全状態を把握できる。
{
  "cooldown_until": null,         // 既存: 全体のクールダウン(単一アカウント時の後方互換)
  "last_event": { ... },          // 既存: 最後のrate limitイベント
  "history": [ ... ],             // 既存: イベント履歴(24h TTL)
  "accounts": [                   // 新規: アカウント別の状態(active_countはメモリのみ、永続化しない)
    {
      "name": "account-a",
      "cooldown_until": null,
      "last_rate_limit": null
    },
    {
      "name": "account-b",
      "cooldown_until": 1743984000.0,
      "last_rate_limit": 1743983700.0
    },
    {
      "name": "account-c",
      "cooldown_until": null,
      "last_rate_limit": null
    }
  ],
  "updated_at": 1743984001.0
}

保存先: logs/rate_limit_status.json(既存ファイルの拡張)

4. 配分アルゴリズム — アカウント選択〜実行〜リトライのフロー

run_claude() 呼出 pool.acquire() 取得成功? wait=? NG 最短復帰まで待機 上限30分超→Error True 復帰後リトライ RateLimitError False env[CLAUDE_CONFIG_DIR] = account.config_dir OK Claude CLI subprocess実行 結果 pool.release() & return 成功 mark_rate_limited() クールダウン設定 Rate Limit pool.release(現アカウント) pool.acquire(exclude, wait=True) 別アカウントで即リトライ(⑤) 別アカウントでリトライ 完了 pool.release() & raise 他エラー

5. claude_runner.py の変更

5.1 変更方針

_run_claude_inner() 内で、subprocess起動直前にアカウントを取得し、完了時(正常/異常問わず)にリリースする。rate limit検知時は別アカウントでリトライする。

5.2 変更箇所(擬似コード)

# _run_claude_inner() の変更イメージ(約30行追加)

from bot.account_pool import pool  # グローバルインスタンス

async def _run_claude_inner(..., *, _account_name: str | None = None, ...):
    # ① アカウント取得(asyncio.Lock で保護)
    account = await pool.acquire(exclude=_account_name) if pool.enabled else None

    # ② env構築時に CLAUDE_CONFIG_DIR を注入
    env = {k: v for k, v in os.environ.items() if k not in _env_exclude}
    if account:
        env["CLAUDE_CONFIG_DIR"] = account.config_dir

    try:
        # ... 既存のsubprocess実行ロジック(変更なし) ...
        pass
    finally:
        # ③ 必ずリリース
        if account:
            await pool.release(account.name)

    # ④ RateLimitError 検知時: 別アカウントでリトライ
    if is_rate_issue and not result_text.strip():
        if account:
            await pool.mark_rate_limited(account.name, cooldown_seconds=wait_seconds)
            # 別アカウントで即リトライ(1回のみ)
            if not _account_name:  # 初回のみリトライ
                return await _run_claude_inner(
                    ..., _account_name=account.name, ...
                )
        raise RateLimitError(wait_seconds=wait_seconds)

5.3 インターフェース変更

外部インターフェースの変更なし: run_claude() の引数・戻り値は一切変わらない。アカウント選択は内部で透過的に行われる。

内部関数 _run_claude_inner()_account_name: str | None 引数を追加(プライベート、リトライ時の除外用)。

6. Rate Limit 対応

6.1 検知〜リトライの流れ

ステップ処理担当
1Claude CLI が rate_limit_event を出力Claude CLI
2既存パースロジックで rate_limited=True を検知claude_runner.py(既存)
3pool.mark_rate_limited(name, wait_seconds) でクールダウン設定claude_runner.py(追加)
4pool.release(name) で現アカウントを解放claude_runner.py(追加)
5pool.acquire(exclude=name, wait=True) で別アカウントを取得claude_runner.py(追加)
6a別アカウント取得成功 → _run_claude_inner() を再帰呼出claude_runner.py(追加)
6b全アカウント制限中 → 最短復帰まで待機後リトライ。上限超過時は RateLimitError 送出claude_runner.py(追加)

6.2 クールダウンの設計

パラメータ根拠
クールダウン値の決定(優先順) 1. retry_after(CLI提供)
2. resetsAt(CLI提供、時刻から算出)
3. フォールバック: 300秒(5分)
CLI が待機秒数を返す場合はそれを信頼する。返さない場合のみフォールバック(レビュー⑦反映)
クールダウン上限1800秒(30分)過大な値を防ぐ安全弁
リトライ回数1回(別アカウントで即リトライ)2回以上のリトライは全アカウント制限のリスク。既存の Task.retry_count と組み合わせて上限管理
全アカウント制限時最短の cooldown_until まで待機既存backoffの固定待機より効率的(レビュー⑤反映)
フォールバック5分の位置づけ(レビュー⑦反映): あくまでCLIがretry_after/resetsAtを返さなかった場合のフォールバック。Phase 0 Step 0で実測し、CLI提供値の返却率を確認する。返却率が低ければフォールバック値を調整する。pool_cooldown_seconds は .env で変更可能。

6.3 既存リトライ機構との関係

二重リトライの防止: アカウント切替リトライ(account_pool)と既存のタスクリトライ(Task.retry_count)は別レイヤー。

7. AI Ops 監視

7.1 監視対象

AI Ops Monitor(agents/ai-ops/monitor/watcher.py)に _check_pool_status() メソッドを追加。

async def _check_pool_status(self) -> None:
    """アカウントプールの状態を監視

    チェック項目:
    1. 全アカウントがクールダウン中 → CRITICAL通知
    2. 1アカウントが長時間(15分超)クールダウン中 → WARNING通知
    3. 特定アカウントのrate limit頻度が高い → INFO通知(偏り検出)

    データソース: rate_limit_status.json の accounts フィールド(レビュー⑪反映)
    """

7.2 通知条件

条件レベル通知先クールダウン
全アカウントが同時にクールダウン中 CRITICAL #bot-info 5分
1アカウントのクールダウンが15分超 WARNING #bot-info 30分
1アカウントが1時間に3回以上rate limit INFO #bot-info 1時間

7.3 データ読取

既存の reader.pyread_pool_accounts() メソッドを追加。rate_limit_status.jsonaccounts フィールドを読取る。

# reader.py に追加
@dataclass(frozen=True)
class PoolAccountRecord:
    name: str
    cooldown_until: float | None
    last_rate_limit: float | None

def read_pool_accounts(self) -> list[PoolAccountRecord]:
    """rate_limit_status.json の accounts フィールドからアカウント状態を読取"""

8. config.py の変更

8.1 追加設定

class Settings(BaseSettings):
    # ... 既存設定 ...

    # アカウントプール設定
    # 形式: "name1:/path1,name2:/path2,name3:/path3"
    claude_accounts: str = ""
    # クールダウン秒数(デフォルト5分。CLIが値を返さない場合のフォールバック)
    pool_cooldown_seconds: int = 300
    # クールダウン上限(デフォルト30分)
    pool_cooldown_max: int = 1800

8.2 .env の設定例

# アカウントプール(カンマ区切り、各 "名前:config_dirパス")
CLAUDE_ACCOUNTS=account-a:/Users/mnmladmin/.claude-a,account-b:/Users/mnmladmin/.claude-b,account-c:/Users/mnmladmin/.claude-c

# クールダウン秒数(任意、デフォルト300。CLIがretry_afterを返さない場合のフォールバック)
# POOL_COOLDOWN_SECONDS=300
未設定時の挙動: claude_accounts が空文字の場合、AccountPool.enabled = False となり、従来どおり単一アカウントで動作する(後方互換性)。

9. 変更対象ファイル一覧

ファイル操作変更内容行数目安
bot/account_pool.py 新規 AccountPool クラス、asyncio.Lock並行制御、active_count補正、rate_limit_status.json拡張書込み、グローバルインスタンス 〜130行
bot/claude_runner.py 改修 _run_claude_inner() にアカウント取得/解放/切替リトライを追加 +30行
bot/config.py 改修 claude_accounts, pool_cooldown_seconds, pool_cooldown_max の3設定を追加 +6行
agents/ai-ops/monitor/watcher.py 改修 _check_pool_status() メソッドを追加(Phase 1) +40行
agents/ai-ops/monitor/reader.py 改修 PoolAccountRecord + read_pool_accounts() 追加(Phase 1) +20行
.env 改修 CLAUDE_ACCOUNTS 環境変数を追加 +2行
scripts/test_rate_limit_granularity.py 新規 Phase 0 Step 0: rate limit粒度の実測検証スクリプト(レビュー①反映) 〜80行
レビュー⑪反映: pool_status.json の新規作成を廃止。rate_limit_status.json を拡張する方式に変更。

10. 不確実要素・リスク

項目リスク対策
Rate limitの粒度 Anthropicの公開情報にrate limitがアカウント単位かIP/組織単位かの明記なし。
Phase 0 Step 0で実測検証を実施(レビュー①反映)。検証スクリプトで2アカウント並行実行し粒度を確認。
IP単位 → 本設計は凍結。アカウント単位 → Step 1に進む。
CLAUDE_CONFIG_DIRの互換性 Claude CLI のバージョンアップで挙動が変わる可能性あり。
→ 技術実証済み(Issue #329確認事項)。CLI更新時に動作確認を入れる
active_countの不整合 (レビュー③でリスクレベル引上げ)
kill -9やbot再起動で release() がスキップされるケース。
対策:
  • active_countを永続化しない(再起動時は0初期化)
  • try/finally で release() を保証
  • persist() 時に ProcessRegistry と突き合わせて補正
セッション継続(--resume) セッションIDはアカウント固有。rate limitリトライで別アカウントに切り替えると --resume が使えない。
→ リトライ時は新規セッションで実行(one-shot)。Max Planは月額固定のためトークン再消費の金銭コストは0。影響は再読込の数秒〜十数秒のみ(レビュー④)
キーチェーンアクセス 3アカウント分のOAuthトークンがキーチェーンに格納されている前提。
→ Phase 0 Step 0の検証スクリプトで各アカウントの claude auth status も確認

11. Phase 0 スコープ

11.0 文献調査結果(2026-04-14)

結論: 間接証拠は「アカウント単位独立」を強く示唆するが、Anthropic公式の保証なし。実測検証は省略不可。
根拠の種類内容信頼度
Anthropic公式(API) 有料APIキーは「組織単位(Organization level)で制御」と明示。
出典: platform.claude.com/docs/en/api/rate-limits
(ただしMaxプランには適用外)
Anthropic公式(Max) 同一アカウント内でclaude.aiとClaude Codeの使用量は共有。ただし別アカウントの独立性を保証する記述なし。
出典: support.claude.com
(独立性の保証ではない)
ccrotate(OSS) アカウント別に ~/.claude/.credentials.json を管理し切り替えるツール。動作実績あり。CLAUDE_CONFIG_DIR 環境変数で並行運用する実装例もGitHubに存在 (ToS違反リスクあり、Anthropic保証なし)
/api/oauth/usage アカウントごとに five_hour.utilization を個別取得可能
バグ報告 (#12786, #22876) Account Aで制限到達後、Account Bに切替えてもrate limitエラーが出る報告あり。ユーザー仮説「デバイス/マシンレベルの追跡が存在」。Anthropic側の公式説明なし(Closedで終了) (ネガティブ要素)
直接的根拠 Anthropicが「Maxプランはper-account制御」と明示した記述は存在しない

実測検証が必要な理由

レビュー①反映: Phase 0を「Step 0: 実測検証」と「Step 1: 実装」の2段階に分割。コードを書く前にrate limit粒度を確認する。

11.1 Step 0: 実測検証(コード実装前)

scripts/test_rate_limit_granularity.py(〜80行)を作成し、以下を検証する。

検証スクリプトの内容

  1. 各アカウントの claude auth status でキーチェーン状態を確認
  2. 2アカウント(account-a, account-b)で同時に claude -p "hello" --output-format stream-json を実行
  3. 一方を意図的にrate limitに到達させた後、もう一方が制限なく動作するかを確認
  4. rate_limit_eventrateLimitType/utilization を両アカウントで比較
  5. /tmp/claude-{UID}/ の共有による干渉がないか検証(同一UIDで複数 CLAUDE_CONFIG_DIR が共存できるか)
  6. CLI が retry_after / resetsAt をどの頻度で返すか記録
  7. 結果を logs/rate_limit_granularity_test.json に保存

判定基準

結果判定次のアクション
アカウント単位GoStep 1(実装)に進む
IP/組織単位Stop本設計は凍結。IPローテーション等の代替策を別途検討
混合(アカウント+IP)Go(限定的)Step 1に進む。部分的改善として実装
/tmp 干渉あり要対策UID分離(別ユーザ実行)またはCLAUDE_TMPDIR設定で回避後にStep 1

11.2 Step 1: 実装(Step 0でアカウント単位と確認後)

  1. bot/account_pool.py 新規作成
  2. bot/claude_runner.py にアカウント選択ロジック組込み
  3. bot/config.py に設定追加
  4. .env に3アカウント設定
  5. 単体テスト: AccountPool の acquire/release/mark_rate_limited
  6. 統合テスト: 実際に3アカウントでCLI実行し、アカウント振り分けを確認
  7. 実測検証: 1アカウントを意図的にrate limitさせ、別アカウントへの切替が有効か確認

11.3 Phase 0 完了条件(レビュー⑫反映)

「均等利用」を完了条件から削除。目的はrate limit回避であり、均等分散は手段に過ぎない。
  1. rate limit粒度の実測データが取得できている(Step 0)
  2. rate limit発生時に別アカウントへの切替が動作する(Step 1)
  3. 単一アカウント設定(CLAUDE_ACCOUNTS 未設定)でも既存動作に影響なし(Step 1)

11.4 Phase 0 でやらないこと(Phase 1以降)

12. セキュリティ考慮

入力バリデーション

機密情報

アトミック書込み

13. トレードオフ・代替案

検討した代替案

概要採用/却下理由
A. ラウンドロビン 順番にアカウントを割り当て 却下 負荷の偏りを考慮できない。長時間タスクがあると特定アカウントに偏る
B. active_countベース(採用) active_count最小のアカウントを選択 採用 実装がシンプルかつ負荷分散に効果的。total_runsフィールドは廃止(レビュー⑥)
C. 重み付きスコア active_count + rate_limit_history でスコア計算 却下 Phase 0では過剰。rate limitの実測データが不足している段階で重みを調整するのは時期尚早
D. 外部プロキシ HAProxy等でCLI実行をプロキシ 却下 Claude CLIはsubprocess実行のため、HTTPプロキシは不適。アーキテクチャの複雑化
E. サイドカー方式 独立プロセスが全CLI実行を監視・割当 却下 IPC通信が必要で複雑さが桁違い。障害点の増加。現時点では直列挟み込みで十分(レビュー⑩)
F. pool_status.json 分離 アカウント状態を専用ファイルに保存 却下 rate_limit_status.json と情報が重複。AI Opsが2ファイル突き合わせ必要。統合がシンプル(レビュー⑪)

14. レビュー設計決定サマリー

CEOレビュー(12件)の質問と設計決定を一覧にまとめる。

#優先度質問要旨設計決定影響セクション
コード実装前にrate limit粒度を実測すべき Phase 0を Step 0(実測)→ Step 1(実装)に分割。scripts/test_rate_limit_granularity.py(〜80行)を作成。/tmp/claude-{UID}/ 干渉も検証 11.1, 11.3
acquire/releaseにasyncio.Lockが必要 AccountPool._lock: asyncio.Lock を追加。acquire/release/mark_rate_limitedを async メソッドに変更 3.2
active_countの永続化と不整合リスク active_countはメモリのみ(永続化廃止)。起動時にProcessRegistryと突き合わせて再計算。リスクを「低」→「中」に引上げ 3.1, 3.4, 10
--resume削減効果の定量化 設計変更なし。Max Planは月額固定のためトークン再消費に金銭コスト0。影響は再読込の数秒〜十数秒のみ。Phase 0実測後に再評価 10
全アカウント制限時の待機ロジック acquire(wait: bool = False) パラメータを追加。wait=Trueなら最短cooldown_until解除まで待機(上限30分) 3.2, 4, 6.1
スコアリングのtotal_runsは必要か total_runs フィールドを完全削除。スコアリングは active_count のみ。同スコア時はアカウント名の辞書順 3.1, 3.3, 3.4
クールダウン5分の根拠 5分はフォールバック値。CLI提供の retry_after/resetsAt を優先。Phase 0実測で調整 6.2
account_pool.pyの配置場所 bot/account_pool.py で確定。利用者がbot/内に閉じており、既存パターン(process_registry等)と一貫 —(変更なし)
ファイル分離の必要性 分離維持。claude_runner.pyが既に500行超。テスト容易性・責務分離の観点から別ファイルが適切 —(変更なし)
直列挟み込み vs サイドカー方式 直列挟み込み方式を確定。サイドカーはIPC基盤だけで100行超の追加コード。既存の _semaphore パターンと一貫 —(変更なし)
pool_status.jsonをrate_limit_status.jsonと分ける理由 pool_status.json新規作成を撤回。既存の rate_limit_status.jsonaccounts 配列を追加して統合 3.4, 9
Phase 0完了条件の「均等利用」は不要 「均等利用」を削除。完了条件を3項目に絞り込み: 実測データ取得、アカウント切替動作、後方互換性 11.3