diff --git a/FABRIC-3.5.md b/FABRIC-3.5.md index e50a1b1e..389e448b 100644 --- a/FABRIC-3.5.md +++ b/FABRIC-3.5.md @@ -4515,3 +4515,108 @@ left implicit. - ⬜ **B1/B2/B3 (items 27, 32, §XXXII.5.1)** — the Phase 3 blockers, tracked in both documents. **§XXXII.5.1, the delivery hand-off, is the last genuine design question in the reshuffle.** - Design work continues **here**; execution is tracked **there**. + +--- + +## XLIII. B3 SETTLED: the target drains its own queue, at its own outermost checkpoint + +The last genuine design question in the reshuffle (§XXXII.5.1, Phase 3 blocker B3). **Settled, +and once again the mechanism already exists — built for the structurally identical problem and +corrected twice in the field.** + +### XLIII.1 — What the hand-off has to satisfy + +Delivery must cause the payload to **execute inside the target** — `messaging.4th` block 5041: +"Payload is FORTH text, executed by the receiver, same as every other message here." But +§XXXII retired the pump, so **kernel-Hermes may not `VM-EXEC` into anyone.** The payload must +therefore wait somewhere the target consumes under its own power, at a moment when doing so is +safe. + +"Safe" is the hard part: interpreting a payload while the target is mid-word, mid-unwind, or +running nested inside someone else's dispatch is the reentrancy class this codebase has been +bitten by repeatedly (§XX, §XXVIII.2). + +### XLIII.2 — The safe-boundary machinery is already there + +Traced 2026-09-19 in `vm_core.c`: + +- **`sk_vm_at_outermost_interpret()`** — `return g_vm_interpret_depth <= 1;` (`:1164`), with + `g_vm_interpret_depth` incremented and decremented by `vm_interpret()` itself (`:1173`, + `:1206`). +- **A per-word checkpoint** in `execute_colon_word()` (`:946`), gated on that predicate. +- **A defer-don't-lose discipline**, in the checkpoint's own words (`:925-935`): if a word is + "executing nested inside a `VM-EXEC`/`VM-CALL` dispatch… `vm` here **does not own the + physical stack it's currently running on**," so acting would corrupt the enclosing VM's call + chain. It is therefore **"left pending… so the next checkpoint at the outermost frame picks + it up instead of losing it."** +- And it runs **before** the error/abort/exit-colon checks, "deliberately, so a switch never + happens mid-unwind" — acting only when the word left the VM "in a clean, resumable state." + +**That is the exact predicate, the exact placement, and the exact deferral semantics delivery +needs.** It exists because the switcher needed to answer the same question — *when is it safe to +act on this VM?* — and it has already been corrected twice in the field (2026-09-13 ×2, +2026-09-14). + +### XLIII.3 — RULING + +> **Kernel-Hermes enqueues the payload onto the target's own pending queue and publishes the +> fact. It dispatches nothing. The target drains its own queue, in its own context, at its own +> outermost interpret checkpoint — the same checkpoint, the same `sk_vm_at_outermost_interpret()` +> gate, and the same defer-if-nested discipline the switcher already uses.** + +**One published fact, two independent consumers** — which is precisely the shape §XV.3 and +§XXXII established: + +| Consumer | Uses the fact for | +|---|---| +| **The switcher** | eligibility — whether to resume this VM (§XXXII) | +| **The target VM itself** | drain — whether to interpret a pending payload at its next safe boundary | + +**Neither dispatches into anyone.** Kernel-Hermes publishes; the switcher moves control; the VM +does its own work. Three roles, no overlap, no second owner of anything. + +### XLIII.4 — This is §XX's proven-safe pattern, generalized to the whole fleet + +`FABRIC-3.md` §XX already established the safe shape, for Hera specifically. With the pump +skipping her, she drains her own queue via "a plain word dispatch **in her own dictionary**, not +a `VM-EXEC` dispatch into anyone else's input buffer — **no reentrancy risk.**" + +**The pump's defect was never draining; it was draining *from outside*.** Hera was special-cased +into safety. Retire the pump (§XXXII) and **every VM does what Hera already does** — the special +case disappears by becoming universal. That is a relocation of a proven pattern, not an +invention, and it keeps this document's scope discipline intact at the last design question. + +### XLIII.5 — Recursive drain is prevented for free + +Interpreting a payload calls `vm_interpret()`, which **increments `g_vm_interpret_depth`** — so +any checkpoint reached *during* the drain sees `depth > 1` and will not drain again. **The same +counter that defines the safe boundary also makes the drain non-reentrant**, with no flag, no +lock and no new state. + +### XLIII.6 — Three constraints, named rather than discovered later + +1. **`INPUT_BUFFER_SIZE` is 1025** — 1024 content bytes plus NUL, and `.claude/CLAUDE.md` calls + this non-negotiable because `vm_interpret()` is the shared dispatch path for both REPL lines + and `LOAD`ed block content. **A payload above 1024 bytes cannot be interpreted in one + drain.** Today's `MSG-PADDR`/`MSG-PLEN` are out-of-line and carry no such bound. **Kernel- + Hermes needs either a payload bound at send time or a chunking story — decide at build time, + do not discover it at Stage C.** +2. **Starvation is possible and bounded.** A VM in a tight loop never returns to outermost + depth, so never drains. The switcher can still preempt it, but preemption does not create a + boundary — the VM must still reach one. **This is the switcher's existing risk, not a new + one**, and it should be recorded as shared rather than solved twice. +3. **Drain one message per checkpoint, not all.** Draining the whole queue at one boundary runs + unbounded work in a single checkpoint — the §XXII wall pattern in miniature. One per + checkpoint is bounded and fair. **Recommended, not ruled.** + +### XLIII.7 — Punch list + +- ✅ **B3 — SETTLED** (§XLIII): the target drains its own queue at its own outermost checkpoint; + kernel-Hermes publishes and never dispatches. +- ⬜ **Item 44, NEW** — payload bound or chunking, against `INPUT_BUFFER_SIZE` 1025 + (§XLIII.6.1). **Decide before Phase 3.** +- ⬜ **B1 (item 27)** and **B2 (item 32)** remain — both are rulings, not designs. + +**Phase 3's design blocker is cleared. What remains before it is buildable are two decisions, +neither of which requires further investigation:** channels (one membership or negotiation), and +the switch-slot ceiling. **The reshuffle has no undesigned mechanism left.** diff --git a/FABRIC-3.6.md b/FABRIC-3.6.md index f7e3acce..23239bd0 100644 --- a/FABRIC-3.6.md +++ b/FABRIC-3.6.md @@ -135,8 +135,11 @@ one message type owned by exactly one layer, no message shared. recommends the latter; unruled.) - [ ] **B2** — **Item 32**: `SK_SWITCH_MAX_SLOTS` is 16 and §XXXII made the switcher the sole mover of control. Constant bump or table redesign? -- [ ] **B3** — **§XXXII.5.1**: the delivery hand-off is undesigned. **The last genuine design - question, and on the critical path.** +- [x] **B3** — **CLEARED 2026-09-19 by `FABRIC-3.5.md` §XLIII**: the target drains its own + queue at its own outermost interpret checkpoint; kernel-Hermes publishes and never + dispatches. Reuses `sk_vm_at_outermost_interpret()` and the Stage 3 checkpoint. +- [ ] **B4** — **Item 44**: payload bound or chunking against `INPUT_BUFFER_SIZE` 1025 + (§XLIII.6.1). Decide before Phase 3. ## Phase 4 — Category B strip