型付き辺: 出 10 / 入 1
ADR-0101: 審査パイプラインに実行中審査の停止機能を追加する (サーバ側協調キャンセル)
- Status: Accepted
- Mode: Standard
- Kruchten Type: Existence/Property
- Scope: platform
- Implementation Status: Done (ADR-0120 Phase b 統合で R8 含む全スコープ完了。2026-06-05 staging 検証 PASS → §実装時の確定事項 参照)
- 起案者: [email protected]
- 起案日時 (JST): 2026-06-01
- 承認日時 (JST): 2026-06-04 (R13 ゲート通過を確認し Accepted)
- Deciders: [email protected] (単独)
Pipeline 迂回の経緯(監査用注記): 本 ADR は Decision Pipeline で Standard 45-47/50 と合格点に達したが、Cross-Validation が Must 軸
#reliable× critical 盲点で 2 連続差し戻しした。1 回目は parallel_review の gate 境界識別欠落で、起案テキストにevent.metadata.langgraph_node深さ判定を追加して対処。2 回目はその追加した深さ判定そのものを「LangGraph 内部メタデータ依存でバージョンアップで静かに壊れる」(R12) と攻撃し、ゴールポストが前回の修正箇所へ移動した。R12 が要求する「代替 API (interrupt/breakpoint) 調査の完了」や R13 の「ROI 実測 telemetry データ」は本質的に実装時にしか実証できない検証ステップであり、本 ADR では R12/R13 として緩和計画 + 実装時ゲート + 撤退条件として既に captured 済である。ADR-0099(実装スコープ第一級化)/ review-tiering draft が問題提起する「中規模 ADR のゴールポスト移動型 過剰審査」の実例として、Pipeline を迂回し通常 PR で起案する(代表取締役判断 2026-06-01)。Pipeline retroactive validation(ADR-0052)は任意(実装着手時に必要に応じて実施)。
コンテキスト
§1.1 背景
drp の Web UI (/chat) で非同期審査を一度開始すると、途中で停止する手段が一切ない。起案者が入力ミスや方針変更に気づいても、全 10 gate (triage → socratic → body_generation → scoring → cross_validation → consistency → parallel_review → policy_alignment → slug → numbering、所要数分) が完走するまで待つしかない。本パイプラインは Opus を body/scoring/consistency/reviewClaude の各ノードで多重に呼ぶため、不要と分かった審査でも完走分の Anthropic API コスト (月額 USD 200 規模、ADR-0085 で削減に着手済) が発生し続け、ブラウザを閉じてもサーバ側の queue consumer は走り続けて課金される。
§1.2 現状 (As-Is)
- フロント (public/chat.html):
pollSessionが /chat/status/:sessionId を 1〜3 秒間隔でポーリングし、gateProgress からステップ表示を更新。 - ルート (src/index.ts): /chat/start (起案投入→queue enqueue)、/chat/status/:sessionId、/chat/answer、/chat/create-pr のみ。停止用エンドポイントは無い。
- queue consumer (src/queues/pipeline_consumer.ts): LangGraph を
streamEventsで実行し、for await (event of stream)ループ内で各 gate の on_chain_start/on_chain_end ごとに Durable Object へ gateProgress を patch。 - Durable Object (src/do/pipeline_session.ts): セッション状態は
status: 'queued' | 'running' | 'complete' | 'error'とgateProgressを保持。停止フラグも cancelled status も無い。
実行は queue consumer の単一ループが握っており、外部からそのループに「止まれ」と伝える経路が存在しない。gate には parallel_review (複数 LLM 呼び出しを並列実行するノード) が含まれ、streamEvents は on_chain_start/on_chain_end のほか on_chain_stream/on_tool_start/on_llm_start 等を混在で emit する。
§1.3 課題
「実行中の審査を止めて API コストを止める」手段が必要。UX 上の問題に加え、不要審査が完走するまで Anthropic API コスト (月額 USD 200 規模) が発生し続け、ブラウザを閉じてもサーバ側で課金が継続する。
§1.4 制約・要件
- gate 境界での協調キャンセル (実行中ノードまでは中断しなくてよい) で、残り未実行 gate のコストを確実に停止できること。
- Cloudflare Queue の at-least-once 配信下でゾンビ審査復活が発生しないこと (R4)。
- DO 強整合 + finally patch + stream クローズで status 不整合・AsyncGenerator リークを防ぐこと (R5/R6/R9)。
- サーバ側で所有者認可を行い、第三者による任意 cancel が不可能であること (R11)。
- ADR-0082 telemetry スキーマと整合し、cancelled run を error と区別して記録できること。
§1.5 目標 (To-Be)
POST /chat/cancel/:sessionId + DO cancelRequested フラグ + consumer の gate 境界協調キャンセルにより、不要審査を gate 境界で確実に止め以降の Opus 課金を抑止する。Non-Goals: 実行中ノード (Opus 1 呼出) の即時中断 (AbortController 配線は追加スコープ、撤退条件で格上げ判定)。
決定
サーバ側で協調キャンセル (cooperative cancellation) を実装する。POST /chat/cancel/:sessionId (所有者認可ミドルウェア付き) を新設し、DO に cancelRequested: boolean と status 値 'cancelled' を追加、/chat/start 時に running/queued セッションが存在する場合は 409 Conflict を返す排他制御を DO に持たせる。consumer は streamEvents ループの各 gate の on_chain_end (top-level graph の event.metadata.langgraph_node 深さ判定で識別、parallel_review 等 subgraph 内部 node を境界と誤認しない) で cancelRequested を確認し、立っていればループを break → stream.return?.() で AsyncGenerator を明示クローズ → finally ブロックで status='cancelled' を DO patch → PR 自動作成と telemetry の通常記録をスキップ (cancelled として記録)。フロントは単一トグルボタン (開始/停止) とし、DO status を真実源として描画、sessionId を localStorage に保持してリロード時に状態復元する (重複起動防止の主防御はサーバ側 409、localStorage はベストエフォート)。
実装時の確定事項 (2026-06-04)
ADR が「実装 PR で確定」「実装前に調査」とした論点を着手時に確定した。起案 (2026-06-01) 以降に main へ入った実装 (EC-3 watchdog / ADR-0109 loopbreaker / ADR-0089 partial / shared_triage early-reject / telemetry v8) をデグレさせないことを主眼に、実コードを 3 観点で調査した結果:
- R13 (ROI ゲート・実装前必須): 本番 D1 telemetry を照会し直近 8 日 (2026-05-27〜06-03) で 118 件 ≒ 月 440 件ペース。閾値「月 20 件未満なら保留」を大きく超過 → 実装着手は正当。
- R12 (境界識別):
event.metadata.langgraph_node深さ判定でなく、現 consumer が既に持つevent.name && GATE_KEYS.includes(event.name)の node 名 allowlist (pipeline_consumer.ts) を流用する。これは R12 が挙げた代替「node 名 allowlist」そのもので、LangGraph 内部メタデータ依存を増やさず gate 境界キャンセルを実現する。 - R10 (telemetry 非アトミック): ADR は Google Sheets API v4 前提だが、現実装の telemetry は D1 (行書込はアトミック) であり R10 の懸念は moot。cancelled は既存列
rejection_reason_code='user-initiated-cancel'+rejected=falseの sentinel で記録 (スキーマ変更なし、EC-3 watchdog のwatchdog-timeoutと同方式)。 - R11 (認可): 第一候補どおり sessionId=UUID v4 推測耐性 + 既存 BasicAuth で代替 (per-session owner field・署名トークンは見送り)。
- R1/R5 (cancel↔complete race):
POST /chat/cancelはcancelRequested=trueフラグのみ立て、status 遷移 (cancelled/complete/error) は consumer が単独所有する設計とし、race を構造的に解消 (DO 単一スレッドで consumer が順次決定)。 - EC-3 watchdog 連携 (デグレ防止の要): 新 status
'cancelled'をsession_watchdog.tsの終端判定 (isTerminalStatus/stuckTimeoutMs) に含め、cancelled patch 時に alarm を解除する。含め忘れると cancelled session の alarm が残り watchdog が error に上書きするため必須。 - R8 (AbortController 実行中ノード即時中断): ADR の Non-Goal → 別 PR。本実装は gate 境界キャンセルのみ。4 週後レビューで実行中ノード分が削減効果の 30% 超なら Must 格上げ (撤退条件)。
- corrigendum (2026-06-22): ADR-0120 Phase b で統合実装済 —
drp/src/workflows/pipeline_run_workflow.tsrunWithCancelAbortで 1 秒 polling + AbortSignal 伝播により実行中 LLM HTTP を ≤1 秒で abort・課金停止。2026-06-05 staging でcancel_abort_in_stepPASS。撤退条件のレビュー (4 週後) は不要となり、本 ADR スコープは Done に到達。
- corrigendum (2026-06-22): ADR-0120 Phase b で統合実装済 —
- 409 排他 (複数タブ二重起動防止): defer (代表取締役確認 2026-06-04)。アクティブ session 追跡には session-index KV (新インフラ) が要り、DO が enumerable でないため複雑。solo・単一ユーザーでは価値が低く、R4 ゾンビ復活 (at-least-once) は consumer 冒頭の冪等 early-return で別途防御するため本機能の目的は阻害しない。follow-up。
判断基準 (Decision Drivers)
3.1 評価軸
| # | 軸 | 重要度 (係数) | 案件特有の解釈 |
|---|---|---|---|
| 1 | #efficient | [Must] (×2.0) | 不要審査の以降コスト (Opus 多重呼出) を実際に止め、残り未実行 gate 分を確実に削減できる |
| 2 | #reliable | [Must] (×2.0) | cancel が gate 境界で効き、at-least-once 重複でゾンビ審査が復活しない(R4)。DO 強整合でフラグ消失せず(R6/R7)、parallel_review の部分完了や AsyncGenerator リークで状態不整合・課金継続を起こさない(R6/R9)、cancelled/complete のレースで状態が不整合にならない(R1/R5)。境界識別の LangGraph 内部依存はバージョン pin + CI 統合テストで managed risk として扱う(R12) |
| 3 | #operable | [High] (×1.0) | 単一トグル UI・リロード耐性・サーバ側 409 排他で運用が分かりやすい |
| 4 | #maintainable | [Medium] (×0.5) | endpoint + 認可 + DO state + consumer + フロントの変更範囲、cancelled 分岐と gate 境界識別の保守 |
K.O. criterion: Must 軸 (#efficient / #reliable) いずれかで score < 3 の案は不採用。
3.2 評価軸 × 案スコア表
| 軸 | 係数 | 採択案 (gate 境界協調キャンセル) | 案A (クライアント側のみ中断) | 案B (hard kill: queue 破棄 / DO reset) | 案C (現状維持) |
|---|---|---|---|---|---|
#efficient | ×2.0 | 4 (残り gate 分を確実停止、実行中 1 ノードは残る) | 1 (サーバ継続課金) | 5 (実行中ノードも止まる) | 1 |
#reliable | ×2.0 | 3 (DO 強整合 + 冪等 + CAS + stream クローズで担保。境界識別の LangGraph 内部依存は pin+CI で managed、残存リスクは認める) | 3 (UI のみで破綻はしない) | 2 (強制終了で cancelled 状態/PR-skip/telemetry が不整合) | 4 |
#operable | ×1.0 | 4 | 3 | 2 | 2 |
#maintainable | ×0.5 | 3 | 5 (実装最小) | 3 | 5 |
| 加重和 (正規化) | 0.745 | 0.491 | 0.636 | 0.527 | |
| K.O. 通過 (Must ≥3) | ✓ | ❌ (#efficient=1) | ❌ (#reliable=2) | ❌ (#efficient=1) |
加重和 = Σ(score × 係数) / (5 × Σ係数)、Σ係数 = 5.5。採択案のみ K.O. 通過。
なお #efficient=4 のスコア根拠は「キャンセル時点の残り未実行 gate 分を確実に止める」ことに基づくが、実際の削減率は「ユーザーがどの gate でキャンセルするか」に依存する (triage キャンセルなら約 90% 削減、scoring/consistency 通過後なら高コスト gate の大半が完了済で 20-30% 程度)。「月額 USD 200 規模」は全完走ベースの概算であり、期待コスト削減率の精密化 (実測キャンセル分布による body_generation/scoring/consistency 通過後比率の定量化) は telemetry 蓄積後の 4 週後レビューで検証する (Confirmation 参照)。#reliable は当初 self-score 4 だったが、境界識別の LangGraph 内部メタデータ依存 (R12) は実装時にしか完全実証できない残存リスクであることを正直に反映し 3 に修正した (K.O. は通過)。
検討した代替案 (Alternatives Considered)
- 採択案 (gate 境界協調キャンセル):
POST /chat/cancel+ 所有者認可 + DO のcancelRequestedフラグ + 409 排他 + consumer が gate 境界 (top-level node 深さ判定) で確認し break →stream.return?.()+ finally patch →cancelled状態へ。残り未実行 gate のコストを確実に止めつつ cancelled を明示し telemetry/PR-skip を整合的に扱える。冪等・CAS・DO 強整合で at-least-once 重複(R4)・レース(R1/R5)・parallel 部分完了(R9)を防御。 - 案A (クライアント側のみ pollSession 中断 + UI リセット): サーバの queue consumer は走り続け Opus 課金が継続 → 本来の目的(コスト停止)を達成しない、
#efficient=1 で K.O.。撤退時のフォールバック先 (縮退手順は撤退条件参照)。 - 案B (hard kill: queue メッセージ破棄 / DO reset で強制終了): 協調でなく即時強制終了。実行中ノード含め最速で止まるが、cancelled 状態の整合的記録・PR 作成スキップ・telemetry 区別ができず DO 状態が不整合になりうる、
#reliable=2 で K.O.。 - 案C (現状維持): 停止手段なし。不要審査でも全 gate 完走まで課金継続、
#efficient=1 で K.O.。 - 採択案の追加スコープ (Non-Goal 寄り): 実行中 1 ノード (Opus 呼出) まで止めたい場合は AbortController を consumer→node→LLM (gateway.ts createLlm) に配線する(R8)。本 ADR は gate 境界キャンセルを核とし、AbortController 配線は追加スコープ/別 PR とする。ただし 4 週後レビューで「実行中ノード分のコストが全削減効果の 30% を超える」場合は次スプリントの Must 要件に格上げする (撤退条件参照、サンクコスト罠回避)。
影響 (Consequences)
§5.1 正の影響 (Good)
- 実コスト停止: 不要審査を止めて以降の Anthropic API コストを実際に削減できる (ADR-0075 コスト監視 / ADR-0085 prompt caching と同じコスト統制系の改善)。
- 正直な UX: 「止めたら本当に止まる」。ブラウザを閉じてもサーバが課金し続ける現状を解消。
- 状態整合: cancelled という明示状態を持つことで、ポーリング側・telemetry 側が「異常終了 (error)」と区別して扱える。
- 重複起動の抑止: サーバ側 409 Conflict 排他制御 + UI トグルボタン + リロード時のサーバ状態 (DO status) 復元により、複数タブ・複数デバイスでも審査中の二重起動を防止。サーバ側 R4 冪等性との二重防御でコスト無駄打ちを減らす。
§5.2 負の影響 (Bad)
- 実行中ノードは中断不可: gate 境界チェックのため、キャンセル時点で走っている 1 ノード分のコストは発生する (全完走よりは大幅減)。R6 で
stream.return()クローズしなければ AsyncGenerator リークで output 課金が継続する恐れがあり、明示クローズが必須。 - 変更範囲: endpoint 追加 + 所有者認可ミドルウェア + DO state 拡張 (フラグ + status 値 + 409 排他) + consumer ループ改修 (境界深さ識別 + finally patch + stream クローズ) + フロント。クライアントのみの案より広い。
- 保守対象の増加: cancelled status を扱う分岐 (telemetry 記録、PR 作成スキップ、ポーリング終了判定) を各所に追加する。LangGraph の streamEvents イベント構造 (parallel subgraph の on_chain_end 非決定的順序、metadata 構造のバージョン依存) と結合するため、将来の gate 追加・rename・LangGraph バージョンアップ時に境界検出が壊れないよう統合テストで継続検証する (R12)。
§5.3 中立・トレードオフ (Neutral / Trade-offs) と Risks
gate 境界という粒度は「実装単純さ × 即時性」のトレードオフであり、完全な即時停止は AbortController 配線 (追加スコープ) でのみ可能。以下に Risks を列挙する。
- R1 レース: cancel 要求が status='complete' 確定後に届くケース。DO 側で complete 後は cancelRequested を無視する (status 遷移を complete/error 優先) ことで回避。
- R2 DO 整合性: patch の eventual consistency により、cancel フラグが次の gate 境界まで読めない遅延。許容 (粒度が gate 境界のため)。
- R3 telemetry の歪み (サイレント障害): cancelled run を error として記録すると ADR-0082 のメトリクスが歪む。cancelled は専用に記録 (または記録対象外) とする。さらに cancelled が増えると「gate 成功率」「平均実行時間」「エラー率」のベースラインから除外され、triage/socratic で高頻度にエラーが起きていてもユーザが cancel することでエラーが telemetry から消えアラート不発になる恐れがある。対策: cancelled 理由を
user_initiated/error_inducedに区別して記録し (cancel 押下前の最後の gate のエラーフラグ・応答時間異常を DO に保持)、error_inducedを error 系メトリクスに合流可能にする。観測指標に「cancelled run のうちエラー gate を含む割合」を追加 (撤退条件参照)。 - R4 at-least-once 重複起動 (ゾンビ審査復活, critical): Cloudflare Queue は at-least-once 配信。同一 sessionId が重複配信され、先行 consumer が status=cancelled を書く前に後続 consumer が起動すると、cancelRequested を見ずに審査が復活し、本機能の目的である #efficient のコスト抑止が成立しなくなる。対策: (a) consumer 冒頭で DO status が cancelled/complete/error なら処理を始めず early return、(b) cancelRequested は POST /chat/cancel だけが false→true に遷移させ consumer は読むだけ、(c) queue メッセージに enqueueTimestamp を埋め DO の startedAt と比較して重複/陳腐メッセージを破棄する冪等チェック。
- R5 並行/二重 cancel: ボタン連打・ネットワーク再送で同一 sessionId に複数の cancel が来るケース。POST /chat/cancel は「cancelRequested が既に true、または status が running 以外」なら 200 OK を返す冪等実装とする。consumer 側の break 後 patch も status==running を条件とする CAS で、cancelled/complete 済み DO への二重 patch・telemetry 二重記録を防ぐ。
- R6 DO 整合性 + AsyncGenerator リソースリーク (critical): Cloudflare Durable Object は オブジェクト ID ごとに全世界で単一インスタンスを保証する (single-threaded・単一ロケーション・強整合)。
POST /chat/cancelの書き込みと consumer の読み取りは同一 sessionId 由来の DO ID 経由で必ず同一インスタンスに到達するため、「別 DO インスタンス/キャッシュ層参照による cancelRequested フラグ消失」は DO-by-ID アクセスでは構造的に発生しない。一方、LangGraph の streamEvents が返す AsyncGenerator はbreakだけでは内部の Opus HTTP 接続と LangChain コールバックキューが解放されないため、waitUntil で延命される Workers 上で output トークン課金が続くリスクがある。対策: (a) break 直後にstream.return?.()、(b)finallyブロックで必ず DO patch、(c) cancelRequested 書込はstorage.putの Promise を必ず await し失敗時は 503 を返してリトライを促す。検証: cancel 書込→consumer 読取が同一 DO ID で観測される統合テスト + break 後の実行時間計測 + DO storage 障害模擬を CI に追加。 - R7 DO storage 部分障害 (put 成功後 read 不能) の懸念は検証で払拭: Cloudflare DO は output gates により「外部から観測される put 成功 = ディスク flush 確認済み」を保証する。同一インスタンス内の put→get は write-buffer read-through で常に最新値を返す。よって「put が成功を返したが consumer の次の gate 境界 read で値が返らない」は DO の設計上発生しない。対策は put() を await するだけで十分で、既存実装は全 put を await 済 (src/do/pipeline_session.ts:37/74/96 で確認)。
- R8 キャンセルの実コスト削減効果の正確な見積もり (検証済): Anthropic 課金は input をリクエスト時に全額確定・output は実生成トークン分のみ従量。よって gate 境界キャンセルで確実に止まるのは未実行 gate 分の全コスト。一方、実行中ノードは現状 AbortController が未配線 (src/queues/pipeline_consumer.ts:238 で streamEvents に signal 未注入、ChatOpenAI.invoke にも signal なし=コードで確認済) のため中断できず、その input は確定済・output は生成完了まで課金される。削減見積もりは「キャンセル時点の残り未実行 gate 数 × 平均 gate コスト」で算出し過大評価を避ける。
- R9 parallel_review gate の部分完了による状態不整合 (high): parallel_review が内部で
Promise.allや LangGraph 並列ブランチで複数 Opus を並列実行する場合、on_chain_end は全並列呼び出しが揃って初めて発火するため、途中で cancelRequested が立ってもノード起動を抑止できず、部分的な review 結果が DO に書かれた状態で status='cancelled' になり gateProgress に中途半端なデータが残る。対策: (a) parallel_review については on_chain_start でもキャンセルチェックを追加し並列ブランチ起動自体を抑止、(b) gateProgress の各ステップにpartial: booleanフラグを追加し UI 側で部分完了を区別、(c) parallel_review ノードの実装を実装時に確認し ADR 補足に明記。 - R10 telemetry partial write (high): ADR-0082 の TelemetryRecord を Google Sheets に書く場合、Google Sheets API v4 の batchUpdate はスプレッドシート全体のアトミック操作を保証せず、リクエスト内の各 Request が独立して適用される (個々の Request が失敗しても他は適用される)。よって「batchUpdate で atomic 書込」は誤りで、対策は 冪等 upsert (行 ID キー) + リトライ + 手動リカバリ手順 とする。さらに cancelled フラグと
user_initiated/error_induced区別列を追加する場合、既存完走レコードと異なるスキーマが混在し、既存 ARRAYFORMULA/QUERY/フィルタビューが列ずれで誤動作する可能性がある。ADR-0082 担当と事前にスキーマ変更レビューを実施し、既存数式・フィルタへの影響チェックリストを Confirmation に追加 (下記)。 - R11 認可不在の悪用 (critical): 所有者認可がサーバ側に無い場合、sessionId を取得・推測した第三者が
POST /chat/cancel/:sessionIdを呼んで任意の審査を中断でき、審査プロセスの完全性が損なわれる。対策: §決定 で明記したサーバ側所有者認可ミドルウェアを必須化。認可方式の方針: 個人開発・単一ユーザー環境であるため、第一候補として sessionId に UUID v4 (122 bit entropy) を用い推測耐性で代替し、署名付きトークン/Cloudflare Access はオーバーエンジニアリングとして見送る方向で実装時に確定する。署名付きトークンを採用する場合は HS256 + Workers Secrets での鍵保管 + 24h 有効期限 + localStorage 漏洩リスクの明記が必要で、0.25 人日の見積もりを超過する可能性が高い。実装 PR で方式を一つに確定する。 - R12 LangGraph 内部メタデータ依存 (critical):
event.metadata.langgraph_nodeのネスト階層表現 (区切り文字・depth カウント方式) は @langchain/langgraph の内部実装に依存し、公開 API として安定性が保証されていない (v0.2→v0.3 で metadata 構造の破壊的変更実績あり)。深さ判定が node 名一致より優れる根拠は「将来の gate 追加・rename で静かに壊れない」点だが、LangGraph バージョンアップで同様に静かに壊れうる。対策: (a)package.jsonで@langchain/langgraphを exact pin (キャレット/チルダ禁止)、(b) バージョンアップ時に gate 境界識別の統合テスト実行を必須化し CI に組み込む、(c) LangGraph の interrupt / breakpoint API や node 名 allowlist など代替手段の実現可能性を実装前に調査し、深さ判定が最善か再評価した結果を実装 PR に記載する。本リスクは実装時にしか完全実証できないため #reliable self-score を 4→3 に修正し managed risk として扱う。 - R13 投資対効果の前提が未検証 (high): 「月額 USD 200 規模」「20-30% 削減」「2.5 人日」の前提となる「月あたり審査実行件数」「平均実行時間」「ユーザーからの停止要求頻度」「過去 4 週の実請求額・無駄推定額」「ADR-0085 caching 効果が出るまでの期間」がいずれも未計測で、ROI を事前検証していない。撤退条件「8 週で cancel 利用 0 回なら撤去」は実装後 2 ヶ月経過で初めて無駄と判断する後追い設計。対策 (実装前ゲート): (a) ADR-0082 既存 telemetry から月あたり審査件数と平均実行時間を確認、(b) 件数が月 20 件未満なら停止機能実装を保留し件数計測のみを先行して実需確認後に再判断、(c) ADR-0085 caching 効果が出るまで 3 ヶ月待てない緊急性の根拠 (実請求額・停止要求件数) を実装 PR Description に明記、(d) 実需が確認できない場合は本 ADR 自体を Superseded とする選択肢を残す。
撤退条件 (Rollback Plan)
判定は 4 週ごとのレビュー(担当: 代表取締役)、telemetry の cancelled run 件数・gate 実行数・status 不整合件数で観測する。
| 判定指標 | 閾値 | 代替アクション |
|---|---|---|
| キャンセルが gate 境界で確実に効かない(フラグ無視・レースで status 不整合) | デプロイ後 4 週で 2 件以上発生 | サーバ側協調キャンセルを撤去し案A(クライアント側のみの pollSession 中断 + UI リセット)に縮退 |
| 停止機能が実利用されずコスト削減効果が無視できる | 導入後 8 週で cancel 利用 0 回 | 機能を撤去し案A に縮退(UI トグルのみ残す) |
| ゾンビ審査復活(R4)が観測される | 4 週で 1 件以上 | 冪等チェック(early-return / enqueueTimestamp 比較)を強化、解消せねば撤去 |
| 実行中ノード分のコストが全削減効果の 30% を超える | 4 週後レビュー時点 | AbortController 配線 (R8 追加スコープ) を次スプリントの Must 要件に格上げ(サンクコスト罠回避) |
| cancelled run のうちエラー gate を含む割合が増加 (サイレント障害の疑い) | 4 週で 30% 超 | cancelled 理由の user_initiated / error_induced 区別を強化し、error_induced を error 系メトリクスに合流させて品質指標の歪みを是正 |
| LangGraph バージョンアップで境界識別が壊れる (R12) | 1 件でも検知 | exact pin で固定継続、interrupt/breakpoint API or node 名 allowlist へ移行 |
| 事前 ROI 検証で月審査件数 < 20 件 (R13) | 実装着手前 | 停止機能実装を保留、件数計測のみ先行、実需確認後に再起案 |
案A 縮退時のデータ整合手順 (cancelled status と cancelRequested フラグの不可逆性対応):
- DO の status 判定コードで
cancelledをcompleteと同等に扱う互換レイヤーを案A でも維持する (pollSession の終了判定が未知値で狂うのを防ぐ)。 - telemetry の cancelled レコードの扱い方針 (error に統合するか別集計のまま保持するか) を ADR-0082 と事前に合意する。過去レコードを error に再ラベリングする場合は時系列データ汚染を避けるため migration スクリプト + バックアップを必須化。
- cancelRequested フラグを案A 縮退時に削除する migration スクリプトを用意するか、案A のコードが cancelRequested を無視する明示実装を行う (R4 の early-return が案A で誤発動するのを防ぐ)。
Confirmation
観測可能 KPI で判定する (複数指標併用)。
- cancel 実効性 = 100%: cancel 押下→次 gate 境界で break する成功率 (R4/R5/R6/R7/R9 を含む統合テスト + 実機)。
- コスト削減の実証: cancelled run の実行 gate 数が完走 run より少ないこと (telemetry の gate 実行数で確認)。削減量は「キャンセル時点の残り未実行 gate 数 × 平均 gate コスト」で算出 (R8、過大評価回避)。
- 実行中ノード分のコスト計測 (4 週後レビュー): 実行中ノードの平均所要時間と break 後の実コスト (output token 課金) を telemetry で実測し、30% 超なら AbortController 配線を Must 格上げ。
- 期待コスト削減率の精密化 (4 週後レビュー): telemetry の実測キャンセル分布から「どの gate でキャンセルされたか」のヒストグラムを生成し、body_generation/scoring/consistency 通過後キャンセル比率を定量化、ADR-0085 caching との損益分岐を実測検証。
- ゾンビ審査復活率 = 0 (R4): at-least-once 重複配信で cancelled 後に審査が復活する件数 (冪等 early-return テスト + 実機監視)。
- レース整合の不整合 = 0 件 (R1/R5)。
- parallel_review 部分完了の整合性 = 0 件の孤児 (R9): gateProgress の
partialフラグが UI に正しく反映されること (統合テスト)。 - telemetry 冪等 upsert の動作確認 (R10): Google Sheets API v4 の batchUpdate は atomic 保証なし。行 ID キーでの冪等 upsert + リトライ + 手動リカバリ手順が動作すること、ADR-0082 担当との事前スキーマ変更レビューで既存 ARRAYFORMULA/QUERY/フィルタビューに影響しないことをチェックリストで確認すること。
- LangGraph バージョン固定の遵守 (R12):
package.jsonで exact pin、CI に gate 境界識別の統合テストを組み込み、バージョンアップ時に必ず実行されること。interrupt/breakpoint API or node 名 allowlist の代替調査結果を実装 PR に記載すること。 - 認可不在の悪用が不可能 (R11): 他人の sessionId による cancel が 401/403 で拒否されること (e2e テスト)。認可方式 (UUID v4 推測耐性 or 署名付きトークン) を実装 PR で確定し、選択方式に応じた具体仕様 (鍵管理・有効期限・失効) を記載すること。
- 重複起動の防止: 複数タブから /chat/start を同時送出した場合、後続が 409 Conflict で拒否されること (e2e テスト)。
- 事前 ROI 検証ゲート (R13): 実装着手前に ADR-0082 既存 telemetry から月審査件数 + 平均実行時間 + 過去 4 週の実請求額・無駄推定額を確認し、月 20 件未満なら実装保留・件数計測のみ先行する判断を実装 PR Description に記載すること。
- UX: 停止ボタン押下後に polling が停止しボタンが「審査開始」に戻ること。
- 検証手段: 統合テスト (R4/R5/R6/R7/R9/R10/R11/R12 + DO storage 障害模擬 + LangGraph 境界識別 + 認可) + 実機 + telemetry の cancelled 集計。実行頻度: 実装 PR で 1 回 + 4 週ごとのレビュー + 随時 (LangGraph バージョンアップ時)。違反時対応: 撤退条件を発動し案A への縮退 or AbortController Must 格上げ or 本 ADR Superseded。
コスト試算
- 実装工数: endpoint 追加 (
0.25 人日) + 所有者認可ミドルウェア (0.25、方式により増減) + DO state 拡張 (cancelRequested + cancelled status + 409 排他, ~0.25) + consumer ループ改修 (早期 return / 冪等 / CAS / 境界深さ識別 / stream クローズ / finally patch, ~0.75) + フロント (トグル + リロード復元, ~0.5) + 統合テスト (R4/R5/R6/R9/R10/R11/R12, ~0.5) = 約 2.5 人日 (main 領分)。 - 追加インフラコスト: gate 境界ごとの DO read (10 gate × 数〜数十 ms)。月数十円未満想定。
- 金銭支出: 個人開発のため 0 円 (自工数)。削減見込みは gate 境界以降の Opus ノード分で、効果と ADR-0085 caching との損益分岐は 4 週後レビューで実測検証。
- 緊急性の定量的根拠 (R13 ゲートで確定): 過去 4 週の実請求額・無駄推定額・停止要求件数・ADR-0085 効果出現までの期間見積もりは実装着手前の事前 ROI 検証ゲート (R13) で計測・確定する。月審査件数が 20 件未満なら本機能の実装を保留し件数計測を先行する。
実装後 corrigendum (2026-06-04, DRP-379): 単一トグルボタン → 開始/キャンセル分離ボタンへ変更
本 ADR は immutable。本節は ADR-0031 corrigendum 方式による実装後の追記で、決定本文は変更しない。
- 変更: §決定の「フロントは単一トグルボタン (開始/停止)」を撤回し、
審査開始(submit) と審査をキャンセル(独立type="button") の分離ボタンに変更 (PR にて実装)。 - 理由: 実運用で誤連打 (ダブルクリック) により 1 打目で審査開始 → 2 打目がトグル化済みボタンを踏んで即キャンセルになる事故が発生 (起案者 = Decider 代表取締役の変更指示、2026-06-04)。
- 決定本文の意図は維持: 重複起動防止はサーバ側 409 排他 (主防御) + 審査中の開始ボタン disabled (二重 submit 防止) で従来どおり担保。キャンセル予約 (queued 前クリック) / リロード復元セッションの停止も分離ボタンで同等に動作。
- 副次効果: キャンセルが
type="button"になったため form required バリデーションの影響を構造的に受けなくなり、DRP-375 のformNoValidate回避策は不要になった (撤去済み)。 - 回帰テスト: mocked-route e2e (
test/e2e/ui/chat_state_machine.spec.ts) を分離ボタンの状態機械で全面更新 + 誤連打回帰テスト (ダブルクリックで cancel が飛ばない) を追加。
参照 (References)
- 関連 ADR: ADR-0019 (LangGraph 基本アーキテクチャ、本 ADR が前提とする consumer/DO/endpoint 構造)、ADR-0075 (LLM コスト監視)、ADR-0082 (TelemetryRecord v2、cancelled run の
user_initiated/error_induced区別スキーマ整合)、ADR-0085 (prompt caching、コスト削減の補完関係)、ADR-0052 (retroactive validation、本 ADR の Pipeline 迂回に対し任意実施)、ADR-0099 / review-tiering draft (過剰審査の問題提起、本 ADR は迂回の実例) - 関連 PR/Issue: -
- 外部資料 (一次資料、URL は実装 PR で確定):
- Cloudflare Durable Objects 公式ドキュメント: 単一インスタンス / 強整合 / output gates / write-buffer read-through (R6/R7 の根拠)
- Cloudflare Queues 公式ドキュメント: at-least-once 配信仕様 (R4 の根拠)
- Anthropic API ドキュメント (platform.claude.com/docs): input 即時確定 / output 従量課金 / streaming 仕様 (R8 の根拠)
- Google Sheets API v4 batchUpdate: 非アトミック仕様 (R10 の根拠)
- @langchain/langgraph: streamEvents / metadata.langgraph_node 仕様、interrupt/breakpoint API (R12 の根拠)