Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
Meta Vens
f6ffe16fd2 Verse map: the identity table names the unsent awaiting row state
Closes follow-up map-row-unsent-awaiting (ruling unsent-awaiting-row-is-awaiting).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 20:55:52 +02:00
Meta Vens
35d0fd2797 Verse map: run identity re-map after Soul s1 (tick named by world clock ordinal, frozen tick document, controller lookup by command id)
Body facts R1-R8, the identity/lifecycle/authority table for tick, cell,
command id, clock advance, run, run sequence and call ledger, and the fix
order for specs verse-run r3, verse-run-lookup, verse-run-refusals and
verse-run-claim. Research note: how event-sourced workflow engines and
lockstep sims persist turn identity for resume and replay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 12:03:22 +02:00
Meta Vens
5f5f23f909 Verse sim: target r3 and phase A cut order
Imagination pass (session self-2026-10-10-ag) folds the seven 2026-10-10
verse-sim rulings into target revision 3, fills the model page cells they
settle, and records phase A's body facts and cut order. The r3 draft is
folded into the target.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:14:00 +02:00
Meta Vens
d8179506dd Verse sim: port inventory, model page and target r3 draft
Imagination 0b pass (session self-2026-10-10-ag) under rulings
verse-sim-pipeline, gestalt-machinery-port and teardown-was-authority-sprawl.
Old gestalt machinery inventoried at f9f019b; seven operator questions
admitted. Target r3 is a draft until the questions are ruled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 05:37:20 +02:00
Meta Vens
3a61eab3da Map: the Bonsai host under ruling bonsai-idle-unload-host
Body facts B1-B11 from the PrismML fork source and the Eyes report
(Raven unreachable at mapping time: every Raven-side claim is unprobed),
the Bonsai lifecycle row on the model page, and the rationale for
sleeping inside llama-server instead of a reaper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 19:58:48 +02:00
3 changed files with 700 additions and 34 deletions

View file

@ -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).

View file

@ -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-<release>` | 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/<file>`.
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/<CultRecordKey>`); `(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).

View file

@ -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.