Repository navigation
R-CoSim rung 1: engram-dst-cosim — the coordinator↔host boundary simulator (+ the #570 window) - #783
Conversation
…ler cores The three decision-feeding host->coordinator endpoints (live-manifest publish, sandbox_ownership, sandbox_owner) had their logic inline in the axum handlers. Extract each into a `pub` `*_core(&SharedState, ...)` fn that holds the real store-level logic (including the ADR 0092 non-terminal ownership predicate), with the handler thinning to an extractor + JSON wrapper. Zero behavior change — the run_once pattern applied to handlers. This lets the ADR 0098 R-CoSim boundary simulator drive the exact coordinator code path the host-agent's `CoordControlPlane` calls hit, without duplicating the predicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBLK8qJSNJQK722E1n2omR
…ssue #570) The ADR 0045 D5 idle-eviction fast path marks a session Idle and clears `sessions.sandbox_id` the instant `snapshot_begin` returns, while the snapshot upload finalizes in a host-owned background job. During that window the host's teardown-reconcile sweep lists the still-resident (paused) VM, asks the coordinator "does this session still own it?", gets `false` (the D5 unbind cleared the binding), counts orphan strikes, and SIGKILLs the VM mid-upload — cancelling the finalize. No snapshot row lands, and the next resume falls back to a stale periodic checkpoint, rewinding completed work (the 2026-07-02 incident, three-for-three). The fix reuses the signal the host already has: the periodic checkpointer skips a sandbox whose `capture_in_flight` (capture lock held) is true; the reconcile ownership check just never consulted it. Add `capture_in_flight` to the `ReconcileBackend` trait, wire it into `gather_input`'s exemption set (alongside migration-role / live-capture), and implement it on `PooledReconcileBackend` via `PooledBackend::capture_in_flight`. A mid-capture sandbox is now exempt: the coordinator's transient D5 unbind is not orphan truth while the host is finalizing the capture. The exemption is scoped — a genuinely-unowned sandbox with no capture in flight is still reaped after the strikes (new unit tests pin both). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBLK8qJSNJQK722E1n2omR
…-CoSim, rung 1) The coordinator sim (engram-dst) and host sim (engram-dst-host) are disjoint by construction — separate cargo closures, separate clocks — so the coordinator↔host boundary, where #570/#739/#602/#216/85e0298a all lived, is explored by neither. This new crate is that boundary: rung 1 wires BOTH real sides against ONE shared SimClock + SimMetadataStore and fakes only the transport between them. What is real vs faked: - Coordinator side (real): a full AppState/Services replica over the shared SimMetadataStore + SimClock/SimEntropy; real op verbs (create_boot, evict, resume) driven inline; the three host->coordinator answers run their real handler cores (live_manifest_publish_core, sandbox_ownership_ core, sandbox_owner_core). - Host side (real): a purpose-built CosimHost running the REAL host-agent flows — the EvictionFinalizer capture + run_eviction_finalize_attempt legs over a real ChunkedDiskBackend/ChunkStore, and the REAL reconcile_once teardown tick — keyed by the coordinator's id-space. - Faked (only transport): the bridge — CosimHostClient (coordinator->host verbs drive the host's real ops) and CosimCoordControlPlane (host-> coordinator calls the real cores). Standing oracle (assert_idle_snapshot_durable): the coordinator reaching Idle via eviction implies the snapshot its resume path selects is durable and at-or-above the eviction cursor — exactly what #570 violates. Scenarios (directed, pinned seeds): - smoke: create -> serve -> idle-evict -> resume, all real code. - unbind_vs_teardown_reconcile: the #570 red/green. RED replays the pre-fix reconcile (adversarial `honor_capture_signal=false`) — it reaps the mid-capture VM, the finalize is cancelled, and the oracle FIRES (resume rewinds to the stale cursor-3 checkpoint below the cursor-5 eviction). GREEN honors the capture signal (the fix) — exempt, finalize completes, durable snapshot at the eviction cursor, oracle holds. - publish_committed_but_ack_lost: a lost publish ACK is safe — the retry is idempotent w.r.t. the durable manifest pointer, and a publish from a departed binding is dropped as Stale, not applied. - postroll_rehydrate_vs_sweep_vs_unpause: the ADR 0090 survivor-reattach ownership leg of the 731df805/#739/#769 family — a HostLost survivor with a dropped local binding is repaired (not reaped), while a terminal session's leftover VM is still reaped. (The full family's NBD/generation/ un-pause data-plane model stays engram-dst-host's domain; rung 2.) CI: a `test-cosim` nextest lane (directed scenarios, not a swarm binary), gated on the crate's own spanning release closure via the detector's new `test_cosim` flag, added to the CI Gate's needs (never individually required). Documented divergence from the ADR plan: SimHost owns a fixed slot id-space and assumes it owns the session/sandbox ids; at this boundary the coordinator mints those ids, so we reuse the real extracted flows SimHost is built on rather than SimHost itself. A second constraint the code forced: the coordinator's enqueue path detaches op drives onto tokio::spawn (benign for engram-dst's zero-I/O SimHostClient, but the real chunk-store/ finalize I/O starves in a background task under a directed paused-clock harness), so the harness drives ops inline via enqueue_claim + drive_claimed — the same verb body, drained deterministically on-task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBLK8qJSNJQK722E1n2omR
|
The latest Buf updates on your PR. Results from workflow CI / buf (pull_request).
|
|
Orchestrator review: the #570 fix is the narrowest true shape — the reconcile ownership check now consults the same capture-lock signal the periodic checkpointer always used, as a gather_input exemption alongside migration-role/live-capture; the host-internal sim's honest 'always false' impl correctly documents why this race is boundary-only territory. Architecture divergence (real extracted flows keyed by coordinator ids, not SimHost wholesale) is well-reasoned. One follow-up assertion wanted (not blocking): a unit test pinning that a QUARANTINED finalize releases the capture lock — the exemption must never become a permanent reap-shield; today that's implied by the finalize convergence oracle, not pinned directly. Merging on green. 🤖 Generated with Claude Code |
…bridge-fault exploration (#784) (#807) * R7: the shared NBD device-plane model + the coordinator rehydrate-list core (#784) ADR 0098 R-CoSim rung 2 foundation. Extract the P7/#806 device-plane world model — generation, the real NbdSlotAllocator, served_by/kernel_owner/parked, the guest_holds_device proof-of-death input — into a pub, reusable `engram_dst_host::device_plane::DevicePlane` keyed by SandboxId. Every decision delegates to the REAL host-core verdicts (sweep_verdict incl. the #806 holder table, resume_data_plane_served, is_local_survivor_candidate); every slot lease comes from the REAL NbdSlotAllocator. The disk backend stays the owning host's concern (rebuilt after `serve` reports a newly-served device) so the plane is pure device bookkeeping. Also extract `register_rehydrate_list_core` from the coordinator's `register` handler (the run_once-for-handlers pattern rung 1 established) so the co-simulated host's register-rehydrate leg drives the EXACT listing the real coordinator names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * R7: co-simulate the NBD device-plane family at the boundary + capture-lock pin (#784) ADR 0098 R-CoSim rung 2, deliverables 1 + 5. Port the device plane into the CosimHost (via the shared DevicePlane, keyed by the coordinator-minted SandboxIds — rung 1's documented divergence), and drive the full survivor family co-simulated, all real code on both sides: - host-agent roll (new generation; survivors resident, kernel_owner persists at the dead gen); - register-rehydrate against the REAL coordinator listing (register_rehydrate_list_core over the bridge): coord-list pass → #739 local ChainHeadRecord pass → stale-binding sweep; - the REAL sweep_verdict incl. the #806 (liveness × holder) table — a dead-owner device a live guest still holds is PARKed, never severed; - the un-pause data-plane gate (resume_data_plane_served); - the coordinator's REAL host_lost_straggler_sweep with the #782 probe/strike arms — including the #777 tension end-to-end across the boundary (a bound HostLost row whose VM probes ALIVE past the 60s min-age is DEFERRED via ask-the-host, then settles at the strike cap; a departed VM settles at once). Directed pins: the fixed happy path (listed survivor re-served, un-pause lands), the #806 ungated PARK-then-reserve-zero-loss, the un-pause gate firing on a disconnected dead-guest plane, and both straggler-sweep arms. Standing oracles added: severed-live-holder (#806), slot-accounting (P7 #3), and cross-boundary ownership agreement. Capture-lock release pin (deliverable 5): a QUARANTINED eviction finalize releases the capture lock (capture_in_flight → false), driven through the REAL run_eviction_finalize_attempt by sabotaging the staging dir — so #783's teardown-reconcile capture-in-flight exemption can never become a permanent reap-shield. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * R7: the seeded boundary swarm + bridge faults + standing oracles (#784) ADR 0098 R-CoSim rung 2, deliverables 2 + 3. A `--seeds` swarm over the coordinator↔host boundary (`sim-cosim` binary + the in-lane `tests/swarm.rs` replay-twice/convergence checks), small world (1 replica, 1 host, ≤3 sessions — the state-space product is the risk; bounded and documented). The scheduler interleaves REAL coordinator drivers, REAL host steps, host-agent rolls, and bridge faults (the applied-commit-but-ack-lost window reached via explicit perturbations that mirror the effect outcomes: DropHostBinding = lost bind ack, ForceHostLost = the partition window, ForceTerminal = lost destroy). Determinism (audit items 6-8): per-seed runtime dropped at the boundary + the named 600s watchdog (the engram-dst run_seed shape); world entropy only; BTreeMap/index-ordered picks; coarse explicit time steps so no fs-I/O clock auto-advance can tip a decision threshold. Replay-twice is byte-identical (verified in-lane + via the CLI). Standing oracles checked each step + at quiescence: severed-live-holder (#806), slot-accounting no-plane-leak (P7 #3), the ACTIVE-serve cross-boundary ownership split-brain guard, idle⇒durable-snapshot (#570, at quiescence post-finalize-drain), teardown/ownership completeness, and bounded convergence (every session terminal-or-stable, no Evicting/HostLost wedge). Three swarm firings RCA'd honestly while landing (each a real cross-system finding or a fidelity gap, no oracle weakened): - ownership oracle fired on a terminal session's served device (teardown window, not a re-home split-brain) and on an evicted sandbox mid-finalize (paused, benign): scoped the every-step guard to ACTIVELY-serving planes (live backend, non-terminal), added the STRONG quiescence completeness closure so the terminal/eviction cases stay covered; - #570 idle-durability fired on the D5-Idle-before-async-finalize window (a real prod window): moved to quiescence post-finalize-drain (detection of a cancelled-finalize loss preserved); - Evicting-wedge: a Roll decoupled from rehydrate left an unrehydrated Active session (unreachable in prod — a roll's startup always rehydrates); coupled Roll → register-rehydrate faithfully. - next_free_device spare-lease overlap (a real device-double-allocation bug in the shared plane) fixed. PR window: 0..40 x 600 chaos+calm, ~1.2s (per-seed ~29ms), well under 5 min. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * R7: wire the boundary swarm into CI + the nightly cosim-swarm job (#784) ADR 0098 R-CoSim rung 2, deliverable 4. The existing `test-cosim` lane gains a fixed PR swarm window (0..40 x 600 chaos + calm, ~1.2s/window, well under 5 min); nightly-sim.yml gains a `cosim-swarm` job with date-derived non-overlapping windows (200 chaos + 80 calm x 1500, sized below the host swarm's 480/night since the co-sim runs both sides per step) whose --failure-report feeds the same slug-deduped issue filing with a "nightly cosim:" title prefix. CI Gate already needs test-cosim (unchanged). YAML validated with python3 yaml.safe_load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * R7: ADR 0098 bookend — R-CoSim rung 2 (the boundary device-plane family + swarm) (#784) Record the Wave 7 arc: the reusable DevicePlane extraction (+ the deferred SimHost migration, honestly noted), the co-simulated roll→rehydrate→sweep→ un-pause + straggler #777 family, the seeded boundary swarm with its determinism/I/O-audit verdict and small-world bounding, and the four swarm firings each RCA'd without weakening an oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * R7: in-memory blob store for the co-sim (determinism, #784) Swap both blob tiers to the deterministic in-memory engram_sim::MemBlobStorage (audit items 6/7): the host's ChunkStore (was a per-run tempdir LocalBlobStorage) and — more importantly — the coordinator replica's blob/chunk_store, which shared a PROCESS-GLOBAL temp_dir()/engram-dst-cosim-blobs across every SimWorld (a cross-world contamination + real-fs-latency clock-drift hazard, the #793/#799 class, latent under the directed tests but fatal to a multi-seed swarm). The ChunkedDiskBackend's own ChunkCache still uses the SimFs tempdir (inherent to reusing the REAL backend); its I/O never feeds a decision (the swarm's coarse explicit time crosses every threshold). Replay-twice stays byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What this is
ADR 0098 Phase 3 R-CoSim, rung 1: the residual-risk item #2 the ADR named but left unbuilt — the coordinator↔host boundary is not co-simulated.
engram-dstdrives the real coordinator overengram-sim;engram-dst-hostdrives the real host-agent flows over its own world; the two are disjoint by construction (separate cargo closures, separate clocks), so the boundary where #570, #739, #602, #216, and 85e0298a all lived is explored by neither. This addscrates/engram-dst-cosim: both real sides wired against one sharedSimClock+SimMetadataStore, only the transport faked.Real vs faked, each side
AppState/Servicesreplica over the sharedSimMetadataStore+SimClock/SimEntropy; real op verbs (create_boot, evict, resume); realidle_detector/queue_scannerCosimHostClient— theHostClientverbs driveCosimHost's real opsCosimHostrunning the REALEvictionFinalizercapture +run_eviction_finalize_attemptfinalize over a realChunkedDiskBackend/ChunkStore, and the REALreconcile_onceCosimCoordControlPlane— the 3CoordControlPlanecalls invoke the REAL coordinator handler cores (extracted topubfns inhost_http)The "wire" is a direct function call into the real handler logic — that is the fidelity rung 1 buys over the toy
SimCoordClient(a BTreeMap).Standing oracle (
assert_idle_snapshot_durable): the coordinator reachingIdlevia eviction implies the snapshot its resume path selects is durable and at-or-above the eviction cursor — exactly what #570 violates.The #570 verdict: REPRODUCED, and fixed
Reproduced.
tests/unbind_vs_teardown_reconcile.rsdrives the real D5 fast path:snapshot_beginreturns (capture lock held, finalize in flight) → coordinator marksIdle+ clearssessions.sandbox_id. The realreconcile_oncethen lists the still-resident paused VM, asks the realsandbox_ownershipcore, getsfalse(D5 unbind), strikes twice, and destroys the VM mid-upload — cancelling the finalize. No snapshot row lands; the oracle fires (resume would rewind to the stale cursor-3 periodic checkpoint, below the cursor-5 eviction). Red-then-green is a single adversarial knob,honor_capture_signal, replaying the pre-fix reconcile that never consulted the capture-in-flight signal.Fixed (separate commit, host-side, true to "production drives the design"). The host already has the signal — the periodic checkpointer skips a sandbox whose
capture_in_flight(capture lock held) is true; the reconcile ownership check just never consulted it. The fix addscapture_in_flightto theReconcileBackendtrait and togather_input's exemption set, implemented onPooledReconcileBackendvia the existingPooledBackend::capture_in_flight. GREEN: the mid-capture VM is exempt, the finalize completes, the durable snapshot lands at the eviction cursor, the oracle holds. The exemption is scoped — a genuinely-unowned sandbox with no capture in flight is still reaped (unit tests near the fix pin both).Not changed-since: the D5 fast path + teardown-reconcile still exhibit the exact window described in the issue.
Scenarios
Stale, not applied.HostLostsurvivor with a dropped local binding is repaired (not reaped); a terminal session's leftover VM is still reaped. Pins the HostLost stage-2 recoverability predicate divergence: dead_host settles on ANY snapshot row, reconcile filters recoverable #777 tension's current behavior (repair) with the open design call noted.CI
New
test-cosimnextest lane (directed scenarios, not a swarm binary), gated on the crate's own spanning release closure via the detector's newtest_cosimflag, added to theCI Gate'sneeds:— never individually required (AGENTS.md). The PR's own coordinator + host-agent changes trip the flag, so the lane runs here. YAML validated withyaml.safe_load;just hakari verifyclean.Honest gaps / what rung 2 needs
engram-dst-host'sSimHostdomain — rung 1's boundary host reuses the finalize/reconcile flows keyed by the coordinator's id-space, not the slot machinery. The full postroll family (register-rehydrate ↔ stale-sweep ↔ un-pause gate ↔ the coordinator straggler sweep, together) is rung 2.SimHost::newowns a fixed slot id-space and assumes it owns the session/sandbox ids; at this boundary the coordinator mints them, so we reuse the real extracted flowsSimHostis built on rather thanSimHostitself.session_ops::enqueuedetaches op drives ontotokio::spawn(benign forengram-dst's zero-I/OSimHostClient, which settles in one poll, but the real chunk-store/finalize I/O starves in a background task under a directed paused-clock harness). The harness drives ops inline viaenqueue_claim+drive_claimed— same verb body, drained deterministically on-task. This is where the "directed, not swarm" shape of rung 1 shows.🤖 Generated with Claude Code
https://claude.ai/code/session_01PBLK8qJSNJQK722E1n2omR