Skip to content

fix(engine): run the sweep as an idle job and admit a grafted version restore - #1917

Merged
FSM1 merged 7 commits into
mainfrom
fix/idle-sweep-and-grafted-restore
Sep 19, 2026
Merged

FSM1 merged 7 commits into
mainfrom
fix/idle-sweep-and-grafted-restore

Conversation

@FSM1

@FSM1 FSM1 commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Part of #1911.

This change also lets a write grantee restore a prior version of a file below a proved grafted write root. The owner decision for this is #1886 (comment): a version restore publishes a new head from a prior version and deletes no version, the same as an edit. Bin restore, purge, prune and version delete stay refused.

Part 1: the idle sweep job

Problem

blueprint/engine.md "sweep" says that the sweep runs as an idle-cadence Scheduler job. run_sweep_job had no production caller. The only sweep was the task that a cut enqueues, capped at three passes. A sweep that failed, or a restart after a cut, left interior nodes at the old epoch until a write reached them.

Change

  • Engine::start spawns the idle sweep job (spawn_sweep_job in crates/engine/src/facade.rs). Each sweep_cadence it runs one pass over each due scope.
  • A scope is due when all of these are true:
    • This vault owns it: the vault root at its held name, or a scope root that the own-vault boundary walk proved (walked_read_epochs). A grafted scope is never a target.
    • Its known read epoch is above GENESIS_EPOCH. For the vault root the epoch comes from the durable epoch floor. For a nested scope it comes from the walk.
    • No pass of this session confirmed it converged at that epoch.
  • Nothing new is durable. The pending state comes from state the engine already holds: after a restart every scope past its genesis epoch is due one time, and the pass computes the work-list from published records, as before.
  • One Sweeper builds the owner rotation net for both the cut task (three passes) and the idle job (one pass per round). run_sweep_job in crates/engine/src/rotation/sweep.rs now takes the per-scope sweep as a closure, because each scope needs its own ascent authority.
  • A write grantee does not run the sweep. blueprint/engine.md says that the sweep is "Runnable by any write-capable client", but this change wires the job only for the scopes that the owner's own vault holds. A write grantee's session runs the job only over the scopes of its own vault, never over a grafted scope.
  • The idle job reports each result through SweepRun: Swept(result), or SessionEnded when the session keys are gone. The job then stops.
  • The lagging read arm (ADR 0021) is not in this change. fix(engine): an epoch-lagged file does not read after a cut #1911 stays open for it.

Tests

In crates/engine/tests/mount_convergence.rs, new section "The lazy wave a cut leaves behind":

  • the_sweep_a_cut_enqueues_re_seals_the_folder_it_left_behind: the test drives the tasks that RotateNow spawned, with no clock advance, and the folder is re-sealed at the new epoch.
  • after_a_restart_the_idle_sweep_converges_what_a_failed_sweep_left: the cut's own sweep fails (all endpoints fail), the device restarts, and the idle job converges the lagging folder with no write to it.
  • a_read_only_member_never_runs_the_wave: a read grantee runs three sweep cadences after the owner's cut and no record on any endpoint changes. The owner's idle job then re-seals the folder.

The restart test and the read-only test fail when the job is not spawned. crates/engine/tests/facade.rs and the facade loop test now count the job among the tasks that start spawns, and check that it stops when the engine drops.

Part 2: version restore below a proved grafted write root

Verdict: admitted

Command::RestoreVersion journals OpKind::RestoreVersion. The drain arm publish_restore_version (crates/engine/src/sync/drain.rs) rotates the named version to the head of the file's own versions list and republishes the file record. It keeps every version, prunes nothing, and calls no vault surface: no bin index, no retire ledger, no doomed-name journal. So it is a write like a new version, and it is not a bin restore. The facade now admits it for a proved write pass. DeleteVersion, Restore, and Purge stay refused.

Tests

  • Facade (WriteGrantee harness, real facade and real drain): a_write_grantee_authors_every_admitted_write_inside_the_granted_scope restores the first version and checks the head and the prior list. The version restore case moved out of a_write_grantee_is_refused_what_leaves_the_scope_or_reaches_the_owners_surfaces.
  • Two engines: a_write_grantees_version_restore_reaches_the_owner in crates/engine/tests/mount_convergence.rs. The write grantee restores the prior version of a file that the owner wrote. The owner reads the restored content as the head and the outgoing head as the prior version.
  • Both tests fail when the refusal comes back.

Web

DetailsDialog, FileDetails and VersionHistory take one required prop, access: ScopeAccess ('owner' | 'write-grant' | 'read-only'), in place of two booleans. offers(access, command) is the one rule for which version write each access shows: owner shows restore and delete, write-grant shows restore only, read-only shows neither. DetailsDialog also closes an open confirmation when the new access does not offer its command.

Tests in versions.test.tsx:

  • offers a restore and no delete in a share granted for writing.
  • retires a delete confirmation and keeps a restore one when the access drops to a write grant.

Shared file

crates/engine/tests/mount_convergence.rs is also changed by #1914 (lane G20). This change adds its tests in the middle of the file, before the section "A write staged across a cut", and does not change any function that #1914 changes. A move of the new tests to a separate file needs a shared helper module for about 15 helpers of that file, so they stay in it.

Local runs

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo clippy -p cipherbox-engine -p cipherbox-wasm --target wasm32-unknown-unknown -- -D warnings, cargo test -p cipherbox-engine (all pass), pnpm -r typecheck, pnpm lint, pnpm lint:tracker-refs, and the apps/web vitest suite (763 tests).

Reviews run before the draft

  • Inline simplify, security and crypto-privacy passes. No new primitive, KDF edge, wire field or durable record.
  • CodeRabbit CLI (--agent --base-commit): 1 minor finding. The test snapshot read only the first endpoint. Fixed: it now reads every endpoint.
  • After the /code-review findings: inline simplify and code-review (Standards and Spec) passes, then CodeRabbit CLI (--agent --base-commit) with 0 findings.

Body checks / follow-ups filed

Note

Run sweep as an idle background job and allow version restore from grafted write scopes

  • Adds an idle sweep job spawned at session start that periodically discovers owned scope targets and runs one sweep pass per due target, recording convergence until a newer epoch makes the target due again (crates/engine/src/facade.rs)
  • Generalizes the Sweeper and run_sweep_job to accept a per-invocation pass cap and per-target sweep callback, returning SweepRun to distinguish a completed sweep from SessionEnded (crates/engine/src/rotation/sweep.rs)
  • Removes the graft refusal from the RestoreVersion command path so a proved write grantee can stage a version restore; DeleteVersion remains graft-refused
  • Replaces the boolean writable prop with a three-way ScopeAccess classification (owner, write-grant, read-only) across DetailsDialog, FileDetails, and VersionHistory, making version restore available to owners and write grantees while delete stays owner-only
  • Pending confirmations in DetailsDialog are now retired command-specifically when access drops below the required level
  • Behavioral Change: session startup now spawns three background tasks (liveness, resolve-tick, idle sweep) instead of two; the idle sweep job runs at sweep cadence and exits on SessionEnded. Write grantees can now restore versions, authoring a new version position without rewinding history

Macroscope summarized e1477c5.

Summary by CodeRabbit

  • New Features

    • Added more precise sharing permissions for file version actions.
    • Users with write access can restore earlier versions, while deleting versions remains owner-only.
    • Read-only users can continue viewing file details and version history.
    • Background synchronization now continues after changes and can recover when devices reconnect.
  • Documentation

    • Updated shared-scope guidance to clarify unavailable owner-only sharing and version-deletion actions.
  • Bug Fixes

    • Version restores now correctly update the current file and preserve the previous version in history.

@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Warning

Review limit reached

Next included review available in 31 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: FSM1/cipher-box/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 2ff76790-614c-4b0f-8acd-0c1fe361adb6

📥 Commits

Reviewing files that changed from the base of the PR and between e1477c5 and 65fce80.

📒 Files selected for processing (3)
  • crates/engine/src/rotation/sweep.rs
  • crates/engine/src/rotation/sweep/sim.rs
  • crates/engine/src/rotation/sweep/tests.rs

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: FSM1/cipher-box/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 64269fb9-36ca-4515-8229-e63d1e787d11

📥 Commits

Reviewing files that changed from the base of the PR and between d1b9b94 and e1477c5.

📒 Files selected for processing (14)
  • apps/web/src/components/file-browser/DetailsDialog.tsx
  • apps/web/src/components/file-browser/FileBrowserActions.tsx
  • apps/web/src/components/file-browser/details/FileDetails.tsx
  • apps/web/src/components/file-browser/details/VersionHistory.tsx
  • apps/web/src/components/file-browser/details/details.test.tsx
  • apps/web/src/components/file-browser/details/versions.test.tsx
  • crates/engine/src/facade.rs
  • crates/engine/src/lib.rs
  • crates/engine/src/rotation/mod.rs
  • crates/engine/src/rotation/sweep.rs
  • crates/engine/src/rotation/sweep/tests.rs
  • crates/engine/src/testkit/fakes/record_store.rs
  • crates/engine/tests/facade.rs
  • crates/engine/tests/mount_convergence.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The web file details flow now uses scoped access for version commands. The engine now supports reusable sweep jobs, idle convergence sweeps, and write-granted version restores. Tests cover permission transitions, background tasks, restoration, and convergence.

Changes

Scope-aware version access

Layer / File(s) Summary
Access model and permission wiring
apps/web/src/components/file-browser/...
Details components now pass ScopeAccess values from file-browser actions through version history.
Command-specific controls and confirmation state
apps/web/src/components/file-browser/details/DetailsDialog.tsx, apps/web/src/components/file-browser/details/VersionHistory.tsx
Restore and delete controls use separate access checks. Pending confirmations close only when their command is unavailable.
Access behavior coverage
apps/web/src/components/file-browser/details/*test.tsx
Tests cover owner, write-grant, and read-only access, including confirmation transitions.

Idle sweep processing and restore convergence

Layer / File(s) Summary
Sweep job contract
crates/engine/src/rotation/sweep.rs, crates/engine/src/rotation/sweep/tests.rs, crates/engine/src/{lib.rs,rotation/mod.rs}
SweepRun represents sweep results or session termination. run_sweep_job now receives caller-supplied sweep logic.
Idle sweep integration
crates/engine/src/facade.rs, crates/engine/src/testkit/fakes/record_store.rs
The facade builds sweepers, discovers owned targets, starts the idle job, and tracks convergence.
Grafted restore behavior
crates/engine/src/facade.rs, crates/engine/tests/mount_convergence.rs
Version restores use the normal write path. Tests verify restored heads and retained history.
Background task and convergence tests
crates/engine/src/facade.rs, crates/engine/tests/{facade.rs,mount_convergence.rs}
Tests cover idle-job lifecycle, failed sweeps, delayed convergence, spawned re-sealing, and read-only sessions.

Priority: ➖ Normal

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

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Engine
  participant IdleSweepJob
  participant Sweeper
  participant ScopeRecords
  Engine->>IdleSweepJob: start idle sweep job
  IdleSweepJob->>Engine: discover owned unconverged scopes
  IdleSweepJob->>Sweeper: sweep each target
  Sweeper->>ScopeRecords: re-seal unsettled records
  Sweeper-->>IdleSweepJob: return SweepRun
  IdleSweepJob-->>Engine: continue at sweep cadence
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 13 files. (1 skipped: 1…
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two primary changes: running the sweep as an idle job and allowing grafted version restores. It is concise and specific.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

The sweep ran only as the task a cut enqueues, capped at three passes.
A sweep that failed, or a restart after a cut, left interior nodes at
the old epoch until a write reached them.

The engine now spawns the idle sweep job at start. Each sweep cadence it
runs one pass over every scope this vault owns that is past its genesis
epoch and that no pass of this session confirmed converged at its
current epoch. Nothing new is durable: a restart starts with every such
scope due. A granted scope is never a target.

Part of #1911.
A version restore reorders the file's own history: the named version
becomes the head and the outgoing head becomes the newest prior
version. It drops no version, prunes nothing and touches no bin, so it
is a write like a new version. The facade now admits it for a proved
write pass. A version delete stays refused.

The web details dialog shows the restore control again in a share
granted for writing, and still hides the version delete there.
@FSM1
FSM1 force-pushed the fix/idle-sweep-and-grafted-restore branch from a380c82 to e1477c5 Compare September 19, 2026 12:23
@FSM1
FSM1 marked this pull request as ready for review September 19, 2026 16:53
@FSM1

FSM1 commented Sep 19, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

The PR appears safe to merge; the previously reported premature-convergence path is now fully corrected and no new actionable issue remains.

Summary

This revision fixes the previously reported sweep-convergence bug by preserving index-repair CAS races as retryable work.

  • Records a lost index-repair race in SweepOutcome.
  • Includes that state in worth_another_pass(), preventing the idle job from marking the scope converged prematurely.
  • Adds focused coverage showing both the one-pass retry signal and eventual repair on a later pass.
  • The previous finding was also manually resolved and is no longer outstanding.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Sweep detects missing child-scope index] --> B[Attempt index repair]
    B -->|Publication succeeds| C[Report repaired index]
    B -->|CAS race lost| D[Set index_repair_lost_race]
    D --> E[worth_another_pass returns true]
    E --> F[Idle sweep keeps scope due]
    F --> A
Loading

Reviews (2) · Last reviewed commit: "fix(engine): keep a scope due when its i..."

Comment thread crates/engine/src/facade.rs
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

A sweep pass whose child-scope index repair lost the CAS now reports
index_repair_lost_race, and worth_another_pass counts it. The idle job
then does not record the scope as converged, so a later round re-derives
and republishes the index.
@FSM1
FSM1 merged commit f4d6aa9 into main Sep 19, 2026
43 checks passed
@FSM1
FSM1 deleted the fix/idle-sweep-and-grafted-restore branch September 19, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant