Skip to content

feat(stop-tool-call-leak): tool call leak の hard-fail 経路対応 (ADR-061) - #326

Merged
aloekun merged 7 commits into
masterfrom
fix/adr-061-tool-call-leak-hardfail-recovery
Jul 28, 2026
Merged

aloekun merged 7 commits into
masterfrom
fix/adr-061-tool-call-leak-hardfail-recovery

Conversation

@aloekun

@aloekun aloekun commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • hard-fail 経路(合成エントリで turn がエラー終了)では Stop hook が発火しないため、次の UserPromptSubmit で取り逃がした leak を検知し再実行を促す回収層を追加(主因対処)
  • scan_tail が合成エントリ(isApiErrorMessage / model:"<synthetic>")でチェーンを打ち切り、直前の実 leak を取り逃がす問題を修正(副因対処)
  • 単一 exe を hook_event_name で Stop / UserPromptSubmit に分岐。回収層は非ブロッキング(additionalContext + 任意 systemMessagedecision:block は出さない)
  • config に prompt_recovery_enabled / recovery_system_message_enabled を追加(code default OFF、ADR-039)、kill-switch は既存 env を両経路共通化
  • ADR-061 新規作成 + ADR-053 dogfood 追記、828764ce incident 由来の unit / e2e fixture を追加

Context

  • Why / Trigger: improve workspace のセッション 828764ce で、ADR-053 で対処済みのはずの tool call leak が 2 回発生し、いずれも block されなかった。約 150 セッションの corpus 調査で、内部リトライも失敗した turn がハーネスの合成エントリで終了する hard-fail 経路(Stop hook 不発火)を特定した。
  • Root cause(2 層): 主因 = hard-fail 経路では Stop hook イベント自体が発火しない(上流 Claude Code の挙動)。副因 = scan_tail が合成エントリでチェーンを打ち切る。
  • Scope decision: 主因は Stop hook では構造的に届かないため UserPromptSubmit 回収層で対処。回収は「最後の assistant 活動が hard-fail leak」に限定し、合成を伴わない末尾 leak は Stop の領分として除外(再誘導ループ回避)。上流への issue 報告は別途承認制のため本 PR 外。
  • pre-push review が指摘した非ブロッキングな DRY(run_check / run_recovery の transcript 読み込み重複)は follow-up 候補として保留。

Validation

  • cargo test --workspace: 全 crate pass(hooks-stop-tool-call-leak: 57 unit + 13 e2e)
  • cargo clippy --workspace -- -D warnings: clean / pnpm lint:md: 0 error / tsc: エラーなし
  • 実 incident transcript(828764ce)での手動 smoke: Stop block・UserPromptSubmit 回収の発火 3 ケース + 正常終了 turn での非発火 2 ケースを実測確認
  • pnpm push pre-push review: verdict=APPROVE(security + simplicity、最新 master docs(adr): weekly-review 検出の ADR drift を修正 #325 への rebase 後の再検証を含む)

References

  • ADR-061(新規): tool call leak の hard-fail 経路対応
  • ADR-053(dogfood 追記)/ ADR-039 / ADR-059 / ADR-049 / ADR-060
  • incident: session 828764ce(2026-07-27/28)

Summary by CodeRabbit

  • 改善

    • ツール呼び出しが実行されずテキストとして出力された場合でも、次の入力時に検知して再実行を促す回収処理を追加。
    • 必要に応じて、ユーザー向けの通知メッセージを表示可能にしました。
    • 合成的なエントリをまたぐケースでも、ツール呼び出し漏れを適切に検知できるよう改善。
  • 設定

    • 回収処理とユーザー通知の有効・無効を個別に切り替えられる設定を追加。
    • 緊急時に機能全体を無効化できるキルスイッチを維持。

aloekun added 6 commits July 28, 2026 20:12
ハーネスが tool call parse 失敗の内部リトライも失敗した turn を
isApiErrorMessage:true / model:"<synthetic>" の合成 assistant エントリで終端する。
この合成エントリは type:"assistant" だが leak を持たないため、scan_tail が
「非 leak の最終 assistant」としてチェーンを打ち切り、直前の実 leak を取り逃がす。

is_synthetic を追加し、isMeta user と同様チェーンを切らずスキップする。
828764ce incident の 2 パターン (leak→合成、leak→合成→実user→isMeta→leak→合成) を
再現する unit テストを追加。
…61 主因)

