Skip to content

feat(filesystem): CAS-guarded delete_if_version on RootFilesystem - #5749

Merged
henrypark133 merged 12 commits into
mainfrom
feat/filesystem-delete-if-version
Jul 8, 2026
Merged

henrypark133 merged 12 commits into
mainfrom
feat/filesystem-delete-if-version

Conversation

@henrypark133

@henrypark133 henrypark133 commented Jul 7, 2026 •

Copy link
Copy Markdown
Collaborator

What / why

Adds the CAS-delete primitive required by the subagent await-edge delivery design (P1.0b, PR1 merge-gate): put already takes a CasExpectation precondition but delete is blind, so no store can remove a record only-if-unchanged. RootFilesystem::delete_if_version(path, expected) is additive — default impl is Unsupported (same pattern as put), so the ~20 existing blind-delete call sites keep their behavior and no implementor breaks.

Semantics (deliberately distinct from both blind delete and put's CAS diagnosis):

  • Single-key only — no subtree / event-log / sequence-counter sweep; a Postgres pin test locks the statements to one round-trip with no OR-joined subtree band (mirrors the PUT_*_SQL idiom).
  • Two-branch failure diagnosis — absent row → NotFound (already gone, benign for idempotent cleanup); row present at another version → VersionMismatch{expected, found} (gone stale). This is new logic: put's CAS paths collapse the absent case into VersionMismatch{found: None}.
  • CasExpectation::Absent is meaningless for a delete and fails closed with Unsupported, decoded once by a shared record_version_to_i64/required_delete_version path used by all three backends. That same shared conversion also narrows RecordVersion into the backend's i64 column type: an out-of-range expected_version (> i64::MAX) surfaces FilesystemError::CorruptRecordVersion before any statement runs, rather than a silently wrapped bind parameter that could never match a real row.
  • Version tokens are not generation-stable (ABA) — a path's version restarts at 1 on a fresh put after a prior delete, so an expected_version captured before a delete+recreate cycle can match a different incarnation of the same path. delete_if_version is a sound standalone precondition only for paths that are never recreated; callers that do recreate paths must pair every successful delete with an unconditional postcondition recheck. Documented on the trait method itself.

Implemented for every RootFilesystem: in-memory, libSQL, Postgres, HsmBackend (delegates to its inner backend), and the composite/scoping layers that forward the unified entry plane — ScopedFilesystem, CompositeRootFilesystem, MountScopedRootFilesystem (ironclaw_host_runtime), and SkillManagementRootFilesystem (ironclaw_skills). Out of scope by design: LocalFilesystem (already rejects Version CAS for put) and StorageTxn (multi-key transactions are a separate atomicity story).

Test evidence

  • crates/ironclaw_filesystem cargo test --all-features: 217 passed, 0 failed (110 lib + 8 catalog_contract + 75 db_root_filesystem_contract + 24 filesystem_contract).
  • ironclaw_skills and ironclaw_host_runtime (own the SkillManagementRootFilesystem / MountScopedRootFilesystem forwarding wrappers): compile and test clean under this change (host_runtime's 3 pre-existing Docker-socket test failures are environmental — no Docker daemon in this sandbox — and unrelated to this diff).
  • Dual-backend contract tests (in-memory + libSQL + Postgres, the latter skipping without a reachable DB per suite convention): correct-version delete succeeds and subsequent get is empty; stale version loses with VersionMismatch{Some(v1), Some(v2)} while the entry survives at the concurrent writer's bumped version; missing path → NotFound; the event log at the same path survives the CAS delete (single-key, where blind delete sweeps it); composite routes delete_if_version to the matched mount; out-of-range expected_version (u64::MAX) surfaces CorruptRecordVersion on both libSQL and Postgres with the entry left untouched.
  • Atomic zero-row classification (round 4 review fix): the conditional DELETE and the NotFound-vs-VersionMismatch diagnosis now run as one unit instead of two separate round trips, closing a window where an external delete+recreate could commit in between and misclassify the outcome. libSQL wraps both in one BEGIN IMMEDIATE / COMMIT transaction on a single connection (mirrors put's existing idiom, and keeps the call to one connection checkout). Postgres folds both into one WITH statement: a locked CTE takes a SELECT ... FOR UPDATE row lock, and the deleted CTE depends on it (path IN (SELECT path FROM locked)) so Postgres sequences lock-then-delete instead of running them independently; the Postgres SQL pin test now also asserts the FOR UPDATE lock and the CTE dependency are present.
  • Mutation-checked (broken deliberately, confirmed RED for the right reason, reverted): collapsing absent→VersionMismatch flipped the NotFound assertions in both the in-memory and libSQL tests; deleting despite a mismatch flipped the entry-survival assertion; bypassing the overflow guard (expected_version.get() as i64 instead of record_version_to_i64) flipped the new libSQL overflow test from CorruptRecordVersion to a bogus VersionMismatch; dropping FOR UPDATE from the Postgres CTE flipped the SQL pin test.
  • In-memory backend verified already atomic under its single state.lock().await guard spanning the whole read-check-delete sequence — no change needed there.
  • Postgres SQL pin test: both delete arms are one statement, single-key, is_dir = FALSE, version-guarded, row-locked before the conditional delete.
  • cargo fmt; cargo clippy --all --benches --tests --examples --all-features: zero warnings; cargo build --tests --all-features clean.
  • Plan + diff reviewed by thermo-nuclear + approach/local-patterns/maintainability reviewers across all four rounds; every actionable finding (Postgres named-SQL-const + pin-test idiom; triplicated CAS-expectation decoding; HSM/Scoped/MountScoped forwarding gaps; Any/Absent branch coverage; overflow-guard coverage; atomic zero-row classification; this PR-body sync) is addressed in this diff.

🤖 Generated with Claude Code

put has a CAS precondition (CasExpectation) but delete is blind, so no
caller can remove a record only-if-unchanged. Add an additive trait
method delete_if_version(path, expected) with a default Unsupported
impl (put's pattern) — ~20 blind-delete call sites are untouched.

Semantics: single-key only (no subtree/event-log/sequence sweep, unlike
blind delete); absent row → NotFound (already gone, benign) vs row at
another version → VersionMismatch{expected, found} (gone stale) — a new
two-branch diagnosis, since put's CAS paths collapse absent into
VersionMismatch{found: None}; CasExpectation::Absent is meaningless for
a delete and fails closed with Unsupported (decoded once in
CasExpectation::required_delete_version, shared by all backends).

Implemented for in-memory, libSQL, and Postgres (named SQL consts +
single-round-trip/single-key pin test, mirroring the PUT_*_SQL idiom),
with ScopedFilesystem and CompositeRootFilesystem passthroughs.

Tests: dual-backend contract coverage (correct-version delete, stale
version loses with entry surviving at the bumped version, missing path,
single-key event-log survival) in db_root_filesystem_contract.rs plus
in-memory crate tests and composite routing in catalog_contract.rs;
Postgres tests skip without a reachable DB per suite convention.
Mutation-checked: collapsing absent into VersionMismatch and deleting
despite a mismatch each turn the pinned assertions red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 01:26
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@ironloopai

ironloopai Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

⏳ IronLoop Review Status

Head: bfb8cb9a7df960e22f43750ec2dbf36097436d8d
Result: No reviewer jobs are scheduled yet.
Next: Run @ironloopai review to start reviewers.
Updated: 2026-07-08T03:11:01.137Z

Current reviewers:

Reviewer State Verdict Findings Last update
none Queued N/A No reviewer jobs scheduled yet. N/A
Reviewer summaries
Reviewer Detail
none No reviewer jobs scheduled yet.
Recent activity
Time Reviewer State Detail
N/A N/A Waiting No progress events recorded yet.
Available commands
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent-id-or-alias>
  • @ironloopai status
Run metadata

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

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 01:26 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 7, 2026
@coderabbitai

coderabbitai Bot commented Jul 7, 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: ecc98083-cf15-49bc-87c4-ffb7ebd25cc7

📥 Commits

Reviewing files that changed from the base of the PR and between cb24f43 and bfb8cb9.

📒 Files selected for processing (1)
  • crates/ironclaw_host_runtime/src/invocation_services.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added CAS-style delete_if_version for version-checked single-entry deletes across supported backends, including composite routing, scoped/hosted permission checks, and management-layer delegation.
    • Backends without support now fail closed with Unsupported for delete.
  • Bug Fixes
    • Missing paths consistently return NotFound; stale versions return VersionMismatch without altering the latest record; directory rows are treated as not found.
    • Preserves per-path event history on successful CAS deletes.
  • Tests
    • Added contract, ABA-hazard pinning, overflow guards, SQL-shape/atomicity checks, and concurrent “exactly one winner” delete storms (in-memory, LibSQL, Postgres).

Walkthrough

Adds delete_if_version across the filesystem trait, backend implementations, and wrapper layers, with CAS delete behavior plus route, permission, and contention coverage.

Changes

CAS-conditional delete_if_version

Layer / File(s) Summary
Trait API
crates/ironclaw_filesystem/src/root.rs, crates/ironclaw_filesystem/src/record.rs, crates/ironclaw_filesystem/src/in_memory.rs, crates/ironclaw_filesystem/src/hsm.rs
Adds RootFilesystem::delete_if_version with default Unsupported, updates CasExpectation::Version docs, implements CAS delete in InMemoryBackend, and routes HsmBackend::delete_if_version to the inner backend with coverage for missing, stale, current, and ABA cases.
LibSQL and Postgres delete
crates/ironclaw_filesystem/src/libsql.rs, crates/ironclaw_filesystem/src/postgres.rs, crates/ironclaw_filesystem/tests/db_root_filesystem_contract.rs
Implements exact-version delete in both database backends, adds transactional/statement validation, and covers missing, stale, current, overflow, directory-scope, ABA, and atomicity cases in contract tests.
Routing and delegation
crates/ironclaw_filesystem/src/catalog.rs, crates/ironclaw_filesystem/src/scoped.rs, crates/ironclaw_filesystem/src/scoped/tests.rs, crates/ironclaw_filesystem/tests/catalog_contract.rs, crates/ironclaw_host_runtime/src/invocation_services.rs, crates/ironclaw_host_runtime/src/invocation_services/tests.rs, crates/ironclaw_skills/src/management.rs, crates/ironclaw_skills/src/management/tests.rs
Wires delete_if_version through mount selection, permission resolution, and wrapper delegation, with route and permission tests.
Concurrent delete contention
crates/ironclaw_filesystem/tests/concurrent_cas_storm.rs
Adds delete contention helpers and multi-backend storm tests that assert exactly one successful delete per round.

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

Suggested reviewers: think-in-universe, ilblackdragon

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Conventional-commits title clearly matches the new CAS-guarded delete_if_version filesystem primitive.
Description check ✅ Passed The description is detailed and covers the change, validation, and follow-up, but it doesn't follow the repository's exact template headings.
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: reviewer

Verdict: ❌ Changes requested
Findings: 1 blocking / 0 notes
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Head: 58160f8a98c0b3a5e04d5c1910c2f3b5bd1a15d1

Run details

Status: Current
Needs human: no
Needs validation: no

**Inline candidates:** 1

Summary

The new CAS-delete API is implemented for the main SQL/in-memory backends and composite routing, with focused contract tests. One existing backend now advertises CAS+Delete but still inherits the new Unsupported default, so generic callers can get a runtime failure despite mount capabilities saying the backend supports the needed primitives.

Findings

1. ❌ [MEDIUM] Implement CAS delete for HSM backend or stop advertising CAS delete support

Location: crates/ironclaw_filesystem/src/root.rs:174-179
Adding RootFilesystem::delete_if_version with an Unsupported default leaves existing implementors on the default unless they opt in. HsmBackend currently advertises Capability::Delete plus TxnCapability::Cas and delegates put/get/delete to its in-memory backend, but it does not override this new method, so a /secrets mount backed by HSM will pass capability validation and then fail at runtime for the new CAS-delete path. Please either delegate HsmBackend::delete_if_version to inner.delete_if_version(...) or adjust its declared capabilities/API contract so callers cannot treat it as CAS-delete capable.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloop review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloop review when the fix may affect multiple areas.
  4. Use @ironloop status to check queued/running/completed/stale/stalled state while reviewers run.

/// and the meaningless-for-delete `CasExpectation::Absent` is rejected
/// with [`FilesystemError::Unsupported`]. Default impl is `Unsupported`,
/// same as [`put`](Self::put): backends opt in natively.
async fn delete_if_version(

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 new default means existing backends silently become Unsupported for CAS delete. HsmBackend still advertises Delete + TxnCapability::Cas but does not override this method, so generic callers can pass mount capability checks and then fail at runtime. Please delegate the HSM implementation to its inner backend or make the capability contract distinguish that this backend cannot serve CAS delete.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6d97cd2: HsmBackend.delete_if_version now delegates to self.inner.delete_if_version, mirroring the existing put/get/stat/delete delegations. Grepped all other impl RootFilesystem in the crate — LocalFilesystem is the only other production backend and it doesn't override capabilities() (defaults to all-false), so it never overclaims Delete/Cas and isn't affected. Added hsm::tests::delete_if_version_delegates_to_inner_backend (wrong version → VersionMismatch, correct version deletes, re-delete → NotFound) — mutation-verified: reverting the delegation to Err(Unsupported) turns this RED.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in 6d97cd2 (round 1): HsmBackend::delete_if_version now delegates to its inner backend, with delegation coverage. Predates this session (round 5) — leaving this thread unresolved for the reviewer/maintainer of record, but confirmed hsm.rs's current state already addresses it.

@github-actions

github-actions Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.34% (280958 / 329213 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 329213 lines now vs 320188 at floor capture (+9025 lines, +2.82%) — not a material change

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

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.34% — 280958 / 329213 lines

Per-crate breakdown (65 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 347
ironclaw_skill_learning 0% 0 / 61
ironclaw_wasm_sandbox_core 7.37% 7 / 95
ironclaw_runtime_policy 33.2% 80 / 241
ironclaw_event_projections 43.34% 673 / 1553
ironclaw_run_state 52.36% 222 / 424
ironclaw_authorization 53.54% 461 / 861
ironclaw_triggers 60.32% 1736 / 2878
ironclaw_observability 61.54% 16 / 26
ironclaw_mcp 63.15% 581 / 920
ironclaw_reborn_cli 64.58% 3988 / 6175
ironclaw_webui_v2 66.17% 2449 / 3701
ironclaw_filesystem 66.82% 3780 / 5657
ironclaw_reborn_migration 67.01% 1172 / 1749
ironclaw_memory 67.12% 747 / 1113
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_trust 72.88% 661 / 907
ironclaw_reborn_event_store 73.01% 944 / 1293
ironclaw_capabilities 74.08% 1658 / 2238
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_extractors 74.72% 538 / 720
ironclaw_first_party_extensions 77.62% 5410 / 6970
ironclaw_llm 77.88% 19479 / 25013
ironclaw_product_context 78.57% 11 / 14
ironclaw_wasm_product_adapters 80.58% 1510 / 1874
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_reborn_openai_compat 80.95% 956 / 1181
ironclaw_memory_native 81.86% 3226 / 3941
ironclaw_secrets 82.33% 2716 / 3299
ironclaw_wasm 82.54% 950 / 1151
ironclaw_events 83.47% 1762 / 2111
ironclaw_processes 84.06% 965 / 1148
ironclaw_turns 84.14% 13061 / 15523
ironclaw_host_api 84.8% 3131 / 3692
ironclaw_threads 85.16% 3278 / 3849
ironclaw_projects 85.92% 659 / 767
ironclaw_product_workflow 86.1% 10709 / 12438
ironclaw_network 86.12% 670 / 778
ironclaw_auth 86.32% 2727 / 3159
ironclaw_common 86.59% 1472 / 1700
ironclaw_slack_v2_adapter 86.79% 1806 / 2081
ironclaw_reborn_config 86.98% 1730 / 1989
ironclaw_product_adapters 87.29% 3207 / 3674
ironclaw_skills 87.35% 4336 / 4964
ironclaw_hooks 87.84% 9916 / 11289
ironclaw_product_adapter_registry 87.96% 526 / 598
ironclaw_reborn_traces 88.23% 11707 / 13268
ironclaw_extensions 88.26% 2631 / 2981
ironclaw_reborn_identity 88.43% 344 / 389
ironclaw_host_runtime 88.93% 17484 / 19661
ironclaw_reborn_composition 89.08% 69047 / 77508
ironclaw_conversations 90% 2924 / 3249
ironclaw_approvals 90.51% 1507 / 1665
ironclaw_reborn 91.19% 17477 / 19166
ironclaw_event_streams 91.48% 1009 / 1103
ironclaw_reborn_webui_ingress 91.68% 2094 / 2284
ironclaw_loop_support 92.22% 14231 / 15432
ironclaw_attachments 93.06% 630 / 677
ironclaw_telegram_v2_adapter 94.01% 2447 / 2603
ironclaw_resources 94.24% 3618 / 3839
ironclaw_agent_loop 94.54% 8753 / 9259
ironclaw_safety 94.81% 3669 / 3870
ironclaw_first_party_extension_ports 95% 3094 / 3257
ironclaw_outbound 95.59% 3556 / 3720

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 (4 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_oauth v1-only: consumed only by root ironclaw (src/auth/oauth.rs); no crates/* dependents. Crate's own doc comment confirms v1-only. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@railway-app

railway-app Bot commented Jul 7, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 8, 2026 at 3:19 am

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add CAS-guarded single-key delete_if_version to RootFilesystem across supported backends without changing existing blind delete behavior.
Stats: 6 findings (from 8 raw, 6 after dedup/filter) across 4 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 2

conventions

  1. Medium New filesystem API is missing from the contract docs (crates/ironclaw_filesystem/src/root.rs:174-180, confidence 90) — anchor: crates/ironclaw_filesystem/AGENTS.md:7 (no diff position — body only)
    The diff adds public RootFilesystem::delete_if_version semantics, but crates/ironclaw_filesystem/AGENTS.md says docs/reborn/contracts/filesystem.md is the source of truth before behavior changes. That contract still lists only delete in the permission table and its API sketch omits delete_if_version, so consumers cannot derive the delete permission, single-key behavior, backend support/default Unsupported behavior, or NotFound/VersionMismatch/Unsupported contract from the authoritative docs.

tests

  1. Medium Scoped CAS delete boundary is untested (crates/ironclaw_filesystem/src/scoped.rs:541-550, confidence 90) — anchor: crates/ironclaw_filesystem/src/scoped.rs:541
    The new public ScopedFilesystem::delete_if_version resolves the scoped path with Delete permission before delegating to the root backend, but the added tests call only root/composite/backend APIs. A wrong permission operation or path resolution at the scoped boundary would not fail any current test.
  2. Medium libSQL Any delete branch has no contract test (crates/ironclaw_filesystem/src/libsql.rs:1005-1010, confidence 85) — anchor: crates/ironclaw_filesystem/src/libsql.rs:1005
    The libSQL tests exercise Version current/stale/missing behavior, but no test calls delete_if_version with CasExpectation::Any. That leaves the unconditional single-key DELETE SQL branch and its second-delete NotFound behavior unexercised for this backend; the in-memory Any test does not cover the libSQL query path.
  3. Medium Postgres Any delete branch has no runtime test (crates/ironclaw_filesystem/src/postgres.rs:1502-1502, confidence 85) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1502
    The Postgres contract test covers Version current/stale/missing and the SQL pin test checks statement shape, but no runtime test calls delete_if_version with CasExpectation::Any. A regression in the unconditional execution path or its zero-row NotFound diagnosis would not be caught.

performance

  1. Medium Stale CAS deletes open a second libSQL connection (crates/ironclaw_filesystem/src/libsql.rs:1020-1022, confidence 82) — anchor: crates/ironclaw_filesystem/src/libsql.rs:1020
    When a version-guarded delete affects zero rows, the diagnostic path calls self.current_version(path), which opens a fresh libSQL connection instead of using the existing connection that performed the DELETE. In a contended CAS-delete path, each loser now pays an extra connection open and SELECT, amplifying SQLite single-writer contention and transient file-open pressure.

maintainability

  1. Medium Narrow delete_if_version to the only supported precondition (crates/ironclaw_filesystem/src/root.rs:174-177, confidence 75) — anchor: crates/ironclaw_filesystem/src/root.rs:174 (no diff position — body only)
    The new public trait API accepts the full CasExpectation enum even though the stated need is version-guarded deletion. That broad parameter commits the contract to two extra modes: Absent, which every backend rejects as meaningless, and Any, which creates a second unconditional single-key delete path with no named production caller. A RecordVersion parameter would satisfy the CAS-delete need without adding hidden blind-delete semantics under a CAS-named method.
    Also flagged by: approach/Low


/// Delete the single entry at `path` only when its version satisfies
/// `expected`. See [`RootFilesystem::delete_if_version`].
pub async fn delete_if_version(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Scoped CAS delete boundary is untested.

The new public ScopedFilesystem::delete_if_version resolves the scoped path with Delete permission before delegating to the root backend, but the added tests call only root/composite/backend APIs. A wrong permission operation or path resolution at the scoped boundary would not fail any current test.

Fix: Add tests::scoped::delete_if_version_requires_delete_permission_and_routes_to_backend covering no-delete PermissionDenied and successful CAS delete through a scoped mount.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6d97cd2: added scoped::tests::delete_if_version_denies_when_delete_missing (mount has read/write/list but not delete → PermissionDenied{operation: Delete} before backend dispatch) and scoped::tests::delete_if_version_succeeds_with_delete_and_routes_to_backend (correct-version CAS delete through the scoped mount actually removes the entry, proving it isn't a passthrough stub). Mutation-verified both: hardcoding the Delete permission check to true turned the first test RED.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in 2c1bc52 (round 2): scoped/tests.rs now has delete_if_version_denies_when_delete_missing and delete_if_version_succeeds_with_delete_and_routes_to_backend covering exactly this boundary. Predates this session — leaving unresolved for the reviewer of record.

Comment thread crates/ironclaw_filesystem/src/libsql.rs Outdated
)
.await
}
None => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — libSQL Any delete branch has no contract test.

The libSQL tests exercise Version current/stale/missing behavior, but no test calls delete_if_version with CasExpectation::Any. That leaves the unconditional single-key DELETE SQL branch and its second-delete NotFound behavior unexercised for this backend; the in-memory Any test does not cover the libSQL query path.

Fix: Add tests::db_root_filesystem_contract::libsql_delete_if_version_any_deletes_single_key_unconditionally covering Any delete, event log survival, and second Any returning NotFound.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6d97cd2: added libsql_delete_if_version_any_deletes_single_key_unconditionally in tests/db_root_filesystem_contract.rs — Any deletes regardless of current version, the event log at the same path survives (single-key, not a subtree sweep), and a second Any against the now-absent row is NotFound rather than a silent no-op.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Moot as of round 3 (5e5fe58, predates this session): delete_if_version's signature was narrowed from CasExpectation to a plain RecordVersion (maintainer verdict on the ABA finding), so Any/Absent are no longer representable at this API at all — there's no Any branch left to test. Leaving unresolved since the resolution predates this session; flagging so the thread isn't mistaken for still-open.

)
.await
}
None => cached_execute(client, DELETE_IF_VERSION_ANY_SQL, &[&path.as_str()]).await,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Postgres Any delete branch has no runtime test.

The Postgres contract test covers Version current/stale/missing and the SQL pin test checks statement shape, but no runtime test calls delete_if_version with CasExpectation::Any. A regression in the unconditional execution path or its zero-row NotFound diagnosis would not be caught.

Fix: Add tests::db_root_filesystem_contract::postgres_delete_if_version_any_deletes_single_key_unconditionally covering Any delete, event log survival, and second Any returning NotFound.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 6d97cd2: added postgres_tests::postgres_delete_if_version_any_deletes_single_key_unconditionally in the existing env-gated contract suite (skips cleanly via postgres_root() -> None when no DATABASE_URL/IRONCLAW_FILESYSTEM_POSTGRES_URL is set). Same shape as the libsql sibling: Any deletes unconditionally, event log survives, second Any on the now-absent row is NotFound. Ran against a live Postgres in this environment and it passed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same as the sibling libSQL Any-delete thread: moot since round 3 (5e5fe58) narrowed delete_if_version to a plain RecordVersion, removing CasExpectation::Any/Absent from this API entirely. Predates this session; leaving unresolved for the reviewer of record.

HsmBackend advertised Delete+Cas but fell through to Unsupported at
runtime; now delegates to its inner backend. Adds scoped-mount
permission/routing tests, drops the libsql delete's connection before
the zero-rows current_version lookup (pool-exhaustion guard ahead of
#5751), and adds Any-branch contract tests for libsql and postgres.

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

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add CAS-guarded delete_if_version to RootFilesystem with backend implementations and contract tests while preserving existing blind deletes.

Stats: 4 net-new findings (from 7 raw; duplicate live-thread findings suppressed) across 4 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0.

Findings

  1. Medium Filesystem contract docs omit the new API (crates/ironclaw_filesystem/src/root.rs:166-180, confidence 100) — anchor: crates/ironclaw_filesystem/AGENTS.md:7
    The diff extends the public RootFilesystem contract with delete_if_version, but docs/reborn/contracts/filesystem.md still omits the CAS delete operation from its API sketch and contract text. CasExpectation's public docs also still describe the enum as put-only, so the source-of-truth contract and generated API docs drift from the new public behavior.

  2. Medium Scoped CAS delete lacks stale-version propagation coverage (crates/ironclaw_filesystem/src/scoped.rs:550, confidence 75) — anchor: crates/ironclaw_filesystem/src/scoped.rs:550
    The new scoped wrapper is tested for delete permission denial and correct-version success, but not for forwarding a stale CasExpectation::Version to the backend. A regression that accidentally ignored expected and performed an unconditional delete would still pass the current scoped tests.

  3. Medium libSQL CAS delete lacks Absent rejection coverage (crates/ironclaw_filesystem/src/libsql.rs:993, confidence 75) — anchor: crates/ironclaw_filesystem/src/libsql.rs:993
    The libSQL backend calls required_delete_version, but backend contract tests cover Version and Any only. The PR defines CasExpectation::Absent as fail-closed Unsupported for delete, and no libSQL test asserts that error path leaves the entry intact.

  4. Medium Postgres CAS delete lacks Absent rejection coverage (crates/ironclaw_filesystem/src/postgres.rs:1491, confidence 75) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1491
    The Postgres backend calls required_delete_version, but runtime contract tests cover Version and Any only. The same fail-closed Absent contract should be pinned for Postgres so a backend-specific bypass cannot silently become an unconditional delete.

/// and the meaningless-for-delete `CasExpectation::Absent` is rejected
/// with [`FilesystemError::Unsupported`]. Default impl is `Unsupported`,
/// same as [`put`](Self::put): backends opt in natively.
async fn delete_if_version(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Filesystem contract docs omit the new API.

This adds RootFilesystem::delete_if_version, but docs/reborn/contracts/filesystem.md still omits the CAS delete operation from its API sketch and contract text. CasExpectation's public docs also still describe the enum as put-only, so both the source-of-truth contract and generated API docs drift from the new behavior.

Fix: Update docs/reborn/contracts/filesystem.md with delete_if_version semantics, Delete permission requirements, single-key behavior, and the scoped/root API shape; also update the CasExpectation enum docs to mention delete semantics and that Absent is rejected for delete.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed the rustdoc half in 2c1bc52 — scoping to this PR: CasExpectation's doc comment now says it's the precondition for both put and delete_if_version, spells out the two meaningful delete branches (Version = guarded delete, Any = unconditional) and states that Absent is rejected with Unsupported for delete rather than being folded into an unconditional delete; each variant's own doc comment got the delete-specific caveat too. delete_if_version's own rustdoc on the RootFilesystem trait (root.rs) already stated the NotFound/VersionMismatch/Unsupported taxonomy from the original PR, so no change was needed there. Deliberately NOT touching docs/reborn/contracts/filesystem.md here — that's owned by the docs PR #5748 landing separately; updating it in both PRs would just create merge friction.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Declining to fix in this session: the 'Initial Rust API sketch' section (docs/reborn/contracts/filesystem.md §14) is already broadly stale relative to the current unified entry-plane API — it doesn't show put/get/CasExpectation either, which predate this PR. Bolting on just delete_if_version would misrepresent the doc's currency; recommend a follow-up pass that modernizes §14 wholesale. The CasExpectation-docs half of this comment is now accurate again: round 3 (5e5fe58) narrowed delete_if_version off CasExpectation entirely, so CasExpectation genuinely is put-only.

let started_at = live_latency_started_at();
let virtual_path =
self.resolve_with_permission(scope, path, FilesystemOperation::Delete)?;
let result = self.root.delete_if_version(&virtual_path, expected).await;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Scoped CAS delete lacks stale-version propagation coverage.

The new scoped wrapper is tested for delete permission denial and correct-version success, but not for forwarding a stale CasExpectation::Version to the backend. A regression that ignored expected and performed an unconditional delete would still pass the current scoped tests.

Fix: Add scoped::tests::delete_if_version_rejects_stale_version_and_preserves_entry, using a stale version through ScopedFilesystem::delete_if_version and asserting VersionMismatch plus entry preservation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2c1bc52: added scoped::tests::delete_if_version_rejects_stale_version_and_preserves_entry — puts, bumps to v2, then calls ScopedFilesystem::delete_if_version(scope, path, CasExpectation::Version(v1)) (stale), asserts FilesystemError::VersionMismatch{expected: Some(v1), found: Some(v2)}, then reads the entry back and asserts it's still present at v2. Mutation-checked: patched delete_if_version to ignore its expected argument and forward CasExpectation::Any to self.root instead — this new test goes RED (panics 'expected an error') while the existing permission-denial and correct-version-success tests stay green, proving only this test catches an expected-swallowing regression. Reverted after confirming.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in 2c1bc52 (round 2): scoped/tests.rs's delete_if_version_rejects_stale_version_and_preserves_entry drives a stale CasExpectation::Version through ScopedFilesystem::delete_if_version and asserts VersionMismatch + entry preservation. Predates this session; leaving unresolved for the reviewer of record.

// Single-key CAS delete: unlike `delete`, no subtree/event/sequence
// sweep. `is_dir = 0` scopes it to the record plane, matching `put`'s
// Version arm and `current_version`.
let required_version = expected.required_delete_version(path)?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — libSQL CAS delete lacks Absent rejection coverage.

The libSQL backend calls required_delete_version, but the backend contract tests cover Version and Any only. The PR defines CasExpectation::Absent as fail-closed Unsupported for delete, and no libSQL test asserts that error path leaves the entry intact.

Fix: Add db_root_filesystem_contract::libsql_delete_if_version_rejects_absent_expectation covering Unsupported and no deletion when CasExpectation::Absent is passed.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2c1bc52: added libsql_delete_if_version_rejects_absent_expectation to tests/db_root_filesystem_contract.rs — puts an entry, calls delete_if_version(CasExpectation::Absent), asserts FilesystemError::Unsupported{operation: Delete}, then re-reads and asserts the entry survives unchanged. Ran live against a real libSQL DB and passed. Mutation-checked: reverting CasExpectation::required_delete_version's Absent arm to Ok(None) (unconditional delete) turns this test RED (panicked on unwrap_err with an Ok(()) value) while the sibling Version/Any tests stay green — confirms the assertion is load-bearing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Moot as of round 3 (5e5fe58, predates this session): delete_if_version's signature was narrowed to a plain RecordVersion, so CasExpectation::Absent is no longer representable at this API — there's no Absent branch left to reject. Leaving unresolved since the resolution predates this session.

path: &VirtualPath,
expected: CasExpectation,
) -> Result<(), FilesystemError> {
let required_version = expected.required_delete_version(path)?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Postgres CAS delete lacks Absent rejection coverage.

The Postgres backend calls required_delete_version, but runtime contract tests cover Version and Any only. The same fail-closed Absent contract should be pinned for Postgres so a backend-specific bypass cannot silently become an unconditional delete.

Fix: Add postgres_delete_if_version_rejects_absent_expectation to the env-gated Postgres contract suite, asserting Unsupported and entry preservation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 2c1bc52: added postgres_tests::postgres_delete_if_version_rejects_absent_expectation to the env-gated Postgres contract suite in tests/db_root_filesystem_contract.rs — puts an entry, calls delete_if_version(CasExpectation::Absent), asserts FilesystemError::Unsupported{operation: Delete}, then re-reads and asserts the entry survives at its original version/body. No live Postgres is reachable in this environment (no docker, no IRONCLAW_FILESYSTEM_POSTGRES_URL/DATABASE_URL), so the test exercises the documented clean-skip path here; the assertion logic mirrors the sibling Version/Any tests that ran live in the prior lane. Mutation-checked: reverting required_delete_version's Absent arm to Ok(None) (fall through to unconditional delete) turns this RED alongside the libSQL counterpart.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same as the sibling libSQL Absent-rejection thread: moot since round 3 (5e5fe58) removed CasExpectation from delete_if_version's signature entirely. Predates this session; leaving unresolved for the reviewer of record.

…coverage

Round-2 review fixes for #5749:
- postgres/libsql: Absent rejects with Unsupported, entry preserved
- scoped: stale Version through delete_if_version surfaces
  VersionMismatch and forwards `expected` rather than swallowing it
- CasExpectation rustdoc now covers delete_if_version semantics

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 17:06

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Jul 7, 2026
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 17:08 Destroyed
henrypark133 added a commit that referenced this pull request Jul 7, 2026
Apply 7 review findings plus one import from PR #5749: header metadata
shape, bounded boot-recovery concurrency + roster cardinality, a
roster-before-edge-consistent rewrite of the residual delete/roster race
(and its test), batched (not per-edge) settled-drain transcript writes,
a bounded LIMIT-8 run-budget query in place of an open-ended walk, a
lease (owner+expires_at, lazy expiry) on the human-priority reservation
marker, in-path agent/project axis encoding for await-edge and roster
paths (mount alone only partitions tenant/user), and the delete_if_version
CAS contract in the filesystem contract doc. Thermo-reviewed twice: first
pass caught an off-by-one in the budget query and a placeholder-spelling
drift, both fixed and re-verified clean.

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

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add an additive CAS-guarded delete_if_version primitive to RootFilesystem across supported filesystem backends with contract tests.

Stats: 2 findings (from 6 raw, 2 after dedup/live-thread suppression) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0

Bugs

  1. High CAS delete can remove a recreated row with the same version (crates/ironclaw_filesystem/src/postgres.rs:1650-1663, confidence 75) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1650
    The guarded delete only checks the row's numeric RecordVersion, but a path deleted and recreated with CasExpectation::Absent starts back at version 1. A stale caller that read v1 before delete/recreate can later call delete_if_version(..., Version(v1)) and delete the new row, so the delete is not actually guarded against an unchanged record. The same version-only token shape exists in libSQL and in-memory. Also flagged by: performance/High.

Maintainability

  1. Medium Default CAS delete leaves MountScopedRootFilesystem out of sync (crates/ironclaw_filesystem/src/root.rs:174-180, confidence 75) — anchor: crates/ironclaw_filesystem/src/root.rs:174; crates/ironclaw_host_runtime/src/invocation_services.rs:363
    The new default Unsupported method makes forwarding wrappers easy to miss. MountScopedRootFilesystem delegates capabilities from the inner root and forwards delete after permission/path resolution, but it does not override delete_if_version, so host-runtime filesystem views can advertise the inner backend's CAS/Delete support and then fail with Unsupported at call time.

) -> Result<(), FilesystemError> {
let required_version = expected.required_delete_version(path)?;
let deleted = match required_version {
Some(expected_version) => {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

High — CAS delete can remove a recreated row with the same version.

The guarded delete only checks the row's numeric RecordVersion, but a path deleted and recreated with CasExpectation::Absent starts back at version 1. A stale caller that read v1 before delete/recreate can later call delete_if_version(..., Version(v1)) and delete the new row, so the delete is not actually guarded against an unchanged record. The same version-only token shape exists in libSQL and in-memory.

Fix: Make the CAS token generation-stable across delete/recreate, for example by preserving a monotonic per-path version/tombstone or adding a row generation id, then match delete_if_version on that token instead of a resettable row version.

Also flagged by: performance/High

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maintainer decision made pre-session in 5e5fe58 (round 3): explicitly declined generation-stable tokens, narrowed the signature to a plain RecordVersion instead, and documented the ABA hazard as a caller invariant on the trait doc. This session added regression pins for that exact documented behavior (delete_if_version_is_vulnerable_to_aba_across_delete_recreate_cycles, in_memory + libsql + postgres, round A/B) but did not revisit the generation-stable-tokens design call itself — leaving unresolved since that decision predates this session.

/// and the meaningless-for-delete `CasExpectation::Absent` is rejected
/// with [`FilesystemError::Unsupported`]. Default impl is `Unsupported`,
/// same as [`put`](Self::put): backends opt in natively.
async fn delete_if_version(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Default CAS delete leaves MountScopedRootFilesystem out of sync.

The new default Unsupported method makes forwarding wrappers easy to miss. MountScopedRootFilesystem delegates capabilities from the inner root and forwards delete after permission/path resolution, but it does not override delete_if_version, so host-runtime filesystem views can advertise the inner backend's CAS/Delete support and then fail with Unsupported at call time.

Fix: Add delete_if_version to MountScopedRootFilesystem beside delete, resolving with FilesystemOperation::Delete and forwarding the expected CasExpectation to the inner root, with a caller-level test for the scoped host-runtime path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in 5e5fe58 (round 3): MountScopedRootFilesystem::delete_if_version now resolves with FilesystemOperation::Delete and forwards to the inner root, with delegation coverage. Predates this session; leaving unresolved for the reviewer of record.

…-only API

Round 3 review fixes for PR #5749:

- MountScopedRootFilesystem (ironclaw_host_runtime) and
  SkillManagementRootFilesystem (ironclaw_skills) both forward
  capabilities() to their inner backend but omitted delete_if_version,
  so a CAS-capable mount silently fell through to the trait default
  Unsupported. Both now resolve/delegate delete_if_version like their
  existing delete override. Workspace-wide audit of every
  RootFilesystem-wrapping decorator found only these two plus the
  already-fixed HsmBackend and the already-correct
  CompositeRootFilesystem — delegation tests added for both new fixes
  and mutation-checked (MountScoped) by reverting the override.

- Maintainer verdict on the ABA finding: narrow delete_if_version's
  signature from CasExpectation to a plain RecordVersion (Any/Absent
  are unrepresentable for a delete) and document the ABA hazard as a
  caller invariant on the trait method's rustdoc instead of adding
  generation-stable tokens. Threaded the signature change through
  in_memory/libsql/postgres/scoped/catalog/hsm and every call site;
  along the way this also fixed a pre-existing libsql.rs build break
  (delete_if_version referenced a nonexistent self.current_version).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 19:49
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 19:49 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

Round 3 review fixes pushed (5e5fe58).

Finding 1 (Medium — MountScoped wrapper hole): Fixed. MountScopedRootFilesystem (ironclaw_host_runtime/src/invocation_services.rs) advertised the inner backend's capabilities verbatim and forwarded delete, but had no delete_if_version override, so a CAS-capable mount silently hit the trait default Unsupported. Added the override beside delete (same resolve(path, FilesystemOperation::Delete) permission check, then self.root.delete_if_version(...)).

Workspace-wide audit of every type implementing RootFilesystem that wraps an inner Arc<dyn RootFilesystem> (forwarding/decorator pattern):

  • HsmBackend (ironclaw_filesystem/src/hsm.rs) — already fixed in round 1.
  • CompositeRootFilesystem (ironclaw_filesystem/src/catalog.rs) — already correct, routes to the matched mount's backend.
  • MountScopedRootFilesystem (ironclaw_host_runtime/src/invocation_services.rs) — had the hole, fixed here.
  • SkillManagementRootFilesystem (ironclaw_skills/src/management.rs) — also had the same hole (overrides delete but not delete_if_version), fixed here too.
  • Everything else implementing RootFilesystem in the workspace (in ironclaw_memory_native, and all the #[cfg(test)]-only mock backends across ironclaw_threads/ironclaw_authorization/ironclaw_turns/ironclaw_run_state/ironclaw_outbound/ironclaw_secrets/ironclaw_resources/ironclaw_reborn_composition/etc.) either wraps a different backend trait (not Arc<dyn RootFilesystem>) or is a terminal/test backend, not a forwarding decorator — out of scope for this hole.

Delegation tests added for both fixed wrappers, mirroring the HSM delegation test (seed via CAS put, wrong-version → VersionMismatch, correct version → deletes). Mutation-checked the MountScoped fix by reverting the override — the new test goes RED (VersionMismatch assertion fails) as expected, confirming it wasn't a vacuous pass.

Finding 2 (High — ABA, per verdict): Applied per the maintainer verdict. delete_if_version's signature is narrowed from CasExpectation to a plain RecordVersion — Any/Absent are unrepresentable for a delete, so this also makes the round-2 "rejects absent" tests moot by construction (deleted, along with the Any-unconditional tests/SQL arm, per the verdict). The ABA hazard is now documented as a caller invariant on RootFilesystem::delete_if_version's rustdoc (root.rs): version tokens aren't generation-stable across a delete+recreate cycle, and callers that recreate paths must pair every successful delete with an unconditional postcondition recheck.

The signature change threaded through every implementer and call site (in_memory.rs, libsql.rs, postgres.rs, scoped.rs, catalog.rs, hsm.rs, plus both finding-1 wrappers and all contract tests). While rewriting libsql.rs's CAS-miss diagnosis I also fixed a pre-existing build break on this branch (delete_if_version referenced a nonexistent self.current_version; replaced with a reconnect + the existing current_version_libsql(&conn, path) free function, matching the two-checkout-avoidance comment already in place).

Verified: cargo fmt, cargo clippy --all --benches --tests --examples --all-features (zero warnings), cargo check --workspace --all-features --tests (green), and full test runs for ironclaw_filesystem (110+8+73+24 passing) and ironclaw_skills (219 passing). ironclaw_host_runtime is green except 3 pre-existing sandbox_process tests that require a local Docker daemon (SocketNotFoundError("/var/run/docker.sock")), unrelated to this change and unaffected by it (confirmed via full-suite rerun before/after).

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add CAS-guarded delete_if_version to RootFilesystem with backend support so records can be deleted only when unchanged.

Stats: 5 net-new findings (from 9 raw; duplicate live-thread findings and low-value nits suppressed) across 3 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0.

Performance

  1. Medium Failed CAS delete diagnosis races concurrent writers (crates/ironclaw_filesystem/src/libsql.rs:934-946, confidence 78) — anchor: crates/ironclaw_filesystem/src/libsql.rs:934
    After the conditional DELETE affects zero rows, delete_if_version drops the connection and opens a new one before reading the current version. A concurrent delete/recreate or update in that window can flip NotFound into VersionMismatch or VersionMismatch into NotFound, even though the new API documents those branches as distinct CAS outcomes.

  2. Medium Zero-row CAS delete classification is not atomic (crates/ironclaw_filesystem/src/postgres.rs:1653-1660, confidence 76) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1653
    The DELETE and follow-up current-version SELECT run as separate READ COMMITTED statements. A concurrent writer can mutate or remove the row between them, so callers may see NotFound for a stale-version delete or VersionMismatch for an absent row despite the new API documenting distinct failure semantics.

Tests

  1. Medium libSQL CAS delete overflow branch is untested (crates/ironclaw_filesystem/src/libsql.rs:913, confidence 75) — anchor: crates/ironclaw_filesystem/src/libsql.rs:913
    delete_if_version now propagates record_version_to_i64(path, expected_version)?, but the libSQL contract tests only cover missing, stale, and current versions. No caller-level test passes an out-of-range RecordVersion and asserts CorruptRecordVersion instead of a silent mismatch or backend error.

  2. Medium Postgres CAS delete overflow branch is untested (crates/ironclaw_filesystem/src/postgres.rs:1642, confidence 75) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1642
    postgres_delete_if_version_with_client now propagates record_version_to_i64(path, expected_version)?, but the Postgres contract tests only cover missing, stale, and current versions. No caller-level test exercises the typed CorruptRecordVersion path for an out-of-range expected version.

Conventions

  1. Medium PR body still says HsmBackend is out of scope (crates/ironclaw_filesystem/src/hsm.rs:115-126, confidence 100) — anchor: .claude/rules/review-discipline.md:52
    The current PR body lists HsmBackend as out of scope, but the diff implements HsmBackend::delete_if_version and adds HSM coverage. The review-discipline rule says a PR's title and body must match its diff, so the description is now stale after the HSM fix landed.

Comment thread crates/ironclaw_filesystem/src/libsql.rs Outdated
Comment thread crates/ironclaw_filesystem/src/postgres.rs Outdated
// Single-key CAS delete: unlike `delete`, no subtree/event/sequence
// sweep. `is_dir = 0` scopes it to the record plane, matching `put`'s
// Version arm and `current_version_libsql`.
let expected_raw = record_version_to_i64(path, expected_version)?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — libSQL CAS delete overflow branch is untested.

delete_if_version now propagates record_version_to_i64(path, expected_version)?, but the libSQL contract tests only cover missing, stale, and current versions. No caller-level test passes an out-of-range RecordVersion and asserts CorruptRecordVersion instead of a silent mismatch or backend error.

Fix: Add db_root_filesystem_contract::libsql_delete_if_version_rejects_out_of_range_expected_version covering RecordVersion::from_backend(u64::MAX) returns FilesystemError::CorruptRecordVersion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 1792aeb: added db_root_filesystem_contract::libsql_delete_if_version_rejects_out_of_range_expected_version — RecordVersion::from_backend(u64::MAX) -> FilesystemError::CorruptRecordVersion, and the entry is confirmed to survive untouched (the guard fires in record_version_to_i64 before any DELETE runs). Mutation-checked: temporarily bypassing the guard (expected_version.get() as i64 instead of record_version_to_i64) flipped the assertion to a bogus VersionMismatch — confirmed RED for the right reason, then reverted.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in 1792aeb (round 4): libsql_delete_if_version_rejects_out_of_range_expected_version asserts RecordVersion::from_backend(u64::MAX) surfaces CorruptRecordVersion with the entry preserved. Predates this session; leaving unresolved for the reviewer of record.

path: &VirtualPath,
expected_version: RecordVersion,
) -> Result<(), FilesystemError> {
let expected_raw = record_version_to_i64(path, expected_version)?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Postgres CAS delete overflow branch is untested.

postgres_delete_if_version_with_client now propagates record_version_to_i64(path, expected_version)?, but the Postgres contract tests only cover missing, stale, and current versions. No caller-level test exercises the typed CorruptRecordVersion path for an out-of-range expected version.

Fix: Add db_root_filesystem_contract::postgres_delete_if_version_rejects_out_of_range_expected_version covering RecordVersion::from_backend(u64::MAX) returns FilesystemError::CorruptRecordVersion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 1792aeb: added db_root_filesystem_contract::postgres_tests::postgres_delete_if_version_rejects_out_of_range_expected_version — same shape as the libSQL sibling, env-gated (skips cleanly via postgres_root() -> None without a reachable DATABASE_URL/IRONCLAW_FILESYSTEM_POSTGRES_URL), asserts CorruptRecordVersion and that the entry survives untouched.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Same as the sibling libSQL overflow thread: fixed pre-session in 1792aeb (round 4) via postgres_delete_if_version_rejects_out_of_range_expected_version. Predates this session; leaving unresolved for the reviewer of record.

self.inner.delete(path).await
}

async fn delete_if_version(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — PR body still says HsmBackend is out of scope.

The current PR body lists HsmBackend as out of scope, but the diff implements HsmBackend::delete_if_version and adds HSM coverage. The review-discipline rule says a PR's title and body must match its diff, so the description is now stale after the HSM fix landed.

Fix: Update the PR body to include HsmBackend support, or remove the HsmBackend implementation and tests from this PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 1792aeb: updated the PR body via gh pr edit. Scope now lists every backend that implements delete_if_version (in-memory, libSQL, Postgres, HsmBackend) and every forwarding wrapper (ScopedFilesystem, CompositeRootFilesystem, MountScopedRootFilesystem in ironclaw_host_runtime, SkillManagementRootFilesystem in ironclaw_skills), notes the narrowed RecordVersion -> i64 overflow guard (CorruptRecordVersion before any statement runs), and states the ABA (version-not-generation-stable) invariant. Footer (🤖 Generated with [Claude Code](https://claude.com/claude-code)) preserved.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed pre-session in round 4 (1792aeb's commit message notes the PR-body sync). Confirmed the current PR body correctly lists HsmBackend as delegating to its inner backend, not out of scope. Predates this session; leaving unresolved for the reviewer of record.

Review round 4 (PR #5749): the zero-rows NotFound/VersionMismatch
diagnosis in delete_if_version ran as a second, separate statement on
both libSQL and Postgres, leaving a window for a concurrent
delete+recreate to commit in between and misclassify the outcome.

libSQL now wraps the conditional DELETE and the diagnosis SELECT in
one BEGIN IMMEDIATE/COMMIT transaction on a single connection (same
idiom as put). Postgres folds both into one WITH statement: a `locked`
CTE takes SELECT ... FOR UPDATE, and the `deleted` CTE depends on it so
the lock is held before the conditional delete runs; the SQL pin test
now asserts FOR UPDATE and the CTE dependency.

Also adds the missing out-of-range expected_version coverage on both
backends (RecordVersion::from_backend(u64::MAX) -> CorruptRecordVersion,
entry preserved), and syncs the PR body's stale HsmBackend-out-of-scope
claim with the actual diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 21:13 Destroyed

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add CAS-guarded delete_if_version support to RootFilesystem and supported filesystem backends for subagent await-edge delivery.

Stats: 4 findings (from 4 raw, 4 after dedup) across 3 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 2.

Bugs

  1. High Postgres CAS delete references a missing CTE column (crates/ironclaw_filesystem/src/postgres.rs:1651-1651, confidence 100) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1651
    DELETE_IF_VERSION_ATOMIC_SQL uses path IN (SELECT path FROM locked), but the locked CTE only selects version. Every Postgres delete_if_version call will fail at statement execution with an undefined-column error instead of deleting, returning NotFound, or returning VersionMismatch.

Tests

  1. Medium Default CAS delete Unsupported path is untested (crates/ironclaw_filesystem/src/root.rs:181-187, confidence 75) — anchor: crates/ironclaw_filesystem/src/root.rs:181
    RootFilesystem::delete_if_version adds the opt-in default that existing non-overriding backends rely on, but the added tests only call overriding backends or forwarding wrappers. A regression that changed the default Unsupported fallback would not fail.

Conventions

  1. Medium PR body still describes the old CasExpectation delete API (crates/ironclaw_filesystem/src/root.rs:181-185, confidence 100) — anchor: .claude/rules/review-discipline.md:52 (no diff position — body only)
    The diff exposes delete_if_version(path, RecordVersion), but the PR body still says CasExpectation::Absent fails closed for delete and references required_delete_version plus Any/Absent branch coverage. The PR body should describe the current RecordVersion-only API.

Local Patterns

  1. Low RecordVersion rustdoc still describes the old CAS surface (crates/ironclaw_filesystem/src/record.rs:180-182, confidence 75) — anchor: crates/ironclaw_filesystem/src/record.rs:180 (no diff position — body only)
    RecordVersion says versions are only compared via CasExpectation::Version, but this PR adds RootFilesystem::delete_if_version(path, RecordVersion) as a second public comparison path. Update the exported primitive docs to mention both usages.

Note: this contains a High finding that would normally use REQUEST_CHANGES, but GitHub rejects request-changes reviews on self-authored PRs, so this was posted as COMMENT.

Comment thread crates/ironclaw_filesystem/src/postgres.rs
Comment thread crates/ironclaw_filesystem/src/root.rs
henrypark133 and others added 4 commits July 7, 2026 15:18
…ault-Unsupported test

Round-5 review findings on PR #5749:

1. `DELETE_IF_VERSION_ATOMIC_SQL`'s `locked` CTE only projected `version`,
   while the `deleted` CTE's predicate referenced `path IN (SELECT path
   FROM locked)`. Verified live against Postgres 16 (EXPLAIN VERBOSE):
   Postgres does not error on the missing projection — it silently
   resolves the unqualified `path` as a correlated reference back to the
   outer DELETE's own row, making the join trivially self-referential
   rather than a real semi-join against `locked`. That happened to still
   return correct results here because the DELETE's other predicates
   already pin the exact row, but it's a latent correctness footgun tied
   to a design intent (force lock-then-delete sequencing via a genuine
   CTE dependency) that the implicit correlation doesn't actually
   provide. Fix: project `path` explicitly in `locked`'s SELECT list, so
   the semi-join is real. Confirmed identical behavior before/after via
   direct psql repro (delete-success, version-mismatch, path-absent) and
   the full live postgres_tests suite (76/76).

2. Added `delete_if_version_default_returns_unsupported` in root.rs,
   reusing the existing `DefaultBoundedBackend` test fixture, to pin the
   previously-untested trait-default `Unsupported` arm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y + ABA pin

Round-A self-review (3 sonnet subagents: bugs/correctness, concurrency,
tests) over the full PR diff. Bugs/correctness reviewer found nothing
beyond the already-fixed CTE bug. Fixes from the other two lenses:

Concurrency:
- libsql delete_if_version now validates expected_version (overflow
  guard) before self.connect()/BEGIN IMMEDIATE, so an out-of-range
  version fails closed before taking a pool checkout and the SQLite
  write lock — avoids needless contention under CAS storms.
- Reworded three test comments (in_memory.rs, db_root_filesystem_contract.rs
  x2) that overclaimed "concurrent-writer interleaving" for what are
  actually single-task sequential scripts; they now point at
  concurrent_cas_storm.rs for genuine parallel coverage.

Tests:
- Added a delete_if_version concurrency storm (all 3 backends):
  WRITERS-way contention racing to delete_if_version the same path at
  a known version across DELETE_STORM_ROUNDS recreate/delete cycles,
  asserting exactly one winner per round and no backend/infrastructure
  errors. The prior storm test only ever exercised put/cas_update
  concurrency — delete_if_version (this PR's new op, and the target of
  the BEGIN IMMEDIATE/FOR UPDATE atomicity fix) had none.
- Added delete_if_version_is_vulnerable_to_aba_across_delete_recreate_cycles
  in in_memory.rs, pinning the ABA hazard the trait doc comment already
  documents (version restarts at 1 after a full delete, so a stale
  token can match a later incarnation) — previously asserted only in
  prose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ ABA parity + postgres ordering

Round-B self-review (3 fresh sonnet subagents, same 3 lenses, over the
Round-A-updated diff, explicitly checking Round A for regressions).
Bugs/correctness and concurrency reviewers found no regressions from
Round A. Fixes from the tests lens plus one concurrency polish item:

- Round A's new delete_if_version concurrency storm (concurrent_cas_storm.rs)
  is real coverage for N-way contention/pool-exhaustion, but a sharp
  tests-lens finding showed it cannot discriminate a regression to the
  pre-1792aebb2 two-statement delete+diagnose race (every racer shares
  one pre-fetched version; nothing recreates the path mid-round, so
  ordinary single-row locking passes the test with or without that
  fix). Added a deterministic, non-flaky pin instead:
  libsql.rs::delete_if_version_diagnosis_reuses_the_delete_connection_under_a_size_one_pool
  builds a size-1 pool and drives the stale-version (0-rows) branch —
  if the diagnosis ever checks out a second connection, it deadlocks
  against itself and times out. Verified this discriminates: temporarily
  reintroduced a duplicate self.connect() and confirmed the test fails
  with a pool-checkout-timeout error, then reverted.
- Reworded run_delete_storm's doc comment to stop overclaiming it
  guards the atomicity fix; it now correctly describes what it proves
  (contention/pool-exhaustion correctness) and points at the new
  deterministic test and postgres's existing SQL-shape pin test for the
  atomicity regression itself.
- Tightened the storm's win assertion to a per-round check (was summed
  across all DELETE_STORM_ROUNDS, so a 0-win round and a 2-win round
  elsewhere could have canceled out) — Round-B concurrency finding.
- postgres delete_if_version now validates expected_version before
  self.client() (was after), matching libsql's Round-A fix for the same
  reason — consistency finding from the tests lens.
- Added libsql/postgres ABA-hazard pin tests mirroring in_memory.rs's
  Round-A test, since both DB backends share the same "version resets
  to 1 after full delete" precondition and had no equivalent pin.
- Added composite_delete_if_version_returns_mount_not_found, mirroring
  the existing append_batch mount-not-found test (LOW finding, cheap
  gap-fill).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 22:55
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 22:55 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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_filesystem/tests/concurrent_cas_storm.rs`:
- Around line 231-242: The helper that builds the Postgres-backed test setup is
swallowing configured failures in the URL parse, pool build, and migration path,
which turns real regressions into skipped tests. In the test setup function
around `PostgresRootFilesystem::new`, replace the `ok()?` and `is_err() { return
None; }` flow with explicit handling that returns a failure once
`IRONCLAW_FILESYSTEM_POSTGRES_URL` or `DATABASE_URL` is present but invalid.
Keep skipping only when no URL env var is set, and make `run_migrations()`
failures propagate loudly instead of converting them to `None`.
🪄 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: 66280d15-9a44-4b2d-aa52-28c6a21a0279

📥 Commits

Reviewing files that changed from the base of the PR and between 1792aeb and 769051c.

📒 Files selected for processing (7)
  • crates/ironclaw_filesystem/src/in_memory.rs
  • crates/ironclaw_filesystem/src/libsql.rs
  • crates/ironclaw_filesystem/src/postgres.rs
  • crates/ironclaw_filesystem/src/root.rs
  • crates/ironclaw_filesystem/tests/catalog_contract.rs
  • crates/ironclaw_filesystem/tests/concurrent_cas_storm.rs
  • crates/ironclaw_filesystem/tests/db_root_filesystem_contract.rs

Comment on lines +231 to +242
let url = std::env::var("IRONCLAW_FILESYSTEM_POSTGRES_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
else {
return;
};
// silent-ok: an unparsable URL means the environment's Postgres config
// isn't usable here; skip rather than fail on a config format issue
// this test doesn't own.
let Ok(config) = url.parse::<tokio_postgres::Config>() else {
return;
};
.ok()?;
let config = url.parse::<tokio_postgres::Config>().ok()?;
let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
// silent-ok: pool construction failure means the environment can't
// stand up a Postgres pool right now; skip rather than fail this
// storm test on infrastructure it doesn't own.
let Ok(pool) = deadpool_postgres::Pool::builder(manager)
let pool = deadpool_postgres::Pool::builder(manager)
.max_size(4)
.build()
else {
return;
};
.ok()?;
let root = Arc::new(ironclaw_filesystem::PostgresRootFilesystem::new(pool));
// silent-ok: an unreachable/misconfigured Postgres fails migrations;
// skip rather than fail this storm test on connectivity it doesn't own.
if root.run_migrations().await.is_err() {
return;
return None;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don’t turn configured Postgres failures into skipped tests.

ok()? and is_err() { return None; } make malformed DSNs, pool setup failures, and migration regressions pass vacuously. Skip only when no Postgres is configured; fail loud once a URL is present.

As per path instructions, “Fail loud: flag silent-failure patterns — .ok()? dropping errors.” As per coding guidelines, DB/IO reads must not use .ok()? on a Result without an explicit silent fallback reason.

Proposed fix
-    let url = std::env::var("IRONCLAW_FILESYSTEM_POSTGRES_URL")
-        .or_else(|_| std::env::var("DATABASE_URL"))
-        .ok()?;
-    let config = url.parse::<tokio_postgres::Config>().ok()?;
+    let url = match std::env::var("IRONCLAW_FILESYSTEM_POSTGRES_URL")
+        .or_else(|_| std::env::var("DATABASE_URL"))
+    {
+        Ok(url) => url,
+        Err(_) => return None, // silent-ok: optional Postgres storm tests need a configured DSN.
+    };
+    let config = url
+        .parse::<tokio_postgres::Config>()
+        .expect("configured Postgres storm URL must parse");
     let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
     let pool = deadpool_postgres::Pool::builder(manager)
         .max_size(4)
         .build()
-        .ok()?;
+        .expect("configured Postgres storm pool must build");
     let root = Arc::new(ironclaw_filesystem::PostgresRootFilesystem::new(pool));
-    if root.run_migrations().await.is_err() {
-        return None;
-    }
+    root.run_migrations()
+        .await
+        .expect("configured Postgres storm database must run filesystem migrations");
📝 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
let url = std::env::var("IRONCLAW_FILESYSTEM_POSTGRES_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
else {
return;
};
// silent-ok: an unparsable URL means the environment's Postgres config
// isn't usable here; skip rather than fail on a config format issue
// this test doesn't own.
let Ok(config) = url.parse::<tokio_postgres::Config>() else {
return;
};
.ok()?;
let config = url.parse::<tokio_postgres::Config>().ok()?;
let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
// silent-ok: pool construction failure means the environment can't
// stand up a Postgres pool right now; skip rather than fail this
// storm test on infrastructure it doesn't own.
let Ok(pool) = deadpool_postgres::Pool::builder(manager)
let pool = deadpool_postgres::Pool::builder(manager)
.max_size(4)
.build()
else {
return;
};
.ok()?;
let root = Arc::new(ironclaw_filesystem::PostgresRootFilesystem::new(pool));
// silent-ok: an unreachable/misconfigured Postgres fails migrations;
// skip rather than fail this storm test on connectivity it doesn't own.
if root.run_migrations().await.is_err() {
return;
return None;
let url = match std::env::var("IRONCLAW_FILESYSTEM_POSTGRES_URL")
.or_else(|_| std::env::var("DATABASE_URL"))
{
Ok(url) => url,
Err(_) => return None, // silent-ok: optional Postgres storm tests need a configured DSN.
};
let config = url
.parse::<tokio_postgres::Config>()
.expect("configured Postgres storm URL must parse");
let manager = deadpool_postgres::Manager::new(config, tokio_postgres::NoTls);
let pool = deadpool_postgres::Pool::builder(manager)
.max_size(4)
.build()
.expect("configured Postgres storm pool must build");
let root = Arc::new(ironclaw_filesystem::PostgresRootFilesystem::new(pool));
root.run_migrations()
.await
.expect("configured Postgres storm database must run filesystem migrations");
🤖 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_filesystem/tests/concurrent_cas_storm.rs` around lines 231 -
242, The helper that builds the Postgres-backed test setup is swallowing
configured failures in the URL parse, pool build, and migration path, which
turns real regressions into skipped tests. In the test setup function around
`PostgresRootFilesystem::new`, replace the `ok()?` and `is_err() { return None;
}` flow with explicit handling that returns a failure once
`IRONCLAW_FILESYSTEM_POSTGRES_URL` or `DATABASE_URL` is present but invalid.
Keep skipping only when no URL env var is set, and make `run_migrations()`
failures propagate loudly instead of converting them to `None`.

Sources: Coding guidelines, Path instructions

…oses

- Add a live two-connection Postgres integration test that drives the
  actual delete-then-recreate race delete_if_version's atomicity fix
  targets, asserting NotFound (not a misclassified VersionMismatch).
  Manually verified it fails against a reverted non-atomic
  implementation, confirming it's non-vacuous.
- Harden the SQL string-inspection test against two concrete mutants
  that kept its substring checks green while breaking atomicity
  (FOR UPDATE SKIP LOCKED, misplaced version guard).
- Add explicit-directory-row exclusion tests for delete_if_version on
  both DB backends (mirrors existing put-side coverage).
- Prove the libsql size-1-pool connection returns to a clean, reusable
  state after a VersionMismatch, not just that it didn't deadlock.
- Add delete=false-but-other-grants-true denial coverage at the
  MountScopedRootFilesystem layer (mirrors the ScopedFilesystem-layer
  test).
- Document why delete_if_version takes a bare RecordVersion instead of
  CasExpectation; fix an unverifiable PR-number comment reference.

Found by a fresh multi-lens review cycle (correctness/SQL-tracing,
concurrency/pool, test-adequacy, API-contract, security, maintainability)
run after 4 external + 2 internal review rounds; only test-adequacy
gaps surfaced, no correctness/concurrency/security majors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 7, 2026 23:30 Destroyed
@henrypark133

Copy link
Copy Markdown
Collaborator Author

Review-until-clean loop — converged after 2 cycles

Ran the maintainer-directed loop: 3 fresh sonnet reviewers per cycle over the full diff, rotating lenses, until a full cycle raises zero MAJOR findings.

Cycle 1 — correctness/SQL-tracing, concurrency/pool/txn, test-adequacy

Lens Majors Notable minors
Correctness (SQL-vs-schema, error-path walking) 0 none
Concurrency/pool/txn interplay 0 put's Version-CAS arm retains the pre-fix torn-diagnosis race (pre-existing, out of scope — deferred); asymmetric regression-pin strength (libsql = live deadlock-discrimination test, postgres = SQL string test only)
Test adequacy / can-this-test-fail 2 —

Cycle 1 majors (both fixed):

  1. delete_if_version_statements_are_single_round_trip_and_single_key (postgres.rs) is a string-substring test — two concrete mutants (FOR UPDATE SKIP LOCKED, misplaced version = $2) satisfy every assertion while breaking atomicity. Fixed: added assertions rejecting both, plus a genuine live two-connection Postgres integration test (postgres_delete_if_version_stays_notfound_under_concurrent_delete_recreate_race) that drives the real delete-then-recreate race and asserts the correct outcome. Manually verified non-vacuous by reverting the atomicity fix and confirming this new test goes red (VersionMismatch{found: Some(999)} instead of NotFound), then restored the fix.
  2. Postgres feature isn't exercised by this PR's CI gate (pre-existing, repo-wide CI wiring gap, not scoped to this PR) — deferred, noted for a follow-up; verified locally against live Postgres in this session instead.

Cheap minors also fixed: pool-recovery follow-up assertion on the libsql size-1-pool test; explicit-directory-row exclusion tests for delete_if_version on both DB backends (mirrors existing put-side coverage).

Cycle 2 — API-contract coherence, security/permission layering, maintainability

All three lenses: zero majors. Findings were all minor:

  • No doc note explaining why delete_if_version takes a bare RecordVersion instead of put's CasExpectation — fixed (added rationale to the trait doc comment).
  • A delete=true, read=false, write=false mount grant can learn a path's current version via VersionMismatch{found} without ever having read access — informational, not a permission-boundary bypass (caller already holds destructive rights to reach the path); deferred, not actionable without a broader permission-model change.
  • Test-matrix gap: the ScopedFilesystem-layer denial test (delete=false-but-other-grants-true) had no MountScopedRootFilesystem-layer counterpart — fixed, added hosted_scoped_virtual_filesystem_delete_if_version_denies_when_delete_missing.
  • One comment cited an unverifiable PR number (#5751) for the pool's one-checkout invariant — fixed, repointed to the actual tracked issue (#5466, per libsql_pool.rs).

Zero majors in cycle 2 → converged. (Stopped at 2 of the allotted 3 cycles.)

Final verification

  • cargo fmt — clean
  • cargo clippy -p ironclaw_filesystem -p ironclaw_host_runtime -p ironclaw_skills --all-features --tests --benches --examples — zero warnings
  • cargo test -p ironclaw_filesystem --all-features (live local Postgres) — 237/237 passing, including the new live-race test
  • cargo test -p ironclaw_skills --all-features — 219/219 passing
  • cargo test -p ironclaw_host_runtime --all-features — 299/302 passing; the 3 failures are the known-pre-existing /var/run/docker.sock-not-found failures in this sandbox (unrelated to this change)

Pushed as cb24f430f.

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Add CAS-guarded delete_if_version to RootFilesystem and supported backends for only-if-unchanged filesystem deletes.

Stats: 2 fresh findings (from 5 raw, 2 after dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0. Duplicate-covered by current live threads: 2. Merged same-line overlap: 1.

Local Patterns

  1. Low libSQL CAS-delete diagnosis can report a read operation (crates/ironclaw_filesystem/src/libsql.rs:1390, confidence 75) — anchor: crates/ironclaw_filesystem/src/libsql.rs:1390
    delete_if_version_libsql_inner reuses current_version_libsql for the zero-row diagnosis, but that helper maps query/row errors as FilesystemOperation::ReadFile; the sibling Postgres CAS-delete path maps the same delete-side query failure as Delete.

Tests

  1. Low Postgres corrupt stored version path is untested (crates/ironclaw_filesystem/src/postgres.rs:1695, confidence 75) — anchor: crates/ironclaw_filesystem/src/postgres.rs:1695
    The new Postgres diagnosis converts locked_version through record_version_from_i64, but the contract tests only cover out-of-range expected_version input, not a corrupt persisted row version.

Duplicate-Covered Raw Findings

  • The ABA/generation-stability security finding is already covered by unresolved thread PRRT_kwDORHZ7Z86PBheo.
  • The Postgres storm setup .ok()?/silent-skip finding is already covered by unresolved CodeRabbit thread PRRT_kwDORHZ7Z86PEmVd.

// 0 rows: absent row → NotFound (already gone, benign); row present
// at another version → VersionMismatch (gone stale). Distinct from
// put's diagnosis, which collapses absent into VersionMismatch.
if let Some(found) = current_version_libsql(conn, path).await? {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — libSQL CAS-delete diagnosis can report a read operation.

delete_if_version_libsql_inner reuses current_version_libsql for the zero-row diagnosis, but that helper maps query/row errors as FilesystemOperation::ReadFile. The sibling Postgres CAS-delete path maps the same delete-side query failure as FilesystemOperation::Delete, and the surrounding libSQL delete statements also use Delete, so a libSQL diagnosis failure from a delete call would surface with the wrong operation shape in errors and telemetry.

Fix: Either pass the caller operation into current_version_libsql or add a delete-specific current-version helper so the delete_if_version diagnosis maps backend errors with FilesystemOperation::Delete.

Also flagged by: tests/Low (libSQL corrupt stored version path is untested)

return Err(FilesystemError::VersionMismatch {
path: path.clone(),
expected: Some(expected_version),
found: Some(record_version_from_i64(path, raw)?),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Postgres corrupt stored version path is untested.

The new Postgres diagnosis converts locked_version through record_version_from_i64, but the contract tests only cover out-of-range expected_version input. No test corrupts the persisted row version and verifies delete_if_version returns CorruptRecordVersion from this new propagation path.

Fix: Add db_root_filesystem_contract::postgres_tests::postgres_delete_if_version_reports_corrupt_found_version covering a stale delete against a row whose stored version is negative.

Copilot AI review requested due to automatic review settings July 8, 2026 03:10
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5749 July 8, 2026 03:11 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

henrypark133 added a commit that referenced this pull request Jul 8, 2026
* docs(reborn): canonical subagent thread-harness design; supersede WU-C durability spec

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

* docs(reborn): fix stale delivery-layer pointers, WebUI authz, roster race, safety-scan claims

- Add supersession banners to phase-1/2/3 docs and tighten README pointers
  so implementers land on thread-harness-design.md for the delivery/
  durability layer instead of the dead tombstone/reconciler/gate-store path.
- thread-harness-design.md: state the children-endpoint authorization
  contract (owner-bound TurnScope resolution, no existence oracle) and add
  the cross-user not-found case to the required P5.2 test.
- thread-harness-design.md §4.0: close the residual roster-prune race with
  two compensating self-heal checks (spawn-side and boot-side) and split the
  required test accordingly.
- Correct README's "child output is safety-scanned" claims (§6 table, §8
  item 7) — no SafetyLayer wiring exists on this ingress path today; describe
  the actual controls (framing/sanitisation + approval-bubbling) and point at
  the tracked platform-wide follow-up.

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

* docs(reborn): fold 4 maintainer amendments into thread-harness design

Gate resolution now accepted from any owner-authenticated surface
(surfacing stays root-only, CAS'd single-winner makes two doors safe);
abandon is mode-scoped (background parent-completion is normal
delivery, not abandonment); new §6a covers human-priority reservation
+ interrupt/take-over with no content queue; capacity semantics made
explicit (non-terminal cap counts gate-parked children deliberately,
never release-on-park, subagent_extend re-claims at admission).

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

* docs(reborn): round-2 review fixes for subagent thread-harness design

Apply 7 review findings plus one import from PR #5749: header metadata
shape, bounded boot-recovery concurrency + roster cardinality, a
roster-before-edge-consistent rewrite of the residual delete/roster race
(and its test), batched (not per-edge) settled-drain transcript writes,
a bounded LIMIT-8 run-budget query in place of an open-ended walk, a
lease (owner+expires_at, lazy expiry) on the human-priority reservation
marker, in-path agent/project axis encoding for await-edge and roster
paths (mount alone only partitions tenant/user), and the delete_if_version
CAS contract in the filesystem contract doc. Thermo-reviewed twice: first
pass caught an off-by-one in the budget query and a placeholder-spelling
drift, both fixed and re-verified clean.

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

* docs(reborn): round-3 review fixes for subagent thread-harness design

Ten review findings: lazy-recovery admission rejection instead of
inline recovery, two-trigger ThreadBusy-heal test, run-budget boundary
tests, delete_if_version fail-closed TDD cases + ScopedFilesystem
sketch + default-Unsupported body, reservation-release-before-delete
ordering, and flattening the scope roster to one file per scope (the
prior nested-directory encoding made a single global list_dir miss
every scope not directly under the roster root).

Plus three verdict items from a parallel review lane: delete_if_version
narrows to (path, expected_version: RecordVersion) with no
CasExpectation param, an ABA caller-invariant for recreated paths, and
LiveSourceRoute gate-kind dispatch so approval/auth prompts in a
shared-conversation tree page the owner's preference target instead of
the shared channel.

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

* docs(reborn): round-4 review fixes + full-document thermo pass

Three maintainer-flagged findings on the subagent thread-harness
design:

- Roster filename delimiter collision: validate_scope_id has no
  underscore restriction, so the flattened `__`-joined scope key could
  genuinely collide (e.g. tenant="a__b"/user="c" vs tenant="a"/
  user="b__c" both produce the same prefix). Each component is now
  percent-encoded before the join, with a named collision regression
  test.
- Close-sequence stale version: the 3-step edge close named the wrong
  version token for delete_if_version — using the terminal-state CAS's
  version instead of the reservation-release CAS's (the last write),
  which would fail VersionMismatch on every close. Fixed consistently
  across §2, §4.0, and §5.5.
- Lazy recovery fan-out: per-scope lazy recovery tasks now share the
  same BOOT_RECOVERY_MAX_CONCURRENT_SCOPES limiter as the boot pass
  instead of fanning out unbounded; foreground admission still never
  blocks on it.

Followed by a full-document (not diff-scoped) thermo-nuclear pass per
maintainer request: fixed stale delete_if_version line citations drifted
from the still-open shipped PR, added the missing MountScopedRootFilesystem/
SkillManagementRootFilesystem/HsmBackend wrappers to the passthrough list
(verified against origin/feat/filesystem-delete-if-version), and added
P3.3 (per-flavor model override) to the PR3 staging row it was missing
from.

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

* docs(reborn): round-5 review fixes for subagent thread harness design

Addresses 5 PR review findings on thread-harness-design.md:
- Mark stale general/researcher flavor naming in sibling docs as
  historical, pointing to §10 (current: general/explorer/coder/planner).
- Escalate the child->parent drain safety-scan gap to a hard PR2
  prod-enable gate, with a small drain-scoped fallback scan if platform
  SafetyLayer ingress wiring hasn't landed yet.
- Extend the boot-recovery limiter with a bounded pending queue and
  per-tenant in-flight cap, so one tenant's cold-boot burst can't starve
  others.
- Shard the scope roster into 256 blake3-hash-prefix directories so boot
  enumeration is memory-bounded instead of one unbounded global list_dir;
  reject query()+Page (OFFSET-based, unsafe under concurrent mutation)
  and materialize-with-tripwire alternatives with rationale.
- Close a roster-prune crash window by making spawn's post-edge self-heal
  an unconditional version-bumping touch instead of get-then-put-if-absent.

Thermo-nuclear pass + two rounds of adversarial self-review (mechanism-
correctness, implementability, internal-consistency) surfaced and fixed
further issues: a double-release race in the reservation-release tri-state
(§5.5) now closed by a bounded, edge-lifecycle-scoped idempotency key
rather than the generic LRU-evictable idempotency cache; an invented
"admission token" reference corrected to the real TurnRunId already in
scope; stale "single global list_dir" phrasing left behind in three spots
after the sharding change; and a stale line-count citation.

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

* docs(reborn): round-6/7 convergence fixes for subagent thread-harness design

Three review-until-clean cycles (9 fresh reviewers) over the full doc set.

Canonical design (thread-harness-design.md):
- §5.5 round-7: released_children dedup entry pruned BEFORE delete_if_version
  (two records, no spanning txn — delete-then-prune orphaned entries on crash,
  breaking the boundedness claim); check-insert-decrement pinned as one atomic
  single-record CAS; stalled in-flight releaser named as accepted residual
  with a two-party invariant and a detection story; crash-injected tests
- §8.3 new: derived System-wake streak cap (K=16, Human resets) closes the
  unbounded spawn→settle→wake cascade; subsumes never-built
  AutonomousContinuationBudget; honest liveness bound + thread-wide trade-off
- §3: retires all three dead production_readiness sibling fields (tombstone,
  autonomous-budget :363, restart-reconciler :364) with verified citations
- §4.5: roster prune no longer boot-only (close-path opportunistic prune);
  write-ordering superset base case pinned by test
- §6/§7: subagent_activation_provenance field pulled forward to PR2 (P2.4) —
  fixes a staging inversion where PR2's cap read a PR4 field
- §9.2/§6a: owner check named as surface-layer requirement with non-owner
  rejection tests; human_waiting reservation owner-gated
- §8.2: trigger-2 Continue-only scope note; trigger-3 drains without activate
- §10: required tests added for per-flavor budget/model overrides
- citation fixes: llm_admin/llm_key_store path, row_store path,
  resume_turn_once :2616, over-release floor :1716-1720

Sibling docs: README/phase-1/2/3 superseded annotations completed for every
RestartReconciler/AutonomousContinuationBudget/#4147 mention (6 previously
un-annotated contradictions); phase banners gain point-in-time drift
disclosures with verified shipped names; phase-1 banner scope widened to
§3.2/§3.3/§3.6; contracts/filesystem.md sketch uses FilesystemOperation::Delete
(no DeleteIfVersion variant exists); static-vs-dynamic diagram relabeled to
await-edge files and regenerated.

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

* docs(reborn): cycle-4 certification fixes for thread-harness design

Cycle-4 fresh-reviewer pass (whole-system, mechanism re-walk, adversarial)
found 1 major + 3 cheap minors:

- MAJOR: $4.0 Consequences (b) still described the roster prune as
  boot-only, contradicting $4.5's round-7 close-path-prune ruling in the
  same document — now caller-agnostic with an explicit pointer.
- $6a/$9.2: 'README $8.2' citations pointed at a nonexistent subsection;
  now 'README $8 item 2 (Approval ownership)'.
- $8.3: young-thread under-K admit branch stated explicitly, mirroring
  $6's spelled-out rule instead of leaving it implied.
- $13 cross-map $4.3 row: round-6 boot-pass-drop clause added for
  symmetry with the round-4/5 entries.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@henrypark133
henrypark133 merged commit fab83e9 into main Jul 8, 2026
62 checks passed
@henrypark133
henrypark133 deleted the feat/filesystem-delete-if-version branch July 8, 2026 04:01
@ironclaw-ci ironclaw-ci Bot mentioned this pull request Jul 8, 2026

This branch was successfully deployed

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants