# ADR-0027d: AuditContext writer 失敗時のセマンティクス

**Status**: Proposed
**Date**: 2026-05-13
**Depends on**: ADR-0027 (AuditSeal 分離)

---

## Context

ADR-0027 は AuditSeal ライフサイクルにおいて OS が管理する 2 つの書き込み
操作を定義している:

1. **AuditContext 書き込み** — skill run 開始時: 最初のフェーズが実行される
   前に OS が `audit/context/<run_id>.json` を書き出す。
2. **AuditSeal 書き込み** — skill run 完了時: 最後のフェーズが完了した後に
   OS が `audit/seals/<run_id>.json` を書き出す。

親 ADR では **どちらかの書き込みが失敗した場合に何が起きるか** を defer した。
これが重要な理由:

- AuditContext はスキルが実行される前に書き出される; ここでの失敗はスキル開始を
  ブロックするか、コンテキストレコードなしで続行することを明示的に許可する
  必要がある。
- AuditSeal はスキル完了後に書き出される; ここでの失敗はスキルが出力を生成した
  が seal がないことを意味する — 機能的な失敗ではなく compliance のギャップ。

この 2 つの書き込みポイントは異なる失敗モードと異なる結果を持つが、核心の
設計的緊張を共有する: **fail-open** (レコードなしで続行) vs. **fail-closed**
(監査レコードが書き込めない場合はブロックまたはアボート)。

AuditSeal 機能はオプトイン (ADR-0027 §2 でデフォルト off)。失敗セマンティクスは
機能が有効か無効かにかかわらず一貫していなければならず、エンタープライズ
compliance の期待と OSS ライトユーザーの寛容さの両方に対応する必要がある。

---

## Decision drivers

- **エンタープライズ compliance 要件**: 規制環境では通常 fail-closed が求められる
  — 監査レコードが書き込めない場合、監査対象の行動は進むべきでない (または
  integrity 失敗としてフラグされるべき)。
- **OSS ライトユーザーの寛容さ**: AuditSeal を「あると良いもの」として有効にした
  ライトユーザーは fail-open を好むかもしれない — 一時的なディスク書き込み
  失敗で長い skill run をアボートすべきでない。
- **一時的な失敗への耐性**: ディスク書き込み失敗はしばしば一時的なもの
  (ファイルシステムがまだマウントされていない、一時的な I/O エラー)。
  リトライループで問題を解決できる可能性がある。
- **Init/teardown のタイミング**: AuditContext 書き込みはスキル開始時に発生し、
  スキル作業が始まる前 — fail-closed ブロックのコストは低い (まだ何も計算
  されていない)。AuditSeal 書き込みはスキル完了時に発生し、全スキル作業の後
  — fail-closed アボートのコストは高い (ユーザー視点では全作業が失われる)。
- **ギャップ検出**: seal が欠落している場合、監査 verifier は「スキルが実行されたが
  seal が書き込まれなかった」と「スキルが一度も実行されなかった」を区別できる
  必要がある。これには AuditContext レコード (開始時に書かれる) の存在か、
  ギャップマーカーが必要。
- **Plan-mode との相互作用**: sub-ADR 0027c の Option B (PlanSeal) はプラン完了時の
  3 番目の書き込みポイントを導入する; その失敗セマンティクスはこの sub-ADR の
  決定と一貫していべきだ。

---

## 検討した Options

### Option A: Fail-open (スキルは実行され、欠落した監査レコードは事後に検出)

どちらかの書き込みが失敗した場合、OS は失敗をログに記録して続行する。スキル
実行は正常に進む。欠落したレコードは監査スウィープでアノマリーとして現れる
**既知のギャップ** となる。

**コンテキスト書き込み失敗**: スキルは AuditContext レコードなしで開始。
**Seal 書き込み失敗**: スキルは完了し出力が配信されるが seal なし。

**Pros:**
- スキル機能が監査インフラの失敗に影響されない。
- 長時間実行スキルが一時的な I/O エラーでアボートされない。
- AuditSeal を軽量な監査可能性のために有効にしたユーザーは、監査カバレッジが
  低下しても skill 実行を得られる — OSS ライトユーザーには許容範囲。

**Cons:**
- コンプライアンスのギャップが監査スウィープまで検出されない可能性がある。
  規制環境では「スキルは実行されたがレコードがない」は受け入れられない監査
  結果となりうる。
- コンテキスト書き込みがサイレントに失敗した場合、最終的な seal の `run_id`
  アンカーがなく verifier が両者をリンクできない。

### Option B: Fail-closed (監査書き込み失敗時にスキルをブロックまたはアボート)

**コンテキスト書き込み** が失敗した場合、スキルは開始しない。OS は呼び出し
元にエラーを返す。

**Seal 書き込み** が失敗した場合、OS は最大 N 回リトライし (設定可能)、その後
`seal_write_failed` イベントを emit し、スキル実行を不完全な監査証跡を持つもの
として扱う。スキル出力が呼び出し元に配信されるかどうかはリトライ結果に依存する。

**Pros:**
- 強力な compliance 保証: 進行が許可された全スキル実行に監査レコードが存在する。
- スキル開始時のコンテキスト書き込み失敗のブロックはコストが低い (まだ何も
  計算されていない)。
- 明示的なリトライポリシー自体が監査可能。

**Cons:**
- スキル開始時の一時的なディスク書き込み失敗がユーザーのリクエストを、作業が
  始まる前にアボートする — 混乱を招く可能性。
- 長い skill run 後の seal 書き込み失敗は簡単にアボートできない (スキル出力は
  すでに存在する); ここで強制アボートはユーザー視点では破壊的。
- リトライ数の設定が運用の複雑さを追加する。

### Option C: デグレードモード (メモリ保持、バックグラウンドリトライ)

書き込み失敗時、OS はレコードをメモリに保持し、バックグラウンドタスクで
書き込みをリトライする。設定可能なウィンドウ内にバックグラウンドリトライが
成功した場合、レコードは正常に書き込まれる。ウィンドウが成功なしに期限切れに
なった場合、OS は `seal_degraded` イベントを emit し fail-open セマンティクスで
続行する。

**コンテキスト書き込み**: コンテキスト書き込みが成功するかリトライウィンドウが
期限切れになるまで (その後 fail-open) スキル開始が遅延される。
**Seal 書き込み**: スキル出力が配信され、バックグラウンドリトライが非同期に
seal を書き込む。

**Pros:**
- スキルを失敗させずに一時的な I/O エラーを gracefully に処理。
- ギャップを宣言する前にリカバリーのウィンドウを提供。
- `seal_degraded` イベントが verifier のための検出可能なシグナルを生成。

**Cons:**
- メモリ保持はリトライウィンドウ中にプロセスがクラッシュするとレコードが
  失われる — まさに AuditSeal が監査可能にしようとしているクラッシュシナリオ。
- バックグラウンドリトライの複雑さが OS 実装面を増加させる。
- リトライウィンドウのタイミングをスキルライフサイクルイベントと調整する必要。

### Option D: reyn.yaml で設定可能 (オペレーターの選択)

失敗セマンティクスは `reyn.yaml` の設定オプションで制御される:

```yaml
audit:
  writer_failure: fail-open   # または: fail-closed, degraded
  writer_retry_count: 3       # fail-closed と degraded モードに適用
  writer_retry_delay_ms: 500
```

オペレーターは環境に適したモードを選択する。エンタープライズオペレーターは
`fail-closed` を選択し、OSS ライトユーザーはデフォルト (`fail-open`) のまま。

**Pros:**
- 単一ポリシーを強制せずエンタープライズと OSS の両要件に対応。
- サプライズのある動作なし: オペレーターが期待を明示的に宣言。

**Cons:**
- 設定面が増加。
- 誤設定 (例: 監査ディレクトリのないテスト環境で `fail-closed`) は混乱を
  招くスキル開始失敗を引き起こす。
- 実装はどのモードがアクティブかにかかわらず 3 つのモード全てをサポートする
  必要があり、複雑さが増加する。

---

## Recommendation (proposed direction)

**Option D (設定可能)** を推奨し、以下のデフォルトを設定する:

| モード | デフォルト対象 | 理由 |
|---|---|---|
| `fail-open` | デフォルト (AuditSeal 無効) | no-op; AuditSeal はオフ |
| `fail-open` | AuditSeal 有効、明示的設定なし | OSS ライトユーザー; スキル機能 > 監査完全性 |
| `fail-closed` | 明示的なエンタープライズ設定 | 規制環境のオプトイン |

**コンテキスト書き込み** と **seal 書き込み** の失敗モードは独立して設定可能:

```yaml
audit:
  writer_failure:
    context: fail-open    # コンテキスト書き込みが失敗してもスキルは開始
    seal: fail-open       # seal 書き込みが失敗してもスキル出力は配信
    retry_count: 3        # 両方に適用; 0 = リトライなし
```

非対称デフォルトの理由:
- スキル開始時のコンテキスト書き込み失敗はブロックコストが低く、エンタープライズ
  ユーザーはここで `fail-closed` を好む場合がある。
- 長い skill run 後の seal 書き込み失敗はブロックコストが高く、エンタープライズ
  環境でも `degraded` または明示的なイベントを伴う `fail-open` がスキル出力の
  アボートより実用的。

**最小限の viable 実装 (最初の AuditSeal リリース)**: `fail-open` モードのみを
実装し、失敗時に `seal_write_failed` イベントを emit する。設定可能な
`fail-closed` パスはエンタープライズ compliance 認証を対象としたフォローアップ
PR で追加する。このシーケンシングにより、より複雑なリトライ/モード選択ロジック
でも最初の AuditSeal リリースがブロックされない。

本 recommendation は実装着手時に再判断すること。

---

## Open questions

1. コンテキスト書き込みが失敗してスキルが続行した場合 (fail-open)、最終的な
   seal 書き込みに `context_hash` アンカーがない。seal は `context_hash: null`
   マーカー付きで書き込まれるべきか、それとも完全に省略される (ギャップを
   作る) べきか?
2. `fail-closed` のコンテキスト書き込みモードについて: 呼び出し元から見た
   observable な動作は? (エラーイベントが emit される? スキル開始レスポンス内の
   特定のエラーメッセージ?)
3. リトライメカニズム (Option D の `retry_count`) について: リトライは書き込み
   時の同期ブロックか非同期バックグラウンドタスクか? Option C のメモリ保持
   リスクは非同期リトライにも適用される。
4. plan-mode (sub-ADR 0027c の Option B PlanSeal) について: `PlanSeal` の writer
   失敗セマンティクスはこの sub-ADR の `seal` モードを継承するか、独立して
   設定可能か?

---

## Related

- ADR-0027: AuditSeal 分離 (親 ADR)
- ADR-0027a: ハッシュチェーントポロジー
- ADR-0027b: config_hash スコープ
- ADR-0027c: seal_unit と plan-mode 統合 (PlanSeal の writer 失敗はこの
  sub-ADR の解決に依存)
- ADR-0001: WAL + スナップショット (メモリ保持リスクのクラッシュリカバリー文脈)
- ADR-0013: 例外認識クラッシュライフサイクル (スキルアボートのセマンティクス)
