---
type: how-to
topic: operations
audience: [human]
---

# Reyn が止まる理由を理解する

Reyn が実行を中断するときは、必ず**次の 3 つのいずれか**が原因です:

1. **ループ検知** — 同じことを繰り返している
2. **タイムアウト** — 時間がかかりすぎている
3. **予算超過** — トークン / USD の上限に達した

各カテゴリは独自の設定名前空間を持ち、エラーメッセージには「このキーを上げれば
継続できる」というヒントが組み込まれています。本ページは、止まる理由と設定キーの
対応関係をまとめます。

> **TL;DR:** 統一された名前空間は、ループ / タイムアウトが `safety.*`、
> 財務的な上限は `cost.*` です。

---

## ① ループ検知 — `safety.loop.*`

ループ検知は*暴走的な繰り返し*を捕まえる仕組みです。フェーズが永遠に再入する、
ルーターが何度もルーティングし直す、委譲チェーンが青天井に伸びる、など。
開発中に到達するのは正常です。本当に必要な反復数なら上限を引き上げ、
そうでないなら原因を調査してください。

| 上限 | 検出対象 | 既定値 | 設定キー |
|---|---|---|---|
| フェーズ訪問回数 | 1 スキル実行内で同じフェーズに入りすぎた | 25 | `safety.loop.max_phase_visits` |
| フェーズ内 act ターン数 | 1 フェーズ訪問内の LLM ↔ op の往復 | 10 | `safety.loop.max_act_turns_per_phase` (skill / phase frontmatter が優先) |
| 1 ターンあたりの router 呼び出し | 1 ユーザーターン内のルーター起動回数 | 3 | `safety.loop.max_router_calls_per_turn` (0 = 無制限) |
| エージェント委譲の深さ | `user → A → B → C` のチェーンが深すぎる | 3 | `safety.loop.max_agent_hops` |
| チェーン内スキル起動回数 | 同一スキルが同一チェーンで起動しすぎた | 無制限 | `safety.loop.skill_calls_per_chain.hard_limit` |
| チェーン内スキルトークン数 | 同一スキルが同一チェーンでトークンを使いすぎた | 無制限 | `safety.loop.skill_tokens_per_chain.hard_limit` |

### エラーの例

```
Phase 'revise' reached max_phase_visits=25.
→ Raise safety.loop.max_phase_visits to allow more iterations.
```

### 修正の例

```yaml
# reyn.local.yaml
safety:
  loop:
    max_phase_visits: 50      # フェーズあたり 50 回まで許可
    max_router_calls_per_turn: 5
```

---

## ② タイムアウト — `safety.timeout.*`

タイムアウトは*時間がかかりすぎている*ものを捕まえます。LLM 呼び出しが遅い、
委譲先が応答しない、フェーズが 1 時間動き続けている、など。本当に時間が
必要なら上限を引き上げ、そうでないなら原因を調査してください。

| 上限 | 検出対象 | 既定値 | 設定キー |
|---|---|---|---|
| LLM 1 呼び出し | 1 回の litellm.acompletion がタイムアウトを超えた | 60 秒 | `safety.timeout.llm_call_seconds` |
| LLM リトライ | 一時エラー時のリトライ上限 | 3 | `safety.timeout.llm_max_retries` |
| フェーズ wall-clock | 1 フェーズ訪問が時間予算を超えた | 無制限 (`0`) | `safety.timeout.phase_seconds` |
| Chain 待機 | マルチエージェントの pending chain が委譲応答を待ちすぎた | 60 秒 | `safety.timeout.chain_seconds` (0 = タイムアウトなし) |

### エラーの例

```
chain timeout: 1 delegate(s) (writer) did not respond within 60s.
→ Raise safety.timeout.chain_seconds to wait longer (0 = no timeout).
```

### 修正の例

```yaml
# reyn.local.yaml
safety:
  timeout:
    llm_call_seconds: 120     # 遅いモデル用
    chain_seconds: 300        # 長時間動く委譲先用
```

---

## ③ 予算超過 — `cost.*`

予算上限は**財務的なキャップ**(トークン数、USD 額、日次 / 月次クォータ) です。
意図的に `cost:` 配下に残し、`safety:` には統合しません。運用者の感覚として、
ループ / タイムアウトは「上げるべき」ことが多いのに対し、予算は「調査するか
明示的に承認する」ことが多いためです。

