starkernel: item 4.2 -- Hermes native on the Stadium (complete)

Migrates Hermes's message/channel lifecycle onto the Stadium's unified
heat/capacity economy: MSG-ALLOC/FREE-NODE and CH-ALLOC/FREE-NODE now
route entirely through stadium_admit()/stadium_evict(), replacing the
old local free-list + independent heat-field mechanism. Eight
kernel-only STADIUM-* FORTH primitives (ADMIT, EVICT, RES@, RES-PULL,
RES-PUSH, HEAT@, HEAT!, WORD-HEAT), VM.stadium_vm_id threaded through
all three vm_core.c dispatch sites (replacing item 4.1's hardcoded
vm_uuid_hera()), and the stadium_owner[idx] fix so evict-credit lands
in the VM that actually admitted a patron, not whoever owned cell 0.

This session's own contribution, on top of that pre-existing
implementation: found and fixed two bugs blocking the item's own K≡1.0
conservation self-check (HERMES-K was reading 0, not 65536):

- Q.SLOT admission-heat fix (capsules/hermes/init.4th): MSG-SEND/
  CH-ACCEPT admitted with Q.1 (the entire fleet-wide "1.0" unit) per
  item, a leftover from before the Stadium migration when each
  message/channel had its own unconstrained heat field. Instantly
  drained the shared, finite reservoir.

- Reservoir floor for word-execution admission (stadium_words.c):
  stadium_word_dispatch() (item 4.1) pulls STADIUM_WORD_HEAT_QUANTUM on
  every word dispatch, not just first admission -- exhausts a VM's
  entire reservoir in ~32 dispatches, starving any application-level
  economy sharing that VM's reservoir before it gets a chance to pull
  anything. word_dispatch_pull() now clamps word-execution's own pulls
  to leave a Q48_ONE/3 floor (same fair-share figure COMMON-CH's own
  floor already uses); application-level pulls are unaffected.

- STADIUM-WORD-HEAT primitive + stadium_words_resident_heat(): the
  floor deliberately leaves word-execution residents holding real
  heat, invisible to HERMES-K's original formula (MSG+CH+reservoir,
  no term for word patrons). Adding this term closes K to exactly
  65536 on all three architectures.

Also rules on two open scope questions in FABRIC.md: MBR-ALLOC/
MBR-FREE-NODE stay off the Stadium (membership records have no heat
field, never did -- the acceptance bullet's inclusion of them was a
completeness gesture predating a check of the actual layout), and
records the effort number (12 implementation files, +759/-120 lines).

Verified: all three architectures boot clean, full self-test passes,
Stadium conservation closes exactly (resident_sum + reservoir =
Q48_ONE) at both the C/Stadium level and the FORTH-level HERMES-K
check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-07 01:49:23 -04:00
co-authored by Claude Sonnet 5
parent 0a7f144367
commit 5a28458b21
19 changed files with 1034 additions and 162 deletions
@@ -271,6 +271,7 @@ int sk_vm_bootstrap_parity(ParityPacket *out) {
* capsule_birth_baby() which never calls vm_init_with_host(). */
capsule_vm_hooks_register();
capsule_vm_registry_init(vm); /* establishes [Hera] console prefix */
vm->stadium_vm_id = vm_uuid_hera(); /* item 4.2 */
vm_physics_init(vm_uuid_hera()); /* Hera: the fleet's root, seeded Q48_ONE */
capsule_run_log_init();
register_mama_forth_words(vm); /* BIRTH KILL START STOP USE + capsule words */
+67
View File
@@ -358,6 +358,10 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
/* Item 4.2 fix (§25.7): record ownership so stadium_evict()'s
* reservoir credit and free-list return land on the VM that actually
* admitted this patron, not whatever owner[idx] held at boot. */
stadium_owner[idx] = (uint8_t)slot;
return idx;
}
@@ -395,6 +399,11 @@ size_t stadium_admit(VMUuid vm_id, const StadiumPatronHeader *candidate) {
stadium_quotas[slot].free_head = link_to_size(stadium_cell_array[idx].header.link);
stadium_cell_array[idx].header = *candidate;
bitmap_set(idx);
/* Same fix as the free-list-pop path above -- stadium_evict() just wrote
* owner[idx] = slot as part of reaping least_dense_index, so this is
* currently a no-op in practice, but it must not be assumed to stay a
* no-op: this is the correctness statement, not a redundant write. */
stadium_owner[idx] = (uint8_t)slot;
return idx;
}
@@ -453,6 +462,45 @@ int stadium_grant_quota(VMUuid new_vm_id, VMUuid from_vm_id) {
return 0;
}
/* Shared by stadium_cell_heat_get()/_set(): resident AND owned by vm_id's
* own quota slot. Returns the quota slot on success, -1 on any refusal. */
static int owned_resident_slot(VMUuid vm_id, size_t cell_index) {
int slot = quota_slot_for_vm(vm_id);
if (slot < 0) return -1;
if (cell_index >= stadium_ncells) return -1;
if (!bitmap_get(cell_index)) return -1;
if (stadium_owner[cell_index] != (uint8_t)slot) return -1;
return slot;
}
uint64_t stadium_cell_heat_get(VMUuid vm_id, size_t cell_index) {
if (owned_resident_slot(vm_id, cell_index) < 0) return 0;
return stadium_cell_array[cell_index].header.heat;
}
int stadium_cell_heat_set(VMUuid vm_id, size_t cell_index, uint64_t new_heat) {
int slot = owned_resident_slot(vm_id, cell_index);
uint64_t old_heat, delta, pulled;
if (slot < 0) return -1;
old_heat = stadium_cell_array[cell_index].header.heat;
if (new_heat == old_heat) return 0;
if (new_heat > old_heat) {
delta = new_heat - old_heat;
pulled = (delta > stadium_quotas[slot].reservoir) ? stadium_quotas[slot].reservoir : delta;
if (pulled < delta) return -1; /* insufficient -- no partial credit, no mutation */
stadium_quotas[slot].reservoir -= pulled;
} else {
delta = old_heat - new_heat;
stadium_quotas[slot].reservoir += delta;
}
stadium_cell_array[cell_index].header.heat = new_heat;
return 0;
}
uint64_t stadium_reservoir_pull(VMUuid vm_id, uint64_t amount) {
int slot = quota_slot_for_vm(vm_id);
uint64_t pulled;
@@ -478,6 +526,25 @@ uint64_t stadium_reservoir_peek(VMUuid vm_id) {
return stadium_quotas[slot].reservoir;
}
int stadium_quota_slot_for_vm(VMUuid vm_id) {
return quota_slot_for_vm(vm_id);
}
uint64_t stadium_resident_sum(VMUuid vm_id) {
int slot = quota_slot_for_vm(vm_id);
uint64_t sum = 0;
size_t i;
if (slot < 0) return 0;
for (i = 0; i < stadium_ncells; i++) {
if (!bitmap_get(i)) continue;
if (stadium_owner[i] != (uint8_t)slot) continue;
sum += stadium_cell_array[i].header.heat;
}
return sum;
}
/*
* FABRIC.md item 3.6 / item 4.1: see stadium.h's doc. Idempotent via the
* item-3.1 discriminator bitmap -- if cell 0 already reads as resident,
+100 -39
View File
@@ -40,25 +40,37 @@
* 2026-08-05: no DictEntry field). `last_decay_tick` is this layer's own
* bookkeeping, separate from DictEntry.physics.last_decay_tick -- that field
* belongs to execution_heat's decay, which item 4.1 does not touch.
*
* item 4.2 fix (FABRIC.md §25.5): keyed by [quota slot][word_id], not just
* word_id. word_id is assigned per-VM (vm->next_word_id in
* dictionary_management.c), not globally unique -- a single shared
* word_id -> cell_index map let two VMs' independently-numbered word_ids
* (e.g. both VMs' own "DUP") alias onto the same slot, so one VM's dispatch
* could cool/heat-pump a cell it did not own and credit/debit the wrong
* VM's reservoir. Exposed only because item 4.2 restored a second VM
* (Hermes) with her own dictionary; invisible with Hera alone.
*/
typedef struct {
size_t cell_index; /* STADIUM_CELL_NONE if not resident */
uint64_t last_decay_tick;
} StadiumWordSlot;
static StadiumWordSlot word_slots[DICTIONARY_SIZE];
static StadiumWordSlot word_slots[STADIUM_MAX_VM_COUNT][DICTIONARY_SIZE];
static int words_initialized = 0;
static uint64_t stat_promotions = 0;
static uint64_t stat_evictions = 0;
static uint64_t stat_promotions[STADIUM_MAX_VM_COUNT];
static uint64_t stat_evictions[STADIUM_MAX_VM_COUNT];
void stadium_words_init(void) {
int slot;
uint32_t i;
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[i].cell_index = STADIUM_CELL_NONE;
word_slots[i].last_decay_tick = 0;
for (slot = 0; slot < STADIUM_MAX_VM_COUNT; slot++) {
for (i = 0; i < DICTIONARY_SIZE; i++) {
word_slots[slot][i].cell_index = STADIUM_CELL_NONE;
word_slots[slot][i].last_decay_tick = 0;
}
stat_promotions[slot] = 0;
stat_evictions[slot] = 0;
}
stat_promotions = 0;
stat_evictions = 0;
words_initialized = 1;
}
@@ -68,6 +80,29 @@ static int cell_is_resident(size_t idx) {
return (bm[idx / 8u] >> (idx % 8u)) & 1u;
}
/*
* word_dispatch_pull - Reservoir pull for word-execution admission, clamped
* to leave a floor for application-level use (FABRIC.md §25.5/§25.7,
* Captain Bob's ruling 2026-08-06). Without this, stadium_word_dispatch()
* pulling STADIUM_WORD_HEAT_QUANTUM on every dispatch -- not just the first
* admission of a given word -- exhausts a VM's entire reservoir within
* roughly 32 total dispatches (65536 / 2048), starving any item-4.2-style
* application economy sharing the same VM's reservoir before it gets a
* chance to pull anything. The floor is Q48_ONE / 3, the same "VM-COUNT=3
* fair share" reasoning capsules/hermes/init.4th's COMMON-CH floor already
* uses -- not a new invented number. Application-level pulls
* (stadium_reservoir_pull() called directly, e.g. via STADIUM-RES-PULL) are
* NOT floored -- only word-execution admission respects this ceiling on
* its own consumption.
*/
static uint64_t word_dispatch_pull(VMUuid vm_id, uint64_t want) {
uint64_t available = stadium_reservoir_peek(vm_id);
uint64_t floor = Q48_ONE / 3;
uint64_t pullable = (available > floor) ? (available - floor) : 0;
uint64_t capped = (want < pullable) ? want : pullable;
return stadium_reservoir_pull(vm_id, capped);
}
/*
* resolve_resident_cell - Self-healing lookup (advisor-flagged reverse
* coherence gap): the map may claim word_id is resident at a cell that was
@@ -77,36 +112,59 @@ static int cell_is_resident(size_t idx) {
* no new coupling from stadium.c into this file. A stale mapping is cleared
* and counted as an eviction on discovery.
*/
static size_t resolve_resident_cell(uint32_t word_id) {
size_t cell = word_slots[word_id].cell_index;
static size_t resolve_resident_cell(int slot, uint32_t word_id) {
size_t cell = word_slots[slot][word_id].cell_index;
StadiumCell *cells;
if (cell == STADIUM_CELL_NONE) return STADIUM_CELL_NONE;
if (cell >= stadium_cell_count() || !cell_is_resident(cell)) {
word_slots[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
cells = stadium_cells();
if (cells[cell].header.identity != (uint64_t)word_id) {
word_slots[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
return STADIUM_CELL_NONE;
}
return cell;
}
uint64_t stadium_words_resident_heat(VMUuid vm_id) {
int slot;
uint32_t i;
uint64_t sum = 0;
StadiumCell *cells;
if (!words_initialized) return 0;
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return 0;
cells = stadium_cells();
for (i = 0; i < DICTIONARY_SIZE; i++) {
size_t cell = resolve_resident_cell(slot, i);
if (cell == STADIUM_CELL_NONE) continue;
sum += cells[cell].header.heat;
}
return sum;
}
void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ticks) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
cell = resolve_resident_cell(word_id);
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell != STADIUM_CELL_NONE) {
StadiumPatronHeader *h = &stadium_cells()[cell].header;
uint64_t elapsed = heartbeat_ticks - word_slots[word_id].last_decay_tick;
uint64_t elapsed = heartbeat_ticks - word_slots[slot][word_id].last_decay_tick;
if (elapsed > 0) {
/* Redirected Loop #3 (§17.7): a FRACTION of the cell's own
@@ -120,17 +178,17 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
h->heat -= cooled;
stadium_reservoir_push(vm_id, cooled);
}
word_slots[word_id].last_decay_tick = heartbeat_ticks;
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
}
h->heat += stadium_reservoir_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
h->heat += word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
return;
}
/* Not resident: Option B starter-grant admission (§17.7). execution_heat
* plays no role -- density is decided entirely by the pulled quantum. */
{
uint64_t pulled = stadium_reservoir_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
uint64_t pulled = word_dispatch_pull(vm_id, (uint64_t)STADIUM_WORD_HEAT_QUANTUM);
StadiumPatronHeader candidate;
uint8_t *raw = (uint8_t *)&candidate;
size_t i;
@@ -152,30 +210,36 @@ void stadium_word_dispatch(VMUuid vm_id, uint32_t word_id, uint64_t heartbeat_ti
return;
}
word_slots[word_id].cell_index = idx;
word_slots[word_id].last_decay_tick = heartbeat_ticks;
stat_promotions++;
word_slots[slot][word_id].cell_index = idx;
word_slots[slot][word_id].last_decay_tick = heartbeat_ticks;
stat_promotions[slot]++;
}
}
void stadium_word_forget(uint32_t word_id) {
void stadium_word_forget(VMUuid vm_id, uint32_t word_id) {
int slot;
size_t cell;
if (!words_initialized) return;
if (word_id == WORD_ID_INVALID || word_id >= DICTIONARY_SIZE) return;
cell = resolve_resident_cell(word_id);
slot = stadium_quota_slot_for_vm(vm_id);
if (slot < 0) return;
cell = resolve_resident_cell(slot, word_id);
if (cell == STADIUM_CELL_NONE) return;
if (stadium_evict(cell) == 0) {
word_slots[word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions++;
word_slots[slot][word_id].cell_index = STADIUM_CELL_NONE;
stat_evictions[slot]++;
}
}
void stadium_words_stats(uint64_t *promotions, uint64_t *evictions) {
if (promotions) *promotions = stat_promotions;
if (evictions) *evictions = stat_evictions;
void stadium_words_stats(VMUuid vm_id, uint64_t *promotions, uint64_t *evictions) {
int slot = stadium_quota_slot_for_vm(vm_id);
if (promotions) *promotions = (slot >= 0) ? stat_promotions[slot] : 0;
if (evictions) *evictions = (slot >= 0) ? stat_evictions[slot] : 0;
}
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
@@ -197,19 +261,16 @@ static void console_put_u64(uint64_t v) {
void stadium_words_print_boot_diagnostics(VMUuid vm_id) {
uint64_t promotions = 0, evictions = 0;
uint64_t resident_sum = 0;
uint64_t resident_sum;
uint64_t reservoir;
size_t ncells = stadium_cell_count();
size_t i;
stadium_words_stats(&promotions, &evictions);
stadium_words_stats(vm_id, &promotions, &evictions);
for (i = 0; i < ncells; i++) {
if (cell_is_resident(i)) {
resident_sum += stadium_cells()[i].header.heat;
}
}
reservoir = stadium_reservoir_peek(vm_id);
/* item 4.2 fix (FABRIC.md §25.5): filtered per-VM -- with two VMs
* holding quotas, summing every resident cell regardless of owner
* (the pre-4.2 behavior) mixed both VMs' conservation totals together. */
resident_sum = stadium_resident_sum(vm_id);
reservoir = stadium_reservoir_peek(vm_id);
console_puts("Stadium words: promotions=");
console_put_u64(promotions);
+6 -5
View File
@@ -684,9 +684,10 @@ void execute_colon_word(VM* vm)
/* item 4.1, FABRIC.md §17.7: feed the Stadium's independent
* conserved heat wire. execution_heat above is untouched by
* this call. vm_uuid_hera() is hardcoded here -- Tripod is
* pruned to Hera alone (item 0.1); revisit at item 4.2. */
stadium_word_dispatch(vm_uuid_hera(), w->word_id, vm->heartbeat.tick_count);
* this call. item 4.2: dispatching VM's own identity, not the
* item-4.1 hardcoded vm_uuid_hera() -- refused harmlessly by
* stadium_admit() for any VM without a granted quota. */
stadium_word_dispatch(vm->stadium_vm_id, w->word_id, vm->heartbeat.tick_count);
uint32_t word_id = w->word_id;
if (word_id < DICTIONARY_SIZE)
@@ -881,7 +882,7 @@ void vm_interpret_word(VM* vm, const char* word_str, size_t len)
entry->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(entry);
stadium_word_dispatch(vm_uuid_hera(), entry->word_id, vm->heartbeat.tick_count);
stadium_word_dispatch(vm->stadium_vm_id, entry->word_id, vm->heartbeat.tick_count);
if (canon && canon != entry)
{
/* Apply decay to canonical entry as well */
@@ -892,7 +893,7 @@ void vm_interpret_word(VM* vm, const char* word_str, size_t len)
canon->physics.last_decay_ns = lookup_ns;
physics_execution_heat_increment(canon);
stadium_word_dispatch(vm_uuid_hera(), canon->word_id, vm->heartbeat.tick_count);
stadium_word_dispatch(vm->stadium_vm_id, canon->word_id, vm->heartbeat.tick_count);
physics_metadata_touch(canon, canon->execution_heat, lookup_ns);
}
sf_mutex_unlock(&vm->dict_lock);