Skip to content

feat(turns): opt-in async write-behind durability mode for the turn-state row store (#6263 Step 3) - #6298

Merged
ilblackdragon merged 11 commits into
mainfrom
refactor/turn-state-write-behind
Jul 20, 2026
Merged

ilblackdragon merged 11 commits into
mainfrom
refactor/turn-state-write-behind

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Step 3 of the turn-state consolidation (#6263). Adds an opt-in async write-behind mode to FilesystemTurnStateRowStore, gated by the crash-consistency chaos suite (#6295) running in both modes. Capability only — no production wiring changes here; flipping the volatile profile onto it (retiring the raw direct authority) is a separate follow-on.

What

TurnStateDurabilityPolicy { WriteThrough (default), WriteBehind }.

  • WriteThrough = today's behavior byte-for-byte (every mutation awaits the durable ack). Default → zero change, no crash-loss window.
  • WriteBehind = a mutation whose durable delta is not recoverability-critical returns Ok immediately after enqueue (background flush); a critical transition awaits the ack. The boundary is_recoverability_critical = is_blocked() || is_terminal() is now a production predicate the suite references.

Critical ops are free durability barriers. The journal is a strictly sequential single-writer, so awaiting a critical op's ack implies the entire prior async tail is durable. A run can therefore only ever lose trailing non-critical churn (Queued/Running) — never a gate-park or terminal a human or the model is waiting on.

Write-behind robustness (write-through gets these free)

  • Backpressure: bounded pending-un-acked window (max_pending_write_behind_deltas, default 128); at the cap a non-critical op awaits the oldest ack. Bounds memory + the crash-loss window.
  • Append-failure halt: the flusher latches degraded + closes on append failure under write-behind (write-through still continues) → no durable gap; mutations then fail fast and reads reload from the rolled-back durable point. Pre-append cross-store-CAS reservations are disabled under write-behind (a critical op's reservation can't find rows the async ops never wrote synchronously).
  • Cache safety: a rejected mutation rebuilds the engine from the cached (unflushed-inclusive) snapshot rather than reload-from-durable, so acked non-critical ops the caller was told succeeded aren't silently dropped.
  • Journal abort-on-drop so a "crashed" store can't flush its queued tail post-crash and race a store reopened over the same backend.

Acceptance gate — the crash suite, both modes (19 tests, 0 ignored)

  • WriteThrough: strict "acked ⇒ durable" projection diff, unchanged.
  • WriteBehind: assert_recoverability_critical_survives stays strict (gate-park + terminal never lost, gate ref + failure detail survive); the full diff becomes a legal-prefix check plus the anti-cheat convergence test — fork the durable bytes before K acked non-critical ops, recover, verify the recovered state is a consistent legal prefix, then re-apply the K lost ops and assert it converges to the model exactly. That proves the loss is redoable work, not corruption, and stops the invariant being weakened to pass a buggy impl. Barrier, append-failure-halt/degrade/recover, and backpressure tests added.

Measured (store-isolated per-transition, InMemoryBackend)

policy conc submit p50/p99 claim p50/p99 complete p50/p99
direct InMemory 1 23µs / 54µs 3.0µs / 7µs 3.7µs / 7µs
WriteThrough 1 1.7ms / 8.6ms 2.3ms / 9.5ms 2.3ms / 9.4ms
WriteBehind 1 189µs / 363µs 59µs / 138µs 2.4ms / 15ms (critical)
WriteThrough 32 17.8ms / 24ms 21ms / 31ms 18ms / 30ms
WriteBehind 32 128µs / 364µs 45µs / 115µs 24ms / 30ms (barrier)

WriteBehind's non-critical writes approach memory speed (~45–190µs, vs write-through's 2ms→20ms durable-ack floor — 10–400×) while critical transitions stay synchronously durable. The heavier complete under load is the expected barrier cost (it flushes the accumulated async tail), amortized because non-critical ops dominate.

Tests

ironclaw_turns 612 passed; crash suite 19 both modes, 0 ignored; ironclaw_runner --features filesystem-goal-store green; clippy -D warnings clean; fmt + pre-commit-safety clean. WriteThrough and all pre-existing #6284 crash-recovery tests untouched.

Follow-on: graceful drain-before-shutdown when the profile is wired onto WriteBehind (abort-on-drop currently discards the un-flushed tail at drop).

🤖 Generated with Claude Code

…tate row store (#6263 Step 3)

Adds TurnStateDurabilityPolicy { WriteThrough (default), WriteBehind } to
FilesystemTurnStateRowStore. WriteThrough is byte-for-byte today's behavior
(every mutation awaits the durable ack) — default deployments get zero
change and no crash-loss window. This PR adds the capability + proves it +
measures it; wiring the volatile profile onto WriteBehind (retiring the raw
direct authority) is a separate follow-on.

WriteBehind: a mutation whose durable delta is NOT recoverability-critical
returns Ok immediately after enqueue (background flush); a critical
transition (is_recoverability_critical = is_blocked() || is_terminal(),
now a production predicate in status.rs the crash suite references) awaits
the ack. Because the journal is a strictly sequential single-writer,
awaiting a critical op's ack implies the whole prior async tail is durable
— critical ops are natural barriers, so a run can only ever lose trailing
non-critical churn, never a gate-park or terminal a human/model waits on.

Write-behind robustness (write-through gets these for free):
- Backpressure: bounded pending-un-acked delta window
  (max_pending_write_behind_deltas, default 128); at the cap a non-critical
  op awaits the oldest ack. Bounds memory + the crash-loss window.
- Append-failure halt: the flusher latches degraded + closes on append
  failure in write-behind (write-through still continues), so no durable
  gap; mutations then fail fast and reads reload from the rolled-back
  durable point. Pre-append cross-store-CAS reservations are disabled in
  write-behind (incompatible: a critical op's reservation can't find rows
  the async ops never wrote synchronously).
- Cache safety: a rejected mutation rebuilds the engine from the cached
  (unflushed-inclusive) snapshot instead of reload-from-durable, so acked
  non-critical ops the caller was told succeeded aren't silently dropped.
- Journal abort-on-drop so a 'crashed' store can't flush its queued tail
  post-crash and race a store reopened over the same backend.

Crash suite parameterized over both modes (19 tests, 0 ignored):
WriteThrough keeps the strict acked-durable projection diff; WriteBehind
keeps assert_recoverability_critical_survives strict (gate-park + terminal
never lost) and replaces the diff with a legal-prefix check PLUS an
anti-cheat convergence test — fork the durable bytes before K acked
non-critical ops, recover, verify prefix, then re-apply the K lost ops and
assert convergence to the model (loss = redoable work, not corruption).
Barrier, append-failure-halt/degrade/recover, and backpressure tests added.

Measured (store-isolated per-transition, InMemoryBackend): WriteBehind
non-critical writes ~45-190us (vs WriteThrough ~2ms c=1 / ~20ms c=32
durable-ack floor — 10-400x), approaching the direct authority; critical
transitions stay synchronously durable in both.

Follow-on: graceful drain-before-shutdown when the profile is wired onto
WriteBehind (abort-on-drop currently discards the un-flushed tail at drop).

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

ironloopai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: d3810e0b1fddd5d673ed9004e7322c27366fd92b
Result: 1/1 reviewers completed without blocking findings.
Next: Ready for normal human review and CI checks.
Updated: 2026-07-20T05:57:05.113Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Approved 0 blocking findings / 2 notes 2026-07-20T05:57:05.098Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Approved; 0 blocking findings; No blocking correctness, durability, or concurrency defect was identified in the exact PR diff. Two non-blocking corrections are needed in the committed benchmark report, which is…
Recent activity
Time Reviewer State Detail
2026-07-20T05:24:56.359Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
2026-07-20T05:42:37.293Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (c12c7b1).
2026-07-20T05:47:53.912Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head d3810e0.
2026-07-20T05:47:53.912Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-20T05:47:54.357Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-20T05:47:56.844Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 4461ce8.
2026-07-20T05:57:05.098Z ironloop/common-reviewer (reviewer) Result captured Approved; 0 blocking findings.
2026-07-20T05:57:05.098Z 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-6298 July 20, 2026 01:57 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added configurable turn-state durability policy (Write Through vs Write Behind) with a bounded cap for pending non-critical deltas.
    • Added drain to flush pending Write Behind acknowledgements, plus public policy configuration and related helpers.
  • Bug Fixes
    • Improved read-your-writes and durable-read correctness under Write Behind, including degraded journal and append-failure behavior with clearer criticality handling.
  • Tests
    • Expanded crash-consistency, concurrency, migration, and graceful-shutdown coverage across durability modes.
  • Chores
    • Updated git ignore rules to avoid tracking vendored frontend node_modules.

Walkthrough

Filesystem turn-state storage now supports configurable write-through or write-behind durability. Critical transitions remain durable barriers, non-critical acknowledgements use bounded deferral, journal failures enter degraded mode, runtime shutdown drains pending work, and tests cover both policies.

Changes

Turn-state durability

Layer / File(s) Summary
Durability policy and criticality contract
crates/ironclaw_turns/src/status.rs, crates/ironclaw_turns/src/memory/mod.rs, crates/ironclaw_turns/src/filesystem_store/..., crates/ironclaw_turns/src/lib.rs
Adds durability policies, recoverability-critical classification, public APIs, and configurable pending write-behind limits.
Journal write-behind failure handling
crates/ironclaw_turns/src/filesystem_store/row_store/journal.rs
Tracks degraded state, halts queued requests after append failure, reports halted acknowledgements, and stops background tasks on drop.
Store commit routing and read consistency
crates/ironclaw_turns/src/filesystem_store/row_store.rs, crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs
Defers non-critical acknowledgements, preserves critical barriers, adds durable-read barriers, and updates cache and mutation handling.
Policy-aware crash recovery validation
crates/ironclaw_turns/tests/row_store_crash_consistency.rs
Validates both policies across recovery, append failures, replay convergence, cancellation, read-your-writes, and backpressure scenarios.
Runtime integration and migration coverage
crates/ironclaw_reborn_composition/src/..., crates/ironclaw_reborn_composition/tests/runtime.rs, crates/ironclaw_turns/tests/filesystem_turn_state_contract.rs, .gitignore
Uses filesystem row stores in runtime composition, drains during shutdown, verifies turn completion and snapshot migration, and ignores vendored frontend dependencies.

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

Sequence Diagram(s)

sequenceDiagram
  participant TurnMutation
  participant FilesystemTurnStateRowStore
  participant DeltaJournal
  participant DurableStorage
  participant RebornRuntime
  TurnMutation->>FilesystemTurnStateRowStore: submit delta
  FilesystemTurnStateRowStore->>DeltaJournal: enqueue delta
  DeltaJournal->>DurableStorage: append batch
  DurableStorage-->>DeltaJournal: durable acknowledgement
  FilesystemTurnStateRowStore-->>TurnMutation: barrier or deferred acknowledgement
  RebornRuntime->>FilesystemTurnStateRowStore: drain pending acknowledgements
  FilesystemTurnStateRowStore-->>RebornRuntime: drain result
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 useful, but it omits most required template sections and the Linked Issue, Validation, and trust-boundary checklist fields. Rewrite the PR body to match the template: add Summary bullets, Change Type, Linked Issue, Validation, Security Impact, trust-boundary checklist, Database Impact, Blast Radius, Rollback Plan, and Review Track.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is conventional-commits style and accurately summarizes the opt-in write-behind durability work.
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.

@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 an asynchronous WriteBehind durability policy for the FilesystemTurnStateRowStore as an alternative to the default WriteThrough policy. In WriteBehind mode, non-critical transitions return immediately after enqueueing, while recoverability-critical transitions (such as gate-parks and terminal states) act as synchronous durability barriers. The changes also introduce backpressure bounds, append-failure halting, and comprehensive crash-consistency tests. The review feedback highlights a critical performance issue where a mutex lock is held across an expensive I/O operation (DeltaJournal::await_ack) in register_write_behind_commit, which should be optimized by releasing the lock before awaiting. Additionally, it is recommended to use exhaustive enum matching instead of the matches! macro in with_durability_policy and is_write_behind to ensure compile-time safety for future enum variants.

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.

Comment on lines +405 to +440
async fn register_write_behind_commit<T>(
&self,
pending: PendingRowCommit<T>,
) -> Result<T, TurnError> {
let PendingRowCommit {
value,
ack,
active_lock_reservations,
run_row_reservations,
critical: _,
} = pending;
debug_assert!(
active_lock_reservations.is_empty() && run_row_reservations.is_empty(),
"write-behind non-critical commits must not pre-append row reservations",
);
let Some(ack) = ack else {
return Ok(value);
};
let cap = self.limits.max_pending_write_behind_deltas.max(1);
let mut window = self.pending_write_behind.lock().await;
while window.len() >= cap {
let Some(oldest) = window.pop_front() else {
break;
};
if let Err(error) = DeltaJournal::await_ack(Some(oldest)).await {
// The oldest pending append failed; the flusher has halted and
// latched the store degraded. Surface the retryable error and
// roll the hot cache back to the last consistent durable point.
drop(window);
self.clear_snapshot_cache().await;
return Err(error);
}
}
window.push_back(ack);
Ok(value)
}

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.

high

Holding the pending_write_behind lock across the expensive I/O operation DeltaJournal::await_ack inside register_write_behind_commit can serialize concurrent threads and severely degrade throughput under write-behind load.

Following the general rule, we should check the queue length under a brief lock, release the lock to perform the expensive await_ack outside of it, and then re-acquire the lock to push the new ack.

    async fn register_write_behind_commit<T>(
        &self,
        pending: PendingRowCommit<T>,
    ) -> Result<T, TurnError> {
        let PendingRowCommit {
            value,
            ack,
            active_lock_reservations,
            run_row_reservations,
            critical: _,
        } = pending;
        debug_assert!(
            active_lock_reservations.is_empty() && run_row_reservations.is_empty(),
            "write-behind non-critical commits must not pre-append row reservations",
        );
        let Some(ack) = ack else {
            return Ok(value);
        };
        let cap = self.limits.max_pending_write_behind_deltas.max(1);
        loop {
            let mut oldest = None;
            {
                let mut window = self.pending_write_behind.lock().await;
                if window.len() < cap {
                    window.push_back(ack);
                    break;
                }
                oldest = window.pop_front();
            }
            if let Some(oldest_ack) = oldest {
                if let Err(error) = DeltaJournal::await_ack(Some(oldest_ack)).await {
                    // The oldest pending append failed; the flusher has halted and
                    // latched the store degraded. Surface the retryable error and
                    // roll the hot cache back to the last consistent durable point.
                    self.clear_snapshot_cache().await;
                    return Err(error);
                }
            }
        }
        Ok(value)
    }
References
  1. Avoid holding a mutex lock across expensive operations, such as I/O or resource construction, inside cache lookups. Instead, check the cache under a brief lock, perform the expensive build outside the lock, and then re-acquire the lock to insert and deduplicate. This prevents serializing concurrent threads and distorting latency measurements in throughput-sensitive code.

Comment on lines +216 to +223
pub fn with_durability_policy(mut self, durability_policy: TurnStateDurabilityPolicy) -> Self {
self.durability_policy = durability_policy;
self.delta_journal.set_write_behind(matches!(
durability_policy,
TurnStateDurabilityPolicy::WriteBehind
));
self
}

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.

medium

Exhaustive enum matching without wildcards (or the matches! macro) is preferred when classifying enum variants for critical behaviors, ensuring that any future variants trigger a compile-time error rather than silently falling through to a default value.

    pub fn with_durability_policy(mut self, durability_policy: TurnStateDurabilityPolicy) -> Self {
        self.durability_policy = durability_policy;
        let write_behind = match durability_policy {
            TurnStateDurabilityPolicy::WriteThrough => false,
            TurnStateDurabilityPolicy::WriteBehind => true,
        };
        self.delta_journal.set_write_behind(write_behind);
        self
    }
References
  1. Prefer exhaustive enum matching without wildcards (or the matches! macro) when classifying enum variants for critical behaviors, ensuring that any future variants trigger a compile-time error rather than silently falling through to a default value.

Comment on lines +308 to +313
fn is_write_behind(&self) -> bool {
matches!(
self.durability_policy,
TurnStateDurabilityPolicy::WriteBehind
)
}

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.

medium

Exhaustive enum matching without wildcards (or the matches! macro) is preferred when classifying enum variants for critical behaviors, ensuring that any future variants trigger a compile-time error rather than silently falling through to a default value.

    fn is_write_behind(&self) -> bool {
        match self.durability_policy {
            TurnStateDurabilityPolicy::WriteThrough => false,
            TurnStateDurabilityPolicy::WriteBehind => true,
        }
    }
References
  1. Prefer exhaustive enum matching without wildcards (or the matches! macro) when classifying enum variants for critical behaviors, ensuring that any future variants trigger a compile-time error rather than silently falling through to a default value.

@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 3 0 3 c6b39ba3f0d7

Head: c6b39ba3f0d7fe91fb3ac23d5ce8f67bec5e327a
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

Found three write-behind correctness issues: successful non-critical writes are not locally readable yet, running cancellations can be lost across a crash, and the backpressure cap does not bound concurrent enqueues.

Findings

Blocking: 3 / Notes: 0

Blocking findings

1. ❌ [HIGH] Running cancellation requests can be lost after reporting success

Location: crates/ironclaw_turns/src/status.rs:76
request_cancel transitions a running run to CancelRequested, which this predicate classifies as non-critical. Write-behind therefore returns success before the cancel state and its idempotency record are durable. A crash in that window recovers the prior Running/Queued state and may execute work that the caller successfully cancelled. Treat CancelRequested as a durability barrier (or override this operation) and add a crash test for cancelling a running run.

2. ❌ [MEDIUM] Write-behind violates read-your-writes for run-state reads

Location: crates/ironclaw_turns/src/filesystem_store/row_store.rs:392
A non-critical submit/claim now returns here before its journal append. However, TurnStateStore::get_run_state still reads and materializes only durable rows, so an immediate same-store read can return ScopeNotFound (or the previous status) until the flusher runs. Route local reads through the hot snapshot while healthy, or explicitly await the relevant ack before exposing these writes; add an immediate submit→get-state test.

3. ❌ [MEDIUM] The backpressure cap is applied after unbounded enqueue

Location: crates/ironclaw_turns/src/filesystem_store/row_store.rs:424
The capacity check occurs after apply has already enqueued the delta into DeltaJournal's unbounded channel. With a stalled flusher and cap=1, one caller waits here, but concurrent callers can each acquire snapshot_state, enqueue another delta, then block on this mutex. The queued deltas can therefore grow without bound and are all lost on a crash, contrary to the advertised memory/crash-window bound. Reserve or await capacity before enqueueing, and test concurrent writers with a stalled append.

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.

Comment thread crates/ironclaw_turns/src/status.rs Outdated
/// and the crash-consistency suite references THIS function (not a copy) as the
/// single boundary write-behind flips.
pub fn is_recoverability_critical(status: TurnStatus) -> bool {
status.is_blocked() || status.is_terminal()

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.

CancelRequested is non-critical here, so a successful cancellation of a running run can be dropped before the write-behind append. After restart the older Running/Queued state can execute despite the acknowledged cancel. Make this transition a durability barrier and add a crash test.

// No durable delta (no-op / empty commit): nothing to flush.
return Ok(pending.value);
}
if self.write_behind_async(pending.critical) {

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 returns a non-critical submit/claim before the append, but get_run_state still reads only durable rows. An immediate same-store get can therefore report ScopeNotFound or stale status until the flusher runs. Preserve local read-your-writes or test/document an intentional consistency boundary.

return Ok(value);
};
let cap = self.limits.max_pending_write_behind_deltas.max(1);
let mut window = self.pending_write_behind.lock().await;

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 cap is checked only after the delta was put on the unbounded journal channel. While one caller waits for an old ack, concurrent callers can still enqueue their deltas before blocking on this mutex, so memory and the actual queued tail are not bounded. Gate capacity before enqueueing.

@railway-app

railway-app Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw 🕒 Building (View Logs) Web Jul 20, 2026 at 5:47 am

@github-actions

github-actions Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 86.21% (319799 / 370944 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 370944 lines now vs 320188 at floor capture (+50756 lines, +15.85%) — 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): 86.21% — 319799 / 370944 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.72% 2096 / 2768
ironclaw_projects 76.48% 400 / 523
ironclaw_reborn_cli 77% 10247 / 13307
ironclaw_llm 78.36% 20306 / 25915
ironclaw_product_context 78.57% 11 / 14
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_run_state 85.61% 458 / 535
ironclaw_channel_delivery 85.79% 1383 / 1612
ironclaw_common 86.13% 1714 / 1990
ironclaw_threads 86.93% 4708 / 5416
ironclaw_turns 86.96% 14936 / 17175
ironclaw_slack_v2_adapter 87.3% 1491 / 1708
ironclaw_skills 87.6% 4471 / 5104
ironclaw_product_adapter_registry 88.06% 531 / 603
ironclaw_product_adapters 88.1% 3384 / 3841
ironclaw_reborn_traces 88.2% 11946 / 13544
ironclaw_hooks 88.35% 10075 / 11404
ironclaw_host_api 88.65% 4389 / 4951
ironclaw_host_runtime 88.71% 18120 / 20426
ironclaw_reborn_openai_compat 89.21% 3778 / 4235
ironclaw_webui 89.33% 7700 / 8620
ironclaw_extensions 89.38% 2971 / 3324
ironclaw_runner 89.65% 17453 / 19469
ironclaw_telegram_v2_adapter 89.7% 2717 / 3029
ironclaw_reborn_composition 90.09% 74858 / 83091
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% 15997 / 17336
ironclaw_attachments 93.06% 630 / 677
ironclaw_agent_loop 94.95% 9416 / 9917
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

All three are write-behind-only; WriteThrough is unaffected. The crash suite
(now 22 tests) passes in both modes; each fix ships a regression test verified
to fail before the fix.

f1 [HIGH] — CancelRequested must be a durability barrier. `request_cancel`
reports success once the transition commits, so a write-behind crash that
reverts a Running run's acked CancelRequested to Running re-executes work the
caller was told was cancelled (and drops its idempotency record). Add
`CancelRequested` to `is_recoverability_critical` (the single boundary the row
store and the crash-suite oracle both key on), so the cancel awaits its durable
ack. Test: `write_behind_cancel_of_running_run_survives_crash` (without the fix
the whole un-flushed tail is lost → ScopeNotFound).

f2 [MED] — read-your-writes. `get_run_state` read only durable rows, but a
non-critical write-behind submit returns Ok after updating the hot snapshot and
before its durable append, so an immediate same-store read returned
ScopeNotFound/stale. Serve `get_run_state` from the hot snapshot under healthy
write-behind (the single-writer authority); write-through and the degraded path
still read durable. Renames `read_run_state_for_cancellation` →
`read_run_state_from_hot_cache` (it was never cancellation-specific). Test:
`write_behind_get_run_state_reflects_unflushed_submit`.

f3 [MED] — backpressure was applied AFTER an unbounded enqueue: concurrent
callers each enqueued into the journal channel, then serialized on the pending
window, so a stalled flusher let the channel grow without bound. Reserve the
pending-window slot and track the ack BEFORE/around the enqueue, both inside
`apply` under the `snapshot_state` lock that serializes enqueue, so the channel
can never exceed the cap. `register_write_behind_commit` is removed;
`commit_pending` short-circuits on the now-`None` ack. Test:
`write_behind_concurrent_writers_under_cap_stay_consistent` exercises the
concurrent reserve→enqueue→track path (the strict peak-depth bound is structural
— the journal channel length is not externally observable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 02:33 Destroyed
@ilblackdragon

Copy link
Copy Markdown
Member Author

Addressed all three IronLoop blocking findings (7d5be7041). All write-behind-only; WriteThrough is byte-for-byte unchanged. The crash suite is now 22 tests, 0 ignored, green in both modes, and each fix ships a regression test I verified fails before the fix.

f1 [HIGH] — CancelRequested is now a durability barrier. Added it to is_recoverability_critical (status.rs) — the single boundary the row store's write-through/write-behind decision and the crash-suite oracle both key on, so request_cancel now awaits its durable ack and a crash can't revert an acked cancel to Running and re-execute cancelled work. Test write_behind_cancel_of_running_run_survives_crash (submit→claim→cancel→crash→recover asserts CancelRequested; reverting the predicate → the un-flushed tail is lost, ScopeNotFound).

f2 [MED] — read-your-writes. get_run_state now serves from the hot snapshot under healthy write-behind (the single-writer authority); write-through and the degraded path still read durable rows. Renamed read_run_state_for_cancellation → read_run_state_from_hot_cache (it was never cancellation-specific). Test write_behind_get_run_state_reflects_unflushed_submit (reverting → ScopeNotFound).

f3 [MED] — backpressure before enqueue. The pending-window slot is now reserved, and the ack tracked, before/around the journal enqueue — both inside apply under the snapshot_state lock that serializes enqueue — so concurrent callers can never grow the journal channel past the cap while a flush is in flight (the flusher drains independently of that lock, so awaiting the oldest ack there is backpressure, not deadlock). register_write_behind_commit is removed; commit_pending short-circuits on the now-None ack. Test write_behind_concurrent_writers_under_cap_stay_consistent exercises the concurrent reserve→enqueue→track path.

One honesty note on f3: the strict peak-depth bound is enforced structurally (reserve-before-enqueue under the serializing lock) rather than asserted by the test — the journal channel length isn't externally observable without a test-only introspection hook, which I didn't add. Happy to add one if you'd prefer a depth assertion. Ready for merge once CI greens.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs (1)

373-411: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

put_loop_checkpoint's ack bypasses the write-behind deferral path — triggers the commit_pending debug_assert and defeats the documented lazy-flush design.

Unlike apply/apply_with_targeted_delta, this path never calls reserve_write_behind_slot() before enqueue nor routes the resulting ack through track_write_behind_ack_if_async(). It passes the raw Some(ack) straight into PendingRowCommit { critical: false, .. } and calls commit_pending.

Under WriteBehind, commit_pending sees ack.is_some() and critical == false, so write_behind_async(false) is true, and the guard fires:

debug_assert!(
    !self.write_behind_async(pending.critical),
    "non-critical write-behind commit must be tracked in apply, not awaited",
);

This is debug_assert!(false, ...) — panics in debug/test builds. In release builds it silently falls through to a synchronous await, contradicting the same function's own comment: "under write-behind it lazy-flushes." Comments/guarantees must match the code — as per coding guidelines, "Comments and documentation that promise guarantees must match the code and tests."

🐛 Proposed fix: route through the same reserve/track pattern as apply()
         let ack = {
             let mut guard = self.snapshot_state.lock().await;
+            if self.write_behind_async(false)
+                && let Err(error) = self.reserve_write_behind_slot().await
+            {
+                *guard = None;
+                return Err(error);
+            }
             let ack = self
                 .enqueue_delta(row_store_durable_delta(delta.clone()))
                 .map_err(|error| match error {
                     RowPersistError::Turn(error) => error,
                 })?;
             if let Some(state) = guard.as_mut()
                 && let Err(error) = state.apply_delta(delta, state.journal_seq)
             {
                 *guard = None;
                 return Err(error);
             }
-            ack
+            self.track_write_behind_ack_if_async(false, ack).await
         };
🤖 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 `@crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs` around lines
373 - 411, Update put_loop_checkpoint to follow the
apply/apply_with_targeted_delta write-behind flow: reserve a write-behind slot
before enqueueing the delta, then pass the resulting acknowledgment through
track_write_behind_ack_if_async before constructing PendingRowCommit. Preserve
critical: false so non-critical checkpoints remain lazily flushed without
triggering commit_pending’s assertion.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs`:
- Around line 373-411: Update put_loop_checkpoint to follow the
apply/apply_with_targeted_delta write-behind flow: reserve a write-behind slot
before enqueueing the delta, then pass the resulting acknowledgment through
track_write_behind_ack_if_async before constructing PendingRowCommit. Preserve
critical: false so non-critical checkpoints remain lazily flushed without
triggering commit_pending’s assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ba4c596b-6a04-47d2-8bfd-f141547191a3

📥 Commits

Reviewing files that changed from the base of the PR and between c6b39ba and 7d5be70.

📒 Files selected for processing (4)
  • crates/ironclaw_turns/src/filesystem_store/row_store.rs
  • crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs
  • crates/ironclaw_turns/src/status.rs
  • crates/ironclaw_turns/tests/row_store_crash_consistency.rs

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ilblackdragon

Copy link
Copy Markdown
Member Author

✅ Ready for merge. CI is fully green (59 pass, 0 fail) on the fix head 7d5be70415e7.

All three IronLoop blocking findings are fixed (7d5be7041), each with a regression test I verified fails before the fix:

  • f1 [HIGH] — CancelRequested is now a durability barrier (is_recoverability_critical); an acked cancel of a Running run survives a write-behind crash instead of reverting to Running.
  • f2 [MED] — read-your-writes: get_run_state serves from the hot snapshot under healthy write-behind.
  • f3 [MED] — backpressure reserved before the journal enqueue (under the serializing lock), so a stalled flusher can't grow the channel unbounded under concurrency.

Crash suite now 22 tests / 0 ignored, green in both WriteThrough and WriteBehind; ironclaw_turns clippy clean (all-features + default); WriteThrough byte-for-byte unchanged. Re-ran @ironloopai review for a fresh verdict on the fix head.

@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 7d5be70415e7

Head: 7d5be70415e72b7490af8783ec585afa5ac697bf
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

Write-behind checkpoints panic in debug builds and do not take the intended async path in release builds.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [MEDIUM] Write-behind checkpoints trip a debug assertion

Location: crates/ironclaw_turns/src/filesystem_store/row_store.rs:408-410
put_loop_checkpoint supplies critical: false with a live ack directly to commit_pending, without reserving/tracking that ack through track_write_behind_ack_if_async. Under WriteBehind, this assertion fires on every checkpoint in debug/test builds; in release it instead waits synchronously, contradicting the intended path. The runner checkpoint adapter calls this method for normal checkpoints. Either keep checkpoints synchronous by marking them critical, or implement the same reserve/enqueue/track flow and read semantics as other async commits; add a WriteBehind checkpoint test.

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.

// A durable ack is present ⇒ write-through, or a critical write-behind
// barrier: await it. A non-critical write-behind commit never reaches
// here — `track_write_behind_ack_if_async` returned `ack: None` above.
debug_assert!(

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.

put_loop_checkpoint reaches this with critical: false and ack: Some(_), but it never calls track_write_behind_ack_if_async. That panics in debug builds under WriteBehind (and blocks synchronously in release). Please either keep checkpoint writes synchronous/critical or route them through the reserved+tracked async path, with a WriteBehind checkpoint regression test.

…path (#6298 IronLoop f4)

The f3 backpressure fix added a `commit_pending` debug assertion that a
non-critical write-behind commit is never awaited there — it must be
reserved+tracked in `apply`. But `put_loop_checkpoint` enqueues under
`snapshot_state` and hands a live ack straight to `commit_pending` with
`critical: false`, bypassing that flow. Under WriteBehind it therefore tripped
the assertion in debug/test builds, and in release waited synchronously —
contradicting the intended lazy flush for a non-critical checkpoint (and leaving
the same unbounded-channel gap f3 closed elsewhere).

Give the checkpoint enqueue the same reserve→enqueue→track flow every other
async commit uses: reserve the pending-window slot before enqueue (under
`snapshot_state`), then `track_write_behind_ack_if_async` returns `None` so
`commit_pending` lazy-flushes. WriteThrough awaits the ack unchanged.

Regression: `write_behind_put_loop_checkpoint_takes_async_path` drives the real
`put_loop_checkpoint` under WriteBehind — verified it panics on the assertion
before this fix. Crash suite now 23 tests / 0 ignored, green in both modes;
clippy clean (all-features + default).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 03:19 Destroyed
@github-actions github-actions Bot added size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Jul 20, 2026

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs (1)

217-224: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the durable-row fallback for write-through and degraded modes.

The AI summary claims get_run_state_for_cancellation conditionally reads from the hot cache, but the implementation unconditionally reads from it. In WriteThrough mode or when the journal degrades (clearing the cache), this will incorrectly fail with ScopeNotFound instead of falling back to durable rows, which breaks cancellations. Match the conditional logic used in get_run_state.

🐛 Proposed fix
     async fn get_run_state_for_cancellation(
         &self,
         request: GetRunStateRequest,
     ) -> Result<TurnRunState, TurnError> {
-        self.read_run_state_from_hot_cache(&request)
-            .await?
-            .ok_or(TurnError::ScopeNotFound)
+        let state = if self.is_write_behind_healthy() {
+            self.read_run_state_from_hot_cache(&request).await?
+        } else {
+            self.read_run_state_from_durable_rows(&request).await?
+        };
+        state.ok_or(TurnError::ScopeNotFound)
     }
🤖 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 `@crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs` around lines
217 - 224, Update get_run_state_for_cancellation to match the conditional
cache-and-durable-row lookup logic used by get_run_state, including fallback to
durable rows in WriteThrough mode and when the journal has degraded or cleared
the hot cache; preserve TurnError::ScopeNotFound only when neither source
contains the run state.
🤖 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_turns/src/filesystem_store/row_store/traits.rs`:
- Around line 393-398: In the write-behind reservation failure branch, remove
the cache invalidation assignment to `guard` and propagate the error from
`reserve_write_behind_slot().await` directly. Keep the existing backpressure
check and `Err(error)` return behavior unchanged.

---

Outside diff comments:
In `@crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs`:
- Around line 217-224: Update get_run_state_for_cancellation to match the
conditional cache-and-durable-row lookup logic used by get_run_state, including
fallback to durable rows in WriteThrough mode and when the journal has degraded
or cleared the hot cache; preserve TurnError::ScopeNotFound only when neither
source contains the run state.
🪄 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: fb6948e0-5686-4b24-86f1-6eefc50bf136

📥 Commits

Reviewing files that changed from the base of the PR and between 7d5be70 and 036c658.

📒 Files selected for processing (2)
  • crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs
  • crates/ironclaw_turns/tests/row_store_crash_consistency.rs

Comment on lines +393 to +398
if self.write_behind_async(CHECKPOINT_CRITICAL)
&& let Err(error) = self.reserve_write_behind_slot().await
{
*guard = None;
return Err(error);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Remove cache invalidation on backpressure reservation failure.

Under write-behind, reservation failure acts as a backpressure mechanism. Destroying the hot cache (*guard = None) before any mutation has occurred forces subsequent operations to rebuild from durable rows. This creates a severe negative performance feedback loop precisely when the system is under heavy load. Propagate the error without invalidating the cache.

⚡ Proposed fix
                 if self.write_behind_async(CHECKPOINT_CRITICAL)
                     && let Err(error) = self.reserve_write_behind_slot().await
                 {
-                    *guard = None;
                     return Err(error);
                 }
📝 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
if self.write_behind_async(CHECKPOINT_CRITICAL)
&& let Err(error) = self.reserve_write_behind_slot().await
{
*guard = None;
return Err(error);
}
if self.write_behind_async(CHECKPOINT_CRITICAL)
&& let Err(error) = self.reserve_write_behind_slot().await
{
return Err(error);
}
🤖 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 `@crates/ironclaw_turns/src/filesystem_store/row_store/traits.rs` around lines
393 - 398, In the write-behind reservation failure branch, remove the cache
invalidation assignment to `guard` and propagate the error from
`reserve_write_behind_slot().await` directly. Keep the existing backpressure
check and `Err(error)` return behavior unchanged.

@ilblackdragon

Copy link
Copy Markdown
Member Author

Addressed the re-review's new finding (036c6586d).

f4 [MED] — put_loop_checkpoint tripped the write-behind assertion. The f3 backpressure fix added a commit_pending debug-assert that a non-critical write-behind commit is never awaited there (it must be reserved+tracked in apply). But put_loop_checkpoint enqueues under snapshot_state and handed a live ack straight to commit_pending with critical: false, bypassing that flow — so under WriteBehind it tripped the assert in debug/test and, in release, waited synchronously (the opposite of the intended lazy flush, and the same unbounded-channel gap f3 closes elsewhere).

Fix: give the checkpoint enqueue the same reserve→enqueue→track flow every other async commit uses (reserve the slot before enqueue under snapshot_state; track_write_behind_ack_if_async returns None so commit_pending lazy-flushes). WriteThrough awaits unchanged.

Regression: write_behind_put_loop_checkpoint_takes_async_path drives the real put_loop_checkpoint under WriteBehind — verified it panics on the assertion before this fix. Crash suite now 23 tests / 0 ignored, green in both modes; clippy clean (all-features + default). Re-running @ironloopai review.

@ironloopai review

Import reordering only — the f4 regression-test imports were added out of sorted
order and tripped the Formatting / Code Style lane. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 03:39 Destroyed
…ry paths (#6263 Step 3)

The runtime does submit_turn -> get_run_state, but get_run_state (and
read_turn_events_after / get_loop_checkpoint) read materialized durable
rows. Under WriteBehind a submit's delta materializes asynchronously, so
the immediate read saw stale/missing rows and returned ScopeNotFound —
every runtime turn would fail. (WriteThrough was unaffected: durable ==
cache there.) The crash suite never caught it because it only ever read
AFTER recovery, never live after an async write.

The naive fix (serve reads from the hot cache) is WRONG and loses data:
the cache is a bounded window, not a superset of durable state —
terminal runs/events are evicted (max_terminal_records / max_events)
while durable rows retain them, and cross-writer freshness needs the
durable read. Four WriteThrough contract tests pin exactly this.

Fix: a read-side barrier. flush_pending_write_behind_for_read() drains
the enqueued-but-un-acked non-critical pending window (awaiting those
durable appends) at the top of the three durable-read helpers, so durable
rows catch up to the just-acked writes BEFORE the read — read-your-writes
consistent, while the durable read keeps its exact eviction /
cross-writer-freshness / scope / cursor / retention semantics. No-op
under WriteThrough (window always empty), so that path is byte-for-byte
unchanged. get_run_state_for_cancellation and get_run_record already read
the cache and were WB-safe; traits.rs untouched.

Coverage gap closed: live read-your-writes tests in BOTH modes (submit ->
immediate get_run_state/get_run_record/read_turn_events_after,
put -> get_loop_checkpoint) plus a property test asserting live
get_run_state tracks the model after every op. Reverting only the source
reproduces model=Queued vs store=ScopeNotFound under WriteBehind.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 04:01 Destroyed

@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_turns/src/filesystem_store/row_store.rs`:
- Around line 458-502: The read barrier in flush_pending_write_behind_for_read
must be shared by concurrent readers rather than allowing each reader to drain
pending_write_behind independently. Add synchronization so one reader performs
the drain and await, while concurrent readers wait for that same barrier result
before proceeding to durable reads; preserve the existing error handling that
clears snapshot cache and returns the retryable error.
🪄 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: 796e2b2f-f643-4083-b842-6edad4e23b13

📥 Commits

Reviewing files that changed from the base of the PR and between cf8f390 and 44ba506.

📒 Files selected for processing (2)
  • crates/ironclaw_turns/src/filesystem_store/row_store.rs
  • crates/ironclaw_turns/tests/row_store_crash_consistency.rs

Comment thread crates/ironclaw_turns/src/filesystem_store/row_store.rs
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 04:58 Destroyed
@github-actions github-actions Bot added the scope: docs Documentation label Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 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
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

1 similar comment
@coderabbitai

coderabbitai Bot commented Jul 20, 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
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

#6298 f5/f6)

f5 [HIGH] — `BeforeSideEffect` checkpoints must be synchronous. f4 made ALL
`put_loop_checkpoint` writes async under write-behind, but a `BeforeSideEffect`
checkpoint is written immediately before a capability's external side effect,
and expired-lease recovery treats the ABSENCE of a durable checkpoint as proof
no side effect ran (and requeues). If it lazy-flushed, a crash after the
capability ran but before the flush would replay a non-idempotent side effect.
Key the checkpoint's criticality on its kind: `BeforeSideEffect` → critical
(synchronous, durable before the capability executes); other kinds stay async
(a gate-park's checkpoint is flushed by the block transition's barrier; a lost
BeforeModel is redoable). Test:
`write_behind_before_side_effect_checkpoint_survives_crash`.

f6 [MED] — evicted-but-durable terminals hidden. f2 routed healthy-write-behind
`get_run_state` through the bounded hot snapshot, which evicts OLD TERMINAL runs
while their durable rows persist and must stay queryable (the eviction
contract). Once `max_terminal_records` is exceeded the hot-cache read returned
`ScopeNotFound` for those durable terminals. On a hot-cache miss, flush the
pending write-behind tail (read-your-writes) then fall back to the durable-row
lookup. Test:
`write_behind_get_run_state_finds_evicted_terminal_via_durable_fallback`.

Both verified failing before the fix. Crash suite now 27 tests / 0 ignored,
green in both modes; clippy clean (all-features + default); fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 05:02 Destroyed
@ilblackdragon

Copy link
Copy Markdown
Member Author

Addressed the two new findings (d218e328e, rebased onto your #6305 flip work).

f5 [HIGH] — BeforeSideEffect checkpoints must be synchronous. f4 made all put_loop_checkpoint writes async under write-behind, but a BeforeSideEffect checkpoint is written immediately before a capability's external side effect, and expired-lease recovery treats the absence of a durable checkpoint as proof no side effect ran (and requeues) — so a lazy-flushed one lost to a crash replays a non-idempotent side effect. Fixed by keying criticality on the kind: BeforeSideEffect → critical (synchronous, durable before the capability executes); other kinds stay async (a gate-park's checkpoint is flushed by the block transition's barrier; a lost BeforeModel is redoable). Test write_behind_before_side_effect_checkpoint_survives_crash.

f6 [MED] — evicted-but-durable terminals hidden. f2 routed healthy-write-behind get_run_state through the bounded hot snapshot, which evicts old terminals whose durable rows must stay queryable. On a hot-cache miss it now flushes the pending tail (read-your-writes) and falls back to the durable-row lookup. Test write_behind_get_run_state_finds_evicted_terminal_via_durable_fallback.

Both verified failing before the fix. Crash suite 28/0-ignored green in both modes; clippy clean (all-features + default); fmt clean.

@ironloopai review

@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 d218e328e45c

Head: d218e328e45c669262403557ab9514127963fe1d
Reason: The generated/vendor payload dominates the diff and prevents an auditable full review of all changed content.
Next: Remove or split the generated frontend node_modules payload into a separately auditable change, then rerun review on the focused source diff.

Run details

Status: Current
Trustworthy review produced: no

Summary

Skipped: the supplied base-to-head comparison is vendor/generated-heavy (4,079 files and 1,385,483 additions; 4,058 paths are frontend node_modules), so a reliable complete review is not feasible within scope.

@coderabbitai

coderabbitai Bot commented Jul 20, 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
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

1 similar comment
@coderabbitai

coderabbitai Bot commented Jul 20, 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
Validation Failed: {"resource":"IssueComment","code":"custom","field":"body","message":"body is too long (maximum is 65536 characters)"} - https://docs.github.com/rest/issues/comments#update-an-issue-comment

@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: 2

🤖 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_reborn_composition/src/runtime.rs`:
- Around line 2566-2575: In RebornRuntime::shutdown, change the turn_state_flush
drain failure log from warn! to debug!, preserving the existing %error field and
message. Keep the diagnostic internal and consistent with the nearby
wait_for_terminal_or_gate logging rule for REPL/TUI-reachable runtime paths.

In `@crates/ironclaw_reborn_composition/tests/runtime.rs`:
- Around line 303-312: Update the assertion message and shutdown comment near
the turn completion check to refer to the store’s WriteThrough policy, not
WriteBehind. Describe shutdown as using the WriteThrough default and its no-op
drain, while preserving the existing assertion and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

Comment on lines 2566 to 2575
#[cfg(feature = "inmemory-turn-state")]
if let Some(turn_state) = &self.turn_state_flush {
turn_state.flush().await;
if let Some(turn_state) = &self.turn_state_flush
&& let Err(error) = turn_state.drain().await
{
tracing::warn!(
%error,
"turn-state WriteBehind drain failed during graceful shutdown; the un-acked \
non-critical tail may not be durable on restart"
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

warn! on the shutdown-drain path contradicts the REPL/TUI logging rule.

RebornRuntime::shutdown is REPL/TUI-reachable, and this same impl block already chose debug! over warn! for exactly this reason (wait_for_terminal_or_gate, ~Line 2745: "debug! not warn! per the logging rule — this runtime is REPL/TUI-reachable"). A degraded-drain diagnostic is internal, not intentionally-rendered user status, so prefer debug! here for consistency.

As per path instructions: "REPL/TUI logging: info!/warn! corrupt the terminal UI — internal diagnostics use debug!".

Proposed change
-            tracing::warn!(
+            tracing::debug!(
                 %error,
                 "turn-state WriteBehind drain failed during graceful shutdown; the un-acked \
                  non-critical tail may not be durable on restart"
             );
📝 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
#[cfg(feature = "inmemory-turn-state")]
if let Some(turn_state) = &self.turn_state_flush {
turn_state.flush().await;
if let Some(turn_state) = &self.turn_state_flush
&& let Err(error) = turn_state.drain().await
{
tracing::warn!(
%error,
"turn-state WriteBehind drain failed during graceful shutdown; the un-acked \
non-critical tail may not be durable on restart"
);
}
#[cfg(feature = "inmemory-turn-state")]
if let Some(turn_state) = &self.turn_state_flush
&& let Err(error) = turn_state.drain().await
{
tracing::debug!(
%error,
"turn-state WriteBehind drain failed during graceful shutdown; the un-acked \
non-critical tail may not be durable on restart"
);
}
🤖 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 `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 2566 - 2575,
In RebornRuntime::shutdown, change the turn_state_flush drain failure log from
warn! to debug!, preserving the existing %error field and message. Keep the
diagnostic internal and consistent with the nearby wait_for_terminal_or_gate
logging rule for REPL/TUI-reachable runtime paths.

Source: Path instructions

Comment on lines +303 to +312
assert_eq!(
reply.status,
TurnStatus::Completed,
"turn must complete over the WriteBehind store, got {:?} ({:?})",
reply.status,
reply.failure_category
);

// Graceful shutdown drains the WriteBehind tail through
// `FilesystemTurnStateStoreKind::drain`; a broken drain wiring surfaces here.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assertion message and comment say "WriteBehind" but this store ships WriteThrough.

The test's own doc comment states the profile ships the row store at the WriteThrough default and the drain is a no-op here, yet the assert message ("turn must complete over the WriteBehind store") and the shutdown comment ("drains the WriteBehind tail") claim otherwise. Align the wording with the actual policy so a future reader isn't misled about what is exercised.

🤖 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 `@crates/ironclaw_reborn_composition/tests/runtime.rs` around lines 303 - 312,
Update the assertion message and shutdown comment near the turn completion check
to refer to the store’s WriteThrough policy, not WriteBehind. Describe shutdown
as using the WriteThrough default and its no-op drain, while preserving the
existing assertion and behavior.

…6298/#6305 IronLoop)

`crates/ironclaw_webui_v2/frontend/node_modules` was committed with no source,
manifest, lockfile, or Cargo.toml — the directory holds *only* vendored deps and
the crate is not even a workspace member. The 4,058-file / ~1.38M-line payload
dominated the diff and caused IronLoop to decline the review as unauditable.

Remove all 4,058 tracked node_modules files (working tree untouched) and add a
`.gitignore` rule so it can't be re-added. No source or build change: nothing
references or builds this vendored tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 05:17 Destroyed
@ilblackdragon

Copy link
Copy Markdown
Member Author

Removed the vendored payload that made IronLoop decline the review (9c91db450).

crates/ironclaw_webui_v2/frontend/node_modules had been committed with no source, manifest, lockfile, or Cargo.toml — the directory held only vendored deps and the crate isn't even a workspace member, so nothing references or builds it. Deleted all 4,058 tracked files (−1,383,176 lines; working tree untouched) and added a .gitignore rule so it can't return. The source diff is now auditable.

For reference, all six write-behind findings from earlier heads are fixed with failing-first regression tests (crash suite 28/0-ignored, both modes): f1 CancelRequested-critical, f2 read-your-writes, f3 backpressure-before-enqueue, f4 put_loop_checkpoint async path, f5 BeforeSideEffect-checkpoint-critical, f6 evicted-terminal durable fallback.

@ironloopai review

@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 9c91db450b2a

Head: 9c91db450b2a6275e03eacfa29d0ff5e15f4c660
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

Write-behind backpressure can drop tracking for an already-successful mutation when the flusher stalls, defeating the pending-window bound and graceful drain guarantee.

Findings

Blocking: 1 / Notes: 0

Blocking findings

1. ❌ [HIGH] Preserve pending acknowledgements when backpressure times out

Location: crates/ironclaw_turns/src/filesystem_store/row_store.rs:457-460
pop_front() removes the oldest successful write's ack before awaiting it, but this call runs inside apply's 15-second outer timeout. If the flusher is stalled, that timeout cancels this future and drops the ack permanently. Later writes see an empty window and can enqueue behind the stalled append, so the channel/loss window is again unbounded; drain() can also return Ok while those acknowledged writes are still in flight, and dropping the store then loses them. Keep the ack tracked until it resolves (including cancellation/timeout paths), and add a stalled-flusher regression that verifies drain does not falsely succeed.

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.

let cap = self.limits.max_pending_write_behind_deltas.max(1);
let mut window = self.pending_write_behind.lock().await;
while window.len() >= cap {
let Some(oldest) = window.pop_front() else {

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.

pop_front() removes the ack before an await enclosed by apply's outer timeout. If the flusher is stalled, cancellation drops this ack permanently; later writes bypass the cap and drain() can falsely report success before an already-acknowledged write is durable. Preserve/reinsert the ack until it resolves, including timeout paths.

Vendored node_modules trees kept getting accidentally committed (the
webui_v2 frontend payload; likely more as frontends land). Replace the
single-path rule with a bare `node_modules/` (no leading slash), which
git matches at ANY depth — so every current and future frontend/tooling
node_modules is ignored, not just the one path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 05:42 Destroyed
…6298 IronLoop f7)

`reserve_write_behind_slot` (backpressure) and `flush_pending_write_behind_for_read`
(read barrier) removed the oldest ack from the pending window BEFORE awaiting it —
`pop_front()` and `drain(..).collect()` respectively. Both run under a caller that
can cancel them (apply's outer timeout; a dropped read future). A cancellation
then dropped the removed ack permanently, so: (1) later writes saw an empty
window and enqueued behind a stalled append — the channel/loss window unbounded
again; and (2) a subsequent flush found nothing and falsely reported success while
the acknowledged write was still un-appended (lost on the next store drop).

Await each ack IN PLACE instead — peek the front, await it by `&mut` (new
`DeltaJournal::await_ack_ref`; a `oneshot::Receiver` is `Future + Unpin`, so
awaiting `&mut` it doesn't consume it), and remove it only once it resolves. A
cancelled await now leaves the ack tracked. The flush holds the window lock across
its awaits (bounded by the pending cap; the journal flusher doesn't take that
lock, so no deadlock), which also keeps its set to the writes present when the
flush began.

Regression: `write_behind_cancelled_flush_preserves_pending_ack` — under a stalled
flusher (new FaultBackend append gate), a timed-out first flush must leave the ack
so a second flush still blocks rather than falsely succeeding. Verified failing
before the fix. Crash suite 29/0-ignored green in both modes; clippy clean
(all-features + default); fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6298 July 20, 2026 05:47 Destroyed
@ilblackdragon

Copy link
Copy Markdown
Member Author

Addressed the HIGH finding (d3810e0b1), and the node_modules .gitignore broadened to a bare node_modules/ so it's caught at any depth (c12c7b11f).

f7 [HIGH] — preserve pending acks across cancellation. reserve_write_behind_slot and flush_pending_write_behind_for_read removed the oldest ack from the window (pop_front() / drain(..)) before awaiting it. Both run under a cancellable caller (apply's outer timeout; a dropped read future), so a cancellation dropped the ack permanently — re-opening the unbounded-channel window and letting a later flush falsely report success while the acknowledged write was still un-appended (lost on the next store drop).

Fix: await each ack in place — peek the front, await it by &mut (new DeltaJournal::await_ack_ref; oneshot::Receiver is Future + Unpin, so awaiting &mut it doesn't consume it), and remove it only once resolved. The flush holds the window lock across its awaits (bounded by the pending cap; the journal flusher doesn't take that lock, so no deadlock).

Regression: write_behind_cancelled_flush_preserves_pending_ack — under a stalled flusher (new FaultBackend append gate), a timed-out first flush must leave the ack so a second flush still blocks, not falsely succeed. Verified failing before the fix. Crash suite 29/0-ignored green both modes; clippy clean (all-features + default); fmt clean.

That's f1–f7 now, each with a failing-first regression test. @ironloopai review

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_turns/src/filesystem_store/row_store.rs (1)

271-280: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Apply the cancellation-safe await pattern to drain().

The #6298 IronLoop f7 cancellation-safety fix was applied to reserve_write_behind_slot and flush_pending_write_behind_for_read, but drain() still pops the ack before awaiting it. If a caller times out while awaiting drain(), the pending ack is dropped and the write-behind guarantee is broken. Use the same front_mut() and await_ack_ref pattern here.

🛠️ Proposed fix
     pub async fn drain(&self) -> Result<(), TurnError> {
         let mut window = self.pending_write_behind.lock().await;
-        while let Some(ack) = window.pop_front() {
-            if let Err(error) = DeltaJournal::await_ack(Some(ack)).await {
+        while let Some(front) = window.front_mut() {
+            let result = DeltaJournal::await_ack_ref(front).await;
+            window.pop_front();
+            if let Err(error) = result {
                 drop(window);
                 self.clear_snapshot_cache().await;
                 return Err(error);
             }
         }
         Ok(())
     }
🤖 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 `@crates/ironclaw_turns/src/filesystem_store/row_store.rs` around lines 271 -
280, Update drain() to await the queue’s front acknowledgment without removing
it first, using front_mut() with DeltaJournal::await_ack_ref. Pop the
acknowledgment only after the await succeeds, preserving the existing
snapshot-cache clearing and error propagation behavior.
🤖 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.

Outside diff comments:
In `@crates/ironclaw_turns/src/filesystem_store/row_store.rs`:
- Around line 271-280: Update drain() to await the queue’s front acknowledgment
without removing it first, using front_mut() with DeltaJournal::await_ack_ref.
Pop the acknowledgment only after the await succeeds, preserving the existing
snapshot-cache clearing and error propagation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3ec56e0d-a398-4d6e-a7c1-51d6b65f4527

📥 Commits

Reviewing files that changed from the base of the PR and between c12c7b1 and d3810e0.

📒 Files selected for processing (3)
  • crates/ironclaw_turns/src/filesystem_store/row_store.rs
  • crates/ironclaw_turns/src/filesystem_store/row_store/journal.rs
  • crates/ironclaw_turns/tests/row_store_crash_consistency.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
✅ Approved 0 2 2 d3810e0b1fdd

Head: d3810e0b1fddd5d673ed9004e7322c27366fd92b
Next: No reviewer action needed.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

No blocking correctness, durability, or concurrency defect was identified in the exact PR diff. Two non-blocking corrections are needed in the committed benchmark report, which is stale and contradicts its accompanying data/current implementation.

Findings

Blocking: 0 / Notes: 2

Non-blocking notes (2)
1. 💬 [LOW] Correct the benchmark's claim that all failures are admission shedding

Location: tools/ironclaw_stress/results/2026-07-20-turn-state-write-behind-libsql/README.md:59-64
The committed JSONL contradicts this conclusion. Write-behind has non-admission failures at c32/c64/c100 (turn_claim_miss, turn_conflict, turn_scope_not_found, and turn_unavailable), so the report should not characterize every failed operation as turn_thread_busy or rule out store failures without investigating those outcomes.

2. 💬 [LOW] Remove the stale WriteBehind read-after-write blocker description

Location: tools/ironclaw_stress/results/2026-07-20-turn-state-write-behind-libsql/README.md:74-84
This is no longer true in this PR: get_run_state now serves the healthy write-behind hot snapshot and only falls back to durable rows after flushing pending writes. Update this report and its production-wiring TODO so they do not state that every WriteBehind turn fails with ScopeNotFound.

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.

Queued, claim = Running) no longer block on the libSQL fsync/ack; only
gate-park + terminal barriers do. The store-tier speedup this flip was meant
to capture is real on the durable backend, not just `InMemoryBackend`.
2. **No livelock, either policy.** Zero `CAS-retries-exhausted`; the row store

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.

The committed JSONL contradicts this conclusion: c32/c64/c100 include turn_claim_miss, turn_conflict, turn_scope_not_found, and (at c100) turn_unavailable, not only turn_thread_busy. Please investigate or qualify these as store/runtime failures rather than reporting all failed operations as expected admission shedding.

store at `WriteThrough`, not `WriteBehind`**, because WriteBehind has a
runtime-breaking read-after-write defect at the store tier:

- `FilesystemTurnStateRowStore::get_run_state` (and the other durable-read query

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 blocker description is stale in this PR. get_run_state now serves the healthy write-behind hot snapshot and falls back after flushing pending writes, so it no longer necessarily returns ScopeNotFound after submit. Please update this report and the related TODO.

@ilblackdragon

Copy link
Copy Markdown
Member Author

✅ Ready for merge. IronLoop Approved (0 blocking) on the current head d3810e0b1; CI green (0 fail; the one pending is the adapters-misc crate bucket finishing).

All seven IronLoop findings across this PR's life are fixed, each with a regression test verified to fail before the fix (crash suite 19 → 29, green in both WriteThrough and WriteBehind):

  • f1 [HIGH] CancelRequested is a durability barrier (no lost-cancel side-effect replay)
  • f2 [MED] read-your-writes: get_run_state serves the healthy-write-behind hot snapshot
  • f3 [MED] backpressure reserved before the journal enqueue (bounded channel)
  • f4 [MED] put_loop_checkpoint routed through the write-behind async path
  • f5 [HIGH] BeforeSideEffect checkpoints kept synchronous (no side-effect replay)
  • f6 [MED] evicted-but-durable terminals stay queryable via a durable fallback
  • f7 [HIGH] pending acks preserved across cancellation (await-in-place; no falsely-succeeding drain)

Plus the accidental webui_v2 node_modules payload removed (−1.38M lines) so the diff is auditable, and .gitignore broadened to node_modules/ at any depth.

The 2 remaining notes are non-blocking LOW items on the tools/ironclaw_stress/results/…/README.md benchmark write-up (its failure-mode characterization, and a stale "every WriteBehind turn fails with ScopeNotFound" blocker line that f2/f6 now make obsolete). I left those to you since they're your stress-test conclusions + production-wiring TODO — happy to update the stale ScopeNotFound line if you'd like.

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6298 — d3810e0b Deployed Jul 20, 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: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant