Stage E: Category B strip -- remove Hermes, messaging.4th, old routing (FABRIC-3.6.md task 4.1-4.4)

All FORTH-owned message types were cut over to kernel-Hermes in Phase 3
(tasks 3.8-3.10). This removes the now-dead FORTH messaging layer and
the Hermes VM itself: capsules/common/messaging.4th, capsules/hermes/init.4th,
the slot-3 VM-NAME-REG pairing convention, and every load-site/birth-site
reference across capsules/init.4th, artemis/init.4th, hestia/init.4th,
doe-campaign.4th (Artemis-only now), capsule_console.c, capsule_mint.c,
capsule_wirebind.c, capsule_birth.c, and kernel_main.c.

Verified on all three architectures: clean boot, mkcapsule --lint clean
(36 files, 0 violations), zero UNKNOWN WORD, identical dict_hash across
amd64/aarch64/riscv64 for every VM, and a full mint -> WIREBIND-attach ->
USE -> relay round-trip exercising the two highest-risk edits
(capsule_console.c/capsule_mint.c).

Found, not fixed: deleting messaging.4th removes SEND-ELEVATE-REQUEST,
which was the only caller of KH-ELEVATE-SEND and the only path to
ELEVATE-GRANT (zuse-eligibility.4th, still loaded at boot) -- Phase 8
PKI's own elevation entrypoint. Needs a decision before Phase 5 close-out.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-22 18:29:51 -04:00
co-authored by Claude Sonnet 5
parent cab9b5a31f
commit 3e201c82a5
23 changed files with 27825 additions and 886 deletions
+6 -1
View File
@@ -792,9 +792,14 @@ CapsuleRunResult capsule_birth_baby(
StadiumPatronHeader vm_patron;
uint8_t *raw = (uint8_t *)&vm_patron;
size_t i;
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: "Hermes" retired
* from this OR-chain -- FABRIC-3.5.md SXXXIV.4's own ruling
* named this exact moment ("removed in Stage E, with the strip,
* not with the Tripod change"). The chain temporarily held four
* names (Hera, Hermes, Artemis, Hestia) through Phases 1-3,
* deliberately, per that same ruling; three names now. */
int is_fleet_foundation =
vm_name_prefix_eq_nocase(capsule_name, "Hera") ||
vm_name_prefix_eq_nocase(capsule_name, "Hermes") ||
vm_name_prefix_eq_nocase(capsule_name, "Artemis") ||
vm_name_prefix_eq_nocase(capsule_name, "Hestia");
+14 -10
View File
@@ -18,17 +18,21 @@
#include "starkernel/kmalloc.h"
#include <string.h>
/* Real, minimal, working content -- just enough vocabulary to send/
* receive messages (common:messaging.4th, same as every other VM).
* No COMMON-CH subscription: a console's own traffic is direct 1:1
* with its paired user VM (CONSOLE-CMD-EVENT), not broadcast, so
* there's no need to resolve an index in Hermes's own routing table
* for it. Needs the "Block NNNN" header capsule_exec_payload()
* requires (§F.18) -- not optional. 4997 is an unused block number. */
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
* common:messaging.4th + MSG-CD-INIT here -- "just enough vocabulary
* to send/receive messages" -- but a console-proxy VM's own traffic
* (CONSOLE-CMD-EVENT) has gone through kernel-Hermes directly
* (sk_hermes_send_one(), repl.c) since task 3.9, with no dependency on
* this VM's own dictionary containing any messaging vocabulary at all.
* common:messaging.4th no longer exists as of this strip, so this
* needed to come out regardless. Genuinely empty now, past the
* mandatory "Block NNNN" header capsule_exec_payload() requires
* (§F.18) -- confirmed live that an empty body is a safe no-op when
* interpreted (task 3.8's own finding: vm_interpret() on an empty
* string is a no-op, no error, no UNKNOWN WORD). 4997 is an unused
* block number. */
static const char CONSOLE_IDENTITY_SRC[] =
"Block 4997\n"
"S\" common:messaging.4th\" EXEC\n"
"MSG-CD-INIT\n";
"Block 4997\n";
CapsuleRunResult capsule_console_birth(const char *console_name, VMUuid parent,
VMUuid *out_vm_id, void **out_vm_ctx)
+11 -9
View File
@@ -53,17 +53,19 @@ extern int blkio_ram_init_state(void *state_mem, size_t state_len,
* capsule_exec_payload() requires (§F.18) -- discovered live, not
* optional. 4999 is an unused block number (see capsules/ block-map).
*
* Loads common:messaging.4th + MSG-CD-INIT exactly like capsule_console.c's
* own CONSOLE_IDENTITY_SRC -- every live VM except Hera owns a MSG-TICK
* word for her per-idle-tick pump to VM-EXEC into (repl.c's Phase C loop).
* A minted identity's RUNCAP-born VM is not exempt from that pump, so
* omitting this left it spamming "UNKNOWN WORD: 'MSG-TICK'" every idle
* tick forever from the moment WIREBIND made the VM live -- found live
* minting a real second identity (FABRIC-2.md §F.26 follow-up). */
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
* common:messaging.4th + MSG-CD-INIT here too, for the same reason
* capsule_console.c's own CONSOLE_IDENTITY_SRC did -- the old MSG-TICK
* idle-pump gate (repl.c's Phase C loop) spammed "UNKNOWN WORD:
* 'MSG-TICK'" on any VM missing the word, found live minting a real
* second identity (FABRIC-2.md §F.26 follow-up). That entire pump
* mechanism is gone as of this same strip (sk_repl_idle() now calls
* sk_hermes_drain_checkpoint() directly, no FORTH word of any kind
* required on the target VM), so the failure mode this once worked
* around no longer exists either -- this can come out clean, not just
* because common:messaging.4th itself no longer exists to load. */
static const char MINT_DEFAULT_PERSONALITY[] =
"Block 4999\n"
"S\" common:messaging.4th\" EXEC\n"
"MSG-CD-INIT\n"
": WELCOME ( -- ) .\" Minted identity -- default personality\" CR ;\n"
"WELCOME\n";
+13 -9
View File
@@ -247,6 +247,11 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
console_println("WIREBIND: console VM birth FAILED");
return;
}
/* FABRIC-3.6.md Phase 4 (Stage E): console_ctx's only use (the
* routing-table VM-NAME-REG call above) is gone -- still populated
* because capsule_console_birth()'s own signature requires an
* out-param regardless, just no longer read here. */
(void)console_ctx;
if (capsule_runcap_birth(dev, sig, user_vm_name, mama_vm->stadium_vm_id, &user_id,
&user_ctx) != CAPSULE_RUN_OK) {
console_println("WIREBIND: user VM birth FAILED");
@@ -326,15 +331,14 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
* continuously by sk_repl_run()'s own main loop rather than a one-shot
* signal at login time. Nothing to call here anymore. */
/* Register the pairing in the console's own routing table, index 3
* -- the fixed convention sk_repl_dispatch_line() (repl.c) uses. */
{
char reg_cmd[sizeof(user_vm_name) + 32];
int n = snprintf(reg_cmd, sizeof(reg_cmd), "S\" %s\" 3 VM-NAME-REG", user_vm_name);
if (n > 0 && (size_t)n < sizeof(reg_cmd)) {
vm_interpret((VM *)console_ctx, reg_cmd);
}
}
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip): the messaging.4th
* routing-table registration that used to happen here (`S" %s" 3
* VM-NAME-REG`) is gone -- sk_repl_dispatch_line() (repl.c) has not
* read that table since task 3.9's own cutover (it resolves the
* paired identity directly via capsule_vm_find_by_name(), not an
* index), and messaging.4th (which defined VM-NAME-REG) no longer
* exists at all as of this strip. This was already dead, just not
* yet removed -- confirmed by reading, not assumed. */
console_puts("WIREBIND: ");
console_puts(username);
+2 -1
View File
@@ -1450,7 +1450,8 @@ static void mama_word_name_to_xt(VM *vm)
* buffer at buf-addr, matching ZUSE-PUBKEY@'s own convention exactly:
* chunk i (0..3) is the 8-byte little-endian encoding of pubkey bytes
* [i*8 .. i*8+7] -- this is that packing's inverse, so a pubkey sent as
* 4 cells (see common:messaging.4th's SEND-ELEVATE-REQUEST) round-trips
* 4 cells (the convention SEND-ELEVATE-REQUEST used before it was
* deleted with messaging.4th, FABRIC-3.6.md Phase 4 Stage E) round-trips
* byte-exact regardless of any individual chunk's sign bit.
*/
static void mama_word_elevate_pubkey_unpack(VM *vm)
+22 -39
View File
@@ -1101,31 +1101,16 @@ static void kernel_main_deep(BootInfo *boot_info) {
* guard exists for), not because she lacks MSG-TICK now -- and calls
* it directly in her own context instead, right after that loop. */
/* Hermes is now a permanent fleet-foundation VM, not self-test
* scaffolding -- FABRIC-2.md D.7 (birth-by-message-only, 2026-08-28):
* the Tripod legs (Hera/Hermes/Artemis) must be alive session-less so
* a later thumbdrive-attach flow has a running Hermes/Artemis to
* message. Previously born, exercised, and KILLed by item 4.2's own
* self-test every boot; that diagnostic exercising is gone, only the
* birth remains. */
console_println("Startup: birthing Hermes (fleet foundation)...");
vm_interpret(mama, "S\" Hermes\" BIRTH");
{
VMRegistryEntry entry;
if (capsule_vm_find_by_name_nocase("Hermes", &entry) == 0 &&
entry.state == VM_STATE_LIVE) {
console_println("Startup: Hermes live");
} else {
console_println("Startup: Hermes birth registry lookup FAILED");
}
}
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: Hermes's own birth,
* born here as a permanent fleet-foundation VM since FABRIC-2.md
* D.7, is retired -- SXXXIV.4's own ruling named this exact moment
* ("Hermes's birth and its is_fleet_foundation entry are removed in
* Stage E, with the strip, not with the Tripod change") and this is
* that strip. */
/* Hestia is the third reconstituted Tripod leg (Hera/Artemis/Hestia,
* FABRIC-3.5.md SII/SIV) -- born here, alongside Hermes, per
* FABRIC-3.6.md task 1.4. Hermes stays live too (SXXXIV.4): the
* fleet is Hera/Hermes/Artemis/Hestia through Phase 1-3, four VMs,
* not three, until Phase 4's Category B strip retires Hermes. Same
* birth-by-name-then-registry-check shape as Hermes/Artemis above.
* FABRIC-3.5.md SII/SIV) -- born here per FABRIC-3.6.md task 1.4.
* Same birth-by-name-then-registry-check shape as Artemis below.
*
* HEADLESS INVARIANT (FABRIC-3.5.md SXVIII.6, FABRIC-3.6.md task 1.9):
* this birth must not set g_wirebind_attached_username, must not
@@ -1149,8 +1134,7 @@ static void kernel_main_deep(BootInfo *boot_info) {
}
/* Artemis is now a permanent fleet-foundation VM, not self-test
* scaffolding -- same reasoning as Hermes's own birth just above
* (FABRIC-2.md D.7). Previously born, exercised, and KILLed by item
* scaffolding -- FABRIC-2.md D.7. Previously born, exercised, and KILLed by item
* 4.6's own self-test every boot; that diagnostic exercising is gone,
* only the birth remains. Artemis's own capsule still runs its own
* self-test plus a 30-rep stress campaign at load
@@ -1268,9 +1252,11 @@ static void kernel_main_deep(BootInfo *boot_info) {
* are confirmed fully born above -- never earlier. This stage has no
* critical-section protection against being switched away mid-setup,
* so registering any earlier would risk the signal firing during
* Hermes/Artemis's own birth sequencing. */
* Artemis's own birth sequencing. FABRIC-3.6.md Phase 4 (Stage E),
* 2026-09-22: Hermes's own registration here is retired along with
* her birth above. */
{
VMRegistryEntry hera_entry, hermes_entry, artemis_entry;
VMRegistryEntry hera_entry, artemis_entry;
if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) {
sk_vm_switch_signal_register(hera_entry.vm_id);
/* Seed the switch mechanism's own "who is running" tracker
@@ -1279,17 +1265,13 @@ static void kernel_main_deep(BootInfo *boot_info) {
* ever happened. */
sk_vm_switch_set_current(mama);
}
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_entry) == 0 &&
hermes_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(hermes_entry.vm_id);
}
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
artemis_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(artemis_entry.vm_id);
}
/* Hestia is the fourth fleet member through Phase 1-3 (FABRIC-3.5.md
/* Hestia is the third fleet member through Phase 4 (FABRIC-3.5.md
* SXXXIV.4) -- FABRIC-3.6.md task 1.5, same registration shape as
* Hermes/Artemis above, added here rather than earlier for the
* Artemis above, added here rather than earlier for the
* identical reason the comment above this block already gives. */
{
VMRegistryEntry hestia_entry;
@@ -1302,22 +1284,23 @@ static void kernel_main_deep(BootInfo *boot_info) {
/* FABRIC-3.6.md task 3.2 (B1) self-test: confirm every live fleet
* member is a common-channel member (the birth-time subscription just
* exercised for real, above -- Hera explicitly, Hermes/Hestia/Artemis
* exercised for real, above -- Hera explicitly, Hestia/Artemis
* through capsule_birth_baby()'s own task 3.2 hook), then create and
* destroy a synthetic private topic against a synthetic VM id (lo=5,
* distinct from every other synthetic id this file already uses --
* lo=1 Stadium quota grant, lo=3 kernel-Hermes alloc/release). Diagnostic
* only, same posture as every other self-test block in this function:
* never used for anything else, never perturbs the real fleet. */
* never used for anything else, never perturbs the real fleet.
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: Hermes's own check
* removed -- she is no longer a fleet member, so requiring her here
* would report a false FAIL, not a graceful skip like the drain and
* channel-open-policy self-tests further down already do for her. */
{
VMRegistryEntry hera_ck, hermes_ck, hestia_ck, artemis_ck;
VMRegistryEntry hera_ck, hestia_ck, artemis_ck;
int fleet_ok = 1;
if (capsule_vm_find_by_name_nocase("Hera", &hera_ck) != 0 ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, hera_ck.vm_id)) fleet_ok = 0;
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_ck) != 0 ||
hermes_ck.state != VM_STATE_LIVE ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, hermes_ck.vm_id)) fleet_ok = 0;
if (capsule_vm_find_by_name_nocase("Hestia", &hestia_ck) != 0 ||
hestia_ck.state != VM_STATE_LIVE ||
!sk_hermes_channel_is_member(SK_HERMES_CHANNEL_COMMON, hestia_ck.vm_id)) fleet_ok = 0;
+86 -134
View File
@@ -444,19 +444,22 @@ static void sk_word_kh_blk_attach_send(VM *vm) {
#define SK_KH_ELEVATE_BUF_SIZE 256
static char g_kh_elevate_buf[SK_KH_ELEVATE_BUF_SIZE];
/* KH-ELEVATE-SEND ( paddr plen -- ok? ): messaging.4th's own
* SEND-ELEVATE-REQUEST builds its payload (a FORTH command string
* calling ELEVATE-GRANT, block 5040) and hands it here instead of
* CH-REQUEST's old COMMON-CH/MSG-SEND path. `from`/`to` are derived the
* same way KH-BLK-ATTACH-SEND's own already does (the calling VM and
* sk_get_mama_vm() -- ELEVATE-GRANT always runs on Hera, per
* zuse-eligibility.4th's own header comment) -- the caller never
/* KH-ELEVATE-SEND ( paddr plen -- ok? ): built for messaging.4th's own
* SEND-ELEVATE-REQUEST (a FORTH command string calling ELEVATE-GRANT,
* zuse-eligibility.4th block 4022), which handed its payload here
* instead of CH-REQUEST's old COMMON-CH/MSG-SEND path. `from`/`to` are
* derived the same way KH-BLK-ATTACH-SEND's own already does (the
* calling VM and sk_get_mama_vm() -- ELEVATE-GRANT always runs on Hera,
* per zuse-eligibility.4th's own header comment) -- the caller never
* supplies either, which is a real correctness improvement over the
* FORTH path it replaces: CH-REQUEST's own "initiator-only gate...
* FORTH path it replaced: CH-REQUEST's own "initiator-only gate...
* refuses if from doesn't match MY-CH-ID" existed only because a caller
* COULD claim to be a different VM by passing the wrong stack value;
* deriving `from` from the C-level calling VM's own identity makes that
* spoof structurally impossible rather than merely gated. */
* spoof structurally impossible rather than merely gated. FABRIC-3.6.md
* Phase 4, Stage E deleted messaging.4th -- SEND-ELEVATE-REQUEST no
* longer exists anywhere, so this word currently has no FORTH caller.
* Found, not fixed; see FABRIC-3.6.md's own Phase 4 entry. */
static void sk_word_kh_elevate_send(VM *vm) {
if (vm->dsp < 1) {
log_message(LOG_ERROR, "KH-ELEVATE-SEND: stack underflow");
@@ -527,49 +530,42 @@ static void sk_cursor_show(void)
console_fb_draw_cursor();
}
/* Reentrancy guards for the MSG-TICK pump inside sk_repl_idle().
/* Reentrancy guards for the kernel-Hermes drain pump inside sk_repl_idle().
*
* sk_repl_idle() runs vm_interpret(mama, ...) (below) to VM-EXEC MSG-TICK
* into every live child VM (FABRIC-2.md Phase C). But sk_repl_idle() is
* itself called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()
* / sk_console_readline(), which run *from inside* the executing VM's own
* vm_interpret once a FORTH word reads input mid-line). vm_interpret() is
* not reentrant -- it resets the VM's single input_buffer/input_pos/
* input_length (vm_core.c) on entry. Calling vm_interpret(mama, ...) at that
* point would re-enter mama's interpreter mid-parse and silently truncate
* the rest of the line; if the pump's own MSG-TICK work then triggers another
* blocking read, it would also recurse unboundedly. Two flags keep the pump
* off every path that could re-enter an interpreter:
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
* Phase C, common:messaging.4th, now removed). The mechanism changed --
* sk_repl_idle() now calls sk_hermes_drain_checkpoint() directly per VM,
* not VM-EXEC -- but the reentrancy hazard these two flags exist for is
* unchanged: drain_checkpoint() still calls vm_interpret() internally
* (on the drained message's own payload), and sk_repl_idle() is itself
* called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()/
* sk_console_readline(), which run *from inside* the executing VM's own
* vm_interpret once a FORTH word reads input mid-line). vm_interpret()
* is not reentrant -- it resets the VM's single input_buffer/input_pos/
* input_length (vm_core.c) on entry. Calling it on mama at that point
* would re-enter her interpreter mid-parse and silently truncate the
* rest of the line. Two flags keep the pump off every path that could
* re-enter an interpreter:
* - g_mama_interpreting: set while Hera is executing a dispatched line, so
* the pump defers to the next safe (prompt) boundary.
* - g_idle_pump_active: belt-and-suspenders; stops recursive re-entry
* from inside the pump's own vm_interpret call.
* from inside the pump's own drain call.
*/
static int g_mama_interpreting; /* zero-initialized (BSS) */
static int g_idle_pump_active; /* zero-initialized (BSS) */
/* FABRIC-3.md SXXII (2026-09-12): the messaging pump below used to walk
* the ENTIRE live-VM registry every idle beat and dispatch a full
* VM-EXEC "MSG-TICK" into every one of them -- O(N) per tick, forever,
* with no cap. Fine at Tripod's original 3-VM scale; live-caught as a
* real bottleneck at just 9 VMs on riscv64 (a std79 turn-attractor
* campaign that completed in ~200-340s on amd64/aarch64 never finished
* on riscv64 at the same VM count, while 8 VMs was fine there too --
* the qemu-instance progress check (utime climbing steadily, ~66-75%
* CPU) ruled out a hang; it was genuinely just this loop, at riscv64's
* slower per-dispatch cost, dominating wall time). The real fleet is
* headed toward hundreds of VMs, at which point this isn't a riscv64
* quirk -- unconditional O(N) per second is a wall on every
* architecture. Fixed by round-robin batching: SK_MSG_PUMP_BATCH live
* VMs get dispatched per beat, not all of them, with a persistent
* cursor picking up where the previous beat left off (wrapping around
* the registry). Bounds both the scan and the dispatch cost to O(K)
* regardless of total VM count -- the tradeoff is that any single VM's
* own queue now drains roughly every ceil(N/K) beats instead of every
* beat, which is still bounded and still matches the pump's own
* existing "best-effort, resumes next beat" contract. */
#define SK_MSG_PUMP_BATCH 4u /* max VM-EXEC MSG-TICK dispatches per idle beat */
static uint32_t g_msg_pump_cursor; /* zero-initialized (BSS) */
/* FABRIC-3.md SXXII (2026-09-12) recorded a real O(N) bottleneck here --
* the messaging pump below used to walk the entire live-VM registry
* every idle beat and dispatch a full VM-EXEC "MSG-TICK" into every one
* of them, live-caught as a wall-time-dominating cost at just 9 VMs on
* riscv64 -- and fixed it with round-robin batching (a persistent
* cursor, a fixed number of dispatches per beat). FABRIC-3.6.md Phase 4
* (Stage E), 2026-09-22: that whole mechanism (MSG-TICK, the batching
* cap, and this comment's own prior form) is gone along with
* common:messaging.4th itself -- see sk_repl_idle()'s own current
* comment, just below, for why the replacement (kernel-Hermes drain)
* does not carry the same O(N) cost and needs no cap. */
static void sk_repl_idle(VM *active_vm)
{
@@ -857,18 +853,25 @@ static void sk_repl_idle(VM *active_vm)
* comment. */
capsule_wirebind_reap_idle_check();
/* FABRIC-2.md Phase C (2026-08-28): distributed messaging pump. Every
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
* MSG-TICK word (see capsules/common/messaging.4th) instead of only
* Hermes having one -- "fully distributed, Hera pumps each VM's
* drain" was the confirmed design. Hera is excluded: she never loads
* common:messaging.4th (kernel_main.c's own comment at the Hermes-
* birth call site explains why -- the STADIUM-* primitives it needs
* are deliberately never registered in her own dictionary, to keep
* her dict_hash off item 4.1's baseline), so MSG-TICK is genuinely
* absent there, not just untried. She is the only VM with a
* persistent idle tick, so she walks the registry once per idle beat
* and VM-EXECs MSG-TICK into every OTHER live VM's own dictionary. */
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip), 2026-09-22:
* this used to be a distributed FORTH messaging pump -- FABRIC-2.md
* Phase C's own design, VM-EXEC'ing MSG-TICK into every live VM's
* own dictionary once per idle beat (common:messaging.4th, now
* removed entirely). That mechanism, and the comment that used to
* sit here explaining it, are both gone: kernel-Hermes's own drain
* (below) has needed no FORTH word since task 3.9, and every
* messaging.4th-owned message type had a real cutover by task 3.10
* (FABRIC-3.6.md task 3.11, Phase 3 gate). The prior version of this
* comment also claimed "Hera never loads common:messaging.4th" --
* that was already wrong before this strip (capsules/init.4th line
* 21 EXECs it directly; task 3.10's own live FIND-based check
* confirmed it), not just made moot by removing the file. She still
* has the only persistent idle tick, so she is still the one that
* walks the registry once per idle beat -- now purely to give every
* live VM's own kernel-Hermes queue a chance to drain, batched the
* same round-robin way the old MSG-TICK dispatch was (SK_MSG_PUMP_
* BATCH below), kept for the same reason: bounding this to O(K)
* regardless of total VM count, not O(N) every beat. */
VM *mama = (VM *)sk_get_mama_vm();
/* Reentrancy guard: never run the pump while mama is mid-interpret
@@ -887,93 +890,42 @@ static void sk_repl_idle(VM *active_vm)
g_idle_pump_active = 1;
{
uint32_t count = capsule_vm_registry_count();
uint32_t dispatched = 0;
uint32_t scanned = 0;
uint32_t i = (count > 0) ? (g_msg_pump_cursor % count) : 0;
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: the SK_MSG_PUMP_
* BATCH cap this loop used to enforce existed to bound the cost
* of the old MSG-TICK VM-EXEC dispatch -- a genuinely expensive
* per-VM vm_interpret() call, confirmed live as a real O(N)
* bottleneck at just 9 VMs on riscv64 (see this section's own
* history above). sk_hermes_drain_checkpoint() (task 3.4) is not
* that: its own fast path is a single global-counter read
* (sk_hermes_pending_total == 0 -> return immediately,
* kernel_hermes.c's own doc comment), so walking every live VM
* every beat costs one cheap integer read per VM in the common
* case (nothing in flight), not an O(N) wall -- the cap this
* loop's own comment history worried about does not apply to
* this call. Removed: every live VM (except mama, handled
* separately below) is now visited every idle beat, matching
* what task 3.9's own testing already exercised, without the
* ceil(N/SK_MSG_PUMP_BATCH)-beat delivery latency the cap was
* accidentally imposing on kernel-Hermes drain specifically. */
uint32_t count = capsule_vm_registry_count();
uint32_t i;
while (count > 0 && dispatched < SK_MSG_PUMP_BATCH && scanned < count) {
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
scanned++;
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
ent.state == VM_STATE_LIVE &&
ent.vm_ptr != (void *)mama) {
/* FABRIC-3.6.md task 3.9, found live 2026-09-22: kernel-
* Hermes's own drain (sk_hermes_drain_checkpoint(), task
* 3.4) only ever runs as a side effect of vm_interpret()
* being called ON the target VM (vm_core.c's own outermost-
* interpret checkpoint hook) -- it has nothing to do with
* the old FORTH MSG-TICK word this loop was built around.
* A WIREBIND identity minted with the STD79-lockdown
* personality (MINT_PERSONALITY_STD79_LOCKDOWN) never loads
* common:messaging.4th, so the MSG-TICK gate just below
* always skips it -- meaning it never got an interpret
* tick at all, meaning a CONSOLE-CMD-EVENT queued for it
* sat in its pending queue forever, silently. Confirmed
* live: sk_hermes_send_one() returned 0 (queued OK) but no
* drain ever ran. Called unconditionally here, independent
* of the MSG-TICK check below -- kernel-Hermes doesn't need
* a FORTH word to exist, it just needs its target VM to get
* an outermost interpret call periodically. Direct C call,
* not routed through VM-EXEC/vm_interpret: drain_checkpoint
* already saves/restores the target's own interpret state
* internally (kernel_main.c's own self-test calls it the
* same standalone way), so no vm_interpret() wrapper is
* needed to reach it from here. */
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
/* FABRIC-3.md, 2026-09-09: a per-VM flag (e.g. "this one is
* std79-locked") doesn't generalize -- any VM whose own
* dictionary lacks MSG-TICK, for whatever reason (never
* loaded common:messaging.4th, a future personality that
* drops it, ACL denial), hits the identical failure. So
* check the actual target VM's own dictionary fresh every
* tick, the same source of truth the interpreter's own ACL
* enforcement uses (vm_core.c), rather than pre-flag specific
* personalities. Skip silently rather than spam "VM-EXEC:
* ERROR in <name>" every idle tick forever for a VM that
* plain doesn't have -- or isn't allowed -- MSG-TICK
* (live-caught 2026-09-07/09). */
DictEntry *msgtick = vm_find_word((VM *)ent.vm_ptr, "MSG-TICK", 8);
if (msgtick && msgtick->acl_allow) {
static const char PFX[] = "S\" MSG-TICK\" S\" ";
static const char SFX[] = "\" VM-EXEC";
char cmd[128];
size_t p = 0;
size_t name_len = strlen(ent.name);
if (name_len > VM_NAME_MAX - 1u) name_len = VM_NAME_MAX - 1u;
memcpy(cmd + p, PFX, sizeof(PFX) - 1u); p += sizeof(PFX) - 1u;
memcpy(cmd + p, ent.name, name_len); p += name_len;
memcpy(cmd + p, SFX, sizeof(SFX) - 1u); p += sizeof(SFX) - 1u;
cmd[p] = '\0';
vm_interpret(mama, cmd);
dispatched++;
}
}
i = (i + 1u) % count;
}
g_msg_pump_cursor = i;
}
/* FABRIC-3.md SXX: Hera now genuinely has her own MSG-ARENA/CH-ARENA
* (the messaging-load root cause above is fixed -- see kernel_main.c's
* own updated comment at the Hermes-birth call site), so her own
* queued messages need draining too, same as every other VM's. She's
* excluded from the loop above not because she lacks MSG-TICK now, but
* because she IS the pump -- self-targeting VM-EXEC would hit the same
* reentrancy class the guard above exists for. No such problem calling
* MSG-TICK directly here, though: it's a plain word in her own
* dictionary, not a VM-EXEC dispatch into anyone else's input buffer.
* Guarded the same defensive way as the loop above, not assumed. */
{
DictEntry *msgtick = vm_find_word(mama, "MSG-TICK", 8);
if (msgtick && msgtick->acl_allow) {
vm_interpret(mama, "MSG-TICK");
}
}
/* Hera's own kernel-Hermes queue needs draining too, same as every
* other VM's -- she is excluded from the loop above not because she
* has nothing to drain, but because she IS the pump: a self-
* targeting call here runs directly, no VM-EXEC/reentrancy concern
* (sk_hermes_drain_checkpoint() is a plain C call, not a dispatch
* into anyone else's input buffer). */
(void)sk_hermes_drain_checkpoint(mama);
g_idle_pump_active = 0;
if (console_tx_count() == tx_before_idle) {