Skip to content

R-CoSim rung 1: engram-dst-cosim — the coordinator↔host boundary simulator (+ the #570 window) - #783

Merged
nikhilunni merged 3 commits into
mainfrom
resilience/wave2-cosim-rung1
Jul 18, 2026
Merged

nikhilunni merged 3 commits into
mainfrom
resilience/wave2-cosim-rung1

Conversation

@nikhilunni

Copy link
Copy Markdown
Contributor

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-dst drives the real coordinator over engram-sim; engram-dst-host drives 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 adds crates/engram-dst-cosim: both real sides wired against one shared SimClock + SimMetadataStore, only the transport faked.

Real vs faked, each side

Real Faked
Coordinator → host full AppState/Services replica over the shared SimMetadataStore + SimClock/SimEntropy; real op verbs (create_boot, evict, resume); real idle_detector/queue_scanner CosimHostClient — the HostClient verbs drive CosimHost's real ops
Host → coordinator CosimHost running the REAL EvictionFinalizer capture + run_eviction_finalize_attempt finalize over a real ChunkedDiskBackend/ChunkStore, and the REAL reconcile_once CosimCoordControlPlane — the 3 CoordControlPlane calls invoke the REAL coordinator handler cores (extracted to pub fns in host_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 reaching Idle via 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.rs drives the real D5 fast path: snapshot_begin returns (capture lock held, finalize in flight) → coordinator marks Idle + clears sessions.sandbox_id. The real reconcile_once then lists the still-resident paused VM, asks the real sandbox_ownership core, gets false (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 adds capture_in_flight to the ReconcileBackend trait and to gather_input's exemption set, implemented on PooledReconcileBackend via the existing PooledBackend::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

CI

New 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 (AGENTS.md). The PR's own coordinator + host-agent changes trip the flag, so the lane runs here. YAML validated with yaml.safe_load; just hakari verify clean.

Honest gaps / what rung 2 needs

  • The NBD/generation/park/un-pause data-plane model stays engram-dst-host's SimHost domain — 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.
  • Divergence from the ADR plan (documented in code): SimHost::new owns 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 flows SimHost is built on rather than SimHost itself.
  • A constraint the code forced: the coordinator's session_ops::enqueue detaches op drives onto tokio::spawn (benign for engram-dst's zero-I/O SimHostClient, 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 via enqueue_claim + drive_claimed — same verb body, drained deterministically on-task. This is where the "directed, not swarm" shape of rung 1 shows.
  • Rung 1 is directed, not a swarm — no random interleaving generator yet. A seeded boundary swarm (with the state-space product deliberately bounded) is the natural rung-2/3 increment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PBLK8qJSNJQK722E1n2omR

nikhilunni and others added 3 commits July 18, 2026 12:37
…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
@github-actions

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow CI / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped⏩ skipped✅ passedJul 18, 2026, 7:39 PM

@nikhilunni

Copy link
Copy Markdown
Contributor Author

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

@nikhilunni
nikhilunni marked this pull request as ready for review July 18, 2026 19:53
@nikhilunni
nikhilunni merged commit 097c831 into main Jul 18, 2026
22 checks passed
@nikhilunni
nikhilunni deleted the resilience/wave2-cosim-rung1 branch July 18, 2026 19:53
nikhilunni added a commit that referenced this pull request Jul 20, 2026
…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>
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