stadium: wire STADIUM_CAPACITY_TICK in as a flat threshold, not a scheduler

Closes FABRIC-2.md's last open §12 Q5 question. fleet_heartbeat_tick_count
is fed by every live VM's own vm_tick(), not one VM's, so it was reaching
HEARTBEAT_INFERENCE_FREQUENCY (shared/borrowed from the per-VM inference
gate) several times faster than intended with more than one VM live -
backwards from FABRIC.md §22.4's required ~1000:1 separation.

What's actually gated turned out to be low-stakes: vm_physics_tick()
(capsule_vm_physics.c:397) is a passive statistics refit - re-sorts a
window of past heat-transfer samples and recomputes a median rate
estimate. It doesn't move heat or arbitrate capacity. Firing too often
just meant a noisier statistic recomputed more frequently than planned,
not incorrect behavior.

Considered and explicitly rejected: scaling the threshold by live VM
count at the check site. That's the first brick of a scheduler - reading
fleet state to adjust a rate dynamically - which this project has
deliberately avoided building. Implemented instead: STADIUM_CAPACITY_TICK
(existing Kconfig symbol, defined but never read by any code path) now
gates vm_physics_heartbeat_tick()'s call directly, replacing the borrowed
HEARTBEAT_INFERENCE_FREQUENCY. Default bumped 1000 -> 4000, a flat
constant picked once for Tripod's known 4-VM topology, same kind of
placeholder as every other frequency knob in Kconfig.kernel - not
computed from anything at runtime. Renamed fleet_last_inference_tick ->
fleet_last_capacity_tick to match. Still one clock, one counter
(fleet_heartbeat_tick_count) - just a bigger flat divisor on it.

Three-arch QEMU acceptance: all clean to ok>, identical Stadium
conservation invariant on all three (resident_sum=43691 reservoir=21845
sum=65536). logs/20260815-093425/amd64, logs/20260815-093521/aarch64,
logs/20260815-093641/riscv64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-15 09:37:58 -04:00
co-authored by Claude Sonnet 5
parent c5442c8377
commit 89d8c08582
10 changed files with 25755 additions and 46 deletions
+30 -26
View File
@@ -763,34 +763,38 @@ all four `.claude/*.md` files, and correcting `.claude/CLAUDE.md`'s own pointer
running both *after* Artemis lands, since Artemis's own storage/timing work is likely to running both *after* Artemis lands, since Artemis's own storage/timing work is likely to
perturb whatever gets measured now — a recommendation, not a ruling made here. perturb whatever gets measured now — a recommendation, not a ruling made here.
**Needs Captain Bob's ruling:** - [x] **§12 Q5's `STADIUM_CAPACITY_TICK` wiring — CLOSED 2026-08-15.** A real, verified
ordering violation, live today: with Hermes restored (item 4.2) and `STADIUM_MAX_VM_COUNT`
(now RAM-derived, Section G) at more than one VM, the shared fleet counter reached
`HEARTBEAT_INFERENCE_FREQUENCY` several times faster than §22.4's required 1000:1
separation intended.
- [ ] **§12 Q5's `STADIUM_CAPACITY_TICK` wiring.** A real, verified ordering violation, live **What was actually at stake, checked before fixing anything:** the thing gated by this
today, not latent: with Hermes restored (item 4.2) and `STADIUM_MAX_VM_COUNT` defaulting to threshold, `vm_physics_tick()` (`capsule_vm_physics.c:397`), is a passive statistics
4, the shared fleet-capacity counter can already reach `HEARTBEAT_INFERENCE_FREQUENCY` up to refit — it re-sorts a sliding window of past heat-transfer samples and recomputes a median
~4× sooner in wall-clock terms than a single VM's own heat-inference gate — backwards from rate estimate. It does not move heat, does not arbitrate capacity, does not decide
§22.4's required 1000:1 separation, confirmed by this document's own §12 Q5 investigation anything. Firing it too often just meant a noisier statistic recomputed more frequently
above. than planned — not incorrect behavior, not a live bug in the urgent sense.
**Correction 2026-08-15 — the fix must stay inside the one-clock rule (`FABRIC.md` §16.4 **Fix, explicitly NOT the shape first proposed.** The initial framing ("threshold scaled
GAP-A1, §17.1 "two measures, one clock... this does not mean two clocks").** `fleet_ by live VM count") was rejected by Captain Bob before implementation — correctly: reading
heartbeat_tick_count` (`capsule_vm_physics.c:157`) is a single counter, incremented once VM count at the check site to adjust a rate dynamically is the first brick of a scheduler,
per call to `vm_physics_heartbeat_tick()`, which itself only ever fires from inside the which is exactly the kind of machinery this project has deliberately avoided building.
execution-paced `vm_tick()` (`vm_runtime.c:141`) — never from a hardware timer. So there is Implemented instead: `STADIUM_CAPACITY_TICK` (existing Kconfig symbol, previously defined
only one clock here, exactly as ruled; the earlier framing of this item ("give the capacity but never read by any code path) now gates `vm_physics_heartbeat_tick()`'s call to
tick its own named constant... independent of per-VM tick counts") was worded ambiguously `vm_physics_tick()` directly, replacing the borrowed `HEARTBEAT_INFERENCE_FREQUENCY`.
enough to read as "add a second clock," which is not the fix and must not be built as one. Default bumped from 1000 to a flat **4000** — a fixed constant picked once for Tripod's
The actual defect is narrower: `vm_tick()` calls `vm_physics_heartbeat_tick()` from *every* known 4-VM topology, same kind of placeholder as every other frequency knob in
live VM's own dispatch (deliberate, `vm_runtime.c:126-138`'s own comment explains why — the `Kconfig.kernel`, not computed from anything at runtime. Renamed
fleet-capacity signal needs to reflect aggregate activity, not just Hera's), so `fleet_ `fleet_last_inference_tick``fleet_last_capacity_tick` to match. Still one clock, one
heartbeat_tick_count` advances on the fleet's *combined* stream while `HEARTBEAT_INFERENCE_ counter (`fleet_heartbeat_tick_count`), unchanged — just a bigger flat divisor on it.
FREQUENCY` was written assuming a single VM's stream. Same one counter, same one clock —
the threshold compared against it just doesn't account for multiple VMs feeding it at once. Files: `Kconfig.kernel`, `include/starforth_config.h`,
**Question for Bob:** the fix is to give `STADIUM_CAPACITY_TICK` a larger *threshold value* `include/starkernel/vm/stadium.h` (comment only),
checked against the existing `fleet_heartbeat_tick_count` (still one clock, one counter — `src/starkernel/capsule/capsule_vm_physics.c`. Three-arch QEMU acceptance, all clean to
just a bigger divisor, e.g. scaled by the live VM count so it restores something closer to `ok>`, identical Stadium conservation invariant on all three
the required 1000:1 separation), not a second tick source. Wire it in that way, or resolve (`resident_sum=43691 reservoir=21845 sum=65536`): `logs/20260815-093425/amd64/`,
the ordering some other way? `logs/20260815-093521/aarch64/`, `logs/20260815-093641/riscv64/`.
**Closed-by-ruling, not reopened this pass** (each already has a recorded "flag and leave" **Closed-by-ruling, not reopened this pass** (each already has a recorded "flag and leave"
decision from Captain Bob — reversing that without being asked would be exactly the decision from Captain Bob — reversing that without being asked would be exactly the
+18 -10
View File
@@ -64,17 +64,25 @@ config STADIUM_CONTAINS_DEPTH_MAX
patrons held inside patrons within one Stadium. patrons held inside patrons within one Stadium.
config STADIUM_CAPACITY_TICK config STADIUM_CAPACITY_TICK
int "Capacity arbitration cadence, in virtual ticks (STADIUM_CAPACITY_TICK)" int "Fleet transfer-slope re-estimation cadence, in virtual ticks (STADIUM_CAPACITY_TICK)"
default 1000 default 4000
help help
How often Hera re-evaluates capacity transfers between VMs How often vm_physics_heartbeat_tick() re-fits the fleet's
(FABRIC.md item 1.4), expressed in virtual ticks -- never transfer-slope estimate (vm_physics_tick() -- a passive median
wall-clock. Default matches the existing precedent at recompute over recent touch samples, not a capacity/transfer
capsule_vm_physics.c's vm_physics_heartbeat_tick() decision itself), expressed in virtual ticks -- never wall-clock.
(HEARTBEAT_INFERENCE_FREQUENCY, also 1000), comfortably past Wired in 2026-08-15 (FABRIC-2.md F.2/§12 Q5): this counter is fed
the "order of magnitude apart from the heat tick" minimum by EVERY live VM's own vm_tick(), not one VM's, so it previously
(FABRIC.md §22.4). No consumer yet -- capacity arbitration shared HEARTBEAT_INFERENCE_FREQUENCY (1000) and fired roughly
itself is not yet on the punch list. (live VM count) times faster than a single VM's own heat-inference
gate -- backwards from the "order of magnitude apart" minimum
(FABRIC.md §22.4). Default of 4000 is a flat, untuned placeholder
picked to roughly restore that separation at Tripod's known
4-VM topology (Hera + two Hermes + Artemis) -- not computed from
live VM count at runtime, deliberately: a fixed constant, same
as every other frequency knob here, not adaptive logic. Real
tuning is DoE work (item 5.1), same treatment as the other
placeholder constants in this file.
config STADIUM_MEMORY_PERCENT config STADIUM_MEMORY_PERCENT
int "Percent of free physical memory the Stadium claims at boot (STADIUM_MEMORY_PERCENT)" int "Percent of free physical memory the Stadium claims at boot (STADIUM_MEMORY_PERCENT)"
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-15T12:09:24Z --> <!-- Generated by mkcapsule --manifest 2026-08-15T13:36:13Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. --> <!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live --> <!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. --> <!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -72,7 +72,7 @@
#define STARFORTH_CONFIG_HEARTBEAT_INFERENCE_FREQUENCY_DEFAULT 1000 #define STARFORTH_CONFIG_HEARTBEAT_INFERENCE_FREQUENCY_DEFAULT 1000
#define STARFORTH_CONFIG_STADIUM_VM_MEMORY_PERCENT_DEFAULT 50 #define STARFORTH_CONFIG_STADIUM_VM_MEMORY_PERCENT_DEFAULT 50
#define STARFORTH_CONFIG_STADIUM_CONTAINS_DEPTH_MAX_DEFAULT 5 #define STARFORTH_CONFIG_STADIUM_CONTAINS_DEPTH_MAX_DEFAULT 5
#define STARFORTH_CONFIG_STADIUM_CAPACITY_TICK_DEFAULT 1000 #define STARFORTH_CONFIG_STADIUM_CAPACITY_TICK_DEFAULT 4000
#define STARFORTH_CONFIG_STADIUM_MEMORY_PERCENT_DEFAULT 1 #define STARFORTH_CONFIG_STADIUM_MEMORY_PERCENT_DEFAULT 1
#define STARFORTH_CONFIG_STADIUM_WORD_HEAT_QUANTUM_DEFAULT 2048 #define STARFORTH_CONFIG_STADIUM_WORD_HEAT_QUANTUM_DEFAULT 2048
#define STARFORTH_CONFIG_STADIUM_WORD_COOL_RATE_Q48_DEFAULT 21845 #define STARFORTH_CONFIG_STADIUM_WORD_COOL_RATE_Q48_DEFAULT 21845
+6 -4
View File
@@ -109,10 +109,12 @@ typedef char stadium_cell_size_check[(sizeof(StadiumCell) == STADIUM_CELL_BYTES)
/* /*
* Items 1.1 and 1.4 named this item as where their Kconfig symbols would be * Items 1.1 and 1.4 named this item as where their Kconfig symbols would be
* implemented. Neither has a consumer yet (item 3.5 for the depth cap, * implemented. STADIUM_CONTAINS_DEPTH_MAX still has no consumer (item 3.5
* capacity arbitration -- not yet on the punch list -- for the tick); these * for the depth cap, not yet implemented). STADIUM_CAPACITY_TICK was wired
* checks only prove the symbols are defined and sane, the same discipline * in 2026-08-15 (capsule_vm_physics.c's vm_physics_heartbeat_tick(), see
* already applied to the byte-count checks above. * FABRIC-2.md F.2/§12 Q5) -- this check now proves a real, live constant
* is sane, not just a placeholder, same discipline already applied to the
* byte-count checks above.
*/ */
typedef char stadium_contains_depth_configured_check[(STADIUM_CONTAINS_DEPTH_MAX > 0) ? 1 : -1]; typedef char stadium_contains_depth_configured_check[(STADIUM_CONTAINS_DEPTH_MAX > 0) ? 1 : -1];
typedef char stadium_capacity_tick_configured_check[(STADIUM_CAPACITY_TICK > 0) ? 1 : -1]; typedef char stadium_capacity_tick_configured_check[(STADIUM_CAPACITY_TICK > 0) ? 1 : -1];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+17 -4
View File
@@ -39,7 +39,7 @@
#include "starkernel/kmalloc.h" #include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h" #include "starkernel/q48_16.h"
#include "starkernel/console.h" #include "starkernel/console.h"
#include "starforth_config.h" /* HEARTBEAT_INFERENCE_FREQUENCY */ #include "starforth_config.h" /* STADIUM_CAPACITY_TICK */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */ #include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
#include "vm.h" /* VM, vm->ssm_l8_state */ #include "vm.h" /* VM, vm->ssm_l8_state */
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */ #include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
@@ -155,7 +155,10 @@ static uint64_t slope_fit_quality_q48 = 0;
* without a forward declaration -- it is now this counter's second * without a forward declaration -- it is now this counter's second
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */ * reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
static uint64_t fleet_heartbeat_tick_count = 0; static uint64_t fleet_heartbeat_tick_count = 0;
static uint64_t fleet_last_inference_tick = 0; static uint64_t fleet_last_capacity_tick = 0; /* renamed 2026-08-15 from
fleet_last_inference_tick -- gated on
STADIUM_CAPACITY_TICK now, not HEARTBEAT_
INFERENCE_FREQUENCY, see vm_physics_heartbeat_tick() */
static vm_physics_node_t *vm_physics_find(VMUuid vm_id) static vm_physics_node_t *vm_physics_find(VMUuid vm_id)
{ {
@@ -454,12 +457,22 @@ void vm_physics_tick(uint64_t now_ns)
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16; fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
} }
/* Gated on STADIUM_CAPACITY_TICK, not HEARTBEAT_INFERENCE_FREQUENCY (fixed
* 2026-08-15, FABRIC-2.md §12 Q5): fleet_heartbeat_tick_count is fed by
* EVERY live VM's own vm_tick() (see the comment above this counter's
* declaration), so it advances several times faster in wall-clock terms
* than a single VM's own tick_count -- comparing it against
* HEARTBEAT_INFERENCE_FREQUENCY (written assuming a single VM's stream)
* meant this fired far more often than §22.4's required ~1000:1 separation
* from a VM's own heat-inference loop. Still one clock, one counter --
* this is a bigger flat threshold on the same counter, not a second tick
* source or anything that reads live VM count to adjust itself. */
void vm_physics_heartbeat_tick(uint64_t now_ns) void vm_physics_heartbeat_tick(uint64_t now_ns)
{ {
fleet_heartbeat_tick_count++; fleet_heartbeat_tick_count++;
if ((fleet_heartbeat_tick_count - fleet_last_inference_tick) >= HEARTBEAT_INFERENCE_FREQUENCY) { if ((fleet_heartbeat_tick_count - fleet_last_capacity_tick) >= STADIUM_CAPACITY_TICK) {
vm_physics_tick(now_ns); vm_physics_tick(now_ns);
fleet_last_inference_tick = fleet_heartbeat_tick_count; fleet_last_capacity_tick = fleet_heartbeat_tick_count;
} }
} }