hard-fail 経路 (ツール呼び出し parse 失敗 → 内部リトライも失敗 → 合成エントリで
turn エラー終了) では Stop hook が発火しないため、Stop hook 側の修正では届かない。

単一 exe を hook_event_name で分岐させ、UserPromptSubmit で「最後の assistant 活動が
hard-fail leak」を検知する回収層を追加。additionalContext (モデル向け、XML 再出力の
禁止を明示) + 任意 systemMessage (ユーザー可視 1 行、ADR-059) の非ブロッキング 2 チャネル
で再実行を促す (decision:block は出さない)。config は prompt_recovery_enabled /
recovery_system_message_enabled を追加 (ともに code default OFF、ADR-039)。telemetry は
Decision::Warn / id suffix "/prompt-recovery" で Stop block と区別する。
…ig 有効化 (ADR-061)

hooks-config.toml の [stop_tool_call_leak] に prompt_recovery_enabled /
recovery_system_message_enabled を追加 (dogfood のため true)。

e2e に実 incident 由来 fixture を追加 (ADR-049):
- synthetic_after_leak_blocks_in_stop_mode: 副因 (合成跨ぎ block) の end-to-end 検証
- recovery_mode_*: UserPromptSubmit で additionalContext / systemMessage 出力、
  clean/leak-only 非発火、kill-switch skip
config assertion に新キーの値 assert を追加 (dev-conventions § fixture は値まで assert)。
settings.local.json.template と settings.json (cloud dispatcher、ADR-060) の
UserPromptSubmit 配列に hooks-stop-tool-call-leak を追加 (feedback-recovery の後、
timeout 5)。同一 exe が hook_event_name で Stop / UserPromptSubmit を分岐する。
settings.local.json は template から build:hooks-settings で再生成 (gitignore 対象)。
…ood 追記

ADR-061 を新規作成 (試験運用、ADR-039 3 点セット): 828764ce incident の背景・
corpus 調査結論・根本原因 2 層 (主因=Stop 不発火 / 副因=scan_tail 合成打ち切り)・
設計決定・bounded lifetime (ADR-053 連動) を記録。
ADR-053 に dogfood 観測 (取り逃がし 2 件、2026-07-27/28) を追記し ADR-061 を相互参照。
CLAUDE.md の ADR index に ADR-061 を追加。実装指示書 (plan md) は完了に伴い削除。
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 78797f10-9811-48ee-9b90-866ed0af8bd7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

tool call leak の hard-fail 経路を UserPromptSubmit で回収する処理を追加しました。合成 assistant を考慮した transcript 走査、non-blocking 出力、設定・フック登録、ADR、ユニットテストと E2E テストを更新しています。

Changes

Tool call leak 回収

Layer / File(s) Summary
Transcript 走査と回収判定
src/hooks-stop-tool-call-leak/src/transcript.rs
合成 assistant の識別、scan_tail の走査継続、scan_recovery による回収条件判定と関連テストを追加しました。
回収出力とイベント分岐
src/hooks-stop-tool-call-leak/src/recovery.rs, src/hooks-stop-tool-call-leak/src/main.rs
UserPromptSubmit 入力を分岐し、additionalContext と任意の systemMessage を含む non-blocking JSON を生成し、回収 telemetry を記録します。
フック登録と設定・ADR
.claude/hooks-config.toml, .claude/settings.json, .claude/settings.local.json.template, CLAUDE.md, docs/adr/*
回収設定、UserPromptSubmit フック、ADR-061、および ADR-053 の関連記録を追加しました。
ユニット・E2E 検証
src/hooks-stop-tool-call-leak/src/main.rs, src/hooks-stop-tool-call-leak/tests/e2e.rs
設定、イベント入力、合成エントリ、回収出力、対象外条件、kill-switch の挙動を検証します。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UserPromptSubmit
  participant hooks-stop-tool-call-leak
  participant scan_recovery
  participant recovery_render
  UserPromptSubmit->>hooks-stop-tool-call-leak: hook_event_name と transcript_path
  hooks-stop-tool-call-leak->>scan_recovery: transcript を走査
  scan_recovery-->>hooks-stop-tool-call-leak: 回収要否とツール名
  hooks-stop-tool-call-leak->>recovery_render: 回収メッセージを生成
  recovery_render-->>UserPromptSubmit: additionalContext と任意 systemMessage
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PRの主変更であるtool call leakのhard-fail経路対応とADR-061を正しく要約しており、簡潔で明確です。
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/adr-061-tool-call-leak-hardfail-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Monitor 分析 (GitHub Actions バックストップ)

  • トリガー: issue_comment (created) / 実行 run
  • CI: CodeRabbit のみ検出、pending (Review in progress)。他の CI check は現時点で未検出。mergeStateStatus は UNSTABLE (pending check 起因と推定)
  • レビュー状況: 人間レビュー 0 件、インライン指摘 0 件。CodeRabbit はレビュー未着 (処理中の自動コメントのみ、findings なし)
  • Verdict: user_decision (レビュー未着のため判定材料なし。CI/レビュー完了後に再評価が必要)

Applicable Findings (Critical / High / Major)

該当なし (レビュー未着)

Applicable Findings (Medium 以下)

該当なし

Filtered (not applicable)

該当なし

差分概要

10 ファイル変更 (ADR-061 新規、既存 ADR-053 更新、hooks-config.toml/settings.json/settings.local.json.template/CLAUDE.md 設定追加、Rust 実装 3 ファイル + 新規 recovery.rs + e2e テスト)。内容は既存の hooks-stop-tool-call-leak exe を hook_event_name で Stop / UserPromptSubmit の2経路に分岐させ、hard-fail 終了経路 (Stop 未発火) での leak 取りこぼしを UserPromptSubmit 側の非ブロッキング回収層 (additionalContext + 任意 systemMessage) で補完するもの。ADR-039 の 3 点セット (config opt-in default OFF / kill-switch / bounded lifetime) を満たす形で構成されており、ユニットテスト・E2E テストも新規ロジック (is_synthetic / scan_recovery / recovery 出力) を広くカバーしている。

次のアクション

  • CodeRabbit のレビュー完了を待って再度この分析を実行する (本 workflow の次回発火時に自動的に再評価される)
  • 現時点で CI check が CodeRabbit の pending のみであるため、ビルド/テスト用の CI workflow が本 PR に対して起動しているか (トリガー条件漏れがないか) を確認することを推奨

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
putComment timed out

@aloekun

aloekun commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review full

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

@aloekun 承知しました。PR #326 の変更全体をあらためてレビューします。

✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 12 minutes.

@aloekun

aloekun commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review full

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

@aloekun 承知しました。PR #326 の変更全体をあらためてレビューします。

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks-stop-tool-call-leak/tests/e2e.rs`:
- Around line 287-297: Update the recovery_mode_includes_system_message test to
reject carriage returns as well as newline characters in systemMessage. Extend
the assertion around msg so both '\n' and '\r' are treated as line breaks while
preserving the existing one-line validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3f7d4245-228b-46b9-a7f5-9c325b2c2056

📥 Commits

Reviewing files that changed from the base of the PR and between 7b9c3d5 and cc9f14b.

📒 Files selected for processing (10)
  • .claude/hooks-config.toml
  • .claude/settings.json
  • .claude/settings.local.json.template
  • CLAUDE.md
  • docs/adr/adr-053-stop-tool-call-leak-detection.md
  • docs/adr/adr-061-tool-call-leak-hardfail-recovery.md
  • src/hooks-stop-tool-call-leak/src/main.rs
  • src/hooks-stop-tool-call-leak/src/recovery.rs
  • src/hooks-stop-tool-call-leak/src/transcript.rs
  • src/hooks-stop-tool-call-leak/tests/e2e.rs

Comment on lines +287 to +297
#[test]
fn recovery_mode_includes_system_message() {
let dir = tempfile::tempdir().expect("temp dir");
let path = write_transcript(&dir, &[assistant_text_entry(LEAK_TEXT), synthetic_entry()]);
let (stdout, _stderr) = run_hook(&stdin_for_event(&path, "UserPromptSubmit"), None);
let out: Value = serde_json::from_str(&stdout).expect("stdout は回収 JSON");
let msg = out["systemMessage"]
.as_str()
.expect("recovery_system_message_enabled=true なので systemMessage が付く");
assert!(!msg.contains('\n'), "systemMessage は 1 行 (ADR-059): {}", msg);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

systemMessage の CR も改行として拒否してください。

現在の検証は \n のみ拒否するため、\r を含むメッセージでも「1 行」テストを通過します。

修正案
-    assert!(!msg.contains('\n'), "systemMessage は 1 行 (ADR-059): {}", msg);
+    assert!(
+        !msg.contains('\n') && !msg.contains('\r'),
+        "systemMessage は 1 行 (ADR-059): {}",
+        msg
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn recovery_mode_includes_system_message() {
let dir = tempfile::tempdir().expect("temp dir");
let path = write_transcript(&dir, &[assistant_text_entry(LEAK_TEXT), synthetic_entry()]);
let (stdout, _stderr) = run_hook(&stdin_for_event(&path, "UserPromptSubmit"), None);
let out: Value = serde_json::from_str(&stdout).expect("stdout は回収 JSON");
let msg = out["systemMessage"]
.as_str()
.expect("recovery_system_message_enabled=true なので systemMessage が付く");
assert!(!msg.contains('\n'), "systemMessage は 1 行 (ADR-059): {}", msg);
}
#[test]
fn recovery_mode_includes_system_message() {
let dir = tempfile::tempdir().expect("temp dir");
let path = write_transcript(&dir, &[assistant_text_entry(LEAK_TEXT), synthetic_entry()]);
let (stdout, _stderr) = run_hook(&stdin_for_event(&path, "UserPromptSubmit"), None);
let out: Value = serde_json::from_str(&stdout).expect("stdout は回収 JSON");
let msg = out["systemMessage"]
.as_str()
.expect("recovery_system_message_enabled=true なので systemMessage が付く");
assert!(
!msg.contains('\n') && !msg.contains('\r'),
"systemMessage は 1 行 (ADR-059): {}",
msg
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks-stop-tool-call-leak/tests/e2e.rs` around lines 287 - 297, Update
the recovery_mode_includes_system_message test to reject carriage returns as
well as newline characters in systemMessage. Extend the assertion around msg so
both '\n' and '\r' are treated as line breaks while preserving the existing
one-line validation.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 PR Monitor 分析 (GitHub Actions バックストップ)

  • トリガー: pull_request_review (submitted) / 実行 run
  • CI: CodeRabbit check は pass (Review completed)。ビルド/テスト系の CI check は本 PR で依然未検出 (analyze は本 workflow 自身)。mergeStateStatus は UNSTABLE、mergeable は MERGEABLE
  • レビュー状況: 人間レビュー 0 件。CodeRabbit が review full (2回目、12:32 実行) の結果として 12:40 に正式レビューを投稿、actionable comment 1 件 (Minor)。reviewDecision は未設定
  • Verdict: user_decision (Medium 以下相当の applicable finding 1 件のみ、Critical/High/Major 無し)

Applicable Findings (Critical / High / Major)

該当なし

Applicable Findings (Medium 以下)

# File (Line) Reviewer Issue Recommended Action
1 src/hooks-stop-tool-call-leak/tests/e2e.rs (287-297) CodeRabbit (Minor) recovery_mode_includes_system_message テストが systemMessage の 1 行検証で \n のみ拒否し \r を見逃す。ADR-059 は systemMessage を 1 行 (\n を含まない) と規定しており、CR のCRのみでは同テストが誤って通過し得る assertion を !msg.contains('\n') && !msg.contains('\r') に拡張 (CodeRabbit 提案 diff を適用可)

Filtered (not applicable)

該当なし

次のアクション

  • 上記 Minor finding (テストの CR 見逃し) の修正適用を検討 (severity Minor のため必須ではないが、ADR-059 の「1行」制約を厳密化する quick win)
  • 本 PR に対しビルド/テスト系 CI workflow が起動しているか (トリガー条件漏れの有無) を引き続き確認することを推奨 (前回分析から継続の懸念事項)

CodeRabbit (Minor): systemMessage の 1 行検証で復帰文字 (CR) も拒否するよう
e2e / unit の assert を厳密化 (LF のみ拒否では CR 入りメッセージが「1 行」テストを
通過するため)。
simplicity-review (DRY): run_check / run_recovery で重複していた transcript
読み込み + fail-open ブロックを load_tail_entries ヘルパーに共通化。
@aloekun
aloekun merged commit dc2293a into master Jul 28, 2026
1 check passed
@aloekun
aloekun deleted the fix/adr-061-tool-call-leak-hardfail-recovery branch July 28, 2026 16:32
aloekun added a commit that referenced this pull request Jul 28, 2026
PR #326 で CodeRabbit が指摘した「systemMessage の 1 行検証が LF のみで CR を
通過させる」ギャップと同型の assert が weekly_review.rs:595 に現存していた
(post-merge-feedback の fact-check で発見)。#326 の e2e / recovery と同じく
!contains('\n') && !contains('\r') に厳密化する。
aloekun added a commit that referenced this pull request Jul 28, 2026
…59) (#327)

* feat(lib-hook-output): SingleLineMessage 型で systemMessage の単一行を構造保証 (ADR-059)

systemMessage の「1 行」不変条件は従来 producer の format! + per-site の
assert!(!msg.contains('\n')) に依存し、`\r` 見落とし (PR #326 CodeRabbit 指摘) の
ように site ごとに再発し得た。

共有 crate lib-hook-output に newtype SingleLineMessage を追加。構築時に \r\n / \n / \r を
単一空白へサニタイズし、内部値が必ず 1 行になることを型で保証する (serde transparent で
wire 形式は素の文字列のまま)。debug_assert で dev 時に producer の改行混入を surface。

* refactor(stop-tool-call-leak): recovery systemMessage を SingleLineMessage に移行 (ADR-059)

RecoveryOutput.system_message を Option<String> → Option<SingleLineMessage> に、
build_system_message の戻り値を SingleLineMessage に変更。生の String を systemMessage に
載せることをコンパイル時に不可能にする。per-site の改行 assert は型保証に置換して削除。

* refactor(session-start): weekly-review systemMessage を SingleLineMessage に移行 (ADR-059)

build_weekly_review_system_message / WeeklyReviewNudge.system_message /
build_session_start_json を SingleLineMessage 型に移行。PR #326 で発見された
weekly_review.rs の CR 未チェックギャップは、型による構造保証で恒久的に塞がれる
(per-site の改行 assert は削除、単一行性は lib-hook-output のテストに集約)。

* docs(adr-059): 単一行不変条件の型による構造保証 (SingleLineMessage) を追補

per-site 検証 (format! + !contains) が site ごとに漏れた経緯 (PR #326 CodeRabbit 指摘 +
diff 外 weekly_review.rs の同型ギャップ) と、lib-hook-output::SingleLineMessage による
ルール→仕組み化 (ADR-042) の決定・スコープ (systemMessage 専用) を追補として記録。

* fix(lib-hook-output): new を全ビルド一律サニタイズに (debug_assert 除去) + ADR 追補日修正

PR #327 CodeRabbit 指摘への対応:
- Major: SingleLineMessage::new の debug_assert がサニタイズより先に改行入力で panic し、
  fail-open 安全網が debug/test で機能せず build 間で挙動が割れていた。assert を除去して
  全ビルド一律サニタイズにする (new が改行を panic せずサニタイズする回帰テスト追加)。
- Minor: ADR-059 追補日 2026-07-29 (未来) を review 基準の 2026-07-28 に修正。
ADR-059 追補の debug_assert 記述も一律サニタイズの根拠に更新。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant