Stage O-B: Hera drains her own mail at the idle point, not on the ~1 s idle beat (FABRIC-3.9.0 §2.5)

- sk_vm_idle_point(may_drain_self): with no frame live and mail pending,
  Hera drains one message and returns to the poll loop. Readline passes
  n == 0 (the mid-edit gate), headless 1, the key read 0.
- Removed sk_repl_idle()'s timer-paced drain of Hera's queue and the two
  flags that guarded it (g_mama_interpreting, g_idle_pump_active) -- the
  idle point's frames check already covers that re-entry hazard.

Probe (reverted), all three ISAs: admission -> drain for mail to an idle
Hera 0-1 beats (was up to 100); a message arriving mid-edit is held until
the line is entered (2080-3587 beats), never printed into typed text; an
event queued during a busy line drains when it ends; BLK-ATTACH at boot 1
beat. Acceptance passes; parity, dict_hash set and relay ledger unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 22:33:33 -04:00
co-authored by Claude Opus 5.5
parent fe55038e9a
commit ee4c401d1c
16 changed files with 55062 additions and 65 deletions
+27 -61
View File
@@ -832,30 +832,13 @@ static void sk_cursor_show(void)
console_fb_draw_cursor();
}
/* Reentrancy guards for the kernel-Hermes drain pump inside sk_repl_idle().
*
* 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 drain call.
*/
static int g_mama_interpreting; /* zero-initialized (BSS) */
static int g_idle_pump_active; /* zero-initialized (BSS) */
/* FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5): the reentrancy flags
* g_mama_interpreting / g_idle_pump_active that used to live here guarded
* this file's timer-paced drain of Hera's own mail inside sk_repl_idle().
* That drain moved to sk_vm_idle_point() (switch.c), which acts only when
* Hera has no frame at all -- not mid-line, not inside KEY/EXPECT/QUERY,
* not inside a drain -- so the hazard the flags existed for (re-entering
* her interpreter mid-parse) cannot arise there, and the flags are gone. */
/* 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
@@ -1174,23 +1157,6 @@ static void sk_repl_idle(VM *active_vm)
* 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
* (a dispatched line, or recursively from within the pump's own
* vm_interpret). vm_interpret() clobbers the VM's single input
* buffer, so re-entering it here while KEY/EXPECT/QUERY blocks inside
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
if (g_mama_interpreting || g_idle_pump_active) {
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
}
return;
}
g_idle_pump_active = 1;
/* FABRIC-3.8.md Stage I (2026-09-24): the loop that used to drain every
* other live VM's queue here, on Hera's stack (G3), is removed. Since
* Stage E every born VM is a switch participant -- births are capped at
@@ -1200,11 +1166,11 @@ static void sk_repl_idle(VM *active_vm)
* here. Shown dead three ways before removal: by the condition it
* required (a live non-participant), by that construction, and at run
* time (a probe printed nothing through a boot, the Stage G interleave,
* a full 8-worker DoE trial and the WIREBIND relay). What remains is
* Hera's own queue: she is the one VM that is never switched to for her
* mail, since she is the one running here. */
(void)sk_hermes_drain_checkpoint(mama);
g_idle_pump_active = 0;
* a full 8-worker DoE trial and the WIREBIND relay). What remained was
* Hera's own queue -- and FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5)
* moved that too, to sk_vm_idle_point(): as an observer acting on
* events, her mail is acted on the moment she is idle, not at this
* beat's ~1 s cadence. Nothing is drained here now. */
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
@@ -1359,7 +1325,8 @@ int sk_console_getkey(VM *active_vm)
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
sk_repl_idle(live_active);
}
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 -- acts only between lines */
sk_vm_idle_point(0); /* FABRIC-3.8.md §3.13.5 -- acts only between lines; a key
* read is someone mid-interaction: no self-drain (O-B) */
arch_relax();
}
}
@@ -1383,7 +1350,8 @@ void sk_repl_headless_wait(VM *mama)
g_last_beat_tick = now;
sk_repl_idle(mama);
}
sk_vm_idle_point(); /* FABRIC-3.8.md §3.13.5 */
sk_vm_idle_point(1); /* FABRIC-3.8.md §3.13.5; no line is being typed: Hera's mail
* is drained here too (O-B) */
arch_relax();
}
}
@@ -1585,8 +1553,11 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
*/
/* FABRIC-3.8.md §3.13.5 (Stage C-b): Hera waiting for a line is
* the idle point -- the coolest VM with work runs now, not at
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's). */
sk_vm_idle_point();
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's).
* FABRIC-3.9.8 Stage O-B: so is Hera's own mail -- but only at
* n == 0, so a drained payload never prints into a line being
* typed (the gate the old idle-beat drain had). */
sk_vm_idle_point(n == 0);
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
continue;
}
@@ -1735,16 +1706,11 @@ static char g_kh_console_cmd_buf[SK_HERMES_CHUNK_MAX_PAYLOAD];
static void sk_repl_dispatch_line(VM *vm, const char *input)
{
/* H1 reentrancy guard: while this dispatched line executes on Hera
* herself, sk_repl_idle() must defer its MSG-TICK pump -- calling
* vm_interpret(mama, ...) from inside a mid-line KEY/EXPECT would
* re-enter mama's interpreter and clobber its in-flight input buffer
* (see the guard's comment at sk_repl_idle()). A child VM's console
* turn leaves mama idle, so the pump stays safe there and the guard is
* only latched for Hera. */
int on_mama = (vm == (VM *)sk_get_mama_vm());
int saved = g_mama_interpreting;
if (on_mama) g_mama_interpreting = 1;
/* (The H1 reentrancy flag, g_mama_interpreting, is gone -- FABRIC-3.9.8
* Stage O-B. It guarded sk_repl_idle()'s timer-paced drain of Hera's
* mail; that drain moved to sk_vm_idle_point(), whose own
* frames.count != 0 check already refuses while this line, or any
* word or drain inside it, is running on Hera.) */
if (sk_repl_line_calls_use(input)) {
vm_interpret(vm, input);
@@ -1807,7 +1773,7 @@ static void sk_repl_dispatch_line(VM *vm, const char *input)
vm_interpret(vm, input);
out:
g_mama_interpreting = saved;
return;
}
/*===========================================================================
+12 -1
View File
@@ -218,13 +218,24 @@ void sk_vm_switch_checkpoint(VM *vm) {
* frame (nothing in progress), so she becomes eligible again only through
* mail or pending console input, or is returned to by a trampoline with
* nothing left to do. */
void sk_vm_idle_point(void) {
void sk_vm_idle_point(int may_drain_self) {
VM *self = g_switch_current_vm;
VM *next;
if (!self || self != (VM *)sk_get_mama_vm()) return;
if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */
/* FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5): Hera acts on her own
* mail the moment she is idle -- one message, then back to the caller's
* poll loop, which calls again. Was sk_repl_idle()'s drain, paced by the
* ~1 s SK_IDLE_BEAT_INTERVAL. Safe here by the check just above: no
* frame means no line, word or drain of hers is in progress, so the
* drain can't re-enter her interpreter. */
if (may_drain_self && sk_hermes_pending_count(self->stadium_vm_id) > 0) {
(void)sk_hermes_drain_checkpoint(self);
return;
}
sk_vm_switch_reap_pending();
next = sk_vm_switch_decide(self, 0);
if (!next) return;