diff --git a/docs/research/verse-tick-identity-prior-art.md b/docs/research/verse-tick-identity-prior-art.md new file mode 100644 index 0000000..04c9210 --- /dev/null +++ b/docs/research/verse-tick-identity-prior-art.md @@ -0,0 +1,61 @@ +# Research note: where a turn's identity lives (verse-run re-map, 2026-10-11) + +Imagination `imagination-gl-tick-identity`, session `self-2026-10-11-ag`. A short +prior-art check before settling tick identity for cut `verse-run` r2. Appended to the +map's "Verse run identity" section. + +## The question + +At Ghostlight `06a8373` a tick is named `now / span` (`cover.rs:46`), the span is not in +the name, and every cell id and command id is re-derived from the opportunities, +attention order, span and cover budget the resuming run holds at that moment +(`run.rs:557-565`). Soul s1 showed a different span or budget on resume yields new ids, +so a subject acts twice in one tick (p1, p2), and a different span reuses a closed +tick's index (p4). + +## Prior art + +- **Durable workflow engines (Temporal, Azure Durable Functions).** The identity of + each step is assigned when the step is first scheduled and recorded in the + execution's event history. On resume the code is replayed against that history and + every command it produces is compared with the recorded one; a difference is a + non-determinism error, not a second execution + ([Temporal: execution failures](https://docs.temporal.io/troubleshooting/execution-failures); + [community thread](https://community.temporal.io/t/non-determinism-error/8279)). + The forum cases are exactly Soul's: a timer duration or a child id computed + differently on replay. The lesson: resume reads the recorded decision; it does not + re-derive it from inputs that may have changed. +- **Deterministic lockstep games.** The turn (frame) number is simulation state, + advanced only by the simulation, and every command is tagged with the turn it runs + in, so all peers and every replay agree on which turn a command belongs to whatever + the wall-clock or turn length + ([Unity discussion](https://discussions.unity.com/t/deterministic-rts-game-engine/467205); + [Gaffer On Games](https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/)). + The turn count, not elapsed time divided by turn length, is the name. +- **Idempotency keys (event-sourced command handlers, payment APIs).** A key answers a + retry with the stored result; the same key with a different request is refused, not + re-executed. Ghostlight's kernel already does this + (`lib.rs:1633-1644`: same caller and body, `AlreadyApplied`; otherwise + `CommandIdConflict`), which is why Soul's p4 clock under another span was refused. + +## What this decides for verse-run + +1. The tick ordinal is world-held: the number of `TimeAdvanced` commits in the world's + journal when the tick opens. It is span-independent, monotone, injective per world, + restored with a world snapshot (so ruling `replay-restored-snapshot` needs nothing + else for a run that starts on a tick boundary), and consecutive (which is what the + cover's rotation window wants, `cover.rs:340`). +2. What a tick decided when it opened (span, cover budget, the partition and each + cell's id and members) is recorded once, before the first cell runs, in a tick + document owned by the run store, and a resuming run reads it instead of deriving a + cover. This is Temporal's history rule at the granularity the verse needs. +3. Command ids stay derived, but only from that frozen identity (world, ordinal, cell + id, subject), never from live inputs, so there is no second copy of them to drift. +4. The controller answers a persisted row whose command the world committed with its + receipt whatever the caller's current scope digest, because the command id is the + idempotency key; a row still in flight keeps the digest check, so no tokens are spent + on a scope that moved. + +Not adopted: a world-state tick counter (a `STATE_SCHEMA` change, and the kernel would +learn ticks exist, which `cover.rs:1-12` forbids); a run-store-only ordinal (a replay +over a restored world with a fresh run store would number ticks from 1 and diverge). diff --git a/notes/ghostlight-verse-map.md b/notes/ghostlight-verse-map.md index bdfb753..1ab2b8c 100644 --- a/notes/ghostlight-verse-map.md +++ b/notes/ghostlight-verse-map.md @@ -126,6 +126,106 @@ No commit after `454fc33` touches code, so every PA.f211-PA.f216 is open on a Draft card with Approve, Seed and Activate as separate buttons (`eve.rs:343-415`). +### The Bonsai host under ruling `bonsai-idle-unload-host` + +Probed 2026-10-09 by Imagination (`imagination-gl-bonsai`, session +`self-2026-10-09-ag`). Fork sources were fetched raw from +`PrismML-Eng/llama.cpp` at `1a07bfa5f4144274c8f1c9963821dd9d9a51854b`; paths +below are `tools/server/` in that tree unless named. + +- **B1. Ghostlight base.** `origin/main` is `4474718`. `git diff --stat + 4fee97f 4474718` over `controllers.rs`, `local_inference.rs`, `play.rs`, + `eve.rs` and `runtime.rs` is empty: every phase-1 anchor in those files holds + at `4474718` (the commits between are the docs-subtract cuts). +- **B2. Who owns the host scripts.** `gamecult-ops` `origin/main` is + `9c12adb`. Its tree has no Bonsai script, unit, Dockerfile or template: the + only Bonsai file is `runbooks/bonsai-local-llm-raven.md`. No copy of + `rss-watchdog.sh` or `idle-reaper.sh` exists anywhere under `F:\Projects`. + `run.sh`, `start-all.sh`, the `Dockerfile`, the template and both scripts + exist only on Raven, in WSL under `/opt/gamecult/bonsai`. Their repo-to-be is + `gamecult-ops`, which is a campaign repo. +- **B3. Raven unreachable during this pass.** `ssh raven` (10.77.0.4:22) and + the LAN address 192.168.178.165:22 both timed out. On Yggdrasil, `curl + http://127.0.0.1:18080/v1/models` exited 7 (connection refused: the tunnel's + listener is not bound). That differs from the exit 52 (accept and close) + recorded above, so both shapes occur. Raven was then reported under live + repair (bootloader damaged by an accidental format of `E:`), and no + further probe of Raven or the tunnel was made. Every Raven claim on this + page is therefore **unprobed** at mapping time. The + contents of `run.sh`, the watchdog and the reaper come from the Eyes file + `F:\Projects\bonsai-host-2026-10-09.md` §1, which read them in full at about + 17:19 local. Residual VRAM while sleeping and the time to wake are + **not measured**. +- **B4. The flag exists at the pin.** `common/arg.cpp:3779-3785` defines + `--sleep-idle-seconds SECONDS`, default -1 (disabled), and rejects 0 and + values below -1. The runbook confirms the image pins `1a07bfa5`. The argv of + `run.sh` in the Eyes file does not pass the flag. +- **B5. What sleep does.** `server-queue.cpp:278-360`: when the queue has had + no task for `idle_sleep_ms`, it marks itself sleeping and runs the sleeping + callbacks. `server-context.cpp:906-921`: entering sleep calls `destroy()`, + which frees the model, contexts and mmproj. Leaving sleep calls + `load_model`, and a failed reload is `GGML_ABORT` (the process dies). A + request that needs the model calls `wait_until_no_sleep()` + (`server-queue.cpp:115-130`, from `server_res_generator`, + `server-context.cpp:4100-4110`) and blocks until the reload returns. So the + first request after sleep is one HTTP call that waits out the reload. It + gets no 503 and no transport error. +- **B6. Endpoints that never wake it** (each one starts with + `create_response(true)`): + - `GET /health` (`server-context.cpp:4526-4537`) is always 200 + `{"status":"ok"}` once the server has started. + - `GET /props` (`:4676-4686`) returns cached props while sleeping, with + `is_sleeping: true` (`get_res_props`, `:4471-4506`; the cache is filled on + entering sleep, `:5411-5418`). + - `GET /v1/models` (`:4946-4955`) is answered from the cache. + - `GET /metrics` is exempt too. + + `README.md:2063-2075` ("Sleeping on Idle") lists the same exemptions and + says they do not reset the idle timer. The queue's sleeping flag clears only + after `load_model` returns (`server-queue.cpp:339-350`), so `/props` reports + `is_sleeping: true` for the whole of a wake. +- **B7. An endpoint that wakes it.** `GET /slots` (`server-context.cpp:4601-4613`) + calls `create_response()` without the bypass, so it waits for a reload. Its + `SLOT_GET` task also resets the idle timer: `server-queue.cpp:24-26` exempts + only `METRICS`. A watchdog that polls `/slots` therefore wakes a sleeping + model, and it keeps an awake one from ever sleeping. +- **B8. Before the first load.** Until the first load completes, every + non-frontend path answers 503 with `{"error":{"message":"Loading + model","type":"unavailable_error","code":503}}` (`server-http.cpp:253-272`). + This covers a fresh start and a Docker restart. Ghostlight already classifies + 503 as retryable (`local_inference.rs:183-189` at `4474718`). +- **B9. Log lines.** A slot starts in `launch_slot_with_task` + (`server-context.cpp:1614`); the reaper greps for `launch_slot_`. A slot's + release logs `stop processing: n_tokens = ...` at info (`:498-502`). +- **B10. Ghostlight with a sleeping or stopped Bonsai today** (at `4474718`): + - `InferencePort` (`controllers.rs:299-311`) has no readiness method. + - `LocalInferencePort` has one client whose overall timeout is + `RESPONSE_TIMEOUT`, 900 s (`local_inference.rs:145-164`, + `controllers.rs:68`). A sleeping model therefore costs the first call its + reload inside one call, well under 900 s. Meanwhile the card shows + "Resolving your turn…" (`eve.rs:530-543`) and nothing says the model is + waking. This is a bounded wait, not a hang, but it is not an honest one. + - A stopped container behind a live tunnel accepts and closes, which today + becomes `InferenceFault::new`; play-faults R3 makes it retryable. A dead + tunnel refuses the connection, which is already retryable. + - A `world.play` admission error reaches the player as `PlayError`'s + `Display` text, through `RuntimeCommandError::Payload` + (`runtime.rs:1399-1402`). + - `admit` returns a key replay at `play.rs:1476`, before it opens a turn or + applies an answer (`:1479`). +- **B11. Raven's lifecycle today** (from the Eyes file; not re-probed): + - The reaper (`bonsai-idle.timer`, every 5 min) runs `docker stop bonsai + sillytavern` when the container is older than 30 min and its log has no + `launch_slot_` in the last 30 min. + - The watchdog (`bonsai-rss.timer`, every 2 min) runs `docker restart bonsai` + when RSS is over 4.5 GiB, deferring while `/slots` shows `is_processing`. + - `run.sh` sets no restart policy unless given `--resident`. + - The container last ran 2026-09-30 10:38:19Z to 11:28:08Z after a desktop + launch. The launcher returns once the stack is up, so WSL stayed up for + about 50 minutes with no attached session. + - Raven's Idunn runs Muninn only, and Ollama and LM Studio cannot load PQ2_0 + (Eyes §2 C and D). + ### A live defect not yet in the findings The live process has logged `Heimdall refresh transport unavailable @@ -206,7 +306,7 @@ One row per persistent kind phase 1 touches. No cell may be empty. | Heimdall private command receipt (Heimdall Postgres) | `(app_slug, idempotency_key)` + request fingerprint | written once per executed command; replayed for an identical request; a different request under the key is refused | Heimdall; its fingerprint rule is a Heimdall follow-up, not a phase-1 cut | | Idunn generation (transient unit, release dir) | `sha256-` | staged → started → Warming → lease → Active → superseded or failed; the previous Active stays until a new one is Ready | Idunn; retention and failed-unit collection are Idunn's follow-up | | v1 body (`ghostlight-dungeon.service`, `/srv/ghostlight`, v1 state) | unit name; paths | retired in fact 2026-09-23; unit still enabled | nobody today; phase 1 assigns its removal to a gamecult-ops cut, data by operator ruling | -| Bonsai lifecycle (Raven containers) | container `bonsai`; scripts under `/opt/gamecult/bonsai` | started by hand ("Start AI Chat"); restarted by the RSS watchdog; stopped by the idle reaper | gamecult-ops/Raven; Dungeon only reads the endpoint; availability policy is an operator question | +| Bonsai lifecycle (Raven container and model) | container `bonsai`; installed from gamecult-ops into `/opt/gamecult/bonsai` (raven-bonsai-scripts) | today: started by hand, restarted by the RSS watchdog, stopped by the idle reaper. Ruled: the server stays up (Docker `unless-stopped`); the model loads on start and on demand and unloads after `BONSAI_SLEEP_IDLE_SECONDS` idle (llama-server sleep); the watchdog restarts only an awake, quiet server; only a human stops it ("Free the GPU"); no reaper | llama-server decides load and unload; Docker decides process restart; the watchdog decides leak restarts; gamecult-ops owns the scripts; Dungeon learns readiness only through its inference port (model-asleep) | | Repository docs (README, AGENTS, live architecture pages, handoff) | path on `main` | rewritten to the live machine; pre-rebuild material leaves `main` by operator ruling | the Ghostlight repo; history in git, dated cut documents and postmortems | ### Where phase 2's verb sets plug in @@ -243,6 +343,16 @@ verbs as the target. - **Faults belong to the card, not the log alone.** PA.f212 is an authority gap: the turn store knew the fault and the projection dropped it. The fix extends the projection; it adds no second record. +- **Bonsai sleeps inside its own server.** The ruling named hosts that + unload on idle. Ollama and LM Studio cannot load PQ2_0, and the pinned fork + already sleeps (B4, B5), so the cut is a flag plus deleting the reaper. + llama-swap would add a process and a second owner of load and unload. It + earns its place only if a sleeping server's leftover VRAM (its CUDA context) + proves too large, and raven-bonsai-scripts measures that before anything + else changes. The watchdog must stop calling `/slots` on a sleeping server, + because that call wakes the model (B7). The waking and away rows belong to + model-asleep and the fault row to play-faults, so neither cut decides the + other's row. - **Retrying inference is safe.** A model call is a proposal the kernel admits or refuses, so a transport fault before any reply is retryable. That is the Ghostlight half of PA.f211. The Raven half (the watchdog's quiet @@ -256,3 +366,386 @@ verbs as the target. Dungeon tests that drive the real client, and nothing runs them. Fixing the gate means a runner with both Rust and Node, which is Idunn's recipe and runner surface: a follow-up, with each phase-1 Hands run doing them by hand. + +## Verse sim: port inventory and model page + +Imagination `imagination-gl-verse-sim-0b`, session `self-2026-10-10-ag`, +2026-10-10. Governing rulings: `verse-sim-pipeline`, `aetheria-sim-first`, +`gestalt-machinery-port` (the old machine is ported, not redesigned), with +`two-ghostlight-consumers`, `verse-verbs-two-render-paths`, +`verse-cast-crossmedia`, `action-scenes-in-game` and +`aetheria-release:ruling:storylets-simulate-sift-lift-weave`. Draft target: +`notes/ghostlight-verse-target.md` (revision 3, which absorbed the draft). Rebuilt kernel read at +`origin/main` `367e2eb`. + +### Where the old machine is + +The multiresolution gestalt machinery is **not** under the parked tag +`parked/ghostlight-pre-rebuild`: that tag parks the docs, Python tools and +fixtures that `main` still carried on 2026-10-09. The Rust machine was +deleted by the kernel rebuild `6bb6869` (2026-09-02, "Rebuild Ghostlight +around explicit world authority", −105,981 lines). Its last complete tree is +the rebuild's parent, **`f9f019b`**, in `crates/ghostlight-dungeon/src/`. +Retrieve with `git show f9f019b:crates/ghostlight-dungeon/src/`. +Its design record is `docs/architecture/ghostlight-multiresolution-agency.md` +(on this branch; the live-fire evidence is at lines 830-936) and the +explainer `docs/articles/multiresolution-gestalts.md`. The autonomous tick +driver that survived `6bb6869` in reduced form was deleted later by play-agent +Cut 1 `d69e9d4` (−2,184 lines). + +### Port inventory + +Old line counts are of the file at `f9f019b`. "Main" means `origin/main` +`367e2eb`. A **forced change** is one the rebuilt ontology +(`docs/architecture/ghostlight-world-ontology.md`, its numbered invariants) +makes unavoidable; everything else ports as designed. + +| Old organ (file, size) | What it held | Depends on, absent from main | Attaches to on main | Forced change | +|---|---|---|---|---| +| Gestalt types (`domain.rs:130-345`) | `GestaltPersonaState` (shared capabilities, knowledge, resources, goals, pressures, home place); `GestaltMemberDelta` (per-person additions and removals over the baseline, equipment, obligations, relationships, memories, `materialized_actor_id`, relevance lease); `GestaltLineage` (fission ancestry by axis) | string ids with `gestalt:`/`member:` prefixes (`canonical_gestalt_member_local_id` strips repeats); free-string capabilities and knowledge | `SubjectKind::Population` (lib.rs:361-364); `PersonaMaterial`; `Knowledge`, `Custody`, `Commitment`, `Pressure` rows on the population subject | **F1** ids are reducer-issued `SubjectId`s, never caller strings (ontology invariants 1-2). **F2** the baseline is the population subject's own typed components, not a parallel state record. | +| Members, individuation, promotion, demotion (`domain.rs:144-262`, `gestalt.rs` 937 lines `GestaltPresencePlanner`, `resolution.rs:2239-2648`) | a member exists as a delta; *individuation* creates the delta; *promotion* materializes an Actor from baseline + delta; *demotion* de-materializes the Actor back into the delta; a model presence planner proposes all three per scene | a second identity layer: the same person is a delta and, at times, an Actor (`materialized_actor_id`) | `CausalBoundary::IndividuationRequired { population, scope }` (lib.rs:4132; ontology "Institutional layers and flow"); `Declaration::Subject` in an elaboration patch | **F3** an individuated member is a `Person` subject from birth and stays one; its delta becomes its own components and `PersonaMaterial` (invariant 4: only subjects decide). **F4** promotion and demotion change *resolution* (singleton cell or grouped cell), never identity; the old `GestaltDemotion { actor_id }` has no port. **F5** a member's knowledge is its own `Knowledge` rows; "effective knowledge = baseline + delta" (`resolution.rs:3130`) is not readable as authority (invariant 6), so individuation copies the baseline facts it needs with `AcquireKnowledge` citing accessible facts. | +| Agency graph (`domain.rs:264-333`, `resolution.rs:104-160`) | `AgencyProfile` per subject: six axes (Geography, Ideology, Authority, EconomyRole, SpeciesBody, Information) as string facets, `detail_debt`, `last_detail_tick`, `active_leaf`; `AgencyRelation` with ten kinds (Containment, Command, Membership, Alliance, Rivalry, Trade, Migration, Communication, Coercion, SharedLocation) and strength | a relation record: main has none ("There is no relation yet", ontology line 50; `EdgeKind` is `Route` only, line 419) | `cover.rs` builds adjacency from containment, membership, jurisdiction, shared place, relation and pressure (ontology "Scale"); the Tangle lens names relations | **F6** a typed relation edge enters the kernel (an `EdgeKind` beside `Route`, with `ComponentOp`s to form and dissolve it): Membership, Command, Alliance, Rivalry, Trade, Coercion, Migration; Containment of subjects and SharedLocation and Communication are already derivable (positions, channels) and do not port as edges. This is the target r2 phase-2 work, now pulled forward. **F7** axis facets are derived from typed components, not stored strings (geography from positions, authority from `Authority`/`Selection`, economy from `Custody`/`Dependency`, information from `Channel`/`Knowledge`); Ideology and SpeciesBody have no typed home and are dropped from the partition until one exists. **F8** `detail_debt` is derived from `last_opportunity_at` (main keeps that partition), not stored per profile. | +| Resolution cover (`resolution.rs` 5,505 lines: `plan_cover`, `validate_cover`, `advance_detail_debt`, merge loss; `domain.rs:345-497`) | budgeted partition into `SimulationCell`s (`Cohesive` or `Arena`), seven-term `MergeLoss`, `ResolutionPolicy` (cell budget 1-240, provider parallelism, epochs), `ResolutionPin` (KeepTogether, KeepSeparate, MinimumIndividualDetail), `ResolutionDemand` (axis weights, focal subjects, horizon), cell leases, causal follow-through windows | the old campaign record as the store for policy, pins, leases and debt | `cover.rs` (1,116 lines): pure `derive_cover`, content-addressed cell ids under `ghostlight.cover.v1`, `TickIndex` derived from the clock, debt rotation with urgency slots | **F9** the cover never enters `WorldState` (cover.rs:6-9). Policy, pins, leases and demand move to the verse runner's own run record, not the kernel. **F10** an `Arena` cell presents partitioned views, never a knowledge union (ontology "Scale" item 4); the old arena knowledge union does not port. `Cohesive` ports as the population subject acting for itself. The merge-loss terms port onto main's partitioner as its grouping cost. | +| Strategic wave pipeline (`scheduler.rs` 3,336 lines, `persona.rs` 6,540 lines cell projection) | demand projection by a cheap model; per-cell Projector → Persona → Interpreter membrane; `CellActionProposal` and attributed `CellInaction`; parallel dispatch under one provider gate; `ResolutionWaveCheckpoint` resuming only failed cells | `ModelPort` and the old persona stack (`persona.rs`) | `controllers.rs` (`NarrativePersona`, `OperationalAgent`, Projector/Interpreter), `RoutedInferencePort`, `WorldMailbox` role ports, `ExerciseDecision`/`DeclineDecision` commands | **F11** a cell's output is one affordance invocation or decline per constituent, each admitted separately against its scope digest (invariants 7, 10); there is no wave-level atomic commit. A refused grouped constituent stays refused (ontology "Scale"). | +| Outcome resolution (`outcome.rs` 4,387 lines; selective semantic verifier) | a batched model resolver returns a typed result per strategic attempt; a same-snapshot verifier advises on risky effects | model-decided outcomes | outcome bands on affordances, chosen by kernel digest (`BandPreimage`), no RNG | **F12** not ported. The kernel decides outcomes (target invariant `kernel-decides`, ontology invariant 7). Strategic activities, institution postures and migrations become catalog affordances (from Aetheria's grammar) whose effects compile to the 34 `ComponentOp`s: posture to `CreateCommitment`, migration to `Relocate`. | +| Fission (`compiler.rs` `GestaltFissionRequest`, `domain.rs:716` `GestaltFissionPreview`, `FissionGestalt` command) | approval-gated split of a population along one axis, member deltas assigned to one child, lineage kept | the `FissionGestalt` kernel command; lineage as its own record | elaboration patches (`submit_elaboration`); `Retire` | **F13** fission is one elaboration patch: declare child populations, Membership edges (F6), relocate each named member's membership, retire or keep the parent. Lineage is the Membership/Command edges plus the commit, not a separate record. **F17** (ruling `gestalt-fission-auto`) the old operator approval gate does not port: a fission answering a derived boundary is admitted during the run and listed in the run record for the operator to see afterwards. | +| Tick driver and away time (`main.rs` `scheduler_loop`, `process_due_ticks`, `advance_one_strategic_tick`; `scheduler.rs:1943` `due_tick_target`) | a background loop on 60 s and 300 s pulses; one strategic tick per idle hour after 15 min, capped at 8 (`away_budget_waits_and_caps_at_eight`); return catch-up before a player's fictional command; yields to live turns; `AdvanceStrategicTick` commits atomically; worlds with no simulatable agency tick without a model | a player and their idleness; the `AdvanceStrategicTick` command; the in-daemon timer | `ClockPort::advance_time` (mailbox.rs:688), `clock.rs` `derive_motion` (routines auto-fulfil, overdue obligations and goals raise pressure, zero inference) | **F14** a verse world has no player, so "away time" is the schedule itself: a run advances N ticks of a configured fiction span. Not ontology-forced; forced by the consumer. **F15** each tick is: derive cover, run cells, submit each proposal, then `AdvanceTime` (cover.rs:43-45 states this order); there is no `AdvanceStrategicTick`. | +| Clock consequences (`clock.rs` 739 lines, `ClockConsequenceBinding`) | a model bound due clocks to agent actions | a model stage on the clock | `clock.rs` `derive_motion`, zero inference | **F16** not ported; main's deterministic motion replaces it. | +| Model stage receipts (`persona_stage_receipt.v1`, written by `advance_one_strategic_tick`) | per-stage receipt keyed by hash, collisions refused | the old store | `LocalInferenceReceipt`/`SdkInferenceReceipt` (request hashes, token counts) | Not forced. Main's receipts carry hashes but not the response body, and nothing found persists them; replay needs the response stored (prior art (c), "Determinism and replay"). | +| Newspaper output path (`newspaper.rs` 6,120 lines, 27 tests; typesetter `tools/typeset_newspaper.py`, `notes/historical-newspaper-layout-grammar.md` and woodcut prompts under the parked tag) | the proof the old machine wrote newspapers at scale: a newsroom of staff and journalists, an editorial agenda, story pitches with conflict axes, articles that cite exact source facts and named people, a copy desk that files grounding findings, a press close checkpoint, Markdown and audit renderers; persisted as `world_newspaper_composition.v3` | the old `Campaign` and `ModelPort` | the commit journal and `WorldSnapshot`; facts with standing; `Channel` reach (news is what a channel's audience could know) | None from the ontology: it reads world state and writes only its own compositions (`advance_world_newspaper` writes the store under its own schema, never a campaign field). It ports as the first pipeline reader, with the copy desk checking citations against committed facts. | +| Tests | ~113 gestalt-related tests: kernel.rs 38 of 71, resolution.rs 16 of 26, scheduler.rs 14 of 19, gestalt.rs 6, persona.rs 18 of 41, outcome.rs 5 of 26, compiler.rs 8 of 73, main.rs 8 of 33 (names matched by script) | the old types | main's cover and clock tests | Port the invariants, not the tests: cover completeness and budget (`aetheria_scale_cover_is_complete_unique_and_budgeted`, `partitions_one_thousand_subjects_into_a_two_hundred_cell_wave`), `rivals_at_budget_one_form_an_arena_not_a_false_collective`, `contradictory_pins_are_rejected`, `detail_debt_rotates_low_budget_attention`, `lease_cannot_preserve_a_cell_disconnected_by_current_topology`, `partial_wave_checkpoint_resumes_only_failed_cells`, `named_member_activity_stays_attributed_to_the_person`, `individuation_cannot_duplicate_an_established_population_identity`, `gestalt_migration_moves_only_the_population_leaf`. Tests of F3-F5, F10, F12 and F16 subjects do not port. | + +Live-fire numbers the old machine left (multiresolution-agency.md:846-889): +the 31-wave, budget-8 run over 24 subjects took 1,211.6 s and 1,468,039 +prompt plus 165,725 completion tokens (60.69% cache hits, 7,567 prompt tokens +per committed consequence); a four-wave golden took 126,826 prompt tokens. +These are the only cost evidence for a cadence and budget ruling. + +Ruling `teardown-was-authority-sprawl` sorts the old tree: the rows above are +the good ideas to port (gestalts, multiresolution cover, pins and debt, +away-time as a driver, checkpointed parallel waves, attributed inaction, +strategic individuation, fission, the newspaper path, stage +receipts). The ad hoc mutation rules stay dead. Each has a replacement on +main: + +| Ad hoc rule in the old tree | Where | What replaces it on main | +|---|---|---| +| caller-supplied string ids and prefix normalisation (`member:`, `gestalt:`, `normalize_legacy_gestalt_member_identities`) | domain.rs:160-195, resolution.rs:160 | ontology invariants 1-2: reducer-issued typed ids | +| two-phase `AdmitEntity` and the flat per-mutation batch, repaired by reconciliation steps | legacy_transition.rs, transition.rs | invariant 3: one atomic `WorldPatch`; `resolve_patch` returns the complete mismatch set | +| wave-level strategic outcome resolver (a model decides whether an attempt worked) | outcome.rs | invariant 7: outcome bands chosen by kernel digest | +| selective same-snapshot semantic verifier on risky effects | outcome.rs, persona.rs effect verifier | invariant 5 (custody conserves) and effect ceilings on the affordance | +| titled verifiers, quotas, `*Qualification`/`*Verification`, complexity rounds, cover-completeness targets | compiler.rs, elaboration.rs | invariant 9: structural admission; `WorldScaleIntent` deficit; stock lenses | +| a stale proposal discarded on any revision change | kernel.rs | invariant 10: scope-digest binding | +| a model binding due clocks to actions | clock.rs | `derive_motion`, zero inference | +| a model presence planner deciding individuation, promotion and demotion | gestalt.rs | `IndividuationRequired` boundary answered by an elaboration patch; promotion and demotion are cover assignment (F4) | +| arena knowledge union and "effective member knowledge" | resolution.rs:3095-3150 | invariant 6: knowledge scoped to the holder; partitioned views | +| prompt-only prohibitions against repeated postures and absence-as-inaction, with text-length bounds | persona.rs contracts (multiresolution-agency.md:873-893) | `DeclineDecision` per constituent; `NoCanonicalChange` refusal of no-op commitments | +| guard that a strategic tick may not write an external subject | kernel.rs:1150-1170 | `ExternallyControlled` controller: an external subject has no opportunity and no turn (world-consumer-api.md:113-130) | +| "two competing plan authorities" check between a plan and a wave | kernel.rs:1142 | one command path per act (`ExerciseDecision`), no plan object | + +### Does main route every mutation through the grammar? + +Checked in `crates/ghostlight/src/lib.rs` at `367e2eb`, `reduce` (lines +1658-1790). The answer splits in two, and the split is a finding. + +- **Holds: one reducer, one closed operation set.** Every command is one of + seven `CommandBody` variants, `reduce` turns it into a typed `WorldEffect`, + and every world change is one of the 34 `ComponentOp`s, six + `Declaration`s, the clock, lifecycle or lens weights. "Adding an operation + is a design change ... never a runtime patch" (ontology "Component + operations"). The whole of `apply_effect` was not read, so "nothing else + writes `WorldState`" is read from the design and the dispatch, not proven + line by line. +- **Does not hold for the world-authored action grammar.** Only + `ExerciseDecision` goes through an affordance (`require_granted`, + `action::exercise`). Three other paths mutate the world without one: + `AdmitPatch` (the owner unconfined, elaborators within jurisdiction, the + consumer within its bound subjects, and `Play`, which may `Mint` and write + `Ruled` facts); `AdvanceTime`, whose `derive_motion` rolls routines and + raises pressure by kernel rule; and `ApproveDraft`, `ActivateWorld`, + `SetLensWeights`. So "applied the action grammar to world state mutation" is + true of the closed operation set and false of Aetheria's grammar in the + `verse-sim-pipeline` sense. Draft target invariant `grammar-governs` makes + the line explicit, and question `grammar-scope-of-mutation` asks where the + operator wants it. + +For the verse: promotion and demotion are not mutations at all (F4). +Away-time results are affordance invocations from cells plus clock motion. +Individuation and fission are elaboration patches. Meddling is owner patches +or run controls. Each is named in the Authority column below; any writer not +named there is forbidden. + +### Model page + +One row per persistent kind the verse sim needs. The Authority column names +the one owner and the forbidden writers; for world state the rule is +invariant `grammar-governs` in the draft target. An empty cell is marked +**EMPTY** and is a finding. + +| Kind | Identity (namespace; injective?) | Lifecycle | Authority (owner; forbidden writers) | +|---|---|---|---| +| Verse grammar: Aetheria's ontology and action grammar (verbs, each with one render path and its participant roles) | `aetheria.verse_grammar` (CultGlobal, one per catalog, carries the revision) and `aetheria.verse_verb` keyed by its canonical kind name, in Aetheria's catalog `GameData/Aetheria.cc` (ruling `verse-grammar-home-ruled`). A world binds (schema id, revision, sha256 over the verbs' canonical encoding sorted by kind): injective per content. | authored and revised through an AetherDb command; a world keeps the revision it was created under; moving a running world to a new revision is not designed (main never migrates), so a revision means a new world until a migration is needed | Owner: Aetheria, for which verbs exist, how each renders and who takes part. Ghostlight owns what each verb does to world state (question `verb-effects-owner`). Forbidden writers: Ghostlight elaborators, the play authority and every run; the kernel refuses a kind or role signature the bound grammar lacks (cut `grammar-binding`). | +| Lore source (AetheriaLore pages, Aetheria `Faction` records) | page path + AetheriaLore commit; `aetheria.faction` CultDocument key + Aetheria commit. Injective per commit. | read at genesis and elaboration; never written by the sim | Owners: AetheriaLore and Aetheria. Forbidden writers: every sim organ (target invariant `canon-one-way`). | +| Verse world (journal and state) | `WorldId`, reducer-issued; `revision` and digest chain | created, approved, activated; one commit per admitted command; never forked today | The kernel, through its closed operation set. Forbidden writers: the runner, the Eve surface and meddling tools except through kernel commands. | +| Elaborated entities: places, resources, facts, channels, routes | `EntityId`/`EdgeId`, reducer-issued; injective | admitted by patch; retired; facts may be Canonical, Claimed or Ruled | Kernel admits; elaborators and the owner propose. Lore-derived facts carry their source (`verse-provenance`). | +| Import key: Aetheria record or lore page ↔ Ghostlight id | `(schema id, record key)` for a catalog record (`aetheria.faction/`); `(vault-relative page path)` for a lore page (phase B). Injective: admission refuses a second subject or entity carrying a key already held. The Aetheria commit is on the world's genesis command, not in the key, so the key survives re-reading a newer catalog. | written once, by the declaration that creates the subject or entity; never changed; retiring the subject retires the key with it, and the key is never reissued | Kernel admission (cut `import-key`). Writers: genesis and elaboration patches only (ruling `grammar-scope-ruled`). | +| Factions (Aetheria factions as actors) | `SubjectId`, kind `Institution` | declared at genesis from `aetheria.faction`; act through offices and catalog affordances; can be retired | Kernel; the faction's `OperationalAgent` controller proposes. Needs relations (port F6) for alliance and rivalry. | +| Gestalts (populations at each resolution) | `SubjectId`, kind `Population` (F1, F2) | declared at genesis or elaboration; migrate by `Relocate`; split by fission (F13); retired | Kernel admits. Fission is an elaboration patch answering a derived boundary, one of the named non-verb writers (ruling `grammar-scope-ruled`), admitted during the run without waiting (ruling `gestalt-fission-auto`). The operator sees it afterwards: the run record lists each fission commit, and the meddling surface shows it in the run history. Forbidden writers: the cover and the runner (they choose resolution, never identity). | +| Individuated members (the people gestalts give up) | `SubjectId`, kind `Person`, with a Membership edge to the population (F3, F6) | born by an elaboration patch answering `IndividuationRequired` or a strategic individuation; never folded back (F4) | Kernel admits; the elaborator or the cell proposes; duplicates of an established identity refused (ported test). | +| How a region moves between resolutions | derived, no identity of its own: a cell id is a content hash under `ghostlight.cover.v1` | recomputed every tick from pins, budget, pressure and debt; a leased cell survives while still connected (old lease rule) | `derive_cover` (pure); pins and budget from the run record; promotion and demotion are only cell assignment (F4, F9). Forbidden writers: the kernel (never sees a cell), any model. | +| Cast (characters the audience follows) | a `Person` `SubjectId` named in the run record's cast roster; no kernel component (being followed changes attention, not the world) | joins or leaves by an operator steering act at a tick boundary (ruling `meddling-first-ruled`); a member who is retired in the world stays in the roster's history | Owner: the operator, through run controls. The cover keeps roster members at singleton resolution (the old `MinimumIndividualDetail` pin). Forbidden writers: models and the runner. Lands in phase D; phase A has no persons. | +| Verse run (one invocation) | `(WorldId, run sequence)`, document `ghostlight.verse_run.v1`; the sequence is claimed from the run store before anything else is opened (see "Verse run identity" below) | claimed manually under a token cap (ruling `verse-cadence-ruled`) → per-tick checkpoint → Completed, BudgetExhausted, Failed or Stopped; a run never resumes itself: a later run resumes the open tick, which is the world's, not the run's | Owner: the run store claims, the verse runner writes its own record (cut `verse-run`). Idunn will own scheduled start and process survival (phase D). Forbidden writers: Dungeon's play loop, an in-daemon timer, the Eve surface. | +| Tick | world-held ordinal: the count of `TimeAdvanced` commits when it opens, plus a tick document `ghostlight.verse_tick.v1` keyed (world, ordinal) freezing span, budget and cells (see "Verse run identity" below; the `now / span` index at 06a8373 collided across spans, Soul s1) | opened (document written before the first cell), closed by its clock commit, or closed by the world when the ordinal moved without it; never reopened | Kernel owns time and the ordinal's source; the run store owns the tick document; the runner owns the order. | +| Run record: policy, pins, demand, roster, checkpoints, spend | inside `ghostlight.verse_run.v1`; phase A carries cap, tick span, cover budget, checkpoints and spend; pins, demand and roster arrive with phase C-D | policy and pins revised by operator act at a tick boundary with an epoch (old `ResolutionControlReceipt` rule); checkpoints per tick, superseded | Runner. Forbidden writers: the kernel. | +| Model proposal and its validation | derived command id (world, tick, cell, constituent), so injective per tick | proposed; admitted, or refused (`ScopeChanged`, precondition, ceiling); a narrative singleton may re-lower once (`interrupted_from`) | Kernel reducer decides. Forbidden writers: an outcome resolver or verifier model (F12). | +| Provider call record (for replay and cost) | `(run id, call sequence)`, document `ghostlight.verse_provider_call.v1`, carrying the provider request sha256, model, response events and token usage | written once per call by the recording port before its output reaches a lane; never edited; replay serves it back by request sha256 | Owner: the recording port in front of the inference lane (cut `call-record`), which also owns the run's spend and refuses a call whose bound exceeds the remaining cap. Forbidden writers: lanes and the runner. | +| Pipeline output: newspaper issues; later episodes and storylets | newspaper: the old `ghostlight.world_newspaper_issue.v3` shape at `f9f019b`, keyed by world and the commit range it covers (ruling `verse-first-consumer-ruled`); episodes and storylets have no kind yet and are outside this target's phases | newspaper: composed, checkpointed at press close, persisted, never edited | Owner: each reader owns its own output (`readers-read`). Forbidden writers: every reader may write only its own output, never the world. Lands in phase D. | +| Operator meddling act | world acts: kernel commands with an idempotency key and the owner principal; steering acts: run-record revisions with the run id and an epoch | committed to the journal (inject) or the run record (steer); never edited afterwards | Owner: the operator, through the verse Eve surface (ruling `meddling-first-ruled`: steer first, then inject). Forbidden: any tool that writes `.cc` files or the journal directly. Lands in phase D. | + +### Cells that could not be filled + +Revision 3's rulings settle every cell the 0b pass left empty except these: + +- Verse grammar, lifecycle: how a running world moves to a new grammar revision. Not needed until a grammar revision meets a world worth keeping; a new world is the answer until then. +- Episodes and storylets: no kind and no owner. Both are outside revision 3's phases. + +## Verse sim: phase A body facts (probed 2026-10-10) + +Imagination `imagination-gl-verse-sim-cuts`, session `self-2026-10-10-ag`. Pinned to +Ghostlight `origin/main` `367e2eb` and Aetheria `origin/master` `b623d51`. Each line is +a source read. + +- **A1 Aetheria content is one CultCache catalog.** `AetheriaStores.Open` + (`Assets/Scripts/ServerShared/AetheriaStores.cs:9`) registers `CatalogTypes` in one + `SingleFileMessagePackBackingStore` over `GameData/Aetheria.cc` (LFS, + `.gitattributes` `GameData/*.cc`). A `[CultGlobal]` catalog type with no record makes + `Open` throw ("catalog globals are authored, never invented"), so a new global and + its record land together. Content is authored by `tools/AetherDb` commands with an + `apply` switch (`Program.cs:25-48`, `PiratesFaction` at 1848 is the newest pattern). +- **A2 Factions are `aetheria.faction` v1** (`Corporations.cs:13-59`): name, short + name, description, personality weights, colours, name file, boss hull, influence, + and `Allegiance`, a map of faction refs to weights. No home place and no members. +- **A3 What the game can film today** is Aetheria's `TaskType` (`Agents/Tasks/AgentTask.cs:24-32`): + Mine, Haul, Tow, Defend, Attack, Explore. That is the ship-action half of the starter + grammar. The conversation half on Ghostlight's side is the kernel-built `speak` + (`patch.rs:2396-2415`): zero roles, colocated audience, carries speech. +- **A4 Ghostlight reads CultCache single files already.** The workspace pins + `cultcache-rs` at CultLib `85f7024`, which carries `SingleFileMessagePackBackingStore` + (`packages/cultcache-rs/src/lib.rs:305`). `crates/ghostlight` depends on it + (`Cargo.toml`), today only for the redb controller-work store. +- **A5 Affordance kinds are open text.** `AffordanceKindName` (`patch.rs:247-254`) is + checked for shape and per-patch uniqueness only (`patch.rs:3135-3151`). Genesis + re-derives its patch through `resolve_patch` (`lib.rs:1939-1993`), so a check inside + resolution holds for genesis and for every later patch alike. +- **A6 World creation is Dungeon-shaped.** `WorldMailbox::create` (`mailbox.rs:105`) + lowers `CreateWorldIntent` (`lib.rs:433-452`), which requires a human subject and + declares one commons where every genesis subject stands. A verse genesis has no + human, so it needs its own intent; it reuses the same `CreateWorld` command + (`lib.rs:414-428`) and `resolve_patch`. +- **A7 The tick driver's parts exist; the driver does not.** `derive_cover` + (`cover.rs:263`), `ControllerRunner::run_cell` (`controllers.rs:3266`, singleton and + grouped cells, derived command ids so a resumed tick is idempotent), + `WorldMailbox::agency_graph` (`mailbox.rs:449`, reserved to the tick driver) and + `PlayPort::advance_time` (`mailbox.rs:688`). Nothing on main calls them in a loop. +- **A8 No lane reaches a hosted cheap model.** The connector lane is CodexConnector, + which runs the Codex executable for GPT models (its daemon manifest allows + `gpt-5.6-*`) and returns no token usage. The local lane speaks OpenAI-compatible + chat completions (`local_inference.rs`), parses `usage`, but refuses any + non-loopback endpoint in its constructor (`local_inference.rs:167`) and carries no + credential. DeepSeek's API is OpenAI-compatible, so the hosted lane is a second + binding of the local port. +- **A9 Token usage stops at the port.** `LocalInferenceReceipt` holds prompt and + completion tokens (`local_inference.rs:42-55`, built at 347-370), but + `InferenceOutput` (`controllers.rs:155-159`) carries only events and a receipt + digest, so nothing above a port can count spend. +- **A10 Principal evidence is constructible.** `VerifiedPrincipalEvidence::new` is + public (`lib.rs:160`); the Dungeon builds it from a Heimdall session. A phase-A CLI + run by the operator on the host builds it from a configured account subject hash. + +## Phase A cut order + +Phase A delivers the first watched run: the operator invokes +`ghostlight-verse run` on Yggdrasil under a token cap and reads, per tick, what each +Aetheria faction did in grammar verbs and what it cost. Specs in `depends_on` order +(admission status below the list): + +1. `verse-grammar` (Aetheria): the two document types, validation, catalog + registration and the AetherDb command that authors the starter grammar. +2. `grammar-binding` (Ghostlight kernel): a world binds a grammar at genesis; the + resolver refuses kinds and role signatures it lacks. No IO. +3. `import-key` (Ghostlight kernel): the subject and entity import key and its + injectivity check. No IO. +4. `grammar-reader` (Ghostlight): reads Aetheria's catalog file, yields the grammar + binding and the faction records; fixture written by Aetheria's own types. +5. `hosted-lane` (Ghostlight): the local port's hosted binding and token usage on + `InferenceOutput`. +6. `call-record` (Ghostlight): the recording port, call records, the cap at the + call door, and the replay port. +7. `verse-genesis` (Ghostlight): the verse creation intent: factions as institutions + under import keys, grammar verbs lowered to affordances, the world bound. +8. `verse-run` (Ghostlight): the run record, the tick loop and the + `ghostlight-verse` binary with `genesis` and `run`. + +Phase A leaves out, on purpose: relations and allegiances (F6, phase B), lore +elaboration (phase B), persons and the cast (phase B-D), pins, debt, fission (phase +C), the Eve surface, Idunn scheduling and the newspaper (phase D). Verb effects are +empty in phase A: a ship verb records the act as a decision event and changes no +component, the same under either answer to question `verb-effects-owner`. + +Admission, 2026-10-10: `grammar-binding`, `import-key`, `hosted-lane` and +`call-record` are in the mind. `verse-grammar` was refused `RepoNotInCampaign` +(`GameCult/Aetheria` is not in the campaign's repos), and `grammar-reader` was then +refused `UnknownDependency` (`verse-grammar`), which holds `verse-genesis` and +`verse-run` behind it. The four held specs are written in full, anchors checked +SAME at their bases, outside the repo; they are admitted once the campaign lists +`GameCult/Aetheria`. + +Rationale for the order: the two kernel cuts are pure and land before anything that +reads a file; the reader's fixture needs Aetheria's types, so it waits on +`verse-grammar`; spend has one owner, the recording port, so the cap is built +before the runner and the runner never counts tokens itself. + +## Verse run identity (re-map after Soul s1, 2026-10-11) + +Imagination `imagination-gl-tick-identity`, session `self-2026-10-11-ag`. Pinned to +Ghostlight `ghostlight-verse/verse-run` at `06a8373` (Hands report `cut-verse-run.h1`, +Soul verdict `cut-verse-run.s1`, eight findings). Prior art is in the research note at +the end of this section. + +### Body facts (source reads at 06a8373) + +- **R1 A tick is named by `now / span`.** `TickIndex::of` (`cover.rs:46-48`) floors + `now` by the span; `derive_cover` (`cover.rs:263-271`) takes `now` and the span and + names the tick so. The span is not in the name, so two spans reach the same index at + different times (Soul p4: two 60-minute ticks, then span 120 gives index 1 again). +- **R2 Every derived id hangs off that index and the live partition.** `cell_id` + (`cover.rs:87-97`) is `(world, tick, size, lowest member)`; the cell's work row + (`cover.rs:100-102`) and each constituent's command (`cover.rs:106-113`) add the cell + id and the tick. The partition depends on the attention order (`lib.rs:4569-4580`: + pressure, then `attention_debt`, which an act resets, `lib.rs:4597-4602`), the cell + budget and the span. `run_tick` derives the cover afresh on every call + (`run.rs:557-565`), so a resuming run with another span, budget or order re-derives + other ids (Soul p1, p2). +- **R3 The clock id is `derived(ghostlight.verse.clock, [world, tick])`** (`run.rs:612-615`). + The kernel answers a known command id by caller and body: same body, + `AlreadyApplied`; another body, `CommandIdConflict` (`lib.rs:1633-1645`). Under + another span the clock body differs, so p4's clock was refused and the run stalled. + No test pins the id (mutants H3 and S1 survive). +- **R4 Rows outlive commits.** A controller-work row persists after its command + commits, and lookup-first refuses a row whose scope digest differs from the caller's + (`binds_same_scope`, `controllers.rs:1589-1593`, used at 3039, 3047, 3119, 3127). An + act moves its own subject's digest, so a resumed committed cell is refused + `OpportunityMismatch` (Hands' deviation). Resuming the row itself would get the + original receipt from the world: `submit_controller_world` asks `controller_receipt` + first (`controllers.rs:4276-4300`), which matches the row's own opportunity and + invocation (`lib.rs:1688-1712`). +- **R5 A grouped cell loses members silently.** `run_group` drops a member whose + opportunity moved before selection into `needs` as text (`controllers.rs:3430-3437`); + a member the world refuses after the first member's act is also text in `needs` + (`controllers.rs:3686-3692`); `absorb` reads only `submissions` and `pending` + (`run.rs:400-403`); `refreshed` drops a group member with no opportunity without a + refusal (`run.rs:429-436`). Soul p3, p7: 2 acts of 4, refusals `[]`. +- **R6 A refused member climbs the order.** No commit means `last_opportunity_at` is not + stamped, so its `attention_debt` grows and it sorts ahead next tick + (`lib.rs:4597-4602`). +- **R7 The sequence has no owner.** `run_manual` takes any sequence its port was opened + for (`run.rs:459-470`) and `save` accepts any sequence up to the next + (`run.rs:306-309`), so an old sequence overwrites its record (Soul p6). The binary + opens the call ledger for `next_sequence` before any record exists + (`main.rs:193-200`), so a start that fails between the two binds that sequence's cap + (Soul p5). +- **R8 Two output leaks.** `run_tick` swallows an operator-log error into zero acts + (`run.rs:631-639`). The binary prints errors with `{:#}` (`main.rs:46`), so + `open_inference`'s source chain reaches stderr (`main.rs:289-293`), and + `ControllerOpenError` echoes the endpoint, the model and the sidecar path + (`controllers.rs:2510-2530`). + +### Identity, lifecycle, authority + +| Kind | Identity | Lifecycle | Authority | +|---|---|---|---| +| Tick | (world, ordinal). Ordinal = number of `TimeAdvanced` commits in the world's journal when the tick opens: world-held, read through a driver-only mailbox door, injective and consecutive per world, independent of span, restored with a world snapshot. Not derived from `now`. | Opened: the run store writes `ghostlight.verse_tick.v1` (world, ordinal, opening now and revision, span, cover budget, oversubscribed, cells) before the first cell runs. Closed by its clock commit (the close names the run). Closed by the world when a later run finds the ordinal past it with the document still open (its clock landed and the close was lost, or another clock writer moved time). Never reopened or deleted. A resuming run with another span or budget finishes the open tick under the frozen ones; its own config applies from the next tick. | The kernel owns time and the count. The run store owns the tick document, and `run_tick` is its only writer. `TickIndex::of` is deleted; `derive_cover` takes the ordinal. Forbidden: deriving a tick from `now`; a run deriving a cover for a tick that has a document. | +| Cell | `CellId` = `(world, ordinal, size, lowest member)` under `ghostlight.cover.v1`, formula unchanged; frozen in the tick document with its kind and member subjects | derived once when the tick opens, frozen, read by every run that works in that tick; each member is bound to the world's current opportunity when its cell runs (the existing `refreshed`), and a member with none is refused, not dropped | `derive_cover` decides the partition, once per tick. Forbidden: re-deriving the partition on resume. | +| A cell's command ids | work row `for_cell(world, cell, ordinal)`; each member's `for_cell_constituent(world, cell, subject, ordinal)`. Derived from frozen identity only, so not stored twice. | issued when the member first runs; answered from the kernel's ledger on any retry; a persisted row whose command committed is answered with its receipt whatever the caller's digest; an in-flight row whose digest moved is refused before any inference; a row awaiting the world that was never sent (`ReadyToSubmit` or `NoProposal`) is resumed as awaiting whatever the digest and submitted, the world refuses it if its scope moved, the narrative lane then re-lowers once (one provider call) and the operational lane ends refused, and the driver labels that refusal `ScopeMoved` in both lanes (Defaulted ruling `unsent-awaiting-row-is-awaiting`) | The controller organ, lookup-first by command id. Forbidden: the driver minting ids; a digest check deciding a committed row's outcome. | +| Clock advance | `derived(ghostlight.verse.clock, [world, ordinal])`, body `AdvanceTime { frozen span }` | submitted once, after every cell of the tick finished; a retry in the same tick has the same body and is answered `AlreadyApplied`; it is what closes the tick | The kernel admits; `run_tick` is the only verse caller. Pinned by a test that re-submits a closed tick's clock. | +| Run | (world, sequence), `ghostlight.verse_run.v1` | claimed (Running, no checkpoints) → per-tick checkpoints naming the ordinal → Completed, BudgetExhausted, Stopped or Failed. A run whose process died stays Running (known gap, report h1 undone). A claim abandoned before the run starts ends Failed(start_refused). | The run store claims; `run_manual` updates only the record its claim names. | +| Run sequence | per world, from 1, claimed by the store as the next free number with the record written in the same call (`RunClaim`, not constructible outside the store, consumed by `run_manual`) | claimed once; never reissued, even when the run never started | The run store. Forbidden: a caller choosing a number; `save` creating a record. | +| Call ledger | `call_run_id(sequence)` in `calls.cc`; cap and start digest fixed at first open | opened only after its sequence is claimed, so an orphaned ledger can never bind a later run's cap | The recording port owns cap and spend; the run record copies the cap, and `run_manual` refuses a port whose run id or cap differs from its claim. | + +### Questions + +- `verse-grouped-shared-room` (admitted, blocks no spec): in a shared room a grouped + cell lands at most one exercise per tick; the others are refused and, by R6, lead the + next tick's order. Recommended: record and carry. The alternative spends a second + inference in the same tick for the refused members. + +### Fix order (specs admitted 2026-10-11, all based on 06a8373) + +1. `verse-run` r3 (r2 superseded for a window split): tick identity (ordinal door, tick document, frozen cover, clock pin). +2. `verse-run-lookup`: controller lookup-first by command id; typed grouped refusals. +3. `verse-run-refusals` (after 1 and 2): every member of a tick's cover ends committed + or refused in the checkpoint. +4. `verse-run-claim` (after 1): run claims, the binary's start order, the operator-log + error and the lane error. + +### Research note: where a turn's identity lives (verse-run re-map, 2026-10-11) + +Imagination `imagination-gl-tick-identity`, session `self-2026-10-11-ag`. A short +prior-art check before settling tick identity for cut `verse-run` r2. Appended to the +map's "Verse run identity" section. + +## The question + +At Ghostlight `06a8373` a tick is named `now / span` (`cover.rs:46`), the span is not in +the name, and every cell id and command id is re-derived from the opportunities, +attention order, span and cover budget the resuming run holds at that moment +(`run.rs:557-565`). Soul s1 showed a different span or budget on resume yields new ids, +so a subject acts twice in one tick (p1, p2), and a different span reuses a closed +tick's index (p4). + +## Prior art + +- **Durable workflow engines (Temporal, Azure Durable Functions).** The identity of + each step is assigned when the step is first scheduled and recorded in the + execution's event history. On resume the code is replayed against that history and + every command it produces is compared with the recorded one; a difference is a + non-determinism error, not a second execution + ([Temporal: execution failures](https://docs.temporal.io/troubleshooting/execution-failures); + [community thread](https://community.temporal.io/t/non-determinism-error/8279)). + The forum cases are exactly Soul's: a timer duration or a child id computed + differently on replay. The lesson: resume reads the recorded decision; it does not + re-derive it from inputs that may have changed. +- **Deterministic lockstep games.** The turn (frame) number is simulation state, + advanced only by the simulation, and every command is tagged with the turn it runs + in, so all peers and every replay agree on which turn a command belongs to whatever + the wall-clock or turn length + ([Unity discussion](https://discussions.unity.com/t/deterministic-rts-game-engine/467205); + [Gaffer On Games](https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/)). + The turn count, not elapsed time divided by turn length, is the name. +- **Idempotency keys (event-sourced command handlers, payment APIs).** A key answers a + retry with the stored result; the same key with a different request is refused, not + re-executed. Ghostlight's kernel already does this + (`lib.rs:1633-1644`: same caller and body, `AlreadyApplied`; otherwise + `CommandIdConflict`), which is why Soul's p4 clock under another span was refused. + +## What this decides for verse-run + +1. The tick ordinal is world-held: the number of `TimeAdvanced` commits in the world's + journal when the tick opens. It is span-independent, monotone, injective per world, + restored with a world snapshot (so ruling `replay-restored-snapshot` needs nothing + else for a run that starts on a tick boundary), and consecutive (which is what the + cover's rotation window wants, `cover.rs:340`). +2. What a tick decided when it opened (span, cover budget, the partition and each + cell's id and members) is recorded once, before the first cell runs, in a tick + document owned by the run store, and a resuming run reads it instead of deriving a + cover. This is Temporal's history rule at the granularity the verse needs. +3. Command ids stay derived, but only from that frozen identity (world, ordinal, cell + id, subject), never from live inputs, so there is no second copy of them to drift. +4. The controller answers a persisted row whose command the world committed with its + receipt whatever the caller's current scope digest, because the command id is the + idempotency key; a row still in flight keeps the digest check, so no tokens are spent + on a scope that moved. + +Not adopted: a world-state tick counter (a `STATE_SCHEMA` change, and the kernel would +learn ticks exist, which `cover.rs:1-12` forbids); a run-store-only ordinal (a replay +over a restored world with a fresh run store would number ticks from 1 and diverge). diff --git a/notes/ghostlight-verse-target.md b/notes/ghostlight-verse-target.md index a9d0f1c..6f03dc5 100644 --- a/notes/ghostlight-verse-target.md +++ b/notes/ghostlight-verse-target.md @@ -1,51 +1,163 @@ -# Ghostlight Verse: Target +# Ghostlight Verse: Target, revision 3 -Date: 2026-10-09. Campaign `ghostlight-verse` in the Eureka mind. Operator direction: -ruling `ghostlight-verse:ruling:verse-cast-crossmedia` (her words). Evidence for the -starting state: `F:\Projects\ghostlight-state-2026-10-09.md` (Eyes, 2026-10-09) and -`F:\Projects\aetheria-story-sifting-prior-art.md` Part 2. +Date: 2026-10-10. Campaign `ghostlight-verse` in the Eureka mind. Imagination +`imagination-gl-verse-sim-cuts`, session `self-2026-10-10-ag`. Supersedes revision 2 +(2026-10-09) where they differ; what revision 2 said that still holds is kept here. +Rulings are cited by id and read in the mind; this page does not restate them. +Evidence: `notes/ghostlight-verse-map.md`, sections "Verse sim: port inventory and +model page" and "Verse sim: phase A body facts"; `gamecult-ops` +`docs/research/llm-world-sim-prior-art.md` at `6b4a874` (*prior art (a)-(d)*); +`F:\Projects\aetheria-story-sifting-prior-art.md`. ## Why -Aetheria needs a public narrative hook. Ghostlight is meant to be the machine that -produces it: load the Aetheria verse, let a persistent cast of characters live in it, -and harvest what happens. Three products are wanted from the same simulated world: +Aetheria needs a public narrative hook, and Ghostlight is the machine that makes it +(ruling `verse-cast-crossmedia`): load the Aetheria verse, let a persistent cast live +in it, and harvest storylets for the game, a serialized cast, and stories a cheap +cinematic pipeline can stream. -1. **Storylets** for the game (ruling `aetheria-release:ruling:storylets-simulate-sift-lift-weave`). -2. **A serialized cast** an audience can follow through episodes. -3. **A cheap cinematic pipeline** that turns episodes into a video stream, so the - characters and conflicts people follow on the channel later turn up in the game. +Ruling `aetheria-sim-first` makes simulating Aetheria the campaign's priority, ahead +of every Dungeon affordance. Ruling `verse-sim-pipeline` sets the pipeline: the lore +is loaded, elaborated, and run as a multiresolution-gestalt world in Aetheria's +ontology and action grammar, on a schedule, feeding the content pipelines, under +operator meddling. Ruling `gestalt-machinery-port` ports the pre-rebuild gestalt +machinery rather than redesigning it, and ruling `teardown-was-authority-sprawl` +keeps the ad hoc mutation rules dead. The map sorts the old tree (`f9f019b`) into +both lists and names sixteen forced changes (F1-F16). -This campaign builds the foundation the three share: a Ghostlight that runs, says -truthfully what it is, and can hold the verse with a persistent cast. The stream -pipeline and the in-game crossover are later campaigns that consume it. +The field agrees with the split: working LLM world simulations keep world state in a +non-LLM store and map free-text intent onto an enumerated, checked action (prior art, +"What the field converged on", items 1-2). Cost comes down by calling the model less +per agent per tick (item 3), which is what the gestalt cover does. Replay comes from +stored responses, not seeds (item 5). ## Phases -1. **Honest and running.** The docs describe the live machine. `main` deploys on - Yggdrasil through Idunn and stays up. The six open play-session findings and the - owed Soul pass close. A human can play a session end to end. -2. **A world that can hold factions and a cast.** Relations, factions as actors, places - and persistent characters enter the ontology, designed from prior art and ruled by - the operator. -3. **The verse loaded.** A genesis built from AetheriaLore and Aetheria's typed - `Faction` records. Facts the lore does not state are proposed and reviewed, never - invented silently. +1. **Honest and running** (revision 2): the Dungeon lane closes its phase-1 work in + parallel. It does not gate the verse. +2. **Phase A, the first watched run.** Aetheria's grammar exists as a CultCache + document set in Aetheria's catalog; a Ghostlight world binds it at genesis and the + kernel refuses any affordance kind it lacks. The canonical verse world is created + from Aetheria's `Faction` records, one subject per record under a checked import + key. A hosted cheap-model lane records every call with its response body and + token usage. The operator starts a manual run under a hard token cap and reads, + per tick, what each faction did and what it cost. Mapped as cut specs (map, + "Phase A cut order"). +3. **Phase B, the verse loaded.** The relation edge (F6) enters the kernel; the + faction records' allegiances become Alliance and Rivalry edges; elaboration from + AetheriaLore declares places, populations as gestalts and named characters as + persons. +4. **Phase C, the gestalt port.** Pins, debt, checkpointed parallel cells, strategic + individuation and fission (F1-F16) on main's kernel; replay of a whole run. +5. **Phase D, meddling and the newspaper.** The operator's Eve surface (steer, then + inject), the ported newspaper as the first reader, then nightly runs. ## Invariants +Labels carried from revision 2 keep their meaning. + - `honest-docs`: README, maps and handoffs describe the machine on `main`; history lives in history. -- `main-runs`: the deployed unit on Yggdrasil is built from `main` and stays up; Idunn owns its lifecycle. -- `kernel-decides`: the kernel owns world state through its closed operation set; models propose, the kernel admits or refuses. +- `main-runs`: every deployed Ghostlight unit is built from `main` and stays up; Idunn owns its lifecycle. +- `kernel-decides`: the kernel owns world state through its closed operation set; models propose, the kernel admits or refuses, and no model decides an outcome. +- `grammar-governs` (ruling `grammar-scope-ruled`): every change to world state is a kernel command reduced through the closed operation set. Every act by a subject is an invocation of an affordance whose kind is in the world's bound grammar. The only other writers are clock motion, elaboration patches answering a derived boundary or deficit, and owner acts recorded with the operator as principal. Nothing else writes. Changing simulation resolution writes nothing. +- `grammar-bound`: a verse world binds one grammar revision by content digest at genesis, and the kernel refuses an affordance kind, or a role signature, that the bound grammar does not carry. +- `resolution-not-identity`: the resolution at which a subject is simulated never changes who exists, what they hold or what they know. A person given up by a gestalt stays a person. +- `one-record-one-subject`: each imported Aetheria or lore record maps to exactly one subject or entity, and the mapping is checked when a patch is admitted. - `verse-provenance`: every world fact loaded from the verse names its source page or record; a fact with no source is a proposal until reviewed. - `canon-one-way`: simulation output never writes back into AetheriaLore or Aetheria's catalog without operator review. -- `cast-persists`: a character keeps identity, memory and commitments across sessions, so episodes can follow them. -- `every-event-renderable`: every event the kernel admits maps to exactly one render path: ship action filmed in Aetheria, or a social verb staged as an Aetheria conversation (portraits and choices; docked face to face, undocked radio over the ship). Summary captions are the only exception (ruling `verse-verbs-two-render-paths`). -- `typed-state`: world state and its exchange are CultCache/CultNet documents; JSON only for schema publication and xenos boundaries. +- `readers-read`: a pipeline consumer reads committed history and writes only its own output. +- `cast-persists`: a character keeps identity, memory and commitments across runs, so episodes can follow them. +- `every-event-renderable`: every event the kernel admits maps to exactly one render path: ship action filmed in Aetheria, or a social verb staged as an Aetheria conversation (ruling `verse-verbs-two-render-paths`). Summary captions are the only exception, and they cover the named non-verb writers: clock motion, elaboration and owner acts. +- `run-resumable`: an interrupted run resumes from its last checkpoint and commits nothing twice. +- `run-replayable`: a run re-executed from its recorded provider responses produces the same journal. +- `run-bounded`: a run never spends past its token cap, and its fiction span, calls, tokens and cost are recorded. +- `dungeon-isolated`: a verse run cannot slow, stop or change Dungeon play. +- `meddling-attributed`: every operator intervention is a typed act, recorded with the operator as principal, through the same doors as every other writer. +- `typed-state`: world state, run records, call records, the grammar and pipeline output are CultCache documents; exchange is CultNet; JSON only for schema publication and xenos boundaries. + +## Defaults (stated, not forks) + +- **The grammar** (ruling `verse-grammar-home-ruled`) is two CultCache document + types in Aetheria's catalog (`GameData/Aetheria.cc`, registered in + `AetheriaStores.CatalogTypes`): a global `aetheria.verse_grammar` carrying the + revision, and one `aetheria.verse_verb` per verb, carrying the verb's canonical + kind name, its one render path (ship action or conversation), its participant + roles and the referent kind each binds, and a description. Aetheria owns which + verbs exist and how each renders; Ghostlight owns what each verb does to world + state (question `verb-effects-owner`). The starter set is authored by an AetherDb + command: the ship-action verbs of Aetheria's current scenario vocabulary and a + small social set that includes `speak`, the kernel-built narrative affordance. +- **Grammar binding.** A world created under a grammar stores its schema id, + revision and content digest (sha256 over the canonical encoding of its verbs, + sorted by kind). Revising the grammar means a new world, or a later migration + designed when needed: the kernel today refuses earlier schemas and never migrates. + A world with no grammar binding (every Dungeon world) keeps today's open catalog. +- **The canonical world** (ruling `verse-world-canonical-ruled`) is one world built + from Aetheria's catalog and AetheriaLore at named commits. Its import key is + `(CultCache schema id, record key)` for catalog records and `(vault-relative page + path)` for lore pages; the subject carries it, and admission refuses a second + subject or entity with the same key. Lore pages enter in phase B. +- **Cast.** A cast member is a `Person` subject marked by a cast entry in the run + record's roster, not by a kernel component: being followed by an audience changes + attention, not the world. The operator curates the roster as a run control + (ruling `meddling-first-ruled`); the cover keeps cast members at singleton + resolution (the old `MinimumIndividualDetail` pin). +- **Fission** (ruling `gestalt-fission-auto`) is admitted during a run as an + elaboration patch answering a derived boundary, one of the named non-verb writers + under `grammar-scope-ruled`. It is not a grammar verb and not gated. The operator + sees it afterwards: the run record lists each fission commit, and the meddling + surface shows it in the run's history beside the injection controls. +- **Separate verse process.** The runner is library code in `crates/ghostlight`; the + binary crate `ghostlight-verse` drives it. Phase A runs are one-shot CLI + invocations on Yggdrasil started by the operator or an agent at her word; the + daemon, its Eve surface and the Idunn schedule arrive with phase D. +- **Inference lane.** The verse uses a hosted OpenAI-compatible lane, DeepSeek + first (ruling `two-ghostlight-consumers`). The lane is main's local + OpenAI-compatible port with a second binding (HTTPS base URL, bearer key read from + a file), not a new transport. The DeepSeek key is the operator's to provision. +- **Cadence** (ruling `verse-cadence-ruled`): manual runs under a hard token cap per + run, which the operator sets per invocation, until cost per tick is measured; then + nightly under a monthly cap she sets. +- **Replay record.** Each provider call's request hash, response events, model and + token usage are stored per run in a CultCache call record. The kernel draws no + randomness, so stored responses are enough for replay (prior art (c)). +- **Exchange.** Aetheria reads what the verse publishes as CultNet documents through + CultMesh, the outbound read `ghostlight-world-consumer-api.md` names as "not in + this pass". The loopback consumer door is not how Aetheria reads. +- **Meddling** (ruling `meddling-first-ruled`) is an Eve/CultUI graph over CultMesh. + Steering controls (pins, focal subjects, cell budget, cast roster, pause, resume) + revise the run record at a tick boundary with an epoch and change no world state. + Injection controls (fact, pressure, commitment, person, ruled fact) are kernel + commands under the owner principal. +- **The newspaper** (ruling `verse-first-consumer-ruled`) ports as the first reader, + bound by `readers-read`, with the copy desk checking citations against committed + facts. ## Not in scope -- The video/stream pipeline (StreamPixels and its renderers) and episode editing. -- Storylet weaving in the Aetheria runtime, and any Aetheria code. -- Restoring the deleted autonomous tick driver as it was; unattended simulation, if wanted, is designed in phase 2. -- The failed `idunn-odin-*` and `idunn-streampixels-*` units on Yggdrasil (not Ghostlight's). +- Dungeon affordances: one-step Begin, vault seeding under Begin, the create flow, + Dungeon's play surface and Bonsai (ruling `aetheria-sim-first`). They continue in + the Dungeon lane and do not gate this target. +- The ad hoc mutation rules of the pre-rebuild tree (map, "Ad hoc rule" table). +- The video and stream pipeline, episode editing, and the people-in-rooms scene + grammar (ruling `scene-grammar-people-in-rooms`). +- Storylet weaving, radio delivery and any other runtime code in Aetheria. On + Aetheria's side only the grammar document types, their validation and the AetherDb + command that authors them are in scope. +- Counterfactual branching of a baseline over player-action archetypes. The journal + must not preclude forking a world at a committed revision, but no fork operation is + built here. +- A live Ghostlight running beside the game. +- The failed `idunn-odin-*` and `idunn-streampixels-*` units on Yggdrasil. + +## Canonical implementations + +- Kernel, cover, clock, grammar binding, import key and the verse runner library: + `GameCult/Ghostlight` `crates/ghostlight`. +- The verse binary, later daemon and Eve surface: `GameCult/Ghostlight` + `crates/ghostlight-verse`. +- The grammar: `GameCult/Aetheria`, `Assets/Scripts/ServerShared` and + `GameData/Aetheria.cc`, authored through `tools/AetherDb`. +- The port source: `GameCult/Ghostlight` at `f9f019b`, `crates/ghostlight-dungeon/src/`. +- Deployment and, from phase D, run start: `GameCult/Idunn`, with the recipe in the + Ghostlight repo.