| 上限 | 検出対象 | 設定キー |
|---|---|---|
| エージェントごとのトークン | 1 エージェントがトークン上限に達した | `cost.per_agent_tokens.hard_limit` |
| エージェントごとの USD | 1 エージェントが USD 上限に達した | `cost.per_agent_cost_usd.hard_limit` |
| 日次クォータ | 当日の合計が `daily_tokens` / `daily_cost_usd` を超えた | `cost.daily_tokens.hard_limit`, `cost.daily_cost_usd.hard_limit` |
| 月次クォータ | 当月の合計が `monthly_tokens` / `monthly_cost_usd` を超えた | `cost.monthly_tokens.hard_limit`, `cost.monthly_cost_usd.hard_limit` |
| レートリミット | モデルごとの requests-per-minute 上限 | `cost.rate_limit_per_minute.<model>` |

((chain, skill) ごとの起動回数 / トークン上限はループ検知側にあり、
`safety.loop.skill_calls_per_chain` / `safety.loop.skill_tokens_per_chain`
を参照してください — 上記 §① 参照。)

### 上限到達時のユーザー承認フロー

(chain, skill) ごとの起動回数キャップは、即時拒否ではなく*対話的な承認*に
切り替えできます:

```yaml
# reyn.local.yaml
safety:
  loop:
    skill_calls_per_chain:
      hard_limit: 5
      ask_on_exceed: true       # ask_user 経由で問い合わせる
      extension_calls: 3        # 承認時に +3 回付与
```

上限到達時に Reyn が *「Skill `X` が上限 5 回に達しました。+3 回追加で
継続しますか?」* と問います。承認は何度でも可能で、毎回 `extension_calls`
分だけキャップが拡張されます。

---

## limit 到達時の挙動 (`safety.on_limit`)

既定の挙動: limit 到達時、 `ask_user` で user に延長を確認します
(= `mode: interactive` / `ask_timeout_seconds: 0` — 無制限待機)。
拒否 / timeout / intervention surface 不在の場合は abort、 Reyn は
`RunResult` を返し、 `status` を `loop_limit_exceeded` /
`phase_budget_exceeded` / `budget_exceeded` のいずれかに設定し、
`partial_data` には最後に完了したフェーズの artifact (= *「今ここまでの
成果物」*) を入れます。ヘッドレス path (= `bus=None` / 非 TTY stdin) は
intervention 不要に自動的に abort 経路へ短絡するので、 `interactive`
既定はどの環境でも安全です。

挙動は `safety.on_limit.mode` で変更できます:

```yaml
# reyn.local.yaml
safety:
  on_limit:
    mode: interactive      # 既定 — ask_user で user に確認、 承認時に limit 延長して継続
    # mode: unattended     # 到達時に abort (= CI / cron / スクリプト実行向けのオプトイン)
    # mode: auto_extend    # N 回まで自動延長、 それ以降は abort
    auto_extend_times: 1   # auto_extend 時のみ参照
    ask_timeout_seconds: 0  # interactive 時のみ参照; 0 = 無制限待機
```

| モード | 用途 |
|---|---|
| `interactive` (既定) | `reyn chat` / TUI / a2a session — user が到達可能で延長判断を返せる |
| `unattended` | CI / cron / スクリプト実行で human を待てない用途のオプトイン、 fail fast |
| `auto_extend` | 信頼済みの長時間タスクで「N 回までは自動延長して良い」と分かっているとき |

**各 limit がどの site で wiring されているか:**

| Limit | Site | モード挙動 |
|---|---|---|
| `safety.loop.max_phase_visits` | `OSRuntime._enter_phase` | interactive / auto_extend |
| `safety.timeout.phase_seconds` | `OSRuntime._check_phase_budget` | interactive / auto_extend |
| `safety.loop.max_act_turns_per_phase` | OSRuntime act-loop | interactive / auto_extend |
| `safety.loop.max_router_calls_per_turn` | `ChatSession._check_and_increment_router_cap` | interactive / auto_extend |
| `safety.loop.max_agent_hops` | `ChatSession._send_to_agent` | interactive / auto_extend |
| `safety.timeout.chain_seconds` | chain timeout watchdog | interactive / auto_extend (再 arm) |
| `safety.loop.skill_calls_per_chain` | spawn budget gate | interactive (= `ask_on_exceed`) |

`safety.timeout.llm_call_seconds` は設計上対象外です — litellm が
`safety.timeout.llm_max_retries` 内で自動再試行するため、 追加の
`ask_user` 層はレイテンシを増やすだけになります。
