Add sk_hermes_alloc(), the heat-coupled allocator -- FABRIC-3.6.md task 2.2, item 28

The piece FABRIC-3.5.md SXXXIII.6 calls "what remains genuinely hard,"
built and proven first per its own recommendation. Added
src/starkernel/vm/kernel_hermes.c (wired into Makefile.starkernel's
LOADER_EXTRA_SRCS -- this repo lists vm/*.c files explicitly, no glob)
and sk_hermes_alloc()'s declaration in kernel_hermes.h.

Checks stadium_reservoir_peek(vm_id) >= SK_HERMES_Q_SLOT before
touching the reservoir at all -- refusal this way needs no rollback,
since nothing was pulled -- with an explicit rollback path
(stadium_reservoir_push) kept defensively for the pull-then-short case,
though nothing in this single-core kernel is expected to reach it.

SK_HERMES_Q_SLOT = Q48_ONE / SK_HERMES_MSG_MAX (2048), deliberately
simpler than messaging.4th's own formula, which reserves a Q.1/3 floor
for COMMON-CH's own Stadium heat -- kernel-Hermes has no such object
(SXXXIII.4/SXXXIII.5's flat membership list carries no heat of its
own), so there is nothing left for that floor to protect.

Self-test in kernel_main.c, same diagnostic-only synthetic-VM pattern
as the existing Stadium quota grant self-test (lo=3, distinct from
that test's lo=1): reads back the actual granted reservoir rather than
assuming a number, derives expected_n from it, allocates to refusal,
and checks the refusal lands at exactly expected_n, the reservoir
doesn't move on the refused attempt (rollback proven, not assumed),
and the final reservoir is exactly reservoir0 minus got_n times
Q_SLOT.

Three-arch boot clean: amd64/aarch64/riscv64 all reach [zuse@Hera] ok>,
zero UNKNOWN WORD, dict_hash unmoved from task 2.1 (pure C, no FORTH
touched). All three print identical self-test arithmetic: reservoir0=
65536 Q_SLOT=2048 expected_n=32 got_n=32 reservoir_after=0. No compiler
warnings.

Noted, not fixed: Q_SLOT's divisor and SK_HERMES_MSG_MAX are the same
32, so reservoir and arena exhaustion land at exactly the same count by
construction -- this test can't distinguish which refusal reason
fired, only that refusal is correct and rolls back correctly.

Deliberately not evidence for stadium_conserved(): allocating alone
(no release yet, task 2.3) leaves pulled heat held off the Stadium
floor, so the two-term check would correctly read false right now if
run mid-hold. That's expected, not a bug -- Stage B (task 2.7) is
defined as "before and after the alloc/free cycle," not "continuously
during." This task's self-test checks reservoir arithmetic directly
instead.

Authorized by Captain Bob ("Yes continue").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-19 21:00:20 -04:00
co-authored by Claude Sonnet 5
parent f10fa7ae83
commit 9d129fdb1c
12 changed files with 27610 additions and 7 deletions
+44 -1
View File
@@ -506,8 +506,51 @@ Hestia; headless policy intact.
in a later task. Item 27 (channel negotiation vs. broadcast, Phase 3 blocker B1) is not
answered by this structure and isn't meant to be — a flat membership list is correct either
way; the negotiation question is about behaviour built on top, not this shape.
- [ ] **2.2** — Allocate: pull `Q.SLOT` from the caller's reservoir; roll back on refusal.
- [x] **2.2** — Allocate: pull `Q.SLOT` from the caller's reservoir; roll back on refusal.
*Check:* N allocs against a known reservoir; refusal at the right count.
2026-09-19 · `logs/20260919-205534/amd64/`, `logs/20260919-205645/aarch64/`,
`logs/20260919-205817/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
`dict_hash` triple unmoved from task 2.1's baseline (pure C, no FORTH touched). All three
print `Kernel-Hermes alloc self-test: PASS` with **identical arithmetic**:
`reservoir0=65536 Q_SLOT=2048 expected_n=32 got_n=32 reservoir_after=0`.
Added `sk_hermes_alloc(VMUuid, SkHermesMessage**)` (`src/starkernel/vm/kernel_hermes.c`,
new file, wired into `Makefile.starkernel`'s `LOADER_EXTRA_SRCS` list since this repo lists
`vm/*.c` files explicitly rather than globbing). Checks
`stadium_reservoir_peek(vm_id) >= SK_HERMES_Q_SLOT` **before** touching the reservoir at
all — refusal this way needs no rollback, since nothing was pulled — with an explicit
rollback path (`stadium_reservoir_push`) kept for the pull-then-short case defensively,
though nothing in this single-core kernel is expected to reach it. `SK_HERMES_Q_SLOT`
defined as `Q48_ONE / SK_HERMES_MSG_MAX` (2048) — **deliberately simpler than
`messaging.4th`'s own formula**, which reserves a `Q.1/3` floor for `COMMON-CH`'s own
Stadium heat; kernel-Hermes has no such object (`SXXXIII.4`/`SXXXIII.5`'s flat membership
list carries no heat of its own), so there is nothing left for that floor to protect.
**Self-test, not a FORTH-visible check** (matching the existing `Stadium quota grant
self-test` pattern already in `kernel_main.c`): mints a second synthetic VM
(`lo=3`, distinct from the existing self-test's `lo=1`) via `stadium_grant_quota()`,
reads back its actual granted reservoir (always a fresh `Q48_ONE` per
`stadium_grant_quota()`'s own code — not assumed), derives `expected_n` from it rather
than hardcoding 32, allocates until refusal, and checks **all three** of: the refusal
lands at exactly `expected_n`, the reservoir doesn't move on the refused attempt (rollback
proven, not just assumed), and the final reservoir equals `reservoir0 − got_n × Q_SLOT`
exactly.
**Noted, not fixed:** `SK_HERMES_Q_SLOT = Q48_ONE / SK_HERMES_MSG_MAX` and
`SK_HERMES_MSG_MAX = 32` together mean reservoir exhaustion and arena exhaustion land at
*exactly* the same count by construction — this test cannot distinguish which refusal
reason fired, only that refusal fires at the right count and rolls back correctly. Not a
defect at this stage (nothing yet needs the arena to outlive the reservoir), but worth
remembering if `SK_HERMES_MSG_MAX` is ever resized independently of `Q_SLOT`'s divisor.
**Deliberately not evidence for `stadium_conserved()`** — per the watch-list raised before
starting Phase 2: allocating alone (no release yet, that's task 2.3) leaves pulled heat
held by kernel-Hermes, off the Stadium floor entirely, so the two-term
`stadium_resident_sum + reservoir == Q48_ONE` check would correctly read **false** right
now if run mid-hold — that is expected, not a bug, and is exactly why Stage B (task 2.7) is
defined as "before and after the alloc/free cycle," not "continuously during." This task's
self-test checks reservoir arithmetic directly instead, which is the right instrument for
what task 2.2 alone claims.
- [ ] **2.3** — Release: return remaining heat via the eviction path. *Check:* reservoir
restored exactly for an undecayed message.
- [ ] **2.4** — The four counters: `held`, `pulled`, `returned`, `consumed` (§XL.4). *Check:*