fix: operator-only VC timeout の occurrence と実適用 budget を root review artifact に保持する (#2897) - #2901
Conversation
…artifact に保持する (#2897) readiness 変換・review merge・root pipeline の 3 境界で落ちていた timeout の identity を、既存 result の値だけを運ぶ bounded な診断として保持する。 - contract_readiness_check.py: 既存 result item の occurrence index・ execution_key_hash・dedup 情報・timeout_provenance を allowlist で source_payload に引き継ぎ、canonical plan digest と results_count を readiness result の top level に載せる - check_issue_contract.py: human_judgment の timeout errors から TIMEOUT_DIAGNOSTICS_V1 (最大 16 件、16KiB 以下) を投影し、 REVIEW_ISSUE_RESULT_V1 の optional field timeout_diagnostics として載せる - run_root_review_pipeline.py は production 変更なし (merged result が そのまま transport の semantic_result と full artifact になる) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Preview URLPreview removed (PR #2901 closed). |
…-occurrence-budget-identity
判定:REQUEST_CHANGES方向性は妥当ですが、マージ前に直したい点が3件あります。 最も重要なのは、診断を保存するための変更が、逆にレビュー結果の転送失敗を起こし得ることです。残り2件も、セキュリティ機構の追加ではなく、診断情報が不完全でもレビュー処理を壊さず、分からないものを正しく 確認中に HEAD が更新されたため、最終的には
優先度 | 指摘 | 主な影響
-- | -- | --
P1 | 診断単体の16 KiB制限では、レビュー結果全体の64 KiB制限を守れない | 診断追加によって capture_failure を発生させ、結果を失う可能性
P2 | 列挙値の型不正で TypeError が発生し、unknown に劣化しない | 任意の診断メタデータがレビュー処理の新しい障害原因になる
P2 | dedup の参照元が不正でも replay 側を binding_verified にできる | 「どの実行の、どの予算か」を検証済みとして誤表示する
以下の再現は、取得コードの該当純粋関数を転記した局所検証と合成入力によるものです。実 Issue での障害を観測したという意味ではなく、リポジトリ全体の pytest/完全な本番経路は、この環境では再実行していません。 1. P1:診断追加で、元は転送できたレビュー結果が |
| 優先度 | 指摘 | 主な影響 |
|---|---|---|
| P1 | 診断単体の16 KiB制限では、レビュー結果全体の64 KiB制限を守れない | 診断追加によって capture_failure を発生させ、結果を失う可能性 |
| P2 | 列挙値の型不正で TypeError が発生し、unknown に劣化しない |
任意の診断メタデータがレビュー処理の新しい障害原因になる |
| P2 | dedup の参照元が不正でも replay 側を binding_verified にできる |
「どの実行の、どの予算か」を検証済みとして誤表示する |
以下の再現は、取得コードの該当純粋関数を転記した局所検証と合成入力によるものです。実 Issue での障害を観測したという意味ではなく、リポジトリ全体の pytest/完全な本番経路は、この環境では再実行していません。
1. P1:診断追加で、元は転送できたレビュー結果が capture_failure になる
対象:
.claude/skills/review-issue/scripts/check_issue_contract.py
build_timeout_diagnostics() のサイズ調整と、merge_readiness_into_review_result() での添付処理。
実装は timeout_diagnostics 単体を最大16件・16 KiB以下にしています。しかし、その直後に送られるのは診断だけではなく、parsed_vc_commands、blocking_issues、その他の情報を含む merged review result 全体です。診断側の制限処理には、その既存部分のサイズが入っていません。
一方、root pipeline の child は print(json.dumps(merged)) で全体を stdout に出し、transport は stdout が 65,536 bytesを超えると capture_failure にします。このため「診断が16 KiB以下」から「転送失敗を起こさない」は導けません。
局所検証の結果
本番と同じ json.dumps() と末尾改行を使い、既存レビュー結果を大きめにした合成入力で確認しました。
診断追加前のレビュー stdout 59,338 bytes
timeout_diagnostics 単体 10,348 bytes
診断追加後のレビュー stdout 69,711 bytes
transport の上限 65,536 bytes
診断単体は上限内なのに、追加前は通る結果が追加後には上限超過します。 この入力はサイズ境界を検証するための合成レビュー JSON で、実際の parser がこの値を出したことまで確認したものではありません。
しかも既存 transport では、deterministic backend でも capture_failure は再試行対象です。したがって、診断の追加が原因で結果を失うだけでなく、不要な再実行へ進む可能性があります。診断追加の目的と逆方向の回帰です。
推奨修正
最終的な送信 JSON 全体の残り容量に合わせて、任意の診断部分を縮めてください。
既存の verdict、failure_class、blocker を保持したまま、診断の occurrences を減らし、truncated_count を更新する形が適切です。診断のヘッダーすら収まらない境界では、任意フィールドの省略規則を用意する方が、レビュー全体を転送失敗にするより有用です。JSON の生バイト列を途中で切ったり、根拠なく transport 上限だけを引き上げたりする修正は勧めません。
Python 公式仕様でも、json.dumps() は文字列を返し、ensure_ascii や separators により出力表現が変わります。容量判定は、実際の送信設定で直列化・エンコードした全体に対して行う必要があります。[Python documentation](https://docs.python.org/3.12/library/json.html)
回帰テストは、現在の「20件の短い timeout」だけでなく、追加前は64 KiB未満、素直に診断を足すと64 KiB超過するケースを既存の production-path test に加えるのが効果的です。PR 本文の「20件でも capture_failure にならない」という確認だけでは、この境界をカバーできていません。
2. P2:診断の型不正が、unknown ではなく例外になる
対象:
contract_readiness_check.py::_bounded_timeout_provenance()
check_issue_contract.py::_timeout_bounded_provenance()
check_issue_contract.py::build_timeout_diagnostics()
producer と consumer の両方に、次の形の判定があります。
if source not in _TIMEOUT_PROVENANCE_SOURCES:
return Nonesource が文字列か確認する前に、frozenset の membership 判定をしています。consumer の execution_source にも同じ問題があります。
正常な fixture の診断メタデータだけを変えると、局所検証では次の結果になりました。
timeout_provenance.source = [] → TypeError: unhashable type: 'list'
timeout_provenance.source = {} → TypeError: unhashable type: 'dict'
execution_source = [] → TypeError: unhashable type: 'list'
execution_source = {} → TypeError: unhashable type: 'dict'
これは CPython の実装とも整合します。集合の membership 判定は対象のハッシュを求めるため、list/dict は単に「許可値ではない」と評価されるのではなく、例外になります。Python 3.12.3 の Objects/setobject.c も確認しました。[GitHub](https://raw.githubusercontent.com/python/cpython/v3.12.3/Objects/setobject.c)
現在の正常 producer がこれらの型を生成する証拠はありません。 ただし本 PR は、不完全な provenance/binding を unknown に劣化させ、診断を新しい workflow gate にしないことを明示的に約束しています。その境界で例外を送出するのは、今回追加した処理の不具合です。
推奨修正
型を先に判定するだけで足ります。
if not isinstance(source, str) or source not in _TIMEOUT_PROVENANCE_SOURCES:
return Noneexecution_source にも同様の判定を入れ、既存の unknown 分岐へ流してください。producer 側だけ直すのではなく、プロセス/ファイル境界を越えた入力を扱う consumer 側も直す必要があります。
ここで必要なのは、不正入力を理由に停止する新しい validator ではありません。 また、処理全体を大きな except Exception で囲む必要もありません。列挙値の境界だけを直し、list/dict を入れても「merge が完了する」「従来の failure_class と route を保つ」「診断は unknown」を確認すれば十分です。
3. P2:参照元が unknown なのに、dedup replay が「検証済み」になる
対象:
check_issue_contract.py::build_timeout_diagnostics() の executed_key_by_index 構築、および _timeout_unknown_reason()。
現在、参照元の索引は、おおむね次の3条件だけで登録されます。
occurrence_index が形式上有効
execution_key_hash が形式上有効
execution_source == "executed"
参照元自身の plan digest や provenance が有効かを確認する前に登録しています。 replay 側の判定は、その索引に同じ execution key があるかを見るため、不正な参照元でも判定を通せます。
局所検証の結果
正常な2件の fixture に対し、実行元である occurrence 0 の plan digest だけを top-level と不一致にしました。occurrence 1 は occurrence 0 の dedup replay のままです。
occurrence 0 / executed
attribution: unknown
reason_code: plan_digest_mismatch
occurrence 1 / dedup_replay → source_result_index: 0
attribution: attributed
reason_code: binding_verified
参照元は「この plan の実行だと確認できない」のに、そこを参照した replay は「確認できた」ことになります。
さらに、同じ実行元を指す fixture で、実行元の timeout を300秒、replay 側を150秒にしても、両方が binding_verified になりました。少なくとも現在の検証処理は、参照関係に結び付いた予算の矛盾を検出していません。
これは攻撃対策の話ではありません。「どの実行にどの予算が適用されたかを後から特定する」という、本 PR の情報品質の中心部分です。既存 baseline 実装でも、dedup 情報は source execution を特定するためのものとして扱われています。
推奨修正
参照元の索引を、単なる index → execution_key ではなく、この readiness result 内で整合を確認できた実行元だけの索引にしてください。
既存の値だけを使い、参照元の plan digest、index、実行種別、execution key、provenance を確認したうえで replay を照合すれば足ります。参照元が不正・欠落、または予算情報が矛盾する場合は、replay 側を既存の dedup_binding_invalid 等で unknown に落とします。
履歴の再読込、予算の再計算、新しい実行 ID、永続 ledger は不要です。 また、この不整合を理由にレビューを止める必要もありません。
維持したい設計と、広げなくてよい範囲
今回の問題は、設計全体をやり直す理由にはなりません。
readiness producer で情報を引き継ぎ、既存の durable artifact に載せる方針は維持すべきです。 enumerate() を未フィルタの results に適用して canonical occurrence index を保持する変更は、エラー配列内の位置と元の実行位置を混同しないために適切です。
現在の履歴から予算を再計算しないことも正しいです。 関連 PR #2382 は immutable history snapshot を複数 consumer へ渡す設計を導入しています。今回もその実行時点の値を運ぶだけに留めるべきで、diagnostics の修正に estimator の再設計を混ぜる必要はありません。
Compact V2 wire と operator-only route を変更しない判断も維持すべきです。 関連 PR #2398 では、wire の値を増やす旧案を撤回し、root が verified な failure_class を読む構成になっています。今回の診断の問題を直すために、その routing 契約を再び広げる必要はありません。
また、Issue #2845 には別経路の transport 容量不整合が既に記録されています。ただし対象は contract snapshot の materialize 処理です。今回の root review stdout と同種の設計上の注意点ではあっても、#2845 の全面解決を #2901 のマージ条件にする必要はありません。
文書上の軽微な改善として、PR 本文の「schema ファイルが変わらないので consumer inventory は N/A」は、もう少し正確にできます。additionalProperties: true により新フィールドをスキーマが受理することと、転送サイズまで含めて consumer が問題なく扱えることは別です。「schema ファイルは不変、producer 出力には任意フィールド追加、確認した consumer はこの経路」程度の追記で十分であり、新しい承認手続きは不要です。 [JSON Schema](https://json-schema.org/understanding-json-schema/reference/object)
検証状況と修正の進め方
PR 差分、新規3テスト、Issue #2897 の本文・OWNER コメント、関連 PR #2382/#2398、Issue #2845、readiness・merge・transport の実装を照合しました。既存テストが確認している通常経路を否定するものではなく、今回の指摘は主にサイズ境界と、壊れた診断情報を受けた際の未検証部分です。
CI については、旧 HEAD 1b8159a… の python-test と ci-verdict-summary の成功を確認しました。一方、最新 HEAD の ci-verdict-summary は取得時点で0件だったため、旧 HEAD の成功を最新 HEAD の成功として扱ってはいません。これは最新 CI の失敗を意味するものではありません。
再現用のスクリプト、実行結果、検証の限界を記した README をまとめました。
pr2901_adversarial_probes.zip局所再現コードと検証結果をダウンロード
実 checkout の該当関数を AST で抽出して検証するモードも含めています。
python3 adversarial_probe.py --repo /absolute/path/to/loop-protocol推奨する修正順は、送信全体の容量調整 → 列挙値の型チェック → dedup 参照元の整合確認です。その後、既存の production-path tests に今回の境界入力を追加して確認するのが最小で有効な進め方です。
「診断のために作業を止めない」という狙いは支持します。その狙いを実装上も成立させる、この3点に絞って修正するのが妥当です。
|
GitHub live state を確認しました。指定コメントは 推奨は 新規 PR を作らず #2901 を resume し、3 finding を repo= 対象:
あなたは既に fresh interactive Claude Code の Auto mode で起動済みです。 目的PR #2901 を current repository の canonical
まで自律実行し、人間が merge 判断できる直前の canonical terminal state まで完遂してください。 これは明示的な implementation / repair work order です。 今回の OWNER comment に記載された 3 finding の修正、必要な focused regression test、既存 PR/branch/worktree の更新、fresh verification、fresh PR review、通常の review/fix iteration、current-head CI 確認を明示的に承認します。 ただし:
0. Prompt 作成時点の live state — 実行時には必ず fresh revalidateこの prompt 作成時点で確認済み:
これらは advisory snapshot にすぎません。 1. 最初に current canonical contract を読む実作業前に current main / current repository から少なくとも以下を読んでください。
過去 session の保存済み 今回の OWNER REQUEST_CHANGES が、それらより新しい corrective authority です。 2. Fresh intake / landing dispositioncurrent canonical intake / landing-disposition producer を使用して current state を判定してください。 PR #2901 が current Issue #2897 の exact linked open/draft candidate として正しく認識される場合、期待する経路は概念的には:
です。 ただし prose から決め打ちせず、current producer の canonical result をそのまま消費してください。 重要:
3. OWNER REQUEST_CHANGES を current fix_delta として扱うOWNER comment: を全文読み、以下 3 件を merge-blocking fix_delta として扱ってください。 current 古い field shape をこの prompt から盲目的にコピーせず、current agent contract を読んで materialize してください。 Finding A — P1: timeout_diagnostics 単体の 16 KiB 制限では transport の full stdout cap を保証できない問題:
必要な修正方針:
focused regression では少なくとも:
という境界ケースを production-path test に固定してください。 Finding B — P2: enum-like value の unhashable 型で TypeError を起こさない対象候補:
必要な性質:
少なくとも list / dict 等の malformed
新しい reason code が本当に必要かは current Issue contract を基準に判断し、既存列挙で表現可能なら増やさないでください。 Finding C — P2: invalid executed source を参照する dedup replay を binding_verified にしない問題:
必要な修正方針:
regression では少なくとも:
4. 修正範囲を広げない今回の主眼は OWNER comment の 3 finding です。 維持するもの:
原則として Issue #2897 current Allowed Paths 内で最小修正してください。 OWNER comment にある #2845 は類似の transport-size concern ですが別経路です。 5. SubAgent の使い方必要な implementation / verification / review は current 少なくとも想定:
必要なら current workflow が指定する他 agent も使って構いません。 重要:
Common Completion Protocol を厳守してください。 各 SubAgent について:
timeout / mailbox update / partial report / background dispatch は completion ではありません。 6. Implementation worker の repair executioncurrent agent contract が許すなら、既存 PR repair として implementation-worker の worker には少なくとも:
worker の self-report は advisory です。 7. Verification修正 commit / push 後、新しい PR HEAD に旧 verification evidence を流用しないでください。 current Issue #2897 の live body から:
を current Issue #2897 の current literal VC をすべて fresh 実行すること。 加えて OWNER findings 用の focused regression tests も worker / appropriate test lane で実行してください。 特に:
AC3 Runtime Verification が current contract 上引き続き immediate / applicable なら、新しい HEAD に束縛した evidence を再生成してください。 前回 PR body にある PR body の Runtime Verification Evidence / Checks / scope coverage 等は、current workflow が要求する範囲で新 HEAD の事実へ更新してください。 8. Step 4 fresh PR reviewtest-runner の:
の双方を確認した後だけ Step 4 へ進んでください。 current その後 reviewer には OWNER comment の 3 finding が本当に閉じたかを明示的に再確認させてください。 特に adversarial に:
pr-reviewer の self-reported mergeability は authority にせず、mergeability は control-plane が live GitHub から独立取得してください。 9. REQUEST_CHANGES / CI failure は自動修復するfresh pr-reviewer が
通常の:
current workflow が許す canonical recovery を行ってください。 HEAD が変わったら、head-bound evidence を fresh に取り直してください。 10. CI / mergeability修正後 HEAD の GitHub Actions を current repository policy どおり評価してください。 古い required CI を current head で確認し、必要なら canonical wait/retry mechanism を使ってください。 Step 5 では live GitHub から同一時点に近い state として:
main drift が発生した場合は current canonical mergeability / reconciliation runbook に従い、必要な evidence だけを再取得してください。 force push はしないでください。 11. 再発防止 Issue今回の 3 finding 自体は #2901 の in-scope fix として解決してください。 ただし作業中に 独立した recurring defect / systemic failure class が実際に再現・確認された場合は、再発防止 Issue の起票まで明示的に承認します。 起票前に必ず:
新規起票が必要なら current repository の:
この prompt は、上記条件を満たす再発防止 Issue の canonical 起票を明示的に承認します。 過去 session で報告された 12. 明示的に承認する作業以下は、この作業目的と current repository contract の範囲内で、人間の iteration ごとの追加承認なしに実行して構いません。
ただしこれは Claude Code permission bypass の承認ではありません。 13. 停止 / escalation 境界以下は停止または canonical escalation としてください。
通常の test failure / review finding / CI failure は停止理由ではありません。 14. 完了条件current PR HEAD に対し fresh に以下を確認してください。
ここまで到達したら merge / auto-merge を行わず停止してください。 Draft → Ready の変更については current 15. 最終報告最後に、具体値付きで簡潔に報告してください。
調査や提案だけで終了せず、current canonical workflow が許す限り ポイントは、今回の3件を「追加hardening」ではなく、#2897 の診断を non-blocking / truthful に保つための直接的な fix_delta と明示したことです。これなら |
…urce 検証を追加 (#2897) - P1: merged result 全体 (実 writer と同じ serialize) の STDOUT_CAP に合わせて optional diagnostic のみを縮小/省略 - P2: source / execution_source の list/dict で TypeError を出さず unknown に劣化 (producer / consumer 両側) - P2: binding 検証済み executed source のみを dedup source として登録し、replay の budget provenance 矛盾は unknown Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Summary
operator-only VC timeout が発生したとき、どの occurrence がどの実適用予算で timeout したかを root review artifact から後で特定できるようにします(Issue #2897)。新しい estimator や ledger は作らず、既存 result が持つ
timeout_provenance/execution_key_hash/ dedup 情報 / canonical occurrence index を、readiness 変換・review merge・durable artifact の各境界で落とさず運ぶだけの変更です。contract_readiness_check.py: 既存 result item の occurrence index、execution_key_hash、dedup 情報、timeout_provenance(5 項目に限定)をerrors[].source_payloadへ allowlist で引き継ぎ、diagnostic_report.canonical_plan_digestと pre-filter のresults_countを readiness result の top level に載せます(not_computedの場合は digest は null のまま補完しません)。check_issue_contract.py: human_judgment の timeout errors からTIMEOUT_DIAGNOSTICS_V1(最大 16 件、超過はtruncated_count/total_timeout_occurrences、直列化 16 KiB 以下)を投影し、REVIEW_ISSUE_RESULT_V1の additive な optional fieldtimeout_diagnosticsとして載せます。binding を検証できない occurrence はattribution: unknownと列挙 reason code を記録し、別 VC や再計算値で補完しません。run_root_review_pipeline.py: production 変更なし(test-only integration point)。merged review result は child stdout 経由でそのまま transport のsemantic_resultと full review artifact になるため、配線は不要と判明しました。structured_blockersに deterministic blocker は追加せず、failure_class/ operator-only route / Compact V2 wire / estimator / budget 計算は変更していません。baseline_vc_preflight.pyも変更していません。受け入れ条件の達成状況
failure_classも従来どおりです。unknownと reason code になり、新しい gate にはなりません。body SHA 不一致は従来どおり fail-closed で診断も生成しません。timeout: false/exit_code: 0のままです。truncated_countを保持し capture_failure になりません。診断に raw command / stdout / env /runner_env_delta/minimal_contextは含まれません。通常成功・非 timeout human_judgment・outer 側 timeout・Compact V2 wire は変化しません。検証コマンド結果
Issue の Verification Commands 7 件(node id は Issue と完全一致)は実装前(本番スクリプトを stash した状態)に fail、実装後に全件 pass しました。
テストは本番の変換経路(fake は
baseline_vc_preflight.run_command()の戻り値と、子 process 起動を同じ script のmain()の in-process 呼び出しへ置き換える adapter のみ)を通します。完成済みのtimeout_provenanceや merged result を直接注入してはいません。unknown 系のネガティブケースだけは、実 artifact から binding 項目を削除または改ざんして劣化 producer を再現します。Allowed Paths 遵守
変更は Issue の Allowed Paths 6 件のみです(本番 2 件 + 新規テスト 3 件、
run_root_review_pipeline.pyは変更なし)。Checks
pnpm typecheck/pnpm lint/pnpm test(108 files, 1943 tests)/pnpm build: すべて passuv run --locked pytest .claude/skills/gemini-cli-headless-delegation/tests/test_model_routing.py: 24 passed.claude/skills/issue-contract-review/tests+.claude/skills/review-issue/tests+.claude/skills/issue-contract-review/scripts/tests: 1392 passed, 1 skipped(実装後の本番変更込みで実行).claude/skills/issue-refinement-loop/tests: 新規テスト含め pass。test_command_registry.py::TestAuthorityTransportPrivilegedExecutorRealSubprocessDispatchの 3 件は main(本変更なし)でも同様に fail する既存の環境依存の失敗で、本変更とは無関係です。.github/ci/python-test-plan.jsonは 3 つの tests ディレクトリを既に登録済みのため追加登録は不要です。OWNER REQUEST_CHANGES への対応(https://github.com/squne121/loop-protocol/pull/2901#issuecomment-5974155871)
timeout_diagnostics単体の 16 KiB ではなく、実 writer と同じjson.dumps+ 末尾 newline + utf-8 の merged result 全体の byte 数がSTDOUT_CAP(65,536)以下になるよう、optional diagnostic 側のみを縮小します(occurrences を減らしtruncated_count/total_timeout_occurrencesを整合)。header すら入らない場合は optional field を省略し、capture_failure にはしません。verdict / failure_class / blockers は不変です。unknown劣化に流します(producer / consumer の両境界)。dedup_binding_invalidになります。Runtime Verification Evidence
AC3 は動作検証 AC(decision: immediate)です。現在の PR head
8b3853a4318594f7f8a8ed18f1fa5b7952fe77a4に束縛して生成した証跡ログartifacts/runtime-verification-AC3-20261003T230514Z.logを inline 引用します(SKIP / fallback を PASS として扱っていません)。Schema Change Applicability
REVIEW_ISSUE_RESULT_V1はadditionalProperties: trueで、optional fieldtimeout_diagnosticsを追加するだけのため schema ファイルは変更していません。(スキーマ変更の適用可否を判定するセクションです)
Schema Consumer Inventory
N/A
reason: schema ファイルの変更がないため。consumer(
reviewer_transport.validate_semantic_result_schema()/compact_review_result)が optional field を受理することは新規テストで確認済みです。(スキーマ変更がある場合の consumer 一覧を記載するセクションです)
Safety Claim Matrix
本変更は診断情報の保持のみで、権限・隔離・hook・secret の境界は変更しません。
(safety-sensitive な変更の有無を判定するセクションです)
Notes
REVIEW_ISSUE_RESULT_V1の top-level はadditionalProperties: trueで、transport のsemantic_result検証・readback・Compact V2 wire のいずれも optional field を受理し、wire には現れません。diagnostic_report.canonical_plan_digestと同一値のみです。timeout error 側にも同じ source 値を bounded に保持し、merge 段では body や現在の history から再計算せず、top level 値と error 側値の比較だけでplan_digest_mismatchを判定します。3d75aac9起点)。