Q6 leaned toward nested for the right conclusion and the wrong reason. Its premise -- that a single region "reintroduces locking, the one mechanism this architecture has otherwise never wanted" -- is false. Every mutex in the kernel build is a no-op (shim.c:415); the kernel compiles STARFORTH_MINIMAL and the shim stubs dict_lock and tuning_lock out entirely. The architecture has not avoided locking, it has locking, inert. The cost Q6 weighs is currently zero, so Q6 cannot be decided on it. Decided nested on six other grounds, the strongest being SMP-readiness: nested keeps messages the only boundary-crossers, so no shared memory and no locks ever, whereas a single region would need real locks and the present no-op stubs would silently become a correctness hole. The most practical is that the outer level already exists and works (§20.1) -- single-region means discarding a working two-level structure. Also records a step-one finding that lands before any Stadium work: enabling timer interrupts introduces genuine ISR-vs-mainline concurrency where none exists today. Making the mutexes real would deadlock a single hart outright, since an ISR spinning on a lock the mainline holds can never be released. The top-half/bottom-half split of §18.4 is the answer, stated as a rule: Nothing in interrupt context may mutate Stadium structure. Ever. That constraint should be written at the stub site so the no-op is not later "fixed" into a spinlock. Opens: two capacities to size rather than one, and elasticity (§12 Q4 / §7 / §17.6c) becomes the live fork now that nesting makes capacity transfer real. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
62 KiB
FABRIC.md — DRAFT
Status: Draft for review. Captures the design session of 3 August 2026. Nothing here is committed. Sections are marked DECIDED, LEANING, or OPEN so you can argue with it rather than inherit it.
1. The claim
StarshipOS currently has four subsystems that each independently implement the same physics: Artemis heats blocks, Hermes ages messages, Console heats dirty cells, ACLs carry heat and TTL. Four implementations, one pattern.
The claim is that this is one mechanism wearing four costumes, and that the dictionary is already the reference implementation of it. Lift the dictionary one level of abstraction and every subsystem becomes an instance rather than a special case.
The argument that decides it: they already have the same wires. Blocks felt different because they are large and live on disk — but size and location are not properties, they are payload details. Strip those away and a block has exactly what a message has.
DECIDED. Direction is not optional. The remaining question is effort, not validity.
2. The arena
A single region of memory, outside any VM, holding everything currently live.
- Bounded capacity. The bound is real and inescapable, and it is what gives K≡1.0 a fixed denominator. Without a hard outer wall, K is bookkeeping rather than a conservation law.
- Allocated at boot, before any VM exists.
- Not part of the heap.
The critical scoping decision, and the one that keeps this from sprawling:
The arena holds what is live. Not everything that exists.
DECIDED.
3. The entry
One structure. No variants, no type field, no subclassing.
| Wire | Meaning |
|---|---|
| identity | handle or name |
| heat | current thermal state |
| TTL | remaining lifetime |
| pin | invariance flag (opposite of TTL, not an extension of it) |
| link | index into the arena, not a pointer |
| code field | what to do when this entry is worked |
| payload | inline if small, by reference if large |
Fixed-size cells. Links are indices, so the arena stays an array — no fragmentation, and tractable for Isabelle later.
The code field is the entire type system. A block's code field migrates. A message's delivers. A VM's ticks. The engine never asks what kind of thing it is holding; it heats, ranks, reaps, and calls the code field.
If you find yourself wanting a type field so the engine can branch on entry kind, the design has gone wrong. The code field already answers that question.
DECIDED, except payload threshold — see Open Questions.
4. Heat
Heat is conferred by traffic, not intrinsic to the entry.
This is the piece that was missing for most of the session. Nothing decides what matters. An entry is hot because activity is concentrated around it — the way a crowd in front of one car makes that corner of the hall hot. Density generates heat; nobody computes it.
Consequences:
- Ranking is read, not decided. There is no scheduler because there is no policy. The arena is simply already in heat order when you look at it.
- K constrains the total, so ordering is forced by conservation rather than by tuned parameters. There is nothing to tune wrongly. This is the defensible distinction from a scheduler and it belongs in the write-up.
- Popularity is self-limiting. A crowded entry is harder to reach, which throttles traffic to it, which cools it. The governor is local and emergent — no global damping constant to pick.
TTL expiry stays unconditional: entries leave at their own time, unscheduled, nobody's decision. Pinning remains the separate, opposite mechanism — invariance, not longevity.
LEANING. The causality is right; the density formulation needs a concrete definition.
5. What is not in the arena
This section exists because forcing everything in is how this design turns into a mess.
- Storage is beneath the arena. The show floor is not the warehouse. Artemis is where entries live when they are not in play. Blocks migrate onto the floor when hot and back out when cold — which is heat-driven block migration, already built. Artemis does not become an arena occupant; it becomes what the arena pages against.
- Devices are beside the arena. The framebuffer is the building's lighting, not an occupant. Console's dirty events are arena entries; the pixels are not.
DECIDED. Three sharp edges, nothing forced.
6. Boot order
The engine cannot be a VM service, because VMs live inside the thing it manages.
- LithosAnanke establishes the arena and starts the engine.
- Hera becomes the first entry in it.
- Hera births everything else, sizing each VM as it goes.
Structurally the same move as minting Zuse's certificate at first boot: a root that cannot be produced by the mechanism it grounds.
LEANING. Order is right; the allocation mechanism is unspecified.
7. Hera
Hera's job becomes arena distribution. This is not a new responsibility — allocating a VM's share is birthing it, and lifecycle is already what Hera is for.
OPEN: whether a VM's share is a hard bound or an elastic one that can grow and shrink under pressure, with capacity transferring between VMs as a conserved operation Hera arbitrates. Elastic is more powerful and more work. Under elasticity, birth sizes the rest volume rather than a cap — a more forgiving thing to have to guess right.
8. The mental model
An auto show hall.
Cars and people, in a building with a fixed capacity. People arrive and leave at their own times. They ask questions and converse — those are the messages. They stand in front of a car for a while and move on. Occasionally one sits in a car, which is the only exclusive thing in the room, scoped to a single object, no global lock.
The hall gets crowded. Crowds get hot.
One discipline to hold: cars and people cannot be two structures. That would be a type field re-entering through a metaphor. They are one entry shape differing only in TTL and code field — a car's lifetime is the show, a person's is a visit; a car's code field is be attended to, a person's is move and attend.
9. The admission test
Before writing code, run this on paper against every candidate entry type. Two questions, both of which must have a non-forced answer:
- What does heat mean for this thing?
- What is its reap event?
| Type | Heat means | Reap is | Verdict |
|---|---|---|---|
| Block | accessed often | migration | passes |
| Message | delivery urgency | delivery | passes |
| VM | runs often | execution / death by cooling | passes |
| ACL | checked often | ? | check |
| Screen cell | ? | redraw, which removes nothing | suspect |
Screen cells are the one to resolve first. A cell never expires — it is a fixed grid position always present. If cells are permanent arena entries, most of the arena is inert and permanently pinned. The likely correct read is that the arena entry is the dirty event, not the cell: transient, honest TTL, and the grid stays outside where it belongs.
Ten minutes on paper. Either it confirms the design or it finds the one case that breaks it, before any code moves.
10. Sequencing
FABRIC.md first, then Hermes native on the fabric, then measure, then Console, then Artemis last.
Reasoning:
- Hermes is unfinished, which is lucky. Finishing it the old way and refactoring later means deliberately writing code already slated for deletion. Build it on the fabric directly and it carries zero migration debt.
- It becomes the proving ground — the fabric gets tested against a real subsystem before anything that currently works is touched.
- It produces the effort number empirically. What Hermes costs is the multiplier for everything else. One data point from real work beats any amount of estimating.
- Artemis reads, writes, and persists reliably today. That is banked. It goes last, because it is the thing you cannot afford to break.
Existing instrument: the POST suite exercises every dictionary word and was already earmarked as the regression gate for the shrink-to-colon-definitions pass. Same tool, second job.
Caution: a green POST suite does not mean K still holds. Those are different claims. The DoE campaign validated K on the current substrate; changing the substrate means re-running it. Automated, but budget for it.
11. Where the debt accrues
- Dual paths — avoidable, and the big one. Never two live heat mechanisms at once. Convert one subsystem completely, prove it, move on. Every shim bridging old and new is debt, and new code will get written against whichever is convenient.
- Speculative generality — avoidable. Only add a wire when a second entry type needs it. Generality that never pays back is still debt.
- The exception — not avoidable, so decide it early. If one subsystem does not fit and gets special-cased, that special case is permanent and worse than not unifying: you carry the general machinery and the exception, and every future reader learns both. This is why the admission test comes before code.
Early signal: ARTEMIS.md, HERMES.md, CONSOLE.md and TRIPOD.md each currently describe their own heat mechanics. After FABRIC.md, each should shrink to roughly three lines — what an entry is here, what heat means, what the reap event is. If any one of them gets longer, that subsystem is fighting the fabric, and you will know which one before writing code.
12. Open questions
- Payload threshold — what size goes inline versus by reference.
- Arena entry header size. Cardinality spans orders of magnitude (dozens of VMs, thousands of messages, potentially very many screen events). The header must be sized for the worst case, and that case is the screen. Sizing this constrains everything else, so settle it early.
- Screen cells: entry-per-cell or entry-per-dirty-event. (Leaning: event.)
- Per-VM share — hard bound or elastic under pressure.
- Loop coupling. Roughly eight feedback loops once Hera and heartbeat depth are counted. The algorithms are known; the risk is interference. Usual discipline is separation of timescales — keep nested loop periods an order of magnitude apart. Cheaper to decide than to debug.
- Whether the arena is one region for the whole system or nested per VM. Nested implies K conserved at each level with messages as the only thing crossing a boundary, which would mean no shared-memory atomicity is ever needed. Single region is simpler but reintroduces locking — the one mechanism this architecture has otherwise never wanted.
13. What this does to formal verification
This may be the largest payoff, and it was not the reason for the change.
Verifying four subsystems means four state models, four conservation arguments, and — the expensive part — proofs about how they interact. That last category grows combinatorially and is where a verification effort usually dies. Unification deletes it outright.
What the design gives Isabelle/HOL, more or less for free:
- One datatype. The arena entry is a single record. Everything else is payload. You
reason about
entryonce rather than about blocks, messages, VMs and events separately. - No pointers. Fixed-size cells with index links means the arena models as a total function over a finite index set — no heap model, no separation logic, no aliasing, no null. This is the single biggest difference between a tractable proof effort and a research project.
- Finite state. Bounded capacity means the state space is finite. Induction over the arena is straightforward, and model checking becomes available alongside theorem proving.
- One conservation theorem. Every engine operation preserves K. Proved once against the engine, it holds for every entry kind — because the engine cannot distinguish them. Previously this was four proofs plus their interactions.
- A clean model boundary. Storage below and devices beside the arena means disk I/O and framebuffer writes sit outside the model, at the C primitive boundary already drawn.
- A trivial initial state. Boot order — kernel, then arena, then engine, then Hera — gives a base case that is trivially conserving, with everything else following by induction on operations.
One constraint this imposes, and it is not optional.
The code field is late-bound behaviour, which is the one part of this that HOL does not like: an arbitrary function stored in a record is higher-order and can wreck termination arguments. The fix is a design rule rather than a proof technique:
The set of code-field behaviours must be a closed enumeration, fixed at build time.
Model it as a datatype of behaviour tags plus a dispatch function and the whole thing stays first-order and tractable. Leave the code field open as a general extension point and you have traded four easy verification problems for one genuinely hard one.
This is consistent with the existing rule that adding a primitive requires rebuilding from source rather than doing it from inside a running system. Worth stating explicitly in the fabric design, because it is the kind of constraint that gets casually violated later by someone adding "just one" dynamic behaviour.
14. Formalism
The thermodynamic analogy holds in places and inverts in one, which matters for the paper but not for the build.
- Fixed capacity → closed system. K≡1.0 → conservation. Capacity transfer → work. These map cleanly.
- Heat is not entropy. Heat is closer to energy or temperature. Entropy would measure how heat is distributed: concentrated is low, uniform is high.
- This matters practically. K is conserved, so K can never tell you anything — it is 1.0 by construction, a correctness check rather than a diagnostic. Entropy over the heat distribution actually varies, and distinguishes idle from productive from thrashing. That is the real instrument, and the quantity worth driving the LED matrix with.
- The inversion: the second law says entropy rises spontaneously. This system does the opposite — it self-organizes, concentrating heat where work happens. That is not equilibrium thermodynamics; it is a driven dissipative system, order sustained by throughput. Prigogine, not Carnot. A stronger claim, but only if stated correctly — writing "thermodynamic system" while entropy decreases unprompted is an easy shot for a reviewer.
Phenomenon first, then mathematics. The formalism follows the phenomenon; it does not gate the build, and it is not finished until it is correct.
15. The whole thing in five lines
- The arena holds the live crowd. Storage is the warehouse. Devices are the building.
- One entry shape. The code field is the only difference between kinds.
- Heat is density, conferred by traffic. Nobody decides.
- Departure is unconditional. Pinning is invariance, not longevity.
- The kernel opens the hall. Hera walks in first.
16. Substrate findings — 2026-08-03
Naming: the arena is now called the Stadium, because src/starkernel/vm/arena.c
already owns "arena" for the PMM-backed VM page allocator — an unrelated concept. Sections
1–15 above still say arena and have not been reconciled.
Four findings from reading the tree. The first three change what step one costs. The fourth changes what the engine is allowed to be.
16.1 There is no interrupt return path on two of three ISAs
The engine has to be driven from outside the VMs (§6), which in a kernel means interrupts. That mechanism does not currently exist on most of our targets.
apic_timer_start()is an explicit no-op stub on aarch64 (arch/aarch64/apic.c:82) and riscv64 (arch/riscv64/apic.c:76). Both say the driver is deferred.heartbeat_tick()is defined on all three architectures and called from exactly one site in the tree:arch/amd64/interrupts.c:337. On the other two it is dead code.- Worse: every vector in
arch/aarch64/isr.S— IRQ included — is a bare branch to a handler that prints and entersfor(;;) wfe.arch/riscv64/isr.Sis the same shape. There is no register save, noERET, noSRET.
So enabling a timer interrupt today halts the kernel on the first tick. The work is not "write a timer driver," it is "build the interrupt return path that was never built."
Consequence for §12 Q5. That question assumes a hierarchy of loop periods kept an order of magnitude apart. Separation of timescales presupposes a time base. There is one real time source, on one architecture; everything else paces off execution count. Q5 cannot be answered on the current substrate — it is downstream of this work, not parallel to it.
16.2 riscv64's time base is a guess
arch/riscv64/timer.c:46 sets s_counter_hz = 1000000000ULL with the comment
/* assume 1 GHz */. The file header concedes rdcycle's frequency is not
architecturally discoverable.
Every heartbeat variance and TIME-TRUST figure riscv64 has produced was computed against
a wrong expected_delta. This has to be fixed as part of any timer work, and it means
riscv64 timing numbers before and after that fix are not comparable.
16.3 The dictionary is already a Stadium
§1 claims the dictionary is the reference implementation. It is stronger than that.
DictEntry today carries six of the seven wires in §3:
| §3 wire | Already in DictEntry |
|---|---|
| identity | name / word_id |
| heat | physics.* (Loop #1) |
| TTL | acl_ttl |
| pin | acl_pinned, plus WORD_PINNED / WORD_FROZEN |
| link | dictionary chaining |
| code field | literally a function pointer |
The dictionary is not analogous to a Stadium entry. It is one, already built and already tested. Everything else is what gets generalized toward it.
But run §9's admission test on it before moving it in. Its reap event is the weak
wire. Blocks migrate, messages deliver, VMs die by cooling — a dictionary word does not
expire. Heat decays to a floor and the word stays; FORGET is manual and rare. That is
the same shape §9 already flags as suspect for screen cells: hundreds of permanently
resident, largely inert entries. It may well be fine, but the dictionary is too central
to wave through, and it is precisely the case §9 exists to catch.
Also: the dictionary is what parity.c hashes. Moving its representation into the
Stadium changes that hash, so every committed baseline in logs/ shifts. Not a blocker —
but a deliberate re-baseline with a before/after record, not something to discover later.
16.4 The engine must stay deterministic — this is a new constraint
Nothing in §1–15 says this, and it binds the engine tightly.
parity.c logs a dictionary hash per VM birth. The DoE's 0.000% CV across 90 runs and the
patent support material both rest on the same capsule producing the same heat state on
every run. Today that holds for a reason worth naming: ticking is execution-driven.
vm_tick() (vm/vm_runtime.c:114) is called from execution paths, and its own header
says "Synchronous (now): Called from main execution loop, every N executions." Same
instruction sequence, same tick points, same decay events, same hash.
Wall-clock ticking does not have that property. Under TCG, elapsed time varies run to run on identical input.
The interrupt may supply pacing, but the engine must fire on tick count, never on elapsed wall time.
Same input → same tick ordinal → same reap and inference events → same hash. This keeps parity intact while still letting compudynamics be genuinely timer-driven.
There is a second, narrower version of the same discipline. heartbeat_tick() measures
inter-tick deltas to derive variance and TIME-TRUST. If the engine's own work ran inside
that handler, the handler's runtime would become part of the interval it measures — the
instrument would be reporting the cost of running the instrument. So the interrupt does
bookkeeping only; the engine runs outside it. The split already exists in the tree and
works: adaptive_check_accumulator / adaptive_pending (include/vm.h:113-114), set at
rolling_window_of_truth.c:372-375, serviced at :1302-1308.
DECIDED unless argued — it is a constraint inherited from what the system already claims, not a new preference.
16.5 What this implies about order
Whatever step one turns out to be, it now has a floor under it: real timer interrupts and a real IRQ return path on all three ISAs. §10's sequencing (Hermes first, as the proving ground) sits above that floor, not below it.
17. Patrons
An occupant of the Stadium is a patron. Blocks, words, ACLs and messages are all patrons. The word is doing real work: it names the category without implying a class hierarchy, and it keeps the metaphor honest — patrons attend, they are not the building.
DECIDED.
17.1 Four patrons die four different ways — and that is not a type field
The observation that prompted this section is correct: these things do not all end the same way. A message is consumed. An ACL lapses. A block should never be destroyed. A word should never be destroyed either.
The reflex is a decision branch on patron kind. That is the type field §3 forbids, and it is not needed — but neither is the opposite over-simplification, which an earlier draft of this section made and which is corrected here.
Heat and TTL are not the same mechanism, and neither is a special case of the other. §3 lists them as separate wires and they must stay separate. A message carries a countdown. A block does not — a block leaves the floor because it cooled, not because a timer expired. Collapsing the two forces the design, which is precisely the failure §11 warns about.
There are three mechanisms, and each patron uses the ones that genuinely apply:
| Mechanism | Nature | Patrons | Departure |
|---|---|---|---|
| TTL | countdown to a definite event | messages, ACLs | expiry |
| Heat decay | continuous, gradual | blocks, words | cooling off the floor |
| Pin | invariance — §3's wire | any | never |
Mapped per patron:
| Patron | Governed by | Reap event |
|---|---|---|
| Message | TTL | delivery — consumed, gone |
| ACL | TTL | expiry |
| Block | heat decay | migration back to Artemis — evicted, not destroyed |
| Word | heat decay | cooling off the floor (see §17.3) |
Two measures, one clock
This does not mean two clocks. Both mechanisms advance off the same tick — the adaptive heartbeat. TTL decrements on a tick; heat decays on a tick. They are two different readings of one counter, not two independent time sources.
That is not a tidiness preference, it is forced. §16.4 requires the engine to fire on tick count so that the same input reproduces the same dictionary hash. Two independent clocks would be two independent sources of nondeterminism and parity would not survive it.
One tick. Two measures. Three mechanisms.
The engine still asks nothing about patron kind. It advances the tick, applies whichever measures a patron carries, and calls the code field when a patron departs. A pinned patron never departs. There is no type interrogation — see §18 for how the dispatch works without one.
17.2 Reaping is not destruction
The block case is the one that makes this work, and §9 already had it right: a block's reap event is migration. §5 puts storage beneath the Stadium, with blocks coming onto the floor when hot and going back off when cold.
So a block is reaped in exactly the sense the engine means — it leaves the floor. Where it goes afterwards is the code field's business, not the engine's. A message's code field ends in delivery; a block's ends in a write-back to Artemis. Same event, different behaviour, no special case.
This is worth stating plainly because "reap" reads as "free" and here it does not:
Reap means leaves the floor. It does not mean destroyed.
DECIDED.
17.3 Words: the dictionary is the warehouse, hot words are the patrons
§16.3 left words as the unresolved patron. Pinning all of them resolves nothing — several hundred permanently resident, largely inert entries is the §9 screen-cell failure with a different label, and it wastes the bounded capacity that gives K a fixed denominator.
The better reading applies §5 unchanged. Storage sits beneath the Stadium. The full dictionary sits beneath it too, and only hot words are on the floor.
This is not speculative — it already exists and is already measured:
src/physics_hotwords_cache.cmaintains the hot-word setcache_hits_deltais column 4 of the DoE CSV, "hot-words cache hits this tick"- execution heat (Loop #1) is what promotes a word; linear decay (Loop #3) is what cools it
So the hot-word population is already a live, moving crowd with an existing promotion rule and an existing cooling rule. It is the crowd. The dictionary is the warehouse it is drawn from, exactly as Artemis is the warehouse blocks are drawn from.
The existing cache is only half-aligned — and that is the argument for doing this
Reading physics_hotwords_cache.c closely turns up something that strengthens the case
rather than weakening it. Heat governs admission to the cache. Nothing governs
departure.
hotwords_cache_promote() (:362-383), when full, writes the new word to
cache[lru_index] and advances that index modulo the size. That is round-robin. The field
is named lru_index, the inline comment at :365 says "LRU eviction: remove oldest entry
(round-robin)", and the doc block at :347 says "round-robin least-recently-used" — which
is a contradiction in terms. Nothing anywhere tracks recency of use. Promotion is gated on
execution_heat > HOTWORDS_EXECUTION_HEAT_THRESHOLD (:283); eviction consults heat not
at all.
The consequence is that the hottest word in the cache can be evicted purely because its slot came up in the rotation.
That is a direct contradiction of §4:
Ranking is read, not decided. There is no scheduler because there is no policy. The arena is simply already in heat order when you look at it.
Round-robin eviction is exactly a policy — an arbitrary one, uninformed by the physics the rest of the system runs on.
This is the strongest practical argument for §17.3. Moving words onto the Stadium is
not a relabeling exercise; it repairs a real defect by deleting the arbitrary half of an
existing mechanism. And it is measurable before and after: stats.evictions,
stats.promotions and stats.cache_hits are already instrumented and already flow into
the DoE CSV.
Consequences if this holds:
- Words need no pin exception. Their reap event is cooling off the floor — the same shape as a block's, one level up.
- §16.3's objection dissolves. The dictionary does not move into the Stadium wholesale; it stays beneath it and pages against it.
- The parity concern in §16.3 narrows considerably. The dictionary's own representation is not what changes — what becomes a patron is the hot set, which is already transient.
- Pin stops being a general-purpose escape hatch and goes back to meaning what §3 says: invariance, for the few things that genuinely must not vary.
LEANING. The mechanism is already built and the fit is clean, but this reframes a direction stated differently earlier the same day, and it deserves longer than a paragraph.
17.4 Open
-
What is a word's TTL, concretely? Heat decay already cools words, but decay-to-cold and TTL-expiry are not obviously the same clock. Either they unify or §17.3 needs a second mechanism, which would be a bad sign.
-
Is the hot-word set bounded today?RESOLVED — yes, hard bounded.DictEntry *cache[HOTWORDS_CACHE_SIZE](include/physics_hotwords_cache.h:168) is a fixed array inside the struct, withHOTWORDS_CACHE_SIZE = 32(:84). Nothing is allocated —hotwords_cache_cleanup()notes there is nothing to free, since the cache holds borrowed pointers the dictionary owns. It is per-VM (vm->hotwords_cache, used atdictionary_management.c:320), not global. This is exactly the inescapable outer wall §2 requires.Two things follow. First, the bound is 32 out of a 453-word Mama dictionary — a very tight floor. Whether that is the right Stadium population or an artifact of the structure having been sized as a lookup cache rather than as a live set is a design input, not a given. Second, the eviction defect in §17.3 above.
Reported, not fixed: in
hotwords_cache_promote(), ifwordis NULL and the cache is full, the guard at:363falls into the inner branch at:364and writes NULL intocache[lru_index]. Unreachable today — every caller passes a non-NULL entry from the bucket search — but the NULL check reads as though it prevents this, and does not. -
ACL reap — §9 still marks this
?. ACL entries carryacl_ttlinDictEntryalready, so this is likely the easiest of the four to close, and it should be closed on paper alongside the others rather than left dangling. -
Does a patron ever change what it is? A block that is written becomes a new block by content-addressing. A word that is redefined is a new word. If nothing mutates in place, that is worth stating explicitly — it removes a whole class of proof obligation in §13.
17.5 The framebuffer is not a patron — it is a utility
DECIDED. This is §5 and §2 applied rather than a new call, but it was close enough to becoming an exception that it is worth writing down explicitly.
Outside the Stadium is not the same as an exception
§11's warning is about a patron kind that needs special handling inside the engine — you end up carrying the general machinery and the carve-out, and every future reader has to learn both. That is the thing to fear, and the fear is correct.
But §5 is not a carve-out. It is a taxonomy. The test for whether something is an exception is: does the engine change because this thing exists? For the framebuffer, nothing changes. The engine never learns about it. That is a boundary, not an exception.
It fails §2's liveness test by definition, not by fiat
§2's scoping decision is the sharpest line in this document: the Stadium holds what is live, not everything that exists. A patron arrives and departs. The framebuffer does neither — it is there from init to power-off. It has no arrival event and no reap event, not because it has been excused from having them, but because it genuinely has none.
Better than "the building's lighting": a utility
§5 calls the framebuffer the building's lighting, which undersells it — that reads like part of the structure. It is closer to the power company: external infrastructure the building consumes. Not the Stadium. Not the basement of the Stadium. A third thing.
That gives three categories, all principled, none of them exceptions:
| Category | Relation | Example |
|---|---|---|
| Warehouse | beneath | Artemis, the dictionary (§17.3) |
| Stadium | the floor | patrons |
| Utility | beside | framebuffer, and devices generally |
What is live is the dirty event — and it is not a fifth patron kind
Run §9's two questions on it:
- Heat means — a region written often is hot. A scrolling log, a blinking cursor. A static border is cold. Traffic confers heat, identically to everything else.
- Reap is — redraw. Consumed by being painted.
Consumed on delivery, carries a TTL, dies on arrival. A dirty event is a message whose recipient happens to be the framebuffer. It does not extend the patron taxonomy; it is the message patron with a different destination.
Which yields a symmetry worth keeping:
| Patron | Code field terminates at | Which lives |
|---|---|---|
| Block | Artemis | beneath |
| Dirty event | framebuffer | beside |
Both are code fields finishing outside the Stadium. Neither is special.
This closes the last ? in §9. The screen-cell row resolves to: the event is the
patron, the grid is not.
The sizing argument, independently
A framebuffer is several megabytes of fixed device memory. Making it a patron means either blowing the bounded capacity that gives K a fixed denominator (§2), or forcing the by-reference payload path to exist for exactly one pathological object — which would decide §12 Q1's payload threshold on the worst possible case. Sizing a design around its single largest outlier is how the header ends up wrong for the other ten thousand entries.
Not a patron does not mean no physics
Worth stating so it is not lost: excluding the framebuffer from the Stadium says nothing about whether compudynamic concepts apply within it. A utility can have its own internal dynamics — heat over regions, decay, adaptive refresh — without being a Stadium participant. The power company has physics too.
OPEN, deferred. What those dynamics are is a question for when the framebuffer work actually happens. It does not gate the Stadium, and it should not be designed speculatively now.
17.6 Sizing and allocation — the Stadium should be dynamic, but not heap-allocated
§3 says the Stadium stays an array with index links. That is right, but it is stated in a way that invites the wrong objection, because "array" and "fixed at compile time" are not the same thing — and it is the second one that is genuinely objectionable.
A hardcoded capacity is arbitrary: HOTWORDS_CACHE_SIZE = 32 is a number someone picked,
and §17.4 shows exactly how that ages. A contiguous block of fixed-size cells, sized at
boot from the memory budget and addressed by index, is dynamic in every sense that matters
operationally while remaining an array in every sense §3 and §13 depend on.
Four positions, with what each costs:
| What it is | Cost | |
|---|---|---|
| a | Capacity fixed at compile time | Arbitrary bound. What the hot-words cache does today. |
| b | Sized at boot, contiguous, index-linked | None. Retains every property below. |
| c | Contiguous but resizable at runtime | K's denominator moves; couples to §7 |
| d | Per-entry allocation, pointer links | Forfeits §13 |
Why (b) is free
The Stadium is established before any VM exists (§6), so boot is already the moment its capacity is determined. Deriving that capacity from available memory rather than from a constant costs nothing and gives up nothing. Cells stay uniform, links stay indices, the region stays contiguous.
LEANING toward (b).
Why (d) is expensive — by this document's own argument
§13 is unambiguous:
No pointers. Fixed-size cells with index links means the arena models as a total function over a finite index set — no heap model, no separation logic, no aliasing, no null. This is the single biggest difference between a tractable proof effort and a research project.
Per-entry heap allocation gives that up and takes several things with it:
- The finite state space. Bounded capacity is what makes induction over the Stadium straightforward and what puts model checking on the table alongside theorem proving.
- §2's hard outer wall. Without an inescapable bound, K is bookkeeping rather than a conservation law — §2 says this in as many words.
- The engine's simplicity. This is a freestanding kernel with
kmalloc.c/pmm.cand no libc. Allocation in the reap path means the engine can fail to allocate, which means the engine needs a failure mode, which means it is no longer the thing §3 describes. An engine that can fail is a different engine.
Fragmentation is the least of it, though §3 is right that indices avoid that too.
Why (c) is the genuinely open one
A contiguous region that grows and shrinks as a whole keeps index links and keeps the proof structure — the capacity becomes a parameter rather than a constant, which HOL handles without difficulty. What it complicates is K, since the denominator moves.
This is not a new question. §7 already has it open for per-VM shares: "whether a VM's share is a hard bound or an elastic one that can grow and shrink under pressure, with capacity transferring between VMs as a conserved operation Hera arbitrates." Elasticity at the Stadium level and elasticity at the per-VM level are the same question asked at two scales, and they should be answered together rather than separately.
OPEN, and coupled to §7 and to §12 Q6 (one Stadium or nested per VM). Note that if Q6 resolves to nested-per-VM, (c) becomes considerably more attractive — capacity transfer between VMs is the whole point of that arrangement, and a fixed per-VM bound would waste it.
The rule this reduces to
Dynamic in capacity. Static in structure.
Decide how big the Stadium is at runtime. Do not decide what an entry is, or how entries are addressed, at runtime.
18. The engine — L0
The engine that holds the patrons is a loop like the others, and it needs a name in the same scheme. L1–L7 are taken by the existing feedback loops; L8 is the Jacquard mode selector. The engine sits beneath all of them, so: L0.
18.1 L0 and L8 bookend the gated loops
This produces a structure worth drawing, because it explains why two of the ten are different in kind:
L8 Jacquard mode selector always on, ungated
─────────────────────────────────────────────────────
L1 … L7 feedback loops gated by L8
─────────────────────────────────────────────────────
L0 the Stadium engine always on, ungated
L1–L7 are gated: L8 switches them on and off, 128 configurations over seven bits.
The two bookends are ungated, and for symmetric reasons:
- L8 cannot be gated because something has to decide the gates. A selector that could deselect itself has no defined behaviour.
- L0 cannot be gated because it is what holds the patrons the other loops operate on. Switch it off and nothing is reaped, the Stadium fills and stays full, and K stops being conserved. That is not a mode, it is a failure state.
This is the same argument §6 makes about boot order. The thing that manages existence cannot be a participant in what it manages — not for VMs, and not for loops.
DECIDED.
18.2 The Jacquard accounting is an exclusion, not an extension
The obvious reading of "add L0" is that the selector grows a bit: 7 bits becomes 8, 128 configurations become 256.
That is the wrong move, and §18.1 is why. L0 is not gateable, so it has no bit. The gate word stays seven bits wide and the selector stays at 128 states.
This is worth stating explicitly because the alternative is expensive: widening the gate word would invalidate the 128-configuration L8 table, the DoE campaign already run against it, and the existing results. There is no reason to pay that, and the design does not ask us to.
L0 is accounted for in Jacquard by being deliberately absent from it.
DECIDED.
18.3 Dispatch: enumerate behaviours, not kinds
§13 already requires a closed enumeration:
The set of code-field behaviours must be a closed enumeration, fixed at build time… Model it as a datatype of behaviour tags plus a dispatch function and the whole thing stays first-order and tractable.
So a fixed enum with fixed dispatch is mandatory, not a concession to practicality. But there are two things one could enumerate, and only one of them preserves §3:
| Enumerate | Engine asks | Cost of a fifth patron | |
|---|---|---|---|
| ✗ | patron kinds — BLOCK, WORD, ACL, MESSAGE |
"what are you?" | touch the engine |
| ✓ | behaviours — MIGRATE, DELIVER, EXPIRE, COOL |
nothing; calls dispatch(tag) |
none |
Both are closed, both are fixed at build time, both are equally provable. Only the second keeps the engine ignorant of its contents, which is the property §3 exists to protect. Two patrons may share a tag; a new patron that migrates costs zero engine changes.
The branching Captain Bob is right to want is real and it is allowed — it lives in the dispatch function over a closed tag set, not in the engine asking patrons what they are.
DECIDED.
18.4 One tick
L0 advances on the adaptive heartbeat. Everything derived from time is derived from that one counter:
- TTL decrements per tick (messages, ACLs)
- Heat decays per tick (blocks, words)
Two measures, one clock — see §17.1. §16.4 forces this: the engine must fire on tick count for the same input to reproduce the same dictionary hash, and two independent time sources would be two independent sources of drift.
18.5 CLOSED — the adaptive rate does not break determinism, and here is why
The concern: the heartbeat is adaptive — faster, slower, window wider, narrower. If it adapts off timing measurements, the adaptation is machine-dependent and §16.4 fails. If it adapts off execution-derived state, tick ordinals still map deterministically to work and parity survives.
Traced end to end on 2026-08-03. The dictionary-parity chain is clean. Resolution (1) — adaptation inputs are execution-derived, TIME-TRUST stays diagnostic — is already the de-facto design.
Evidence, in the order it decides the question:
-
TIME-TRUST is computed and never consumed.
heartbeat_trust()has zero callers in the entire tree.m5_time_trustandm5_variance(include/vm.h:315-316) are declared and never read or written. The only consumer ofts->trustisstarkernel/doe_log.c:98, which writes it to a CSV column. It is measured and reported, never fed back. -
The intent is already documented.
include/starkernel/timer.h:70— "TIME-TRUST thresholds in Q48.16 (for diagnostics, NOT for gating)." -
Every inference-engine input is execution-derived.
vm_runtime.c:626-640populatesInferenceInputsfrom: the rolling window,trajectory_length(fromwindow_pos/total_executions),prefetch_hits/prefetch_attempts,hot_word_count,stale_word_count,total_heat,word_count, and the previous check's baselines. No timing input of any kind. The outputs it applies —adaptive_window_widthandadaptive_decay_slope— therefore depend only on execution history. -
Decay is tick-based, and deliberately so.
vm_tick_apply_background_decay()is handedvm_monotonic_ns(vm)but computeselapsed_ticks = tick_count - last_decay_tick(vm_runtime.c:375). Thenow_nsargument only writeslast_decay_ns. The comment at:373-374says so explicitly: "Tick-based, not wall-clock… now_ns is kept only to refresh last_decay_ns for diagnostics." Someone already defended this exact boundary. -
The parity hash contains nothing time-derived.
capsule_dict_hash_hook()(capsule/capsule_vm_hooks.c:60-70) walks the dictionary hashing exactly two things per entry: the word name andexecution_heat. Notlast_decay_ns, not any timestamp. So even the diagnostic wall-clock field from (4) cannot reach the hash.
Conclusion: §16.4 holds today, and holds by construction rather than by luck.
One real exception, and it is not in the parity path
vm_physics_touch() (capsule/capsule_vm_physics.c:250-313) is wall-clock dependent:
it computes elapsed_us = (now_ns - last_active_ns) / 1000 (:272) and the header comment
at :122 confirms the transfer amount scales with elapsed time. So fleet-level VM heat
is not reproducible run to run the way dictionary heat is.
Scope of that, precisely:
- It touches
node->physicsin the VM registry, notDictEntry.execution_heat, so it does not reach the parity hash and does not invalidate the existing claim. vm_physics_tick()(:366) explicitly discards itsnow_nsargument ((void)now_ns;), so only the touch path is affected.- With Hera alone this is nearly inert. It becomes live again when Hermes and Artemis return.
This is a pre-existing condition, not something the Stadium introduces. But it is exactly the pattern L0 must not inherit, and it is worth knowing that fleet K figures and dictionary parity have different reproducibility guarantees today.
The invariant this should become
Determinism currently survives on convention plus one good comment. That is too thin for something load-bearing. L0 should make it explicit:
Anything that influences patron state advances on tick count. Wall-clock time may be recorded for diagnostics and must never be an input to a decision.
DECIDED, and it supersedes the "leaning (1)" in the earlier draft of this section.
19. Mass, density, and what K actually is
§4 is marked LEANING with the note that "the density formulation needs a concrete definition." This section supplies it. It is the keystone: §4 claims ranking is read rather than decided, and that claim is empty until the thing being read is a number.
The objection that forced this section is the right one. Density is quantity per unit volume, so it implies a mass and a volume. Neither had been named.
19.1 K is already defined, and it is not an occupancy ratio
This has to come first, because the obvious definition of K contradicts working code.
vm_physics_conserved() (capsule/capsule_vm_physics.c:456-461) sums
execution_heat_q48 across live VMs and tests that total against Q48_ONE:
uint64_t sum = vm_physics_fleet_heat_sum();
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
return diff < VM_PHYSICS_EPSILON_Q48;
So:
K is a conserved, normalised heat share. Total heat is always 1.0. Traffic transfers heat to a patron from the others; it does not create it.
K is not occupancy, and defining it as Σmass / capacity would contradict an
implemented, tested mechanism. It stays exactly as it is.
19.2 Three quantities, not one
| Quantity | What it is | Range | Status |
|---|---|---|---|
| Heat | conserved share, moved by traffic | Σ = 1.0 always | already implemented |
| Mass | cells the patron occupies — its footprint | integer ≥ 1 | new |
| Density | heat ÷ mass — heat per cell | derived | new |
Heat is the conserved quantity. Mass is an independent axis and never enters K. Density is the ratio, and it is density in the literal sense at last: quantity per unit volume, where the volume is a patron's own footprint inside the bounded capacity §2 requires.
A patron holding a large share of the fleet's heat in a single cell is dense. A patron squatting on four cells with a negligible share is sparse, and belongs back in the warehouse.
DECIDED.
19.3 Everything else reads off it
The point of §4 is that no policy exists. With density defined, none is needed:
- Ranking — order by density. Read, not computed by a scheduler. §4's first bullet is now true rather than aspirational.
- Admission when full — admit the newcomer if it is denser than the least dense resident, and evict that one. This is a comparison of two intrinsic numbers, not a policy, and it closes the "what happens when the Stadium is full" gap.
- Hysteresis — falls out unpaid-for. A heavy patron needs a proportionally larger heat share to hold its floor space, so a block sitting near the threshold does not oscillate on and off. No damping constant to pick, which is what §4 wanted and could not previously deliver.
- Migration cost is not a separate quantity. An earlier draft of this reasoning treated cost-to-move as its own axis. It is not needed: footprint and cost correlate, because a patron is expensive to move precisely because it is large. Deriving cost from mass avoids introducing a second tunable, which §11 would rightly call speculative generality.
19.4 Correction to §4 — the self-limiting claim has the wrong mechanism
§4's third bullet states:
Popularity is self-limiting. A crowded entry is harder to reach, which throttles traffic to it, which cools it. The governor is local and emergent — no global damping constant to pick.
The conclusion is right and the mechanism is wrong. In a hall, a crowd physically blocks access to the car. In a computer the inverse is true — a hot entry is easier to reach, since that is the entire purpose of a cache. The metaphor does not survive translation, and no mechanism in this design reproduces the blocking effect because the effect is not real in this substrate.
The real governor is conservation. Heat is zero-sum: total heat is 1.0, so a patron heating up necessarily cools every other patron, and nothing can exceed the ceiling. Popularity is self-limiting because there is a fixed amount of popularity to go around.
This is §4's second bullet — "K constrains the total, so ordering is forced by conservation rather than by tuned parameters" — which was the correct answer already. The third bullet should be struck, not repaired. Designing a mechanism to make the crowd metaphor come true would be fitting the system to the analogy, which §14 already warns against in the other direction.
DECIDED. §4's third bullet is superseded by this section.
19.5 Correction to §4 — "density generates heat" reverses the causality
§4 says "Density generates heat; nobody computes it." Under §19.2 that is backwards, and the confusion is that one word was carrying two meanings:
- Traffic generates heat — activity concentrated on a patron transfers heat share to it. §4's causality is correct with this word substituted.
- Density is heat per cell — derived from heat, downstream of it, and it is the quantity that gets read when ranking.
The corrected statement:
Traffic confers heat. Heat is conserved at 1.0. Density is heat per cell. Ranking reads density.
Nobody decides what matters at any step in that chain. §4's spirit is intact; only the noun was overloaded.
19.6 Open
- What is mass, exactly, for each patron? The definition is "cells occupied," which requires §12 Q1 (payload threshold — inline versus by reference) and §12 Q2 (header size) to be settled first. A patron stored by reference has small mass regardless of payload size, which may be right or may be a loophole — a 1 MB block held by reference would occupy one cell and read as dense. This needs deciding.
- Is mass constant for a patron's lifetime? §17.4 Q4 (do patrons mutate in place) decides this. If mass can change while a patron is resident, density is not stable and ranking may thrash.
- How does traffic transfer heat between patrons, concretely?
vm_physics_touch()does this today for VMs, but it scales the transfer by wall-clock elapsed time (capsule_vm_physics.c:272), which §18.5 forbids for anything influencing patron state. The transfer rule must be restated on tick count before L0 can use it. This is the single most concrete piece of work this section implies.
20. VMs are patrons
§17 named four patrons: blocks, words, ACLs, messages. That list is incomplete, and the omission matters because the missing kind is the only one already implemented.
§9's admission table has always included VM — heat means runs often, reap is death by cooling — and §6 states it directly: "Hera becomes the first entry in it." Those cannot be reconciled with a four-patron taxonomy. VMs are patrons. Chronologically they are the first ones.
DECIDED.
20.1 This is a finding, not a proposal
The outer Stadium already exists in working code:
vm_physics_fleet_heat_sum()sumsexecution_heat_q48across live VMs, andvm_physics_conserved()tests that total againstQ48_ONE(capsule/capsule_vm_physics.c:456-461).- That is a Stadium's K, computed over VM patrons. §19.1's definition of K was derived from it.
- Hera already reaps VMs;
TRIPOD.mdmakes governing existence her defining contract.
So the mechanism §19 describes is not novel at the VM level. It is running now.
20.2 But the outer level is unbounded — fleet K is currently bookkeeping
capsule_vm_physics.c:71-72 describes the VM physics registry plainly:
kmalloc-backed linked list, same pattern as capsule_birth.c's vm_registry_head/vm_registry_count — unbounded, not a fixed array.
Heat is normalised to 1.0 regardless of how many VMs exist. Conservation therefore holds trivially, by renormalisation, rather than because anything is constrained. Measure it and it cannot fail.
§2 anticipated exactly this:
The bound is real and inescapable, and it is what gives K≡1.0 a fixed denominator. Without a hard outer wall, K is bookkeeping rather than a conservation law.
By the design's own test, the fleet K measured to date is bookkeeping. This is not a
reason to distrust the DoE results — they measured what they measured, and per-VM physics
is real — but it does mean VM-CONSERVED? cannot currently fail, and should not be cited
as evidence that conservation is being enforced.
This is the same shape as §17.3's finding about the hot-words cache: adopting the Stadium repairs a defect rather than renaming a mechanism. Here the repair is larger, because bounding the VM population is what converts fleet K from an identity into a constraint.
Consequence for the campaign: any future claim resting on fleet K needs the bound in place first, or it is a claim about arithmetic rather than about the system.
20.3 Nesting — §12 Q6 is less open than it looks
If VMs are patrons, the structure follows without further invention:
Outer Stadium patrons: VMs ← exists today (unbounded)
└── per-VM Stadium patrons: words, blocks,
ACLs, messages ← to be built
K conserved at each level, with messages as the only thing crossing a boundary. That is precisely §12 Q6's nested option — "K conserved at each level with messages as the only thing crossing a boundary, which would mean no shared-memory atomicity is ever needed" — and the outer level is already there.
This does not close Q6 by itself, but it changes the question. The choice is no longer between two greenfield designs; it is whether to formalise a nesting that is already half built, or to collapse it into a single region and discard the level that works.
LEANING nested. See §20.5 for what still has to be settled.
20.4 A VM's mass is the capacity share Hera allocated it — PROPOSAL
Marked as proposal, not finding: VMPhysics currently holds only execution_heat_q48,
last_active_ns and is_live (capsule_vm_physics.c:59-63). There is no share field.
§7 says Hera's job is arena distribution, and that allocating a VM's share is birthing it. If that share is the VM's mass, §19's density definition applies unchanged at the outer level, and §7 stops being abstract.
The payoff is that Hera gets a strictly better lifecycle signal than heat alone:
| VM | Heat | Mass | Density | Reading |
|---|---|---|---|---|
| small, quiet | low | low | moderate | healthy — dense enough, merely small |
| big, idle | low | high | low | sparse — reap or shrink |
| small, busy | high | low | high | dense — a candidate to grow |
Heat alone cannot distinguish starved from small. Density can. TRIPOD.md states that
Hera uses the fleet K view for exactly this question — "Is a child VM healthy? Is a child
VM starved?" — and density is the quantity that actually answers it.
Note this stays within TRIPOD.md's constraint that fleet K is lifecycle telemetry, not
a dispatch mechanism. Density informs whether a VM should exist or change size. It never
decides where work goes; that remains capability-based routing.
20.5 Open
-
Bounding the VM population. What is the outer Stadium's capacity, and what happens at the bound — birth refused, or coldest VM reaped? The latter is consistent with §19.3 but means a VM can die because a new one was born, which needs to be an explicit, stated behaviour rather than an emergent surprise.
-
Is a VM's mass its allocated share, or one cell? §20.4 proposes the share. The alternative — every VM is one entry regardless of size — is simpler but throws away the distinction in the table above, which is the reason to do this at all.
-
What is Hera's own mass?RESOLVED — Hera is pinned, and her eviction is a panic.She is the first patron and she governs the rest, so she is subject to §3's pin wire: invariance, not longevity. That is the correct use of pin rather than an exception to the rules.
But pinning alone is a silent guarantee, and a silent guarantee that fails under load is worse than none. If the engine ever selects Hera for eviction, that is a kernel panic, not a skipped iteration and not a logged warning. The condition is unreachable by construction; reaching it means the invariant is already broken and continuing would run the system without a governor.
State it as an assertion at the eviction site, not as a filter on the candidate set — filtering hides the bug, asserting reports it.
Her mass is still whatever §20.4 resolves for VMs generally. Pinning governs whether she can depart, not how much room she takes.
-
Does the nesting recurse further? A VM's Stadium holds patrons; if one of those patrons were itself a VM, the structure is a tree rather than two levels. Nothing currently requires this, and §11 would call it speculative generality — but it should be ruled out deliberately, since the boot order in §6 does not forbid it.
21. §12 Q6 resolved — nested
Q6: Whether the arena is one region for the whole system or nested per VM. Nested implies K conserved at each level with messages as the only thing crossing a boundary, which would mean no shared-memory atomicity is ever needed. Single region is simpler but reintroduces locking — the one mechanism this architecture has otherwise never wanted.
Resolved: nested. The conclusion Q6 leaned toward is right; the reason it gives is not.
DECIDED.
21.1 The locking premise is false — locking is already free
Every mutex in the kernel build is a no-op. src/starkernel/vm/host/shim.c:415:
void sf_mutex_lock(sf_mutex_t *mutex) {
(void)mutex;
}
dict_lock and tuning_lock (include/vm.h:410,507) are real pthread_mutex_t in the
hosted build (platform_lock.h:58-63), but the kernel compiles with
-DSTARFORTH_MINIMAL=1 (Makefile.starkernel:253) and the shim stubs them out. The stated
rationale is accurate: "Single-threaded kernel: no contention is possible at the VM
level."
So the cost Q6 weighs against the single-region option is currently zero. The architecture has not avoided locking; it has locking, inert. Q6 cannot be decided on this basis.
21.2 Step one introduces real concurrency — and locks are the wrong answer for it
This belongs in §16's substrate work, not here, but it surfaced while resolving Q6 and it lands sooner than anything the Stadium needs.
Once the timer interrupt fires on all three ISAs (§16.1), the ISR preempts the mainline. That is genuine concurrency between two contexts sharing state on a single hart. It does not exist today, which is precisely why the no-op stub is currently safe.
Making the mutexes real would not fix it and would actively break it: on a single hart, an ISR spinning on a lock the mainline holds deadlocks outright, because the mainline can never run to release it. This is a well-known failure and it is easy to introduce by reflex.
The correct answer is already in the design — §18.4's top-half / bottom-half split:
- ISR (top half) touches only a word-sized counter and a flag. Single writer.
- Mainline (bottom half) is the only context that mutates Stadium structure.
No lock, no deadlock, and no reliance on atomicity beyond aligned word access. This is a constraint on the L0 implementation, not a preference.
Nothing in interrupt context may mutate Stadium structure. Ever.
21.3 What actually decides Q6
With locking removed from the argument, six discriminators remain:
| Nested | Single region | |
|---|---|---|
| Matches what exists | hotwords_cache, rolling_window, dictionary are already per-VM; the physics registry is already outer |
collapses a working two-level structure into one |
| Fault containment | a VM cannot corrupt another's Stadium | one bad patron reaches everything |
| Capacity transfer (§7) | meaningful — VMs have shares to trade | no per-VM share exists to transfer |
| K semantics | conserved per level; existing fleet K survives unchanged | fleet K needs re-deriving |
| Verification (§13) | prove the engine once, instantiate at both levels — demonstrates genericity | one arena, marginally simpler |
| If SMP ever happens | messages are the only boundary-crossers → no shared memory, still no locks | needs real locks, and the no-op stubs become a live correctness hole |
The last row is the strongest, and it is what Q6 was reaching for. Nested does not avoid locking today — nothing needs locking today. Nested avoids locking permanently, including in a multi-hart future where the current stubs would silently stop being correct.
The first row is the most practical: §20.1 established that the outer level already exists and works. Single-region means discarding a working structure to build a simpler one, which is a poor trade at this stage.
21.4 The shape this fixes
Outer Stadium patrons: VMs
│ K conserved here
│ bounded — see §20.5 #1
│
├── Hera's Stadium patrons: words, blocks, ACLs, messages
│ K conserved here, independently
│
└── (future VMs) same shape, no special cases
Messages are the only patrons that cross a boundary. Everything else is confined to the level it was born on.
21.5 Consequences and open items
- The no-op mutexes are now load-bearing in a way they were not before. They are correct today and correct under nesting, but only while the top/bottom discipline in §21.2 holds. That discipline should be stated in the code at the stub site, so the next reader does not "fix" the no-op into a spinlock and deadlock the kernel.
- Two capacities to size, not one. §20.5 #1 (outer bound) and §17.6 (per-VM bound) are now distinct questions with distinct answers.
- §12 Q4 / §7 / §17.6(c) elasticity becomes the live question. Nesting is what makes capacity transfer between VMs meaningful, so the hard-versus-elastic decision can no longer be deferred as an abstraction — it is the next real fork.
- §20.5 #4 remains open. Nesting is two levels here. Whether a patron may itself contain a Stadium — a tree rather than two tiers — is still deliberately unruled. Nothing requires it; it should be excluded on purpose rather than by omission.