Skip to content

feat(machine): force-stop machines and native Sandboxes; keep parallel Store writes safe - #656

Merged
yifanxuaaa merged 8 commits into
layerfs_v2_0_0from
integrate/v2-force-stop-latest
Nov 14, 2026
Merged

yifanxuaaa merged 8 commits into
layerfs_v2_0_0from
integrate/v2-force-stop-latest

Conversation

@yifanxuaaa

@yifanxuaaa yifanxuaaa commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Tier: P1; gate: yes (risk:store, risk:format, Store-access gate by hand); owner approved with acceptance of A, N1 and 10 (see comments).
Docs: api/sdk/machine.md (machine.stop force), api/sdk/sandbox.md (forced native close), engine/store/sqlite.md (Last close), control/sandboxes/README.md (record fields), glossary.md.
Durable: NOT_RUN — disabled by owner until explicit reauthorization.

Update (head d303c7b)

Brought up to layerfs_v2_0_0 (27920d3) and reworked after an independent review (records in the comments): read-only Store closes take no lane; native force-stop refusals before a signal stay certain and the stop continues past them; forced native close is gated on a live native holder and records a stopped runtime; record format, Docker refusal and close rules documented.

Evidence at this code: Clippy -D warnings clean on all changed crates (macOS and Linux); macOS 57 tests and live machine_force_native passed; Linux 38 tests passed; Linux mount lane 45/45 binaries, 116 tests passed. Not run: live Docker/Lima providers.

Production LOC against layerfs_v2_0_0 at merge: 160621 -> 161187 (delta +566); core 39787 -> 39787, engine 74765 -> 74818, control 40093 -> 40602, api 5976 -> 5980 (tools/production_loc.py, blob 35e4a6d601fa).


Summary

Adds an explicit force option to machine.stop and makes sandbox.close --force work on native without a Store or daemon. Both stop the runtime through its provider or its own records, never through the machine's worker, and keep disks, Stores, saved history and runtime records.

The branch is three commits on the current layerfs_v2_0_0 tip (03883a63cf, #655):

  1. 1b22005b40: port of the force-stop and native concurrency work (7b5f5a5630) onto v2.
    • machine.stop --force stops providers without a live worker. Sandboxes on the machine are recorded gone, or unresolved when the stop could not be confirmed, with their Workspace frontiers reported unknown.
    • Restores macOS writer and Store lifecycle admission, and treats a stale lease that disappears as already released.
  2. 9d30a87ad2: merges fix(sandbox): carry a full-pool capacity notice into sandbox.get (#649) #655 (no conflicts).
  3. eda9c04f12: forced sandbox.close on native from the Sandbox's own records: no Store open, daemon control call, active-Workspace refusal or retry. The forced native machine.stop uses the same host builder. This is the remaining part of c1cf6fd169.

Validation

macOS, registered private worktree (tools/worktree.py):

  • Warning-denying all-targets Clippy:
    • layerfs-sandbox, layerfs-control, layerfs-api, layerfs-machine, layerfs-platform, layerfs-store, with postgresql,machine;
    • layerfs-cli with postgresql;
    • layerfs-daemon.
  • Live machine_force_native with a matching private daemon: the forced native machine.stop and sandbox.close end the exact daemon, command and mount without opening the Store. Saved data stays readable.
  • Passing suites:
    • layerfs-api machine_* / sandbox_*: 34
    • layerfs-platform machine_force / machine_lima: 17
    • layerfs-persistence writer_admission / sqlite_lock_custody: 12
    • layerfs-sandbox capacity_read / pool_admission: 8
    • layerfs-api sandbox_capacity: 8
    • layerfs-cli capabilities / stream_framed: 7
    • layerfs-sandbox-backend local_stop_mount: 1
    • layerfs-store lease_race: 1
  • Discovery: layerfs help --json advertises force on machine.stop and sandbox.close.

Not run here: Linux/Docker E2E bundles.

Production LOC

  • 1b22005b40: 158658 -> 159105 (delta +447)
  • 9d30a87ad2 (first parent): 159105 -> 159152 (delta +47)
  • eda9c04f12: 159152 -> 159204 (delta +52)

Scope: first-party core, engine, control and API product sources. Method: pinned tools/production_loc.py --root <git archive snapshot> --json.

🤖 Generated with Claude Code

yifanxuaaa and others added 8 commits November 12, 2026 11:34
…writes

Port the verified force-stop and native concurrency changes onto current
layerfs_v2_0_0. Stop providers without a live worker, retain saved data and
runtime registration custody, and advertise machine.stop force.

Restore macOS writer and Store lifecycle admission, and treat disappearing
stale leases as already released. Preserve upstream Sandbox recovery slots,
pool/capacity fixes and API retirements; omit obsolete image override handling.

Ported-from: 7b5f5a56305f6aeb10fb8d96cf15d7ee71c6c316

Validation: PG CLI build and warning-denying all-targets Clippy; API dead-worker
force; live mounted/running native machine and Sandbox force using a matching
private daemon; upstream bridge admission/setup and pool/capacity tests.
Before the unrelated upstream recovery-slot merge, the unchanged persistence,
lease, provider and CLI scopes passed 20 native, one lease, 24 provider and seven
CLI discovery/framing tests; boundary and 182 tooling tests also passed.
The old test daemon's private-config refusal was diagnosed as upstream's
LFSD08-to-LFSD09 startup protocol change; its matching-daemon rerun passed.
No mutation retries were added.

Production LOC: 158658 -> 159105 (delta +447)
Scope: all first-party core, engine, control and API product sources including
runtime SQL, Python and JavaScript; excludes tests, docs, tooling and generated
artifacts. Method: tools/production_loc.py --root <git-archive snapshot> --json
on parent dbd633f and staged tree 9b47c8d.
Counter SHA256: d3148db88f91b09eb74eb659b2f700209776a24d022f09774dc7bb38ea1eecf6
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).

Assisted-by: Cindy (https://cindy.app)
Bring the force-stop port (1b22005, ported from 7b5f5a5630) up to the
current layerfs_v2_0_0 tip 03883a6: #649 carries a full-pool capacity
notice into sandbox.get. Merged without conflicts; the tree is identical to
the resolution staged in layerfs-force-stop-v2-push.

Production LOC: 159105 -> 159152 (delta +47)
Scope: first-parent comparison (1b22005 -> merge); all first-party core,
engine, control and API product sources; tests, docs, tooling and generated
artifacts excluded. Method: pinned tools/production_loc.py (blob 35e4a6d)
--root <git archive snapshot> --json on both snapshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A forced sandbox.close on native now stops the Sandbox from its own
records: no Store open, daemon control call, active-Workspace refusal or
retry. The backend Stop checks the runtime identity and ends its mounts and
processes; Delete checks the physical detach before the runtime goes. Each
recorded or held Workspace is answered as unknown. This is the remaining
part of c1cf6fd169 that the 7b5f5a5630 port did not carry. The forced
machine.stop on native shares the same host builder
(layerfs_sandbox::force::native_host). Also unroll two single-element loops
in the machine_force test, which warning-denying Clippy refused.

Validation (registered private worktree, macOS):
- warning-denying all-targets Clippy: layerfs-sandbox, -control, -api,
  -machine, -platform, -store (postgresql,machine); layerfs-cli
  (postgresql); layerfs-daemon.
- layerfs-api machine_force and live machine_force_native with a matching
  private daemon (native machine.stop and sandbox.close force end the exact
  daemon, command and mount without opening the Store): passed.
- layerfs-api sandbox_* and machine_* suites: 34 passed. Platform
  machine_force/machine_lima 17, sandbox-backend local_stop_mount 1, store
  lease_race 1, persistence writer_admission/sqlite_lock_custody 12, CLI
  capabilities/stream_framed 7, sandbox capacity_read/pool_admission 8,
  api sandbox_capacity 8: all passed.
- The debug CLI's help --json advertises machine.stop and sandbox.close
  force.

Production LOC: 159152 -> 159204 (delta +52)
Scope: all first-party core, engine, control and API product sources;
tests, docs, tooling and generated artifacts excluded. Method: pinned
tools/production_loc.py (blob 35e4a6d) --root <git archive snapshot>
--json on the parent and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #656 up to the v2 tip (64 commits). Conflicts and how they were
resolved:

- writer_admission.rs, lane.rs: both sides enabled the macOS directory
  lane identically; took base (#647).
- lease.rs: base #666 already treats a stale lease its holder dropped as
  released (remove_stale); took base. The PR's lease_race test still
  passes and is kept.
- connection.rs: took base's open rule (#647): a writable open takes the
  lane and releases it after the profile check; a read-only open takes
  none. Kept the PR's ConnectionSlot and State.path so the last-close
  custody in session/lifecycle.rs still applies.
- seal.rs: kept the PR's checked close through ConnectionSlot (a failed
  close restores the connection) and base's order: the lane is held
  through the sidecar check and released after it.
- glossary.md: took base (already says Linux and macOS).
- docs: #671 deleted api/sdk/machine/{README,stop}.md and
  engine/store/backends.md. Their PR content moved to api/sdk/machine.md
  (machine.stop force), api/sdk/sandbox.md (native forced close) and
  engine/store/sqlite.md (last-close lane row).

Deleted test (it asserts the PR's read-only-open admission, which base's
#647 rule does not have): writer_admission.rs
another_process_admission_precedes_read_only_connection_open. The
last-close admission test is kept and passes.

Checks (macOS, toolchain 1.85.1, registered worktree via tools/worktree.py):
- Clippy --all-targets -D warnings: layerfs-persistence; layerfs-api
  (machine, postgresql), -sandbox, -machine, -control, -platform, -store;
  layerfs-cli (postgresql), layerfs-daemon: clean.
- Tests: persistence writer_admission 13, sqlite_lock_custody 2; store
  lease_race 1, lease_stale 1; platform machine_force 9, machine_lima 8;
  cli capabilities 4, stream_framed 3; api (machine, postgresql)
  machine_* and sandbox_* 30 across 17 suites, sandbox_capacity 8: all
  passed.
- rustfmt clean on the resolved files. cargo fmt --check still flags
  api/crates/layerfs-cli/tests/concurrent_reads.rs, which comes from base
  unchanged.
- Not run: machine_force_native (needs a matching private daemon), Linux
  and Docker E2E. Durable: NOT_RUN - disabled by owner until explicit
  reauthorization.

Production LOC: 159204 -> 161104 (delta +1900), first parent eda9c04
against the merged tree; core 39787 -> 39787, engine 74384 -> 74810,
control 39465 -> 40527, api 5568 -> 5980. The delta is the base's 64
commits; against layerfs_v2_0_0 (160621) the PR now adds +483. Reference
absent (retired R9c; last counted 65,417 at tree 498dd19). Method:
tools/production_loc.py (blob 35e4a6d) --root <git archive
snapshot> --json on both trees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refusals before a signal stay certain

Fixes the two blocking findings of the independent review of #656 at
18b4df7.

1. A read-only or diagnostic SQLite session's close no longer takes the
   Store directory's writer lane. The last-close lane added for #656 made
   every session wait for it, so after one uncertain write (a quarantined
   writer keeps the lane) a daemon's reader close blocked forever under the
   reader-set mutex, and with it mount, unmount and session lookup; the
   diagnostic open and control's idle-Store reaper hung the same way. A
   read-only connection cannot checkpoint or remove -wal/-shm, so the lane
   bought no custody there. State now keeps the Store path only for
   writable sessions (close_lane); writable closes are unchanged.

2. A forced machine.stop on native records a Sandbox as uncertain only once
   its stop has been sent. Recorded::for_stop reopens the recorded runtime
   without signalling (identity and boot checks, missing runtime); a refusal
   there now clears the op instead of leaving an uncertain machine.stop on
   native, which the new machine gates turned into a home-wide lockout with
   no call to clear it. Partial progress no longer makes a native refusal
   uncertain either: each native Sandbox is recorded as it stops.
   Recorded::stop_now is for_stop followed by the stop, as before.

Tests added: writer_admission
read_only_close_does_not_wait_for_a_held_writer_lane; api machine_force
native_force_stop_refused_before_any_signal_leaves_no_uncertain_op. Both
fail on 18b4df7's product code and pass here. No test deleted.
Docs: engine/store/sqlite.md (Last close row, including the existing
checkpoint-thread start exception the review noted), api/sdk/machine.md
(machine.stop force on native).

Checks (toolchain 1.85.1, registered worktree via tools/worktree.py):
- Clippy --all-targets -D warnings, macOS and Linux: layerfs-persistence,
  -sandbox, -control, -api (machine, postgresql), -store, -cli
  (postgresql), -daemon: clean. rustfmt clean on changed files.
- macOS: persistence writer_admission 14, sqlite_lock_custody 2,
  idle_checkpoint 4; api machine_force 2, sandbox_records 2, sandbox_lost
  1, sandbox_family 1; platform machine_force 9: passed.
- Linux: persistence writer_admission 15, sqlite_lock_custody 2,
  idle_checkpoint 4; store lease_race 1, lease_stale 1; platform
  machine_force 9; api machine_force 2, sandbox_lost 1: passed.
- Linux mount lane (tools/ci_tests.py mount-targets, 45 binaries from
  layerfs-daemon and layerfs-control, each in a fresh container with
  /dev/fuse, SYS_ADMIN, apparmor=unconfined and a fresh ext4 /tmp, each
  under timeout 120 with --test-threads=1): 45/45 passed, 116 tests
  passed, 3 ignored by their own attributes.
- Not run: machine_force_native (needs a matching private daemon);
  Docker/Lima providers live. Durable: NOT_RUN - disabled by owner until
  explicit reauthorization.

Production LOC: 161104 -> 161117 (delta +13); core 39787 -> 39787,
engine 74810 -> 74811, control 40527 -> 40539, api 5980 -> 5980.
Reference absent (retired R9c; last counted 65,417 at tree
498dd19). Method: tools/production_loc.py (blob 35e4a6d)
--root <git archive snapshot> --json on the parent and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dy and continuation

Fixes the non-blocking findings of the independent reviews of #656 at
18b4df7 and d4d9df9.

Store (persistence):
- A writable session's last close makes one non-blocking lane attempt.
  With the lane it closes as before (checkpoint, WAL/SHM removal under
  the lane). Without it (held elsewhere, or the attempt failed) it sets
  SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE and closes: SQLite then takes no
  exclusive lock, runs no checkpoint and unlinks nothing, so the close
  neither waits behind a writer kept after an uncertain write nor tears
  the WAL down unadmitted, and the connection is no longer leaked. Only
  if that setting fails is the connection kept until process exit.
  (Review findings A and 3.)

Force stop (control):
- native: a refusal before the signal no longer ends the stop; the other
  Sandboxes are still stopped, and the call answers the first refusal
  with detail.refused_sandboxes beside stopped_sandboxes and unknown.
  (C)
- native uncertainty is phase-based only: a Sandbox is uncertain once
  its stop was sent; a code alone no longer makes it so. (B)
- Recording the outcome no longer replaces the stop's own refusal: a
  failure while writing records is reported in detail.custody_errors.
  (6)
- A forced stop may settle an uncertain machine.stop this process left,
  or one whose owner has ended; one a live process still holds is Busy,
  as the op standing table says. (7)

Forced native sandbox.close (control):
- Refused Busy while a live process holds native (e.g. a forced
  machine.stop in progress); an intent an ended process left does not
  stop it. (10)
- Once the runtime stopped, a later failure (delete, registry, folder
  removal) carries detail.runtime_stopped and the record is marked
  stopped instead of left as it was. (5)

Docs: engine/store/sqlite.md (Last close), api/sdk/machine.md (force on
native, refused_sandboxes, custody_errors, settling an uncertain stop;
docker InvalidInput for a container that is not a LayerFS worker, which
the ordinary stop now also checks: finding 9), api/sdk/sandbox.md (forced
native close: no per-command settle or mount check, Busy rule,
runtime_stopped), control/sandboxes/README.md (record format: the
machine_stopped and machine_stop_unknown fields, "v" unchanged, older
builds: finding 8).

Tests:
- Replaced writer_admission
  another_process_admission_precedes_last_connection_close (added by this
  PR; it asserted the waiting close this commit removes) with
  writable_last_close_without_the_lane_neither_waits_nor_tears_down_the_wal
  (no wait and the WAL kept while the lane is held; an admitted close
  removes it).
- api machine_force native_force_stop_refused_before_any_signal_leaves_no_uncertain_op
  now has two Sandboxes and checks the stop continues past the refused one.
- api machine_force_native gains an identity case: a recorded pid naming
  an unrelated live process is refused before any signal, neither process
  is signalled, and no op is left on native.
- The two changed unit tests fail on d4d9df9's product code and pass
  here.

Checks (toolchain 1.85.1, registered worktree via tools/worktree.py):
- Clippy --all-targets -D warnings, macOS and Linux, on persistence,
  sandbox, sandbox-backend (Linux), control, platform, machine (Linux),
  api (machine, postgresql), store, cli (postgresql), daemon: clean.
- macOS: persistence writer_admission 14, sqlite_lock_custody 2,
  idle_checkpoint 4; store lease_race 1, lease_stale 1; platform
  machine_force 9, machine_lima 8; cli capabilities 4, stream_framed 3;
  api machine_force 2, sandbox_records 2, sandbox_lost 1, sandbox_family
  1, sandbox_capacity_worker 3: passed. machine_force_native run live
  with LAYERFS_DAEMON (debug macOS daemon of this tree): 2 passed in
  2.0 s, no daemon or helper left.
- Linux: persistence writer_admission 15, sqlite_lock_custody 2,
  idle_checkpoint 4; store lease_race 1, lease_stale 1; platform
  machine_force 9; api machine_force 2, sandbox_lost 1,
  sandbox_capacity_worker 3: passed.
- Linux mount lane (45 binaries, each in a fresh container with
  /dev/fuse, SYS_ADMIN, fresh ext4 /tmp, timeout 120, --test-threads=1):
  44 binaries passed; kernel_coherence failed once in
  size_never_shrinks_under_concurrent_append_and_refusals_stay_refused
  ("read length: 0 after 1000", a FUSE read racing appends; overlay and
  fuse do not depend on layerfs-persistence, and the test closes no Store
  during the race). One diagnostic rerun of that binary passed 6/6. The
  same binary passed at d4d9df9. Reported as an intermittent failure,
  not counted as a pass. Totals of the first run: 110 passed, 1 failed,
  3 ignored.
- Not run: live Docker/Lima providers. Durable: NOT_RUN - disabled by
  owner until explicit reauthorization.

Production LOC: 161117 -> 161173 (delta +56); core 39787 -> 39787,
engine 74811 -> 74818, control 40539 -> 40588, api 5980 -> 5980.
Reference absent (retired R9c; last counted 65,417 at tree
498dd19). Method: tools/production_loc.py (blob 35e4a6d)
--root <git archive snapshot> --json on the parent and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed Sandboxes are not stopped

Follows the independent review of #656 at b4a0283.

- N2 (regression in b4a0283): a writable session's last close waits
  for the lane again, so it checkpoints and removes -wal/-shm under the
  lane. Closing without it whenever the lane was briefly held left the
  WAL behind, which bucket pull's verify close reports as a failure and
  backup restore's in-use check as InUse. Finding A (a writable close
  waits behind a writer kept after an uncertain write) is therefore kept
  by design: every write behind that writer already waits; read-only and
  diagnostic closes still take no lane. Only a failed wait (interrupted,
  or the directory cannot be opened) closes with
  SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE (no exclusive lock, no checkpoint, no
  unlink; no second attempt), replacing the connection leak (finding 3).
- N3: the transfer leftover-WAL comment names the failed-wait close too;
  control open.rs's comment holds again as written.
- N5: a native Sandbox that never started (no id, no runtime record) is
  neither marked stopped nor listed in stopped_sandboxes.
- N6: a forced native sandbox.close is not refused for an unresolved
  machine.stop intent this process left (it already was not for one an
  ended process left).
- D: the live identity case asserts refused_sandboxes exactly.

Tests:
- Replaced writer_admission
  writable_last_close_without_the_lane_neither_waits_nor_tears_down_the_wal
  (added by this PR in b4a0283; it asserted the behaviour reverted
  here) with writable_last_close_waits_for_the_lane_then_removes_the_wal.
- api machine_force native_force_stop_refused_before_any_signal_leaves_no_uncertain_op
  now uses a refused, an already stopped and a never-started Sandbox.
Docs: engine/store/sqlite.md (Last close), api/sdk/machine.md,
api/sdk/sandbox.md.

Checks (toolchain 1.85.1, registered worktree via tools/worktree.py):
- Clippy --all-targets -D warnings, macOS and Linux, on persistence,
  sandbox, sandbox-backend (Linux), control, platform, machine (Linux),
  api (machine, postgresql), store, cli (postgresql), daemon: clean.
- macOS: persistence writer_admission, sqlite_lock_custody,
  idle_checkpoint; store lease_race, lease_stale; platform machine_force,
  machine_lima; cli capabilities, stream_framed; api machine_force,
  sandbox_records, sandbox_lost, sandbox_family, sandbox_capacity_worker:
  57 passed, 0 failed. machine_force_native live with LAYERFS_DAEMON
  (debug macOS daemon of this tree): 2 passed in 1.9 s, no daemon left.
- Linux: persistence, store, platform machine_force, api machine_force,
  sandbox_lost, sandbox_capacity_worker: 38 passed, 0 failed.
- Linux mount lane (45 binaries, each in a fresh container with
  /dev/fuse, SYS_ADMIN, fresh ext4 /tmp, timeout 120, --test-threads=1):
  45/45, 116 passed, 0 failed, 3 ignored by their own attributes.
- Not run: live Docker/Lima providers. Durable: NOT_RUN - disabled by
  owner until explicit reauthorization.

Production LOC: 161173 -> 161187 (delta +14); core 39787 -> 39787,
engine 74818 -> 74818, control 40588 -> 40602, api 5980 -> 5980.
Reference absent (retired R9c; last counted 65,417 at tree
498dd19). Method: tools/production_loc.py (blob 35e4a6d)
--root <git archive snapshot> --json on the parent and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review finding M1 at 0fd2a99: the comment written for native_runtime
sat on the Native enum and still described an Option. Comment text only.
Checks: Clippy --all-targets -D warnings on layerfs-control (macOS) clean;
rustfmt clean.

Production LOC: 161187 -> 161187 (delta 0); comment-only change. Method:
tools/production_loc.py (blob 35e4a6d) --root <git archive
snapshot> --json on the parent and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yifanxuaaa

Copy link
Copy Markdown
Contributor Author

Review record

Reviewed head: 18b4df7 (integrate/v2-force-stop-latest), merge base 27920d3 with layerfs_v2_0_0.
Tier: P1; gate: yes (risk:store: layerfs-persistence sqlite session/seal/lifecycle, layerfs-store place.rs; risk:format: layerfs-sandbox record/sandbox.rs new fields; by hand: new machine not_held gates change who may use a Store on a machine).
Model: Fable 5.2 (claude-fable-5-2), independent context; diff and policy only, no PR body or commit messages.
Read: persistence sqlite lane/session/seal end to end and their callers (store-service bootstrap, read_service, read_handle, commit lane, catalog publish_lane; control store open.rs, place.rs); all control and api production hunks by diff; test hunks; docs hunks.
Run again by the reviewer: nothing (static review only).

Blocking

  1. engine/crates/layerfs-persistence/src/backend/sqlite/session/lifecycle.rs:37-60: Drop for State takes a blocking directory flock for every session, read-only and diagnostic included. A quarantined writer keeps its writer_hold (writer_admission.rs finish_writer), so after one uncertain write in a daemon, the next reader close (read_service.rs close, under the reader-set mutex) blocks forever, and with it every later mount, unmount and session lookup; also open_diagnostic and control's idle-Store reaper for any Store in that directory. A read-only close cannot checkpoint or remove -wal/-shm, so the lane buys no custody there.
  2. control/crates/layerfs-control/src/machine_force.rs:65,86-99 (with host_process/sandbox.rs:102-108 and the new not_held gates): native_attempt is set before native_stop runs, so a refusal issued before anything was signalled (e.g. recorded pid reused after reboot: "saved local daemon identity changed") records an uncertain machine.stop op on native. The new gates then refuse every sqlite Store, Sandbox and Workspace call on native, and no call clears it (machine remove native is refused for the built-in machine; repeating the force stop fails the same way; ordinary close is now gated).

Non-blocking
3. session/lifecycle.rs:45-53: a failed drop-time acquire leaks the connection until process exit, keeping any open uncertain transaction's write lock; unreported.
4. session/connection.rs:325-331: if Checkpointer::start fails, the raw connection drops with no lane (pre-existing); the new sqlite.md "Last close" row omits this exception.
5. layerfs-sandbox force.rs:59-68 with close.rs:147-152: a partly completed forced close (stop ok, delete or remove_dir_all fails) is recorded as a certain failure, leaving the record running with its runtime stopped (same shape as the ordinary close).
6. machine_force.rs:66-67,74,101-113: ? inside the error handler can replace the original error and drop stopped_sandboxes/unknown detail.
7. machine_force.rs:168-178: force_record admits an uncertain machine.stop op owned by a still-live process.
8. record/sandbox.rs:65-66,88-95: two new record fields without a version bump or format-page update; older builds would show a force-stopped Sandbox as running.
9. docker/power.rs:126-137: the non-forced docker machine stop also changes its refusals (non-worker role label refused; non-running containers skipped).
10. close.rs:107-112: forced native close skips exec settle, unmount guard and the containerGone reclaim branch, and is not subject to the new machine gate (documented in sandbox.md).

Risky-lines scan: no Durable, unsafe, DELETE FROM, rmtree, #[ignore], dependency hunk, busy handler or retry loop in production code; one production remove_dir_all (force.rs:68, mirrors the ordinary close); two asserts re-added unchanged with longer messages; one fixture value changed (stream_worker.rs image).

Not checked by the reviewer: builds and tests; PostgreSQL; Windows/WSL, apple-container and lima at runtime; groups::end, mounts::detach and namespaces reopen internals; the three new test files beyond a risky-lines scan; JS/Python binding signatures against docs.

Author evidence at this head (macOS and Linux, toolchain 1.85.1, registered worktree): Clippy -D warnings clean on all changed crates on both; boundary guard PASS; macOS 79 tests passed; Linux persistence writer_admission 14, sqlite_lock_custody 2, idle_checkpoint 4, store lease_race 1 and lease_stale 1, platform machine_force 9, api machine_force 1 and sandbox_lost 1 passed; api sandbox_family and sandbox_records each fail in the build container because base's kernel guard refuses a Store on its overlayfs /tmp (environment, unchanged tests). Linux mount lane (45 binaries) built, not run. machine_force_native not run.

Verdict: blocked by findings 1 and 2. Owner approval is required in any case (risk:store, risk:format).

@yifanxuaaa

Copy link
Copy Markdown
Contributor Author

Review record

Reviewed head: d4d9df9 (integrate/v2-force-stop-latest). Covers the change since 18b4df7 (8 files, +133/-22) and updates the verdict for the whole PR; the 18b4df7 record stands for everything this push did not touch.
Tier: P1; gate: yes (risk:store: persistence sqlite session connection.rs and lifecycle.rs; risk:format: layerfs-sandbox record/sandbox.rs; by hand: the new machine not_held gates on Store and Sandbox calls).
Model: Fable 5.2 (claude-fable-5-2), independent context; diff and policy only, no PR body or commit messages.
Read: every line of the incremental diff (connection.rs, lifecycle.rs, machine_force.rs, reopen.rs, both doc hunks, both new tests); at head, machine_force.rs native loop and error handler, every State field reference in the sqlite backend, every caller of stop_now/for_stop, layerfs-control Cargo.toml.
Run again by the reviewer: nothing (static review only).

Earlier blocking findings

  1. Unbounded lane wait in Drop for State: resolved. State keeps close_lane only for writable sessions; read-only and diagnostic closes (read_service close under the reader-set mutex, diagnostic open, other daemons' readers) take no lane. Seal unaffected. New test read_only_close_does_not_wait_for_a_held_writer_lane.
  2. Native force-stop lockout: resolved. Recorded::for_stop reopens without signalling; native_attempt is set only after it succeeds, just before stop(0), and cleared after the Sandbox is marked. A pre-signal refusal clears the op on native and marks no Sandbox. New test native_force_stop_refused_before_any_signal_leaves_no_uncertain_op.
    Earlier non-blocking finding 4 (doc vs checkpoint-start failure path): resolved in sqlite.md.

New, non-blocking
A. lifecycle.rs:50-59: a writable close still waits without bound behind a writer that another session or process kept after an uncertain write (that session's writes already blocked forever before this PR). Owner to accept or ask for a non-waiting attempt.
B. machine_force.rs:93: error.code == "Unknown" is uncertain whatever the phase. No pre-signal producer of Unknown found (author checked reopen.rs local/namespaces paths: Failed/InvalidInput only).
C. machine_force.rs:64-74: a pre-signal refusal on one native Sandbox stops the loop; later Sandboxes are not stopped. Workaround: ordinary sandbox close.
D. The new api test covers the "no recorded runtime" refusal, not "identity changed".
E. Still open from the 18b4df7 record: 5, 6, 7, 8 (record format statement), 9 (ordinary docker stop changed), 10.

Author evidence at this head (toolchain 1.85.1, registered worktree): Clippy -D warnings clean on all changed crates, macOS and Linux; both new tests fail on 18b4df7's product code and pass at d4d9df9; macOS and Linux covering tests passed; Linux mount lane (tools/ci_tests.py mount-targets, 45 binaries, each in a fresh container with /dev/fuse, SYS_ADMIN, fresh ext4 /tmp, timeout 120, --test-threads=1): 45/45, 116 tests passed, 3 ignored by their own attributes; boundary guard PASS at 18b4df7 (the push changes no guarded path). Not run: machine_force_native, live Docker/Lima. Durable: NOT_RUN - disabled by owner until explicit reauthorization.

Verdict: mergeable after owner approval (gate: risk:store, risk:format, hand-added Store-access gate).

@yifanxuaaa

Copy link
Copy Markdown
Contributor Author

Review record

Reviewed head: d303c7b (integrate/v2-force-stop-latest).
Records for intermediate heads: b4a0283 (fixes A, B, C, D, 3, 5-10; found N1-N6), 0fd2a99 (resolves N2, N3, N5, N6; D tightened; reopens A by design), d303c7b (resolves M1; comment only).
Tier: P1; gate: yes (risk:store: persistence session lifecycle and connection, layerfs-store place.rs and transfer comment; risk:format: layerfs-sandbox record/sandbox.rs machine_stopped and machine_stop_unknown; by hand: machine not_held gates on Store and Sandbox calls; contract doc control/sandboxes/README.md).
Model: Fable 5.2 (claude-fable-5-2), independent context; diff and policy only, no PR body or commit messages.
Run again by the reviewer: nothing (static review only).

Resolved across the review: blocking 1 (read-only/diagnostic closes take no lane) and 2 (native force-stop pre-signal refusal leaves no op); B, C, 3, 5, 6, 7, 8, 9, N2, N3, N5, N6, M1.
Open, non-blocking, for owner acceptance:

  • A: a writable close waits without bound behind a writer another process kept after an uncertain write (every write in that state already waits; the non-waiting alternative left the WAL behind and broke bucket pull verify and restore's in-use check, N2).
  • N1: after a failed lane wait (EINTR, directory cannot be opened) the close sets SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE and closes, with no second attempt (alternative: leak the connection to process exit).
  • 10: forced native sandbox.close skips per-command settle and the mount check (documented in api/sdk/sandbox.md).
  • D: identity-changed refusal covered end to end only by the live macOS test (needs LAYERFS_DAEMON).
  • M2: the replaced writable-close test proves the wait by a 200 ms window.

Author evidence at d303c7b's code (0fd2a99; d303c7b changes comments only), toolchain 1.85.1, registered worktree via tools/worktree.py:

  • Clippy --all-targets -D warnings on all changed crates, macOS and Linux: clean.
  • macOS: 57 tests passed, 0 failed; machine_force_native live with LAYERFS_DAEMON: 2 passed, no daemon left.
  • Linux: 38 tests passed, 0 failed; mount lane (tools/ci_tests.py mount-targets, 45 binaries, each in a fresh container with /dev/fuse, SYS_ADMIN, fresh ext4 /tmp, timeout 120, --test-threads=1): 45/45, 116 passed, 0 failed, 3 ignored.
  • At b4a0283, kernel_coherence size_never_shrinks_under_concurrent_append_and_refusals_stay_refused failed once (FUSE read racing appends; no dependency on layerfs-persistence) and passed on one diagnostic rerun; recorded as intermittent.
  • Boundary guard PASS. Not run: live Docker/Lima providers. Durable: NOT_RUN - disabled by owner until explicit reauthorization.

Verdict: mergeable after owner approval and explicit acceptance of A, N1 and 10.

@yifanxuaaa

Copy link
Copy Markdown
Contributor Author

Owner approval and acceptance

Head: d303c7b

The owner answered "Accept and merge" in the session after being shown the three remaining trade-offs from the review records above:

  • A: a writable Store close waits without bound behind a writer another process kept after an uncertain write. Every write in that state already waits; the non-waiting alternative left the WAL behind and broke bucket pull's verify close and backup restore's in-use check (N2).
  • N1: after a failed lane wait (interrupted, or the directory cannot be opened), the close sets SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE and closes without touching the WAL, with no second attempt. The alternative is leaking the connection to process exit.
  • 10: a forced native sandbox.close does not settle commands one by one or check mounts first; the runtime's stop ends them (documented in api/sdk/sandbox.md).

Gate: risk:store, risk:format and the hand-added Store-access gate are approved with this acceptance. Per the review policy, a contributor reads the risk:store and risk:format hunks within a week after merge and files an issue for each finding.

@yifanxuaaa
yifanxuaaa merged commit 3a6e842 into layerfs_v2_0_0 Nov 14, 2026
NelsonLongxiang added a commit to NelsonLongxiang/layerfs that referenced this pull request Nov 14, 2026
…l-AI-Lab#682/Ephemeral-AI-Lab#656)

Upstream Ephemeral-AI-Lab#682 (Sandboxes from any local Docker image with a bundled
runtime layer) and Ephemeral-AI-Lab#656 (force-stop machines and native Sandboxes)
bring four unix-bound modules into layerfs-sandbox (docker, images,
image_load, image_artifact) plus machine_force in control. The
modules gate unix; route's create-with-image branch and its two
image helpers sit behind a small facade (neutral answers on
Windows); machine.images/load/prune/stop_force answer Unsupported
with the Ephemeral-AI-Lab#454 pointer; the worker server's image arms are unix-only
arms (no worker runs on Windows). Four conflicts resolved keeping
our Windows admission lane and the fused doctor symlink check.

Production LOC: 161378 -> 161947 (delta +569; upstream features
with the Windows refusals; tools/production_loc.py --json).
jiangkoumo added a commit to jiangkoumo/layerfs that referenced this pull request Nov 14, 2026
Brings the Ephemeral-AI-Lab#136 helper reaping work onto upstream after the reviewer's
first pass: Ephemeral-AI-Lab#656 force-stop of machines and native Sandboxes, Ephemeral-AI-Lab#647 macOS
export-volume flush and exact Busy, Ephemeral-AI-Lab#682 image-loaded Sandboxes, Ephemeral-AI-Lab#681 GC
design, Ephemeral-AI-Lab#642 one-read NFS records.

Conflicts were documentation only, and are resolved into upstream's
structure:
- engine/daemon/control-calls.md: upstream's macOS `sync_volume_np` flush
  row kept; this branch's Attach (mount table read once, `tracked`),
  Unmount (mount-table settlement), ForceUnmount (finishing an export kept
  at Detach) and Helper run (retained queue) rows merged into upstream's
  table.
- engine/kernel-routes/nfs.md: upstream's Commit line kept inside this
  branch's Attach/Unmount/ForceUnmount diagram.
Code merged without conflicts; Ephemeral-AI-Lab#656 touches Sandbox/machine force-stop and
the Store lane, not the NFS export teardown, so the two do not overlap in
the data path. `cargo +1.85.1 check --locked --all-targets` on the changed
crates passes at this tree.

Durable: NOT_RUN - disabled by owner until explicit reauthorization.
Production LOC: 160218 -> 163746 (delta +3528); first parent b49690d.
  core 39787 -> 39787, engine 74627 -> 75078, control 39828 -> 42804,
  api 5976 -> 6077. Reference absent (retired R9c; last counted 65,417 at
  tree 498dd19). Method: python3 tools/production_loc.py --root
  <git archive of b49690d> and --root <the merged working tree>.
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