Skip to content

test+fix(turns): crash-consistency chaos suite + the two crash-recovery defects it found (#6263, #6284) - #6295

Merged
ilblackdragon merged 3 commits into
mainfrom
test/turn-state-crash-consistency-suite
Jul 20, 2026
Merged

ilblackdragon merged 3 commits into
mainfrom
test/turn-state-crash-consistency-suite

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Phase 0 of the turn-state consolidation in #6263: a crash-consistency chaos suite that is the acceptance oracle for the Step 3 async-write-behind change — plus the two real crash-recovery defects it surfaced, fixed here so it lands fully green (no ignored tests).

The suite (crates/ironclaw_turns/tests/row_store_crash_consistency.rs)

Seed-deterministic crash/fault chaos harness over FilesystemTurnStateRowStore:

  • Fault backend over InMemoryBackend: injects write failures (Nth write / next journal append / path prefix) and forks the durable bytes at any moment.
  • Crash primitive: reopen a fresh store over the forked bytes → recovery via journal replay.
  • Generator: seeded StdRng over 4 scopes drawing submit / claim / heartbeat / block(approval|auth) / resume / complete / fail / cancel / recover_expired_leases, incl. idempotent-replay submits and no-op claims; ~1,000 ops across 10+ seeds; seed + op log printed on failure.
  • Oracle: lockstep InMemoryTurnStateStore (the row store's own engine) receives every acked op; recovered snapshot diffed against it, plus internal-consistency invariants and the four [EPIC] error-recoverability endgame — the model recovers from 100% of the errors it sees #6284 crash-recovery invariants (crash → re-drivable; write-failure recoverable + atomic; gate-park + terminal always durable via a named is_recoverability_critical predicate; failure detail survives).

Defect 1 — durability: empty delta desyncs the journal reservation seq

An Ok mutation with an empty durable delta (a claim matching nothing, an idempotent-replay submit, a no-op recover/cancel) advanced the hot-cache journal seq while enqueue_delta skipped the empty append — drifting the reservation seq ahead of the real append log, so a later complete_run's active-lock DELETE materialized below the guard seq and was skipped. After a crash the completed run kept its active lock and the thread was permanently ThreadBusy. Fix: both commit paths early-out on an empty persist_delta (advance nothing), mirroring the existing identity no-op early-out. Generator no longer steers around no-ops.

Defect 2 — #6284: checkpointless crashed run stranded as terminal Failed

recover_expired_leases terminated every expired Running lease as Failed(lease_expired). For a run that crashed before its first checkpoint (before any side effect) #6284 requires it stay re-drivable — lease_expired isn't a genuine invariant. Fix: a Running run with no loop checkpoint at all is re-queued to Queued (re-drivable), bounded by claim_count — at/over max_crash_recovery_reclaims (new limit, default 5) it terminal-fails with a distinct model-visible crash_retry_exhausted, never a silent lease_expired. Checkpointed / Final-only / CancelRequested expiry paths unchanged. This is a shared-engine change, so it corrects crash recovery for both the direct authority and the row store.

Gating on "no loop checkpoint at all" (not "no resumable checkpoint") deliberately avoids re-driving a Final-only, post-side-effect run.

Tests

Full ironclaw_turns green; ironclaw_runner --features filesystem-goal-store green; reborn_integration_lease_wedge green (confirms the checkpointed path is untouched); crash suite 15 passed, 0 ignored (both reproducers are now passing regression tests). Existing tests that pinned the old checkpointless-terminal behavior updated to the re-drivable contract, each noted in the commit. clippy -D warnings clean; pre-commit-safety exit 0.

Follow-up (out of scope): a run whose only checkpoint is a non-resumable Final one still terminates without a resume path — the #6284 checkpointed-terminal dead-end.

🤖 Generated with Claude Code

ilblackdragon and others added 3 commits July 19, 2026 23:04
…re (#6263 Phase 0)

WIP checkpoint: seed-deterministic crash/fault chaos harness over the row
store; oracle = lockstep InMemoryTurnStateStore engine. Green on the
current write-through store (12 tests) and pins two real defects it found
as ignored reproducers (fixed in the following commits):
- no-op empty-delta desyncs the journal reservation seq -> a later
  active-lock DELETE is not durable -> thread stranded after crash;
- recover_expired_leases strands a checkpointless crashed run as terminal
  Failed(lease_expired), violating #6284 (a crash must stay re-drivable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion seq (#6263)

Found by the crash-consistency chaos suite. A mutation whose durable delta
is empty (a claim matching nothing, an idempotent-replay submit, a no-op
recover/cancel) still advanced the hot-cache journal_seq while enqueue_delta
skipped the (empty) backend append — drifting the reservation seq +1 ahead
of the real append log. A later complete_run then pre-reserved its
active-lock row at the desynced higher seq while the DELETE tombstone
materialized at the real lower seq, so write_materialized_row's
current_seq >= journal_seq guard skipped the tombstone: the completed run
kept its active lock durably (live cache 0, recovered state 1) and the
thread was permanently ThreadBusy after a crash.

Both commit paths now early-out on an empty persist_delta: update the hot
cache to the new state at the CURRENT seq (no advance) and skip
reservation + enqueue, mirroring the existing new_snapshot == baseline
no-op early-out but keyed on durable emptiness. The crash suite's generator
no longer steers around no-op mutations; the reproducer is un-ignored and
green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ead of terminal Failed (#6263, #6284)

recover_expired_leases terminated every expired Running lease as terminal
Failed(lease_expired) — a dead end for a run that crashed before its first
checkpoint, i.e. before any side effect. #6284 forbids this: a crash is not
a genuine invariant (allowed terminal set = cancellation / budget /
DriverBug), and a pre-first-checkpoint run is always safe to re-drive.

New resolve_expired_lease classifier:
- CancelRequested -> Cancelled (genuine invariant) — unchanged.
- Running with ANY loop checkpoint -> Failed(lease_expired) + latest
  resumable checkpoint — unchanged. Gating on 'no loop checkpoint at all'
  rather than 'no *resumable* checkpoint' avoids re-driving a Final-only,
  post-side-effect run.
- Running with NO loop checkpoint -> re-queue to Queued (re-drivable),
  bounded by claim_count: at/over max_crash_recovery_reclaims (new limits
  field, default 5) it terminal-fails with a distinct model-visible
  crash_retry_exhausted reason, never a silent lease_expired.

Shared-engine change (applies to both the direct authority and the row
store). Existing tests pinning the old checkpointless-terminal behavior
updated to the re-drivable contract, or to max_crash_recovery_reclaims=0/1
helpers where they exercise the terminal-recovery publish/lock path;
checkpointed, Final-only, and CancelRequested expiry paths unchanged
(verified incl. the lease_wedge integration test). Both crash-suite
reproducers un-ignored and green.

Follow-up: a run whose only checkpoint is a non-resumable Final one still
terminates without a resume path (#6284 checkpointed-terminal dead-end) —
out of scope here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ironloopai

ironloopai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: 9aa733a5dcc0ee6a779bb217b813c17465b11d8f
Result: 1 reviewer declined to produce a review for this head.
Next: Narrow the change or provide the missing context, then re-run the declined reviewer.
Updated: 2026-07-19T23:50:18.505Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Review declined Not reviewed 2026-07-19T23:50:18.494Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Review declined: The supplied base commit is not an ancestor of the head (merge-base is dca1623), so reviewing base..head would conflate this three-commit turns PR…
Recent activity
Time Reviewer State Detail
2026-07-19T23:49:00.959Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 9aa733a.
2026-07-19T23:49:00.959Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-19T23:49:01.701Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-19T23:49:05.842Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 3233c5c.
2026-07-19T23:50:18.494Z ironloop/common-reviewer (reviewer) Result captured Skipped; 0 blocking findings.
2026-07-19T23:50:18.494Z 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.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a2083a9b-dd69-48f8-828d-857e8657f821

📥 Commits

Reviewing files that changed from the base of the PR and between 232d56a and 9aa733a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (9)
  • crates/ironclaw_loop_host/tests/turn_event_publisher_contract.rs
  • crates/ironclaw_runner/tests/turn_scheduler_contract.rs
  • crates/ironclaw_turns/Cargo.toml
  • crates/ironclaw_turns/src/filesystem_store/row_store.rs
  • crates/ironclaw_turns/src/memory/mod.rs
  • crates/ironclaw_turns/tests/filesystem_turn_state_contract.rs
  • crates/ironclaw_turns/tests/per_user_concurrency_cap.rs
  • crates/ironclaw_turns/tests/row_store_crash_consistency.rs
  • crates/ironclaw_turns/tests/turn_coordinator_contract.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added configurable crash-recovery retry limits for checkpoint-less runs.
    • Checkpoint-less expired runs can now be re-queued for recovery or marked failed when retries are exhausted.
    • Added clearer terminal outcomes for cancelled, lease-expired, and crash-retry-exhausted runs.
    • Durable-empty updates now avoid unnecessary journal writes and reservation consumption.
  • Bug Fixes

    • Improved persistence and recovery behavior across crashes, lease expiry, and partial writes.
  • Tests

    • Added extensive crash-consistency, fault-injection, and recovery coverage.

Walkthrough

Lease recovery now re-drives checkpointless runs within a configurable reclaim bound, terminally failing exhausted retries. Row-store durable-empty deltas avoid journal reservations, and extensive crash-consistency tests validate persistence, recovery, locking, and event contracts.

Changes

Lease recovery and durability

Layer / File(s) Summary
Bounded expired-lease recovery
crates/ironclaw_turns/src/memory/mod.rs
Adds configurable checkpointless reclaim limits, re-queue resolution, and crash_retry_exhausted terminal outcomes.
Durable-empty row-store handling
crates/ironclaw_turns/src/filesystem_store/row_store.rs
Applies empty durable deltas without advancing journal sequences, reserving rows, or enqueueing journal writes.
Crash-consistency acceptance coverage
crates/ironclaw_turns/tests/row_store_crash_consistency.rs, crates/ironclaw_turns/Cargo.toml
Adds fault injection, durable-byte replay, seeded chaos testing, and targeted persistence and recovery regressions.
Scheduler and event contract alignment
crates/ironclaw_runner/tests/turn_scheduler_contract.rs, crates/ironclaw_loop_host/tests/turn_event_publisher_contract.rs, crates/ironclaw_turns/tests/*
Updates lease recovery expectations, event reasons, coordinator helpers, and related contract descriptions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant InMemoryTurnStateStore
  participant EventPublisher
  Scheduler->>InMemoryTurnStateStore: recover_expired_leases
  InMemoryTurnStateStore->>InMemoryTurnStateStore: resolve checkpointless lease
  InMemoryTurnStateStore->>Scheduler: re-queue or terminal failure
  InMemoryTurnStateStore->>EventPublisher: publish recovery event
Loading

Possibly related PRs

Suggested reviewers: think-in-universe

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is thorough, but it omits most required template sections and fields, including Change Type, Linked Issue, and Rollback Plan. Add the missing template sections or fill them with explicit N/A/None entries, especially Change Type, Linked Issue, Security Impact, Database Impact, and Rollback Plan.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is relevant and mostly follows Conventional Commits style, summarizing the crash-consistency suite and crash-recovery fixes.
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.

@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 Declined: reviewer

Review at a glance

Disposition Head
⏭️ Review declined 9aa733a5dcc0

Head: 9aa733a5dcc0ee6a779bb217b813c17465b11d8f
Reason: The supplied base commit is not an ancestor of the head (merge-base is dca1623), so reviewing base..head would conflate this three-commit turns PR with 17,606 lines of unrelated divergence. A reliable complete review is not possible within this comparison.
Next: Refresh the reviewer job with the PR's actual common base (or rebase the head onto the stated base) and rerun the review against that ancestor comparison.

Run details

Status: Current
Trustworthy review produced: no

Summary

Review skipped: the supplied base and head do not form the stated PR comparison, and their direct diff is a mega-change spanning 203 files across unrelated subsystems.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a bounded re-drive mechanism for checkpoint-less runs whose leases expire, allowing them to be safely re-queued up to a configurable limit before failing with crash_retry_exhausted (addressing #6284). It also resolves a hot-cache desynchronization issue in the row store by ensuring that empty durable deltas do not advance the journal reservation sequence (addressing #6263). To safeguard these changes, a comprehensive crash-consistency chaos-testing suite has been added. There are no review comments to address, and I have no additional feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.68% (313921 / 366395 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 366395 lines now vs 320188 at floor capture (+46207 lines, +14.43%) — material change (>5%)

⚠️ 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.68% — 313921 / 366395 lines

Per-crate breakdown (65 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_runtime_policy 33.84% 89 / 263
ironclaw_event_projections 43.31% 673 / 1554
ironclaw_observability 61.54% 16 / 26
ironclaw_authorization 62.46% 604 / 967
ironclaw_dispatcher 62.88% 83 / 132
ironclaw_mcp 64.89% 595 / 917
ironclaw_triggers 65.44% 2142 / 3273
ironclaw_filesystem 67.78% 3957 / 5838
ironclaw_channel_host 68.65% 219 / 319
ironclaw_memory 69.2% 773 / 1117
ironclaw_reborn_migration 71.64% 1551 / 2165
ironclaw_trust 72.88% 661 / 907
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_reborn_event_store 74.67% 958 / 1283
ironclaw_extractors 74.72% 538 / 720
ironclaw_capabilities 75.58% 2092 / 2768
ironclaw_projects 76.48% 400 / 523
ironclaw_reborn_cli 77.08% 10250 / 13298
ironclaw_llm 78.36% 20306 / 25915
ironclaw_product_context 78.57% 11 / 14
ironclaw_run_state 79.25% 424 / 535
ironclaw_telegram_extension 80.18% 4842 / 6039
ironclaw_wasm_product_adapters 80.36% 1448 / 1802
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_first_party_extensions 81.06% 5965 / 7359
ironclaw_memory_native 81.17% 3195 / 3936
ironclaw_events 81.95% 1594 / 1945
ironclaw_network 82.98% 673 / 811
ironclaw_reborn_identity 83.59% 433 / 518
ironclaw_processes 83.76% 939 / 1121
ironclaw_secrets 83.79% 2548 / 3041
ironclaw_wasm 84.44% 1069 / 1266
ironclaw_auth 84.81% 3233 / 3812
ironclaw_product_workflow 84.91% 11031 / 12992
ironclaw_reborn_config 85.2% 2055 / 2412
ironclaw_channel_delivery 85.79% 1383 / 1612
ironclaw_common 86.13% 1714 / 1990
ironclaw_turns 86.72% 14676 / 16924
ironclaw_threads 86.93% 4708 / 5416
ironclaw_slack_v2_adapter 87.3% 1491 / 1708
ironclaw_skills 87.58% 4470 / 5104
ironclaw_reborn_composition 88.05% 70659 / 80253
ironclaw_product_adapter_registry 88.06% 531 / 603
ironclaw_host_api 88.1% 3894 / 4420
ironclaw_product_adapters 88.1% 3384 / 3841
ironclaw_reborn_traces 88.2% 11946 / 13544
ironclaw_hooks 88.3% 10036 / 11366
ironclaw_host_runtime 88.69% 18060 / 20363
ironclaw_webui 88.9% 7652 / 8607
ironclaw_extensions 89.38% 2971 / 3324
ironclaw_reborn_openai_compat 89.5% 3778 / 4221
ironclaw_runner 89.64% 17364 / 19370
ironclaw_telegram_v2_adapter 89.7% 2717 / 3029
ironclaw_approvals 90.18% 1598 / 1772
ironclaw_conversations 90.39% 3123 / 3455
ironclaw_event_streams 90.82% 1009 / 1111
ironclaw_resources 91.65% 4476 / 4884
ironclaw_loop_host 92.28% 15577 / 16880
ironclaw_attachments 93.06% 630 / 677
ironclaw_agent_loop 94.88% 9184 / 9680
ironclaw_safety 95.09% 3682 / 3872
ironclaw_outbound 95.52% 3451 / 3613
ironclaw_first_party_extension_ports 95.62% 3672 / 3840

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

@ilblackdragon

Copy link
Copy Markdown
Member Author

✅ Ready for merge — crash-consistency chaos suite + the two crash-recovery defects it surfaced (#6263 Phase 0 / #6284). IronLoop: reviewed, 0 blocking. All 59 CI checks green (the one pending is the Railway preview-env deploy, not a merge gate).

Reviewed both production fixes — sound:

  • Defect 1 (durability) — an Ok mutation with an empty durable delta advanced the hot-cache journal seq while enqueue_delta skipped the empty append, drifting the reservation seq ahead of the real append log so a later complete_run active-lock DELETE was dropped on recovery (leaving a completed run's lock held → permanent ThreadBusy). Fix correctly early-outs both commit paths on an empty persist_delta, keeping the hot cache at the SAME seq (no advance, no append), and invalidates the cache on an apply failure.
  • Defect 2 ([EPIC] error-recoverability endgame — the model recovers from 100% of the errors it sees #6284) — recover_expired_leases terminal-failed every expired Running lease as lease_expired, including runs that crashed before their first checkpoint (no side effect, safe to re-drive). Fix re-queues a checkpointless Running run to Queued, bounded by claim_count vs max_crash_recovery_reclaims (default 5) → distinct model-visible crash_retry_exhausted at the bound, never a silent lease_expired; checkpointed / Final-only / CancelRequested paths unchanged.

Both defects ship as passing chaos-suite reproducers (15 tests, 0 ignored); the shared-engine defect-2 fix corrects both the direct authority and the row store. clippy -D warnings clean; existing tests updated to the re-drivable contract.

@ilblackdragon
ilblackdragon merged commit 8a8c58d into main Jul 20, 2026
67 of 68 checks passed
@ilblackdragon
ilblackdragon deleted the test/turn-state-crash-consistency-suite branch July 20, 2026 00:55
ilblackdragon added a commit that referenced this pull request Jul 20, 2026
Keep PR #6299 current with main's tip so GitHub can compute mergeability and
run the heavy CI lanes. #6295 (turns crash-consistency chaos suite + two
crash-recovery fixes) auto-merged cleanly — only Cargo.lock and
turn_coordinator_contract.rs overlapped, both resolved by git. Verified:
`cargo test -p ironclaw_turns --all-features` — 144 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@serrrfirat serrrfirat mentioned this pull request Jul 29, 2026
18 of 29 tasks

This branch is being deployed

1 in progress deployment
ironclaw-ci-preview / ironclaw-pr-6295 — 9aa733a5 Deployed Jul 19, 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 scope: dependencies Dependency updates size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant