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:
co-authored by
Claude Opus 5.5
parent
fe55038e9a
commit
ee4c401d1c
+58
-1
@@ -1,6 +1,6 @@
|
|||||||
# FABRIC-3.9.8.md — Observers and participants: execution plan and log
|
# FABRIC-3.9.8.md — Observers and participants: execution plan and log
|
||||||
|
|
||||||
**Status: IN PROGRESS.** Done: O-A, the stack-convention fix, S-1a, S-1b, S-1c, M-0, M-A, the dead-VM departure fix and M-B (§2). The design and
|
**Status: IN PROGRESS.** Done: O-A, the stack-convention fix, S-1a, S-1b, S-1c, M-0, M-A, the dead-VM departure fix, M-B and O-B (§2). The design and
|
||||||
its rulings (O-1 to O-4, the discard, and §7/§8's M- and S- rulings) are in `FABRIC-3.9.0.md`.
|
its rulings (O-1 to O-4, the discard, and §7/§8's M- and S- rulings) are in `FABRIC-3.9.0.md`.
|
||||||
|
|
||||||
Each stage:
|
Each stage:
|
||||||
@@ -681,3 +681,60 @@ Evidence is named **before** each stage runs.
|
|||||||
- The relay ledger shows `held=2048`: every event's heat was released.
|
- The relay ledger shows `held=2048`: every event's heat was released.
|
||||||
- Idle log growth is 0, there are no faults, and QEMU exits cleanly. Clean builds on all
|
- Idle log growth is 0, there are no faults, and QEMU exits cleanly. Clean builds on all
|
||||||
three ISAs plus the hosted build.
|
three ISAs plus the hosted build.
|
||||||
|
- [x] **Stage O-B — Hera acts on her own mail the moment she is idle (`FABRIC-3.9.0.md` §2.5).**
|
||||||
|
2026-09-24. Captain Bob: *"go ahead with O-B."*
|
||||||
|
|
||||||
|
**The change:**
|
||||||
|
- **`sk_vm_idle_point(int may_drain_self)`** (`switch.c`): when Hera is running with no frame
|
||||||
|
live and has mail, it drains one message, then returns to the caller's poll loop, which calls
|
||||||
|
it again.
|
||||||
|
- The existing `frames.count != 0` check already refuses whenever a line, word, `KEY`/
|
||||||
|
`EXPECT`/`QUERY` or drain of hers is in progress. So the drain can't re-enter her
|
||||||
|
interpreter mid-parse, which is the hazard the old flags guarded.
|
||||||
|
- **The callers** (`repl.c`):
|
||||||
|
- readline passes **`n == 0`**, keeping the mid-edit gate: a drained payload never prints
|
||||||
|
into a line being typed;
|
||||||
|
- the headless wait passes 1;
|
||||||
|
- the blocking key read passes 0, since a key read is someone mid-interaction, and it is
|
||||||
|
always inside a word anyway.
|
||||||
|
- **Removed:** `sk_repl_idle()`'s timer-paced drain of Hera's queue (one message per
|
||||||
|
`SK_IDLE_BEAT_INTERVAL` = 100 beats, about 1 s), and with it the two flags that guarded it
|
||||||
|
(`g_mama_interpreting`, `g_idle_pump_active`).
|
||||||
|
- Nothing else read them. The dispatch's set/restore of `g_mama_interpreting` is gone too.
|
||||||
|
- `sk_repl_idle()` keeps its USB and block-sync servicing, unchanged.
|
||||||
|
|
||||||
|
**Evidence.** A probe (reverted) stamped each message to Hera with the beat count at
|
||||||
|
admission, and printed the gap when she drained it. On the same boots, run from the console:
|
||||||
|
- `wa` defines `PINGH`, which sends `72 EMIT` to Hera. Both strings are built in `wa`'s own
|
||||||
|
`PAD`, because an `S"` can't nest inside the `S"` that carries the job.
|
||||||
|
- Jobs `: PZW 30000 0 DO LOOP PINGH ; PZW` (idle case) and `3000000` iterations (mid-edit
|
||||||
|
case).
|
||||||
|
|
||||||
|
Logs: `logs/20260924-213049/amd64/`, `-214100/aarch64/`, `-214803/riscv64/`.
|
||||||
|
|
||||||
|
| Case | amd64 | aarch64 | riscv64 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Idle at the prompt**, 3 runs: admission → drain | **0, 0, 0 beats** | 0, 1, 0 | 1, 0, 0 |
|
||||||
|
| **Mid-edit:** the message arrives while `1 2` is typed but not entered; ` + .` Enter about 40 s later | drained only after the line ran (`3 ok`, then `H`), **3587 beats** | drained at the entered line's first word boundary (`H3 ok`), 2080 beats | same as aarch64, 2613 beats |
|
||||||
|
| **Busy:** `EV-BORN` queued while Hera runs `WORKER-BIRTH` | 14 beats, the moment the line ended | 1 | 0 |
|
||||||
|
| **`BLK-ATTACH`** (type 9) at boot, from Artemis | 1 beat | 1 | 1 |
|
||||||
|
|
||||||
|
- Before this stage, a message to an idle Hera waited for the next idle beat: up to 100
|
||||||
|
beats. Now it is 0–1.
|
||||||
|
- The gate held on every ISA: nothing drained while the line was being edited.
|
||||||
|
- Once Enter is pressed the message goes at the first clean boundary. That is either Hera's
|
||||||
|
existing in-word drain checkpoint (FABRIC-3.8 Stage B) at the entered line's first word
|
||||||
|
boundary, or the idle point after the line. Which comes first is timing, so the order of `H`
|
||||||
|
and `3` differs by ISA. Either way it is a line boundary, never inside typed text.
|
||||||
|
- The amd64 log also holds a first pass of the script whose job texts used a bare
|
||||||
|
`DO … LOOP` in interpret mode (`DO: compile-only`). That was a script error, corrected in the
|
||||||
|
same boot, and all the figures above come from the corrected pass.
|
||||||
|
|
||||||
|
**Acceptance** on the clean tree (`logs/20260924-215452/amd64/`, `-220741/aarch64/`,
|
||||||
|
`-222049/riscv64/`):
|
||||||
|
- Boot parity and the `dict_hash` set are unchanged from M-B (546 words,
|
||||||
|
`0x99b01134c3d3049c`).
|
||||||
|
- The O-A checks pass (40/40/48, `HHH`, `3` mid-stream).
|
||||||
|
- The relay and `BLK-ATTACH` flows are unchanged: mint → WIREBIND → `USE` → `3`, with the
|
||||||
|
ledger at the standard `held=2048 pulled=258048 returned=255263 consumed=737`.
|
||||||
|
- Idle log growth is 0, there are no faults, and QEMU exits cleanly.
|
||||||
|
|||||||
@@ -65,8 +65,14 @@ void sk_vm_switch_checkpoint(struct VM *vm);
|
|||||||
/* FABRIC-3.8.md §3.13.5 (Stage C-b, ruling C-5): the idle point. Called
|
/* FABRIC-3.8.md §3.13.5 (Stage C-b, ruling C-5): the idle point. Called
|
||||||
* from the console's input poll loops (repl.c) each pass with no input.
|
* from the console's input poll loops (repl.c) each pass with no input.
|
||||||
* Acts only when Hera is running with no frame live (between lines); then
|
* Acts only when Hera is running with no frame live (between lines); then
|
||||||
* switches to the coolest VM with work, if any. */
|
* switches to the coolest VM with work, if any.
|
||||||
void sk_vm_idle_point(void);
|
*
|
||||||
|
* FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5): Hera's own mail is drained
|
||||||
|
* here first, one message per call, instead of on the timer-paced idle beat
|
||||||
|
* -- she is an observer that acts on events, promptly. may_drain_self is
|
||||||
|
* the caller's gate: readline passes (n == 0), so a drained payload never
|
||||||
|
* prints into a line being typed. */
|
||||||
|
void sk_vm_idle_point(int may_drain_self);
|
||||||
|
|
||||||
/* FABRIC-3.8.md §3.5 (Stage D): 1 if `vm` is parked at its own trampoline
|
/* FABRIC-3.8.md §3.5 (Stage D): 1 if `vm` is parked at its own trampoline
|
||||||
* with nothing in progress -- its frame stack holds exactly one frame, its
|
* with nothing in progress -- its frame stack holds exactly one frame, its
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+27
-61
@@ -832,30 +832,13 @@ static void sk_cursor_show(void)
|
|||||||
console_fb_draw_cursor();
|
console_fb_draw_cursor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reentrancy guards for the kernel-Hermes drain pump inside sk_repl_idle().
|
/* 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
|
||||||
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
|
* this file's timer-paced drain of Hera's own mail inside sk_repl_idle().
|
||||||
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
|
* That drain moved to sk_vm_idle_point() (switch.c), which acts only when
|
||||||
* Phase C, common:messaging.4th, now removed). The mechanism changed --
|
* Hera has no frame at all -- not mid-line, not inside KEY/EXPECT/QUERY,
|
||||||
* sk_repl_idle() now calls sk_hermes_drain_checkpoint() directly per VM,
|
* not inside a drain -- so the hazard the flags existed for (re-entering
|
||||||
* not VM-EXEC -- but the reentrancy hazard these two flags exist for is
|
* her interpreter mid-parse) cannot arise there, and the flags are gone. */
|
||||||
* 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.md SXXII (2026-09-12) recorded a real O(N) bottleneck here --
|
/* 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
|
* 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_
|
* 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)
|
* BATCH below), kept for the same reason: bounding this to O(K)
|
||||||
* regardless of total VM count, not O(N) every beat. */
|
* 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
|
/* 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
|
* 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
|
* 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
|
* here. Shown dead three ways before removal: by the condition it
|
||||||
* required (a live non-participant), by that construction, and at run
|
* required (a live non-participant), by that construction, and at run
|
||||||
* time (a probe printed nothing through a boot, the Stage G interleave,
|
* 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
|
* a full 8-worker DoE trial and the WIREBIND relay). What remained was
|
||||||
* Hera's own queue: she is the one VM that is never switched to for her
|
* Hera's own queue -- and FABRIC-3.9.8 Stage O-B (FABRIC-3.9.0.md §2.5)
|
||||||
* mail, since she is the one running here. */
|
* moved that too, to sk_vm_idle_point(): as an observer acting on
|
||||||
(void)sk_hermes_drain_checkpoint(mama);
|
* events, her mail is acted on the moment she is idle, not at this
|
||||||
g_idle_pump_active = 0;
|
* beat's ~1 s cadence. Nothing is drained here now. */
|
||||||
|
|
||||||
if (console_tx_count() == tx_before_idle) {
|
if (console_tx_count() == tx_before_idle) {
|
||||||
console_cancel_deferred_line_start();
|
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();
|
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
|
||||||
sk_repl_idle(live_active);
|
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();
|
arch_relax();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1383,7 +1350,8 @@ void sk_repl_headless_wait(VM *mama)
|
|||||||
g_last_beat_tick = now;
|
g_last_beat_tick = now;
|
||||||
sk_repl_idle(mama);
|
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();
|
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
|
/* 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 idle point -- the coolest VM with work runs now, not at
|
||||||
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's). */
|
* the next SK_IDLE_BEAT_INTERVAL (that cadence is the timer's).
|
||||||
sk_vm_idle_point();
|
* 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 */
|
arch_relax(); /* PAUSE — reduce power, maintain tight poll */
|
||||||
continue;
|
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)
|
static void sk_repl_dispatch_line(VM *vm, const char *input)
|
||||||
{
|
{
|
||||||
/* H1 reentrancy guard: while this dispatched line executes on Hera
|
/* (The H1 reentrancy flag, g_mama_interpreting, is gone -- FABRIC-3.9.8
|
||||||
* herself, sk_repl_idle() must defer its MSG-TICK pump -- calling
|
* Stage O-B. It guarded sk_repl_idle()'s timer-paced drain of Hera's
|
||||||
* vm_interpret(mama, ...) from inside a mid-line KEY/EXPECT would
|
* mail; that drain moved to sk_vm_idle_point(), whose own
|
||||||
* re-enter mama's interpreter and clobber its in-flight input buffer
|
* frames.count != 0 check already refuses while this line, or any
|
||||||
* (see the guard's comment at sk_repl_idle()). A child VM's console
|
* word or drain inside it, is running on Hera.) */
|
||||||
* 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;
|
|
||||||
|
|
||||||
if (sk_repl_line_calls_use(input)) {
|
if (sk_repl_line_calls_use(input)) {
|
||||||
vm_interpret(vm, input);
|
vm_interpret(vm, input);
|
||||||
@@ -1807,7 +1773,7 @@ static void sk_repl_dispatch_line(VM *vm, const char *input)
|
|||||||
vm_interpret(vm, input);
|
vm_interpret(vm, input);
|
||||||
|
|
||||||
out:
|
out:
|
||||||
g_mama_interpreting = saved;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*===========================================================================
|
/*===========================================================================
|
||||||
|
|||||||
@@ -218,13 +218,24 @@ void sk_vm_switch_checkpoint(VM *vm) {
|
|||||||
* frame (nothing in progress), so she becomes eligible again only through
|
* frame (nothing in progress), so she becomes eligible again only through
|
||||||
* mail or pending console input, or is returned to by a trampoline with
|
* mail or pending console input, or is returned to by a trampoline with
|
||||||
* nothing left to do. */
|
* 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 *self = g_switch_current_vm;
|
||||||
VM *next;
|
VM *next;
|
||||||
|
|
||||||
if (!self || self != (VM *)sk_get_mama_vm()) return;
|
if (!self || self != (VM *)sk_get_mama_vm()) return;
|
||||||
if (self->frames.count != 0) return; /* inside a word (KEY/EXPECT): not idle */
|
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();
|
sk_vm_switch_reap_pending();
|
||||||
next = sk_vm_switch_decide(self, 0);
|
next = sk_vm_switch_decide(self, 0);
|
||||||
if (!next) return;
|
if (!next) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user