ADR-0116: Decision Pipeline のフル run を gate 単位 chunk 実行に再設計し consumer invocation 上限 kill を構造的に解消する
- Status: Superseded by ADR-0120 (2026-06-05。§Phase 0 調査記録は ADR-0120 の決定根拠として参照継続。旧: Accepted accepted-with-residual-risks — 末尾の HITL 残余リスク節参照。PR #1430 merge = 受理の規約により 2026-06-04 受理)
- Mode: Standard
- Kruchten Type: Executive/Property
- Scope: product
- Implementation Status: N/A (案B 本実装は着手せず ADR-0120 Workflows 移行へ・実装不要)
- 起案者: [email protected]
- 起案日時 (JST): 2026-06-04 20:50
- 承認日時 (JST): 2026-06-04 (PR #1430 merge = HITL 受理)
- Deciders: [email protected] (単独)
コンテキスト
§1.1 背景
Decision Pipeline のフル 10 gate 審査 (~16-24 分) は Cloudflare Queues consumer の 1 invocation 時間上限を超え、worker が complete 書込後・ack 前に kill される構造を抱える。2026-06-04 の session bedb9912 実測で at-least-once 再配信により全 gate が 3 周再実行され、通過 result 2 件が差し戻し result に上書き・LLM 課金 ~3 周・kill された 2 周分の telemetry が 0 行 (監査証跡の喪失) が確認された。
§1.2 現状 (As-Is)
緩和ガードは導入済みだが、kill 自体は構造として残存している:
isInflightRedelivery()(PR #1382): running + updatedAt 20 分鮮度内の再配信を ack-skip → 多重実行は止まったが、kill→再配信→skip のサイクルで「最後の周回の telemetry しか残らない」問題は未解決 (DRP-376 残課題②)。さらにこのガードは updatedAt 鮮度のみで判定するため、DO が直前に更新されていると再配信を素通りさせる既知の盲点がある (chunk 化後は gate index と組み合わせないと誤 skip / 素通りの両方が起きる)- queued watchdog 30 分化 + 回復ガード (PR #1398, DRP-374): 直列 consumer の長時間占有が前提のため、閾値が invocation 長に縛られている
§1.3 課題
現構造では (1) 監査証跡 (telemetry) の完全性が run 長に依存して壊れる、(2) ガード網が「kill が起きる前提」の防御的複雑性を増やし続ける、(3) watchdog・キャンセル等の周辺機構の設計余地が侵食される。
§1.4 制約・要件
- 冪等性 (Must): at-least-once 再配信下で二重 LLM 実行ゼロ・telemetry 喪失ゼロを保証
- 監査証跡 (Must): 電子帳簿保存法第8条・中小企業会計指針の訂正・削除履歴要件と整合する試行履歴記録 (skip・再配信・再実行・確定結果の判別可能性)
- DO 制約: Cloudflare Durable Object storage の key あたり 128KB・transaction あたり 128KB 上限
- 既存基盤: Cloudflare Queues + PipelineSessionDO + LangGraph streamEvents を踏襲 (新基盤導入不可)
- 段階導入: feature flag による旧構造への即時 rollback 可能性
§1.5 目標 (To-Be)
consumer の実行単位を「フル run」から「gate 単位 chunk」に分割し、kill 影響を 1 chunk 分に封じ込め、seq 照合による冪等回復で telemetry 完全性を保証する。Non-Goals: kill 発生確率そのものをゼロにすること、Cloudflare Workflows (案C) への移行 (本 ADR は Stepping Stone と位置づけ別 ADR で扱う)。
決定
consumer の実行単位を「フル run」から「gate 単位 (または数 gate の chunk)」に分割する。1 message = 1 chunk を実行し、chunk 完了時に次 chunk の message を再 enqueue、実行状態 (現在 gate・中間 state) は既存の PipelineSessionDO に保存する。本方針は「kill の構造的根絶」ではなく「kill 影響の chunk 単位への封じ込め + 冪等回復の保証」である。各 invocation が数分以内に収まるため、kill の発生確率と影響範囲 (失われる作業量) を 1 chunk 分に縮小する。
kill タイミング別挙動マトリクスと冪等設計 (本 ADR の中核設計)
順序は enqueue → ack を採用する (ack→enqueue 逆順は「ack 後 enqueue 前 kill」で次 gate が永久に起動しないパイプライン停止を生むため不採用)。Cloudflare Queues に enqueue+ack のアトミック化 (transactional outbox) は存在しないため、DO 側の冪等テーブルで等価の安全性を実現する:
- 各 chunk message に 単調増加の chunk_sequence_number + gate_id を付与
- PipelineSessionDO に 「期待する次 chunk_sequence_number」と処理済み chunk の冪等テーブルを保持 (DO storage の transaction で gate 結果書込と同時に原子更新)
- consumer は chunk 実行前に DO へ照会し、chunk_sequence_number が期待値と不一致 (= 処理済み) の再配信は LLM 呼び出しに入らず ack-skip。skip イベント自体を telemetry に記録する (無言 skip にしない)
- DO 単一スレッド前提の限界対策 (CAS ロック): DO の単一スレッド保証は同一 DO インスタンスへの並行リクエスト内に限られ、「DO fetch → LLM 呼び出し → DO 書込」の区間は DO 外で並走するため TOCTOU 競合が発生しうる。これを防ぐため、DO 側に chunk 実行開始時の CAS ロックフラグ (TTL 5 分・kill 時自動解除) を追加し、既にロック済みの場合は即 ack-skip とする
| kill タイミング | 挙動 | 回復 |
|---|---|---|
| chunk 実行中 (gate 処理前) | DO 未更新・ack なし → 再配信 | 冪等テーブル未記録のため通常再実行。LLM 呼び出しは gate 単位なので再課金は最大 1 gate 分 |
| gate 完了・DO 書込後・enqueue 前 | DO に gate 結果 + seq 更新済・ack なし → 再配信 | 冪等テーブルが処理済みと判定 → LLM 再実行なしで 次 chunk の enqueue だけを再実行して ack (enqueue の再送は次 chunk 側の冪等チェックが吸収) |
| enqueue 後・ack 前 | 次 chunk message は配達済 + 現 chunk が再配信 | 同上 — 処理済み判定で enqueue 含め副作用なしの ack-skip。二重 enqueue された次 chunk は seq 一致側のみ実行、他方は skip |
| ack 後 | 正常完了と同じ | 影響なし |
| 2 consumer 同時起動 (TOCTOU) | 両者が DO fetch で「未処理」を得る前後で CAS ロック | 先勝ち側のみ実行、後発はロック検出で ack-skip + telemetry 記録 |
これにより updatedAt 鮮度に依存していた isInflightRedelivery() は seq 照合 + CAS ロックに置換され、「DO が新しすぎてガードが素通りする」既知の盲点も同時に解消される。
LangGraph state の DO 128KB 制約への対策 (事前調査ゲート付き)
DO storage は key あたり 128KB・transaction あたり 128KB の二重制約があり、LangGraph state には body_generation の生成本文・parallel_review の 3 モデル出力が蓄積されるため、フル state の単一 key 保存は最悪ケースで超過しうる。実装着手前の事前調査として、既存 run の telemetry/D1 と DO ログから gate ごとの state サイズ実測を行い、最大値と 128KB に対するマージンを定量化する (未実測のまま着手しない)。設計方針:
- state を「制御メタデータ (現在 gate・seq・冪等テーブル・CAS ロック: 数 KB)」と「gate 出力ペイロード」に分離し、ペイロードは gate ごとに別 key で追記保存 (full snapshot の単一 key 更新をやめる)
- 実測で単一 gate 出力が 100KB (マージン 28KB) を超えるケースが確認されたら、当該 gate のペイロードは R2 へオフロードし DO には参照キーのみ保存を採用条件に昇格する。R2 オフロード時は同一 key への上書き (deterministic key 設計) により kill 再配信下でも R2 write の冪等性を保証する
- 復元時の部分更新不整合は「制御メタデータの seq と gate 出力 key の存在チェック」で検出し、不整合 chunk は失敗扱い → 再配信で再実行
- CI に state サイズ回帰テスト (全 gate の payload 最大値・冪等テーブルサイズを記録・閾値超過で fail) を追加し、将来の gate 追加で静かに超過する事態を防ぐ
state 直列化 drift・LangGraph 公式非サポート挙動への対策
- 直列化/復元は zod スキーマでバリデーションし、schemaVersion を state に埋め込み、版不一致・必須フィールド欠損の復元は即 chunk 失敗 (静かな undefined 伝播をさせない)
- LangGraph の streamEvents を任意の gate 境界で打ち切る操作は公式サポート外であり、graph 内部のチャンネル更新・pending interrupt・reducer の未確定バッファのリークリスクがある。Phase 0 spike で全 gate の打ち切り・復元を実バージョンで網羅検証し、可能なら LangGraph 組み込みの checkpoint 機構 (MemorySaver 等) を活用して公式 API 経由で state 取得・復元する
- LangGraph バージョン固定 + アップグレード時の境界テスト必須を CI に追加し、独自直列化に頼る場合のバージョン依存リスクを ADR に明記
- state フィールドを追加する PR には直列化テストの更新を CI で必須化
監査証跡 (電帳法整合) の強化
「終端 run 数 = telemetry 行数」の一致だけでは不十分 (再実行・skip の履歴が残らない)。chunk 化に合わせて:
- chunk skip・再配信・再実行の全イベントを telemetry に記録し、「全 gate の試行回数・スキップ回数が run ごとに記録される」ことを KPI に追加 (DRP-376 残課題②の恒久解消)
- skip / 再配信イベントの telemetry 書込を DO transaction に原子的に含める設計とし、ack の前後関係でいずれの kill タイミングでも skip 記録が喪失しないことを kill 注入テストのマトリクスで検証 (電帳法「真実性確保」要件への対応)
- telemetry スキーマ拡張:
skip_reason_code/redelivery_count/gate_not_executed_flag/is_authoritative(当該 gate の確定結果フラグ) を追加し、再実行で重複した同一 gate の複数レコードから「正」を第三者が一意に特定できる形式にする - run の終端状態を
completed / failed / cancelled / stalledで明示記録し、60 分進捗のない run は自動で failed に遷移させる。stalled_by_watchdogフラグを付与し、後から正常完了が確認された場合の訂正手順をプレイブックに含める (固定閾値の運用は週次レポートで超過 run_id をレビューし妥当性を再評価) - 電帳法対応の顧問弁護士・税理士に「chunk 再実行ログが訂正・削除履歴要件を満たすか」を Phase 0 の調査対象として事前確認し、確認結果を ADR に追記する
判断基準 (Decision Drivers)
3.1 評価軸
| # | 軸 | 重要度 (係数) | 案件特有の解釈 |
|---|---|---|---|
| 1 | #reliable | [Must] (×2.0) | 二重 LLM 実行ゼロ・telemetry 完全性 (全終端 run が 1 行残る + 全 gate の試行/スキップが記録される)。K.O.: kill 起因の telemetry 喪失が再現するなら不採用 |
| 2 | #efficient | [High] (×1.0) | LLM 課金の冪等 (再配信時の再実行は最大 1 gate 分・通常 0) |
| 3 | #maintainable | [High] (×1.0) | ガード網 (isInflightRedelivery / 終端ガード / 回復ガード) を seq 照合 + CAS へ置換・簡素化できるか。chunk 化で防御的複雑性が逆に増えるなら本末転倒 |
| 4 | #operable | [Medium] (×0.5) | gate 進捗の可視化 (ADR-0089 partial) と整合し、run_id + gate_id + seq で「run X はどの gate で止まっているか」を 1 クエリで返せること。message 数 ~10 倍化に伴う既存アラート閾値の再設計も含む |
K.O. criterion: Must 軸の score < 3 は不採用。
3.2 評価軸 × 案スコア表
| 軸 | 係数 | 案B: gate chunk 化 (採択) | 案A: 現状維持 | 案C: Workflows 移行 | 案D: 2 段 queue 分割 |
|---|---|---|---|---|---|
| #reliable | 2.0 | 5 | 2 | 5 | 3 |
| #efficient | 1.0 | 4 | 2 | 4 | 3 |
| #maintainable | 1.0 | 4 | 1 | 5 | 2 |
| #operable | 0.5 | 4 | 3 | 4 | 3 |
| 加重和 (正規化) | 0.871 | 0.371 | 0.943 | 0.586 | |
| K.O. 通過 (Must ≥3) | ✓ | ❌ | ✓ | ✓ |
加重和上は案C が最高だが、LangGraph 統合障壁が 未調査 (spike 必須) で着手リスクが定量化できないため、本 ADR は案B を採択し案C への Stepping Stone と位置づける (§4 参照)。
検討した代替案 (Alternatives Considered)
- 案A: 現状維持 (ガード網のみ) — 多重実行は止まっているが telemetry 喪失と防御的複雑性の増殖が残る。監査要件 (全 run の証跡) と相性が悪く K.O. 不通過
- 案B (採択): gate 単位 chunk 化 — 既存 DO (状態保存)・Queue (再 enqueue) の延長で実現。LangGraph の streamEvents ループを chunk 境界で打ち切り、state を DO に直列化して次 message で復元
- 案C: Cloudflare Workflows への移行 — durable execution (step 単位の retry・状態永続化) を基盤に委譲できる正攻法で、本 ADR が手製実装する機構と重複。ただし LangGraph 実行モデルとの統合障壁 (streamEvents の step 分割・state 型の差異・API 非互換点) が未調査で移行コストを定量化できない。本 ADR は案C への Stepping Stone と明示的に位置づけ、chunk 化で導入する seq 冪等設計・state 分離設計は Workflows 移行時に捨てる前提の意識的な技術的負債とする。Phase 0 に 案C の最小統合 spike (0.5 人日) を並行実施して比較材料を生成、稼働 3 ヶ月後に案C の ROI 再評価レビューを必ず実施 (詳細は §6・§7)
- 案D: queue を light/heavy の 2 段に分割 — heavy gate だけ別 message 化。案B の部分適用だが境界設計が恣意的になりがち
影響 (Consequences)
§5.1 正の影響 (Good)
- kill 起因の telemetry 喪失ゼロ
- ガードの seq 照合 + CAS ロックへの一本化
- watchdog 閾値を短く戻せる (queued 30 分 → pickup 即時性の回復)
- キャンセルの gate 境界応答性向上 (ADR-0101 整合)
- chunk skip / 再実行 / 確定フラグの telemetry スキーマ拡張により電帳法「訂正・削除履歴」「真実性確保」要件への対応強化
§5.2 負の影響 (Bad)
- state の直列化/復元の実装複雑性 (LangGraph 公式非サポート挙動への依存)
- message 数が gate 数倍 (~10 倍/run)。Queues 課金は微小だが 既存の queue depth 監視アラートが大量誤発報する リスク (alert fatigue) — リリースと同一 PR で「監視設定移行チェックリスト」をリリース前提条件化し、feature flag 切替中は新旧両ルール並走で対応
- DO 冪等テーブルという新しい管理対象 — run 完了後の TTL purge 設計が必要 (purge は Queues の最大再配信遅延 12 時間を十分超える期間を設定、DO object lifecycle: created → in-progress → completed/failed → archived → deleted)
- DO storage write 頻度の増大 (最大 gate 数 10 × (メタデータ + payload) = 20+ write/run) — 週 100 run / 週 500 run の 2 シナリオで月次コスト試算を Phase 0 で実施
- R2 オフロード採用時の write 課金 (deterministic key 上書きで冪等性確保)
- 案C 移行時の migration cost (in-flight run の state 移行・旧 DO object cleanup・telemetry スキーマ互換) が「意識的な技術的負債」として残る — 上限概算を Phase 0 で算出
§5.3 中立・トレードオフ (Neutral / Trade-offs)
- 旧ガード網の削除先送り (サンクコスト罠 + 損失回避バイアス) — chunk 化 PR と同時に旧ガード網削除 PR も作成し feature flag で無効化した状態でマージ、削除 PR のマージ期限を CI の TODO lint と issue due date で機械的に強制。「削除しない」決定には明示的な ADR 更新を要求
- stalled 60 分固定閾値 — 将来の gate 追加・新モデル導入で正常 run が超過するリスク。週次レポートで超過 run_id をレビューし動的算出への移行可否を継続評価
- Google Sheets 等の下流外部 consumer: 本 Decision Pipeline は Cloudflare 内部 (DO/Queue/R2/D1) に閉じており、Sheets 等の外部スプレッドシートへの中間 gate 結果書込は スコープ外 (現状連携なし)。将来 Sheets 連携を追加する場合は「全 gate 完了 = DO completed」をトリガーとする別非同期処理に分離し、chunk 化の影響を Sheets 書込から隔離する設計を別 ADR で扱う
撤退条件 (Rollback Plan)
- Phase 0 spike の結果、本実装見積もりが Phase 0 込み 8 人日 を超える場合は本実装に着手せず、案C (Workflows) 先行 ADR の起案判断へ切り替える (見積もりは第三者レビュー必須)
- Phase 0 spike が 2 人日を超過した時点で中間チェックポイント発動 (担当者単独判断ではなくテックリード合議で継続/撤退判定)
- 導入後 4 週で chunk 間の重複 gate 実行 (冪等破れ) が 1 件で即調査・2 件以上で旧構造へ rollback (feature flag で切替)。閾値の妥当性は run 頻度実績 (現状 週 5-15 run 規模) で 4 週後に再検証
- state 直列化の drift 起因バグが 3 件以上/4 週で発生したら案C の ADR を起案して本 ADR を Supersede
- 監査アラート要件 (skip 記録率 100%・stalled 自動遷移率 100%・電帳法レビュー結果の整合性) を満たさない場合は即 rollback
- rollback 時の in-flight run の処理手順 (stalled → failed 強制遷移・state リセット・手動再実行・DO 冪等テーブル purge) を本実装と同時にプレイブック化 (rollback 自体がデータ不整合を生まないようにする)
- 稼働 3 ヶ月後の案C ROI 再評価レビュー — ADR 採択と同時にカレンダー登録 + 担当者 (起案者) と承認者 (テックリード) を指名。四半期ごとの維持コスト (chunk 境界再設計工数・drift 対応件数・直列化バグ件数) を OKR に含めて定量記録。ROI 移行判断の暫定数値基準: 維持コストが 3 人日/四半期を超えたら案C 移行 ADR 起案 (3 ヶ月後の再評価で更新)。未実施の場合も撤退条件の判定対象としレビュー実施・不実施・結論を本 ADR の Appendix に追記
コスト試算
段階ゲート方式で見積もる:
- Phase 0 (事前調査・判断ゲート): ~1-2 人日 — ①既存 run ログから gate ごとの state サイズ実測 (128KB マージン定量化) ②LangGraph streamEvents の chunk 境界打ち切り + 復元の spike (捨てコード) ③案C (Workflows) + LangGraph streamEvents の最小統合検証 spike (0.5 人日) ④電帳法対応の顧問弁護士・税理士への chunk 再実行ログの法的解釈確認 ⑤DO storage write の週 100 / 週 500 run シナリオ月次コスト試算 ⑥案C 移行時の migration cost 粗見積もり (0.5 人日)
- Phase 0 中間チェックポイント: spike が 2 人日を超過した時点で判断フロー起動 (即時撤退条件)
- 本実装 (Phase 0 通過後): consumer の chunk 境界実装 + state 直列化/復元 + seq 冪等テーブル + CAS ロック + 再 enqueue 配線
2-3 人日、ガード網の seq 照合置換リファクタ ~1 人日、e2e (mocked + 実 LLM) + state サイズ回帰テスト + kill 注入テスト (2 consumer 同時起動含む) ~1 人日 = **4-5 人日 (Phase 0 込み合計 ~5-7 人日)** - 見積もりの二者確認: spike 実施者の確証バイアス回避のため、見積もりレビューは spike 実施者以外の第三者 (別チームメンバーまたはテックリード) が実施。「8 人日」の計測範囲 (Phase 0・本実装・レビュー・テスト・ドキュメント更新を含む総工数) を見積もりシートに明記し PR に添付
- 運用コスト増: Queues message 数 ~10 倍/run (無料枠内・無視できる) + DO write 増分 (Phase 0 試算結果に依存)
Confirmation
- 検証手段:
- telemetry 監査クエリ: 「終端 run 数 = telemetry 行数」一致 + 「全 gate の試行回数・スキップ回数が run ごとに記録される」一致 + 「同一 gate の複数レコードに
is_authoritativeフラグで一意な正レコードが存在」 - 実 LLM full-run e2e (
drp-real-e2e.yml): 16 分級 draft の単発完走 + 全 gate telemetry 記録 - kill 注入テスト (mocked e2e): マトリクスの 4 ウィンドウ (chunk 実行中 / DO 書込後 enqueue 前 / enqueue 後 ack 前 / 2 consumer 同時起動 TOCTOU) を再現し、二重 LLM 実行 0・telemetry 喪失 0・skip イベント記録 100%・CAS ロック動作を確認
- state サイズ回帰テスト (CI): 全 gate payload 最大値 + 冪等テーブルサイズの記録と 128KB 閾値監視
- 直列化 zod バリデーション: schemaVersion 不一致の復元拒否をユニットテストで確認
- 監視設定移行チェックリスト: queue depth アラート閾値の run 数ベース進捗クエリへの切替完了をリリース前提条件としてレビュー
- 旧ガード網削除期限 lint: TODO コメントに期限を埋め込み CI で warning 検出
- telemetry 監査クエリ: 「終端 run 数 = telemetry 行数」一致 + 「全 gate の試行回数・スキップ回数が run ごとに記録される」一致 + 「同一 gate の複数レコードに
- 実行頻度: 隔週 scheduled (既設) + フル run 構造変更 PR ごと + state スキーマ変更 PR ごと (CI 必須化) + LangGraph バージョン更新 PR ごとに境界テスト必須
- 違反時対応: 不一致検出で feature flag OFF (旧構造へ縮退) + 原因調査 issue + in-flight run は failed 遷移と手動再実行手順
- KPI: kill 起因 telemetry 喪失 0 件/月、chunk 重複実行 0 件/4 週 (1 件で即調査)、skip イベントの telemetry 記録率 100%、stalled run の自動 failed 遷移率 100%、外部 consumer から見える chunk 間中間状態の最大時間窓 (Phase 0 で許容値定義)、旧ガード網削除 PR マージ期限遵守
Phase 0 調査記録 (進行中・2026-06-04)
§コスト試算の Phase 0 項目のうち、机上で確定できる ①⑤⑥ を先行実施した (②③ spike と ④ 税理士確認は未実施 — 8 人日ゲートの最終判断は spike 完了後):
① gate ごとの state サイズ実測 (本番 telemetry 122 run)
| フィールド (gate 出力) | 実測 MAX |
|---|---|
| cross_validation_verdicts | 23.9 KB (最大の単一 gate 出力) |
| adr_body (body_generation) | 22.0 KB |
| input_context (起案者入力) | 16.3 KB |
| cross_validation_detail | 15.9 KB |
| blind_spot_findings (socratic) | 12.5 KB |
| parallel_review_detail | 6.5 KB |
| scoring / consistency / policy_alignment | 各 ~1.4 KB 以下 |
| 単一 run の全フィールド合計 MAX | 74.9 KB |
結論:
- フル state の単一 key 保存: 実測 MAX 74.9 KB (128KB の 58%)・各フィールド MAX の単純和 (理論 worst) ~103 KB (80%) + 直列化オーバーヘッド → マージン不足。§決定の「gate ごとに別 key で追記保存」が実測でも必須と確定
- gate 別 key 分離後: 最大の単一 gate 出力 23.9 KB = 128KB の 19% (マージン 104 KB) → R2 オフロード採用条件 (単一 gate 出力 > 100KB) は未達。R2 は不要、将来の超過は state サイズ回帰テスト (本実装) で検知
- 制御メタデータ (seq・冪等テーブル・CAS フラグ) は数 KB → 制約と無関係
⑤ DO storage write の月次コスト試算
write unit (4KB) 換算: メタ 10 + payload 31 unit (worst の 103KB 基準) + 冪等テーブル 10 ≈ **50 unit/run (worst)**:
| シナリオ | run/月 | write unit/月 | 月額 ($1.00/100 万 unit) |
|---|---|---|---|
| 週 100 run | ~430 | ~21.5k | $0.02 |
| 週 500 run | ~2,150 | ~108k | $0.11 |
Workers Paid の同梱枠 (100 万 write/月) 内に収まり実質 $0 — §5.2 の「微小」想定を実測で確認。
⑥ 案C (Workflows) 移行時 migration cost 粗見積もり
- in-flight state 移行は回避可能: 「in-flight run 0 を待って切替」(直列 consumer・run ~16-24 分のため数時間のメンテ窓で足りる) とすれば DO state → Workflows step state の互換層は不要
- 移行コード書換の粗上限: consumer → Workflow 定義 ~2-3 人日 + telemetry スキーマ互換 ~1 人日 + 旧 DO 冪等テーブル/オブジェクト purge ~0.5 人日 = 粗上限 ~4-5 人日
- 捨てる資産 (意識的な技術的負債): seq 冪等設計・CAS ロック・state 直列化層 ≒ 本実装の ~2-3 人日分
② LangGraph chunk 境界打ち切り + 復元 spike (実施済み・2026-06-04)
LangGraph 1.3.2 で本番と同形のトイグラフ (10 直列 node + rejected→END 条件分岐) を用い、公式 checkpoint API のみで chunk 化が成立することを確認した (捨てコード・要点のみ記録):
| 検証 | 結果 |
|---|---|
S2-1: compile({checkpointer, interruptAfter: 全gate}) | 1 invoke = 1 gate で 10 invoke 完走。invoke(null, cfg) が次 1 node だけ実行して再停止 |
| S2-2: cross-process 復元 | saver.getTuple() → checkpoint+metadata を JSON 直列化 → 新 MemorySaver へ saver.put() → 新 graph インスタンスで invoke(null) 再開 — 公式 API のみで成立。独自 state 直列化層は不要 |
| S2-3: 復元後の条件分岐 | scoring で reject → END が復元後も正常終端 |
| S2-4: streamEvents | chunk 実行・resume の両方で on_chain_start/end が取得可 → ADR-0089 partial (進捗 patch) は維持可能 |
| S2-5: checkpoint サイズ | raw payload 75KB に対し checkpoint JSON +1% (77.2KB)。ただし checkpoint は累積 state 全体 (channel_values) を含むため、DO-backed BaseCheckpointSaver 実装側で channel 単位の分割保存が必要 (§決定の「gate 別 key 分離」は checkpoint saver 内部で実現する) |
実装への含意: §決定の「state を DO に直列化して次 message で復元」は、独自直列化でなく DO-backed BaseCheckpointSaver (getTuple/put を DO storage に写像・channel 分割保存) として実装するのが正道。zod 検証・schemaVersion は checkpoint 全体に薄く掛ける。streamEvents の「打ち切り」も for-await の break でなく interruptAfter による公式停止に置換でき、公式非サポート挙動への依存が解消される。実装時注意: put() の channel_versions は tuple のものを忠実に引き継ぐこと (spike では簡略化)。
③ 案C (Cloudflare Workflows) 最小統合 spike (実施済み・2026-06-04)
wrangler 4.94.0 + [[workflows]] binding + nodejs_compat のローカル実行 (wrangler dev) で、WorkflowEntrypoint 内の LangGraph 実行が成立:
step.do('gate-X')1 回 = LangGraph 1 gate。checkpoint JSON を step 戻り値として Workflows に永続化させ、次 step で復元する持ち回りが動作 (4 gate 完走・status: complete・step 出力に checkpoint 記録を確認)- kill/retry 時は完了済み step がキャッシュから返る = seq 冪等テーブル・CAS ロック・再 enqueue 配線が全部不要 (Workflows ランタイムが肩代わり)
- checkpoint ~104KB (worst) は step 戻り値サイズ制限 (1MiB) に十分収まる
8 人日ゲートへの示唆 (判断材料・決定は HITL): §3.2 で案C が加重和最高 (0.943) にも関わらず案B を採択した唯一の理由は「LangGraph 統合障壁が未調査」だった。spike の結果、統合障壁は checkpoint 持ち回りで素直に解け、当初想定より小さい。案B が手製実装する seq/CAS/enqueue (2-3 人日) + ガード置換 (1 人日) は案C では Workflows に委譲され、§5.3 の「捨てる前提の技術的負債」も発生しない。撤退条件 1 項目目「案C (Workflows) 先行 ADR の起案判断へ切り替える」の検討材料が揃ったため、案B 続行 vs 案C 先行起案の判断を起案者へ上げる。案C 残論点: 本番 Workflows の同時実行・課金・terminate (ADR-0101 キャンセル互換)・telemetry/DO progress patch の互換 (step 内から binding 呼び出しは可能)・既存 Queue 経路からの切替設計。
残 Phase 0 項目
- ④ 電帳法対応の顧問弁護士・税理士への chunk 再実行ログ法的解釈確認 — 未実施 (人間アクション)
- 8 人日ゲートの最終判断 (案B 本実装 / 案C 先行 ADR 起案) — ①②③⑤⑥ の材料が揃い判断可能。HITL 判断待ち
参照 (References)
- 関連 ADR:
- ADR-0066 (async queue 化) — 補完 (実行単位の細分化)
- ADR-0089 (progressive streaming) — 補完 (chunk 境界 = partial 書込境界に整合)
- ADR-0101 (協調キャンセル) — 補完 (gate 境界キャンセルが chunk 境界と一致し応答性向上)
- ADR-0103 (EC-3 watchdog) — 補完 (queued 閾値を短縮可能に)
- 関連 PR/Issue:
- PR #1382 (
isInflightRedelivery()導入) - PR #1398 (queued watchdog 30 分化 + 回復ガード)
- DRP-376 (残課題②: kill→再配信→skip サイクルでの telemetry 喪失)
- DRP-374 (watchdog 閾値の invocation 長依存)
- session
bedb9912(2026-06-04 実測根拠) — 旧 ID: MAS-376/MAS-374、2026-06-04 に prefix 振替
- PR #1382 (
- 外部資料:
- Cloudflare Durable Objects storage 制約 (128KB/key, 128KB/transaction)
- Cloudflare Queues 最大再配信遅延仕様
- 電子帳簿保存法 第8条 (訂正・削除履歴保存要件)
- 中小企業会計指針 (真実性確保要件)
- LangGraph streamEvents / MemorySaver checkpoint 公式ドキュメント (Phase 0 spike の一次検証対象 — 検証した版の該当節を spike 結果と共に本節へ反映する)
Known Limitations / Escalated Residual Risks (HITL ratification required)
本 ADR は Cross-Validation で goalpost ループ検知 (2 連続却下・却下盲点が毎回移動) のため、自動審査を打ち切り「残余リスク付き Accept」として起票された (ADR-0109 Part4)。以下の未解決 critical 盲点は、人間レビュー (この PR の merge = 受理 / close = 却下) で最終判断すること。LLM critic による反復審査では収束しないと判定されたものであり、PR レビューが外部検証器となる。
escalate 時点の未解決 critical 盲点と HITL 対応 (本 PR 内で人間レビュアー = 起案者が追補):
- DO 単一スレッド直列化の前提崩壊による冪等テーブル競合 (TOCTOU) → 緩和済: §決定の冪等設計に CAS ロックフラグ (chunk 実行開始時に compare-and-swap で取得・TTL 5 分で kill によるロック放置を自動解除・ロック検出時は即 ack-skip + telemetry 記録) を組み込み、kill タイミング別マトリクスに「2 consumer 同時起動 (TOCTOU)」行を追加、Confirmation 検証手段 3 の kill 注入テストで同ケースの二重 LLM 呼び出し 0 件を必須化済。CV の却下根拠は緩和の不在ではなく「raw inputs に根拠を持たない body-only 緩和は割引く」という審査手続き上の判定であり、人間レビュアーが CAS 設計の技術的妥当性を確認のうえ採択内容の一部として批准する (TOCTOU 窓の指摘自体は正当 — raw inputs の「DO 単一スレッドで直列化される」前提は LLM 呼び出しを挟む区間に及ばないため、CAS ロックなしでは #reliable Must が成立しない)。
上記の緩和は LLM 審査の再収束を待たず人間レビューで確定させたものであり、merge をもって受理 (緩和込みの採択) とする。