Skip to content

fix(runner): a compaction outage is not a driver bug - #6735

Closed
serrrfirat wants to merge 1 commit into
mainfrom
claude/compaction-not-driver-bug
Closed

serrrfirat wants to merge 1 commit into
mainfrom
claude/compaction-not-driver-bug

Conversation

@serrrfirat

Copy link
Copy Markdown
Collaborator

What

loop_failure_kind_name mapped every LoopFailureKind to its run-failure category except one: CompactionUnavailable fell through the _ => "driver_bug" backstop.

The category "compaction_unavailable" already existed in ALL_RUN_FAILURE_CATEGORIES with its own user-facing summary. The only thing missing was the arm producing it.

Why it matters

compaction_unavailable (correct) driver_bug (what happened)
Auto-retriable? yes no
User sees its own summary "The agent runtime reported an internal error. Retry the run, and contact support if it happens again."
Metrics resource outage driver defect

So a transient resource condition that should have re-driven itself from its checkpoint instead killed the run and told the user to report a bug in our code that had not occurred.

Why the compiler could not catch it

LoopFailureKind is #[non_exhaustive], so this downstream crate cannot match exhaustively — the wildcard is forced, not chosen. That is precisely why the variant sat there undetected.

every_loop_failure_kind_maps_to_its_own_category is the substitute for the missing compile-time check. It asserts against each kind's own as_str() rather than a second hand-written list of category literals, so the test cannot drift into the same duplication it polices — the driver's match is already a re-implementation of as_str(), and it is the copy that drifted.

Adding a variant in ironclaw_turns still requires updating both the matcher and the list in this test. The compiler will not say so; this test will.

Validation

  • Red-verified — removing the new arm fails with left: "driver_bug" / right: "compaction_unavailable"
  • cargo test -p ironclaw_runner --lib --no-fail-fast — 356 passed, 0 failed
  • cargo clippy -p ironclaw_runner --all-targets --all-features -- -D warnings — clean

Correction to #6284 item 7

Found while auditing that item's catch-all cleanup. Its other claim does not hold, and I would rather say so than leave it as work someone picks up:

failure_lane() is vacuous — ignores its category argument entirely

failure_lane is correct. The lane it returns is binary — re-drivable from a checkpoint, or terminal-with-an-explanation — and that genuinely depends only on checkpoint presence. The category is used, by retry_disposition, for the finer auto-vs-user-initiated decision. The unused parameter is a documented seam for a future mid-run safety-abort category mapping to FailureLane::Security.

Two implementations do compute the lane by separate paths (failure_lane and RetryDisposition::failure_lane), kept in agreement by disposition_is_consistent_with_failure_lane. That is worth collapsing eventually, but delegating would move the documented Security seam into retry_disposition, where it does not belong — so not in this PR.

Independent of #6684 and #6697 (different crate, different concern).

🤖 Generated with Claude Code

`loop_failure_kind_name` mapped every `LoopFailureKind` to its run-failure
category except one: `CompactionUnavailable` fell through the
`_ => "driver_bug"` backstop.

The category string `"compaction_unavailable"` already existed in
`ALL_RUN_FAILURE_CATEGORIES` and already had its own user-facing summary — the
only thing missing was the arm producing it. Consequences of landing on
`driver_bug` instead:

- `compaction_unavailable` IS auto-retriable
  (`retry_disposition::is_auto_retriable_category`); `driver_bug` is NOT. A run
  that would have re-driven itself from its checkpoint died instead.
- The user was told "The agent runtime reported an internal error. Retry the
  run, and contact support if it happens again." for a transient resource
  condition — reporting our own code as broken when it was not.
- Failure metrics counted a resource outage as a driver defect.

The wildcard itself is forced, not a choice: `LoopFailureKind` is
`#[non_exhaustive]`, so this downstream crate cannot match exhaustively and the
compiler cannot flag a variant that falls through. That is exactly why this sat
undetected.

`every_loop_failure_kind_maps_to_its_own_category` is the substitute for the
missing compile-time check. It asserts against each kind's OWN `as_str()`
rather than a second hand-written list of category literals, so the test cannot
drift into the same duplication it exists to police — the driver's match is
already a re-implementation of `as_str()`, and this is the copy that drifted.

Red-verified: removing the new arm fails with
  left: "driver_bug"  right: "compaction_unavailable"

Verified: cargo test -p ironclaw_runner --lib --no-fail-fast — 356 passed, 0
failed; clippy --all-targets --all-features -D warnings clean.

Found while auditing #6284 item 7's catch-all cleanup. Note for that item: its
other claim — that `failure_lane()` is "vacuous" because it ignores its
`category` argument — does not hold. The lane (re-drivable vs terminal) depends
only on checkpoint presence; the category is used by `retry_disposition` for
the auto-vs-user-initiated decision, and the unused parameter is a documented
seam for a future mid-run safety-abort category. No fix needed there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@ironloopai

ironloopai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: 324771addaf994ed8f6e39b994e5373f75ef3dc5
Result: 1 blocking finding across 1 reviewer.
Next: Address the blocking findings, push fixes, then re-run the relevant reviewer.
Updated: 2026-07-27T17:29:43.080Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Changes requested 1 blocking finding / 0 notes 2026-07-27T17:29:43.069Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Changes requested; 1 blocking finding; The new arm is not on a production compaction-failure path, so this PR does not change the reported runtime behavior.
Recent activity
Time Reviewer State Detail
2026-07-27T17:26:47.542Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 324771a.
2026-07-27T17:26:47.542Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-27T17:26:47.830Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-27T17:26:50.652Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 0621fe8.
2026-07-27T17:29:43.069Z ironloop/common-reviewer (reviewer) Result captured Changes requested; 1 blocking finding.
2026-07-27T17:29:43.069Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6735 July 27, 2026 17:26 Destroyed
@github-actions github-actions Bot added size: M 50-199 changed lines risk: low Changes to docs, tests, or low-risk modules labels Jul 27, 2026
@github-actions github-actions Bot added the contributor: core 20+ merged PRs label Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Compaction-unavailable failures are now categorized correctly instead of being reported as driver bugs.
  • Tests

    • Added coverage to verify that all loop failure types receive their intended categories.
    • Confirmed that only driver bugs use the “driver_bug” category.

Walkthrough

The runner now maps CompactionUnavailable to "compaction_unavailable" instead of "driver_bug". Tests cover every known failure kind and verify the fallback remains exclusive to DriverBug.

Changes

Failure category mapping

Layer / File(s) Summary
Compaction category mapping and regression tests
crates/ironclaw_runner/src/text_loop_driver.rs
CompactionUnavailable receives its dedicated category, and tests verify variant-specific mappings plus the exclusive DriverBug fallback.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: benkurrek

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR explains the fix and validation, but it misses most required template sections and checklist fields. Fill in the template sections: Summary bullets, Change Type, Linked Issue, Validation, Test Strategy, Security Impact, DB Impact, Blast Radius, Rollback, Review Follow-Through, and Review track.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Conventional Commit format is used and the title accurately summarizes the CompactionUnavailable mapping fix.
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.

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.

@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 `@crates/ironclaw_runner/src/text_loop_driver.rs`:
- Around line 274-337: Add caller-level regression coverage for the production
retry-decision path handling CompactionUnavailable, driving the real caller
rather than only loop_failure_kind_name. Assert that the resulting category is
compaction_unavailable and is recognized as auto-retriable, while preserving the
existing mapping unit tests.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: c3b4b385-03fd-4d55-ab92-64195e7ebfa5

📥 Commits

Reviewing files that changed from the base of the PR and between aa8b748 and 324771a.

📒 Files selected for processing (1)
  • crates/ironclaw_runner/src/text_loop_driver.rs

Comment thread crates/ironclaw_runner/src/text_loop_driver.rs

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 1 0 1 324771addaf9

Head: 324771addaf994ed8f6e39b994e5373f75ef3dc5
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

The new arm is not on a production compaction-failure path, so this PR does not change the reported runtime behavior.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [MEDIUM] Compaction mapping change is unreachable in production

Location: crates/ironclaw_runner/src/text_loop_driver.rs:261
loop_failure_kind_name is only called with fixed non-compaction variants in this text-only driver; no production source constructs or passes CompactionUnavailable to it. The planned loop path instead carries a LoopExit::Failed, whose failure category already comes directly from LoopFailureKind::as_str() in ironclaw_turns. The compaction matrix also models this error as a best-effort path that completes rather than failing the run. Consequently this arm and its direct unit test cannot change an observed compaction outage from driver_bug to compaction_unavailable. Trace and fix the actual producer/runner path, with an end-to-end regression test, or remove/retarget this no-op change.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

LoopFailureKind::PolicyDenied => "policy_denied",
// LoopFailureKind is `#[non_exhaustive]`; fail closed if a new variant
// lands in `ironclaw_turns` ahead of this matcher being updated.
LoopFailureKind::CompactionUnavailable => "compaction_unavailable",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This mapping is not reached by a production compaction failure: the text-only driver calls this helper only with fixed non-compaction variants, while the planned-loop LoopExit::Failed path already uses LoopFailureKind::as_str(). Please trace and test the actual failing path; this arm alone cannot change the reported runtime category.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You are right, and this PR's premise is wrong. Retracting the claim.

I traced it after your comment:

  • loop_failure_kind_name has 8 call sites, every one passing a hardcoded literal (InvalidModelOutput ×2, DriverBug ×2, TranscriptWriteFailed, ModelError ×2, CheckpointRejected). No call site passes a dynamic LoopFailureKind, so CompactionUnavailable cannot reach the function at all.
  • The real path is the planned loop's LoopExit::Failed, which uses LoopFailureKind::as_str() directly — and as_str() already maps CompactionUnavailable → "compaction_unavailable". The existing assertion at planned_driver.rs:1000 (failed.reason_kind.as_str() == "checkpoint_unavailable") confirms that shape.

So the user-facing consequence I asserted in the PR body — a compaction outage reporting as driver_bug, losing auto-retry, and telling the user to contact support — does not occur. The retry-lane and summary differences between the two categories are real, but nothing routes a compaction failure through the arm I added.

What I actually did was find a gap in a mapping table and assert a downstream consequence without tracing whether production reaches it. That is precisely what .claude/rules/discovery-claims.md exists to prevent, and your second comment is the trace I should have run first.

Two things follow, and I would rather you pick than have me guess:

  1. Close this PR. The fix is a no-op and the body makes a false claim. Cleanest option.
  2. Keep a reduced version, retitled to what it actually is: the _ => "driver_bug" wildcard is a latent hazard if a future caller ever passes a dynamic kind, and every_loop_failure_kind_maps_to_its_own_category would catch that the moment it happened. That is defensible as defense-in-depth, but it is a maintenance guard, not a bug fix, and the PR must say so.

Your first comment (3659481551) stands either way and is the same lesson: my tests only exercised the helper, never the caller, so they could not have revealed that the caller never passes this variant. Caller-level coverage would have failed to construct the scenario at all, which is the signal I missed.

I have corrected the claim on #6284 as well.

🤖 Addressed by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.54% (307679 / 359687 lines)
  floor:    80.81% (tolerance 0.5pp -> effective floor 80.31%)
  denominator: 359687 lines now vs 377084 at floor capture (-17397 lines, -4.61%) — not a material change

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.54% — 307679 / 359687 lines

Per-crate breakdown (60 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_process_sandbox 33.91% 118 / 348
ironclaw_host_ingress 42.5% 17 / 40
ironclaw_event_projections 43.71% 684 / 1565
ironclaw_observability 61.54% 16 / 26
ironclaw_telegram_v2_adapter 62.35% 631 / 1012
ironclaw_authorization 62.98% 609 / 967
ironclaw_memory 70.15% 919 / 1310
ironclaw_trust 73.21% 664 / 907
ironclaw_filesystem 73.65% 4584 / 6224
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_extractors 74.72% 538 / 720
ironclaw_capabilities 75.45% 2879 / 3816
ironclaw_mcp 76.2% 775 / 1017
ironclaw_projects 76.48% 400 / 523
ironclaw_reborn_cli 78.22% 10693 / 13670
ironclaw_telegram_extension 78.59% 962 / 1224
ironclaw_llm 79.29% 21451 / 27054
ironclaw_wasm 79.72% 735 / 922
ironclaw_memory_native 80.97% 3114 / 3846
ironclaw_auth 81.88% 6679 / 8157
ironclaw_first_party_extensions 82.38% 6682 / 8111
ironclaw_events 82.47% 1604 / 1945
ironclaw_host_api 82.65% 9120 / 11035
ironclaw_processes 83.3% 933 / 1120
ironclaw_reborn_identity 83.8% 450 / 537
ironclaw_operator 84.37% 5558 / 6588
ironclaw_secrets 84.56% 2798 / 3309
ironclaw_reborn_config 85.24% 2102 / 2466
ironclaw_skills 85.27% 4493 / 5269
ironclaw_extension_host 85.43% 18822 / 22032
ironclaw_reborn_composition 85.67% 24387 / 28465
ironclaw_run_state 85.77% 458 / 534
ironclaw_webui 85.87% 10914 / 12710
ironclaw_triggers 85.92% 2783 / 3239
ironclaw_network 85.97% 913 / 1062
ironclaw_reborn_event_store 86.51% 1251 / 1446
ironclaw_hooks 86.63% 9931 / 11464
ironclaw_extensions 86.98% 3669 / 4218
ironclaw_common 86.99% 1772 / 2037
ironclaw_approvals 87.18% 1543 / 1770
ironclaw_threads 87.2% 4851 / 5563
ironclaw_product 87.52% 19698 / 22507
ironclaw_reborn_traces 88.13% 11986 / 13600
ironclaw_turns 88.33% 14317 / 16208
ironclaw_slack_extension 88.47% 1934 / 2186
ironclaw_host_runtime 88.49% 18983 / 21451
ironclaw_reborn_openai_compat 89.32% 3780 / 4232
ironclaw_conversations 90.01% 3164 / 3515
ironclaw_resources 90.84% 4474 / 4925
ironclaw_runner 90.93% 17230 / 18948
ironclaw_event_streams 91.24% 1063 / 1165
ironclaw_loop_host 91.9% 16503 / 17958
ironclaw_attachments 93.06% 630 / 677
ironclaw_outbound 93.91% 4101 / 4367
ironclaw_agent_loop 94.94% 9924 / 10453
ironclaw_safety 95.28% 3858 / 4049
ironclaw_first_party_extension_ports 95.62% 3672 / 3840
ironclaw_runtime_policy 96.56% 813 / 842

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@railway-app

railway-app Bot commented Jul 27, 2026

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6735 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 27, 2026 at 5:36 pm

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Closing: the premise does not hold.

loop_failure_kind_name has 8 call sites, every one passing a hardcoded literal — no path passes a dynamic LoopFailureKind, so CompactionUnavailable never reaches the arm this PR added. The production path (planned loop LoopExit::Failed) uses LoopFailureKind::as_str() directly and already maps the variant correctly.

The category difference I cited was real (compaction_unavailable is auto-retriable, driver_bug is not), but nothing routes a compaction failure through the gap, so the user-facing consequence I claimed has never occurred.

Thanks to the reviewer for pushing on it rather than accepting the fix — the second comment asked for the caller-path trace, and running that trace is what showed there is no caller path.

Retraction and full trace: #6735 (comment)
Epic corrected: #6284 (comment)

Not reopening as a maintenance guard for now — a completeness test whose scenario cannot be constructed in production is weak coverage, and item 7's catch-alls are either forced by #[non_exhaustive] or unreachable. If a future change makes the helper take a dynamic kind, that is the moment the guard earns its place.

@serrrfirat serrrfirat closed this Jul 27, 2026
@serrrfirat
serrrfirat deleted the claude/compaction-not-driver-bug branch July 27, 2026 19:17

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6735 — 324771ad Deployed Jul 27, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant