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
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
today, not latent: with Hermes restored (item 4.2) and `STADIUM_MAX_VM_COUNT` defaulting to
4, the shared fleet-capacity counter can already reach `HEARTBEAT_INFERENCE_FREQUENCY` up to
~4× sooner in wall-clock terms than a single VM's own heat-inference gate — backwards from
§22.4's required 1000:1 separation, confirmed by this document's own §12 Q5 investigation
above.
**What was actually at stake, checked before fixing anything:** the thing gated by this
threshold, `vm_physics_tick()` (`capsule_vm_physics.c:397`), is a passive statistics
refit — it re-sorts a sliding window of past heat-transfer samples and recomputes a median
rate estimate. It does not move heat, does not arbitrate capacity, does not decide
anything. Firing it too often just meant a noisier statistic recomputed more frequently
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
GAP-A1, §17.1 "two measures, one clock... this does not mean two clocks").** `fleet_
heartbeat_tick_count` (`capsule_vm_physics.c:157`) is a single counter, incremented once
per call to `vm_physics_heartbeat_tick()`, which itself only ever fires from inside the
execution-paced `vm_tick()` (`vm_runtime.c:141`) — never from a hardware timer. So there is
only one clock here, exactly as ruled; the earlier framing of this item ("give the capacity
tick its own named constant... independent of per-VM tick counts") was worded ambiguously
enough to read as "add a second clock," which is not the fix and must not be built as one.
The actual defect is narrower: `vm_tick()` calls `vm_physics_heartbeat_tick()` from *every*
live VM's own dispatch (deliberate, `vm_runtime.c:126-138`'s own comment explains why — the
fleet-capacity signal needs to reflect aggregate activity, not just Hera's), so `fleet_
heartbeat_tick_count` advances on the fleet's *combined* stream while `HEARTBEAT_INFERENCE_
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.
**Question for Bob:** the fix is to give `STADIUM_CAPACITY_TICK` a larger *threshold value*
checked against the existing `fleet_heartbeat_tick_count` (still one clock, one counter —
just a bigger divisor, e.g. scaled by the live VM count so it restores something closer to
the required 1000:1 separation), not a second tick source. Wire it in that way, or resolve
the ordering some other way?
**Fix, explicitly NOT the shape first proposed.** The initial framing ("threshold scaled
by live VM count") was rejected by Captain Bob before implementation — correctly: reading
VM count at the check site to adjust a rate dynamically is the first brick of a scheduler,
which is exactly the kind of machinery this project has deliberately avoided building.
Implemented instead: `STADIUM_CAPACITY_TICK` (existing Kconfig symbol, previously defined
but never read by any code path) now gates `vm_physics_heartbeat_tick()`'s call to
`vm_physics_tick()` directly, replacing the borrowed `HEARTBEAT_INFERENCE_FREQUENCY`.
Default bumped from 1000 to a flat **4000** — a fixed 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`), unchanged — just a bigger flat divisor on it.
Files: `Kconfig.kernel`, `include/starforth_config.h`,
`include/starkernel/vm/stadium.h` (comment only),
`src/starkernel/capsule/capsule_vm_physics.c`. 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/`.
**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
+18 -10
View File
@@ -64,17 +64,25 @@ config STADIUM_CONTAINS_DEPTH_MAX
patrons held inside patrons within one Stadium.
config STADIUM_CAPACITY_TICK
int "Capacity arbitration cadence, in virtual ticks (STADIUM_CAPACITY_TICK)"
default 1000
int "Fleet transfer-slope re-estimation cadence, in virtual ticks (STADIUM_CAPACITY_TICK)"
default 4000
help
How often Hera re-evaluates capacity transfers between VMs
(FABRIC.md item 1.4), expressed in virtual ticks -- never
wall-clock. Default matches the existing precedent at
capsule_vm_physics.c's vm_physics_heartbeat_tick()
(HEARTBEAT_INFERENCE_FREQUENCY, also 1000), comfortably past
the "order of magnitude apart from the heat tick" minimum
(FABRIC.md §22.4). No consumer yet -- capacity arbitration
itself is not yet on the punch list.
How often vm_physics_heartbeat_tick() re-fits the fleet's
transfer-slope estimate (vm_physics_tick() -- a passive median
recompute over recent touch samples, not a capacity/transfer
decision itself), expressed in virtual ticks -- never wall-clock.
Wired in 2026-08-15 (FABRIC-2.md F.2/§12 Q5): this counter is fed
by EVERY live VM's own vm_tick(), not one VM's, so it previously
shared HEARTBEAT_INFERENCE_FREQUENCY (1000) and fired roughly
(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
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
<!-- 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. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- 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_STADIUM_VM_MEMORY_PERCENT_DEFAULT 50
#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_WORD_HEAT_QUANTUM_DEFAULT 2048
#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
* implemented. Neither has a consumer yet (item 3.5 for the depth cap,
* capacity arbitration -- not yet on the punch list -- for the tick); these
* checks only prove the symbols are defined and sane, the same discipline
* already applied to the byte-count checks above.
* implemented. STADIUM_CONTAINS_DEPTH_MAX still has no consumer (item 3.5
* for the depth cap, not yet implemented). STADIUM_CAPACITY_TICK was wired
* in 2026-08-15 (capsule_vm_physics.c's vm_physics_heartbeat_tick(), see
* 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_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/q48_16.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 "vm.h" /* VM, vm->ssm_l8_state */
#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
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
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)
{
@@ -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;
}
/* 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)
{
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);
fleet_last_inference_tick = fleet_heartbeat_tick_count;
fleet_last_capacity_tick = fleet_heartbeat_tick_count;
}
}