Commit Graph
2 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 5 12147a1e01 feat(missions): Slice 4 — the two engines composed, with the file handoff proven
`team_engine='composed'` (the third name migration 0069 anticipated) runs a
mission as a durable ZeroClaw graph whose every node is a whole
Claude-Code-in-a-microVM session. Engine Z owns checkpoint/resume, cancellation
and per-node heterogeneity; Engine C owns shared context and cheap fan-out;
neither has the other's asset, which is why this is a composition and not a
compromise.

`MicroVmTurnExecutor` implements the existing `TurnExecutor`, so it inherits the
planners, the checkpoint, the stale-run recovery, `close_finished_phases`, the
evaluator, capture and delivery unchanged — the same trick `SubTopologyExecutor`
already plays with a heavy `run_turn`. Producer side emits ONE `queued` row
carrying the real graph and lets the worker claim it: the durability IS being
worker-driven, and the solo path's `tokio::spawn` has none of it. Still exactly
one `topology_runs` row per unit of work and one completion path — `finish()` is
now that one place, shared by every tier.

THE TRAP, solved and proven. A VM is inject → run → collect → destroy, so a
per-node VM with text-only handoff silently loses every file an earlier node
wrote: node 2 boots from the original checkout, sees nothing, and still reports
success. The mission's host checkout is the medium — every node injects from it
and collects back over it — and two properties make that safe rather than lucky:
`execute_resumable` is strictly sequential, so two VMs never write one directory;
and the vm id is deterministic per (phase, iteration, step), so a duplicate is
refused by the node ("vm already exists") instead of becoming a second writer.

NEGATIVE CONTROL, run rather than assumed: with `repo` swapped for a private
per-node workspace, `a_later_node_sees_an_earlier_nodes_files` FAILS with
`saw:[]`; restored, it passes. The `PhaseVm` seam exists for exactly this — it
models inject/collect through the real `mission_fs` tar path in milliseconds.

Two durability traps this tier walks into, both closed:

  - `requeue_stale` fires at 180s on `updated_at`, and one node here can run for
    an hour. `SubTopologyExecutor` keeps its parent alive from each leaf step;
    there is nothing between the start and end of a VM turn, so the turn holds a
    ticker that touches `updated_at` every 30s and aborts on drop. Without it a
    healthy composed run is requeued mid-node and boots a second VM.
  - the 15-minute stuck-run reaper asks "any step records since it was CREATED?",
    which describes a healthy composed run as readily as a wedged one. Hence
    `REAPABLE_TIERS` — worker-driven minus this tier. Reaping it would be #54 in
    a different costume.

`on_launch` mints no team for a microVM mission, deliberately: claws in
containers are what a VM mission does not use. So `mission_orchestrator::
composed_graph` builds the shape from the team template directly — nodes, roles
and pattern, zero claws provisioned. Per-node `attrs["backend"]` and
`attrs["node_id"]` override the mission's, which is what makes a validator node
on another provider's image a first-class graph node; a malformed `node_id`
fails the node rather than quietly running it where the graph did not ask.

Refusals are recorded as a failed run, not returned as an error: `launch_phase`
is swept every ten seconds, so a returned error is a phase that retries forever
while the log repeats itself.

501 tests pass, clippy clean. NOT yet proven end to end: no composed mission has
run on the fleet, so the resume-after-a-killed-worker leg is argued from the DB
test and the step-numbering test, not from a real two-node run.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-06 13:00:44 -07:00
Omar Sobh 66f730ad16 test(db): a regression net for the three-minute bug, with its negative control
The #54 fix had no test that could see it. Its defining property is that it only
appears past 180 seconds, and `verify-mission-delivery.sh microvm` runs a
90-second mission — so the end-to-end harness written to catch silent failure was
structurally blind to this one. A unit test asserting the allowlist's membership
helps, but would not notice a NEW sweeper added without the filter.

`crates/cm-db/tests/self_driven_runs.rs` tests the real SQL against a migrated
database, in milliseconds instead of eight minutes:

  - a `microvm` and a `session` run, 30 minutes idle and still `running`, must be
    left alone by `requeue_stale` — that is the bug, in one assertion
  - a `team` run in the SAME state must still be requeued, so the fix is "sweep the
    right rows" and not "stop sweeping"
  - the worker must not CLAIM a queued self-driven row, which is what turned a
    healthy run into "missing or invalid graph"
  - the allowlist names only worker-driven tiers

NEGATIVE CONTROL, run rather than assumed: with the tier filter removed from
`requeue_stale`, `requeue_stale_leaves_self_driven_runs_alone` FAILS; restored, it
passes. A guard that cannot detect the bug it was written for is decoration, and
this project has shipped one of those before.

489 tests pass, clippy clean.
2026-08-06 09:52:58 -07:00