Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Frontier Controller

The heart of the explorer is a small loop in one process. It owns every exploration decision. Nothing else decides anything: assertion macros describe interesting behavior, workers execute timelines, and the controller, the Explorer type in moonpool-explorer, turns discoveries into follow-up work.

                controller (Explorer)
                        |
                     frontier          (FIFO queue of jobs)
                        |
            +-----------+-----------+
            |           |           |
          worker      worker      worker
            |           |           |
            +-----------+-----------+
                        |
               discovery journals
                        |
           productive? -> expand once
           barren?     -> the branch dies

Jobs Are Recipes

The unit of work is minimal:

pub struct ExploreJob {
    pub recipe: Recipe,   // Vec<(rng_call_count, seed)>
}

A job says: replay this prefix, then keep going with the fresh randomness of the final segment’s seed. To execute a job, the runner runs one complete simulation from the start with the recipe installed as RNG breakpoints (set_rng_breakpoints). When the call count passes a breakpoint’s target, the stream reseeds and the count restarts at 1.

Every decision in a run is a counted draw on the one simulation stream: executor scheduling, select! offsets, swarm masks, fault coins, the workload’s own draws. So replay is exact, and cheap enough that no live checkpoint is needed. The network and storage profiles are drawn before the reset that starts the count, so a breakpoint can never land on them. Attrition, outage, CPU and gray-failure regimes are drawn after it, so they replay too.

The Discovery Journal

During a run, every globally new discovery goes into a per-run journal as a DiscoveryEvent: the kind, the semantic state_id, and the call_count at that moment. The call count is the coordinate where a child recipe can anchor.

root run (seed 42):
  journal: [floor 2 entered @ call 812, key found @ call 1490]

expansion, anchored at the highest-priority discovery:
  progress > structured state novelty > one-shot coverage
  the latest call count wins inside a class

The six DiscoveryKind values fall into three classes:

ClassKindsExample
ProgressWatermarkImprovement, FrontierAdvance, BucketQualitya numeric watermark moves, more assert_sometimes_all! facts true at once, a bucket’s quality improves
Structured noveltyBucketFirst, BooleanCombinationa new assert_sometimes_each! bucket, a new partial truth combination
CoverageSometimesPassthe first pass of assert_sometimes! or assert_reachable!

This order stops a late, easy coverage hit from pulling the children away from an earlier step toward a hard goal. Every discovery still registers its own exemplar for later continuation scheduling.

The journal holds at most 256 entries (MAX_JOURNAL_ENTRIES). Repeated events for one state coalesce to the best anchor. When the journal is full, a stronger discovery replaces the weakest retained one. Early coverage noise can never hide late progress.

One Expansion Per Run

A run with at least one discovery is productive. The controller expands it exactly once: it enqueues branching_factor children, anchored at the highest-priority discovery. A run that made five discoveries is still one productive execution, not five branching events. The child seeds are derived by hashing the root seed, the base seed, the anchor, the state id, the continuation round and the child index, so the frontier is deterministic too.

A run with an empty journal found nothing the multiverse had not seen. It is not punished or scored. Its branch produces no children and dies. There is no energy accounting. The only global limits are the per-seed run budget, the frontier size and the recipe length.

ExplorationConfig {
    workers: 0,              // default: in-process, deterministic
    max_runs_per_seed: 512,  // default
    branching_factor: 4,     // default
    max_frontier: 1024,      // default
    max_recipe_len: 64,      // default
}

These are the ExplorationConfig::default() values. enable_exploration panics on a zero bound; workers: 0 is valid. The dungeon binary uses max_runs_per_seed: 24_000 with 4 workers, the maze binary 2400 with none.

Who Decides Novelty

Novelty detection lives in the shared assertion region of moonpool-assertions. First passes, new buckets, watermark improvements, frontier advances and partial truth combinations are guarded by atomic compare-and-swap latches. The timeline that wins the latch journals the discovery. Every other timeline, in any process, sees “already known”. There is no check-then-merge race.

The controller is the single owner of everything built on top: frontier, exemplars, statistics and bug recipes. The runner installs one hook so the journal can stamp each discovery: set_rng_count_hook(rng_call_count).

Bug Recipes

A failing exploration run (a workload error, an assert_always! violation, or a worker crash) is recorded with its job’s recipe. The controller keeps at most 4 per seed (MAX_BUG_RECIPES). The root seed is marked failed in the main report, and report.exploration.bug_recipes carries each BugRecipe { seed, recipe }. Replay reproduces the failing timeline bit for bit:

let bug = report.exploration.as_ref().and_then(|e| e.bug_recipes.first())?;

SimulationBuilder::new()
    .workload_factory(|| Box::new(MyWorkload::default()))
    .replay_timeline(bug.seed, bug.recipe.clone())
    .run()?;

replay_timeline sets one seed, one iteration, and installs the recipe for that run.

The next chapter looks at how jobs run while the process count stays fixed.