Stage 3 follow-on: message-arrival eligibility hook + trampoline-blind switch-storm fix (FABRIC-3.md §XXVIII.2)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Implements the message-arrival eligibility signal FABRIC-3.md §XXVIII.1 left
open (has_work per-slot flag, set via new SWITCH-MARK-WORK primitive from
MSG-SEND) so an idle VM never becomes a switch target purely by waiting out
the readiness threshold.

Also root-causes and fixes a second, independent switch-storm: the tick's
"who is current" check used vm_log_attributed_vm(), which can't see a VM
parked in switch.c's own raw trampoline. Replaced with a dedicated
g_switch_current_vm tracked by the switch mechanism itself, and moved
target-slot eligibility reset to the switch decision point instead of
relying on ISR polling to observe a window that can be only a few
instructions wide.

Verified live on all 3 architectures: clean boot to zuse@Hera] ok>, live
cross-VM message dispatch, and (since a quiet log looks identical to a
livelocked storm once the DoE probe is gone) confirmed genuine REPL
liveness via QMP send-key + screendump on aarch64/riscv64, not log
inspection alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
This commit is contained in:
Robert Allan James
2026-09-14 17:39:20 -04:00
co-authored by Claude Sonnet 5
parent 862d7d9c48
commit 66beae7fd4
29 changed files with 121129 additions and 338 deletions
+90
View File
@@ -3804,3 +3804,93 @@ counter/flag, written at the FORTH-side message-enqueue site, read the same ISR-
readiness already is) but not yet implemented -- superseded in priority by this stack-ownership
finding, which had to be fixed first. Revisit once this correction lands.
## XXVIII.2 -- Message-arrival eligibility hook implemented; a second, independent switch-storm
root cause found and fixed in the same pass (2026-09-14)
**Picks up the "still open" item from §XXVIII.1's close: the message-arrival-hook eligibility
signal.** Added `has_work`, a per-slot flag beside the existing `readiness` counter in
`sk_switch_slot_entry_t` (`capsule_vm_switch_signal.c`). `sk_vm_switch_signal_mark_work(vm_id)`
sets it; `sk_vm_switch_signal_tick()`'s pending-switch decision now requires both
`readiness >= SK_SWITCH_READINESS_THRESHOLD` *and* `has_work`, and clears it (alongside
`readiness`) the instant a slot becomes current, same as before. Wired end to end: `MSG-SEND`
(`capsules/common/messaging.4th`, block 5021) now calls a new `SWITCH-MARK-WORK ( caddr u -- )`
primitive (`mama_word_switch_mark_work`, `mama_forth_words.c`) with the message's `to` name,
captured into a new `MSG-SEND-TO-IDX` variable before the existing `Q.SLOT`/`MSG-ALLOC` sequence
consumes the stack. `SWITCH-MARK-WORK` resolves the name via
`capsule_vm_find_by_name_nocase()` and silently no-ops on an unknown or malformed name --
deliberate, since this fires on every message send, not a user-facing command. Registered in
all four of `mama_forth_words.c`'s word-table blocks (`register_mama_forth_words()` x2,
`register_child_vm_words()`), matching how every other VM-scoped primitive in that file is
exposed. A VM that has never been sent anything therefore never becomes a switch target purely
by sitting idle past the readiness threshold -- closes the "wasteful but no longer corrupting"
bounce §XXVIII.1 flagged as its own still-open item.
**While instrumenting this for verification, a second, wholly independent switch-storm root
cause turned up -- not a regression of §XXVIII.1's fix, a different bug in the same
neighborhood.** `sk_vm_switch_signal_tick()`'s "who is current" check used
`vm_log_attributed_vm()` (`vm_core.c`) -- the wrong source. That function tracks nested
`vm_interpret()` calls for log-source attribution and has no way to know a VM is running while
parked inside `switch.c`'s own raw-C trampoline (`sk_vm_switch_entry()`), which never itself
calls `vm_interpret()`. A VM whose only production behavior is that trampoline was therefore
never seen as "current," so its own `readiness`/`has_work` state below never reset by the
current-slot branch of `tick()` -- readiness re-armed on the very next timer tick, forever, once
nothing else happened to independently move attribution around it. Found live: a switch storm,
QEMU pinned near 100% CPU, serial log frozen solid.
**Fix: track "who is physically running" inside the switch mechanism itself, independent of
`vm_interpret()`'s own bookkeeping.** New `g_switch_current_vm` (`switch.c`), exposed via
`sk_vm_switch_current_vm()`/`sk_vm_switch_set_current()` (`switch.h`) -- written unconditionally
by `sk_vm_context_switch()` on both the switch-out and the eventual resume, the same
single-writer-mainline discipline every other piece of Stage 2/3 state already follows. Seeded
once at boot (`kernel_main.c`, right where Hera's slot registers) since she is genuinely running
before any switch has ever happened and there is no other event to hang the initial value on.
`sk_vm_switch_signal_tick()` now reads this instead of `vm_log_attributed_vm()`. An earlier
attempt at fixing this by calling `vm_interpret(self, "MSG-TICK")` from inside the trampoline
itself was tried and reverted -- it traded the switch-storm for a different, unrelated hang (a
stall in `sk_repl_headless_wait()`'s WIREBIND-attachment wait, not root-caused, not worth
chasing once the real fix was found) -- worth recording so it isn't retried.
**A third bug, same root cause (eligibility state read too late), fixed alongside the other
two:** `sk_vm_switch_signal_note_switch_performed()` reset a target slot's `readiness`/`has_work`
by polling `tick()`'s current-slot branch, which depends on a real timer tick landing while that
VM is genuinely "current" -- a window that can be as small as a few instructions for a VM whose
only behavior is the placeholder trampoline's immediate yield-back, too narrow to reliably catch.
Fixed by taking a `target_id` parameter (call site: `vm_core.c`'s Stage 3 checkpoint in
`execute_colon_word()`) and resetting that slot's `readiness`/`has_work` directly, at the switch
decision point, deterministically -- correctness no longer depends on ISR timing at all.
`current_slot`/`sk_vm_switch_current_vm()` remain useful for the DoE-observable columns and for
excluding the running slot from readiness accrual, but are no longer load-bearing for storm
prevention.
**Verification, run live by Claude Sonnet 5 at Bob's request, per this project's
write-probe-capture-revert discipline minus the probe (the temporary `HB-ON` DoE trigger from
§XXVIII.1 stays reverted; this pass verified without it):** all three architectures rebuilt
clean (`make -f Makefile.starkernel ARCH=<arch> clean qemu`, foreground, one at a time, prior
instance fully torn down including leaked `tail -f` helper processes before the next launch) and
boot to `[zuse@Hera] ok>` -- Zuse auto-authenticates from the attached thumbdrive on every arch,
matching the identity-lockdown work from §XI. Live cross-VM message dispatch observed
unprompted (`Stadium: dispatch cell=42634 behaviour=DELIVER msg_idx=0`, attributed to
`[zuse@Artemis]`), confirming the eligibility mechanism is genuinely exercised, not just present
in the binary. **Explicitly called out because it matters for how this was checked:** with the
DoE probe gone, a healthy idle REPL and a livelocked storm produce the identical symptom -- a
quiet serial log -- so log growth alone was not trusted as proof of health on aarch64/riscv64
after a quiet stretch. Verified instead via QMP (`send-key` typing `1 1 + . ` followed by `ret`,
then `screendump`) against the running instance's own QMP socket, confirming a real computed
`2` appended to the log on both architectures -- the same headless-input technique this
project's memory already recommends for exactly this class of ambiguity. All three
architectures: clean build, clean boot, live messaging, correct computation, zero PANIC/assertion
output, no leaked QEMU or helper processes after teardown.
**Found live, flagged, deliberately not fixed (not requested, not this pass's scope):**
`VM-EXEC: ERROR in Artemis` prints deterministically on every boot, on all three architectures,
immediately after Artemis's own birth completes and just before Zuse's thumbdrive
identity-confirmation line -- consistent enough across arches to be a real, reproducible
condition rather than timing noise, but boot continues cleanly past it every time. Worth
someone's attention in a future pass; `mama_word_vm_exec()`'s error-report path
(`mama_forth_words.c` ~line 781) is where the message originates, but the actual failing
`VM-EXEC` call site and cause were not traced this pass.
**Still open:** the MSG-TICK/Stage-3-switch dual-ownership rough edge §XXVIII itself flagged
(both mechanisms can independently move control between the same VMs) is unchanged by this
pass. Stage 4 (WIREBIND-scope extension) remains a ratified-decision-only step, untouched.