# FABRIC-3.9.8.md — Observers and participants: execution plan and log **Status: PLANNED. No stage started.** The design and its open rulings (O-1 to O-4, plus the discard question) are in `FABRIC-3.9.0.md`. Each stage: - needs Captain Bob's explicit go; - gets its own commit; - gets its own three-architecture acceptance (`clean qemu`; amd64, then aarch64, then riscv64; foreground; visible framebuffer; one at a time; coupled images reset before each). Evidence is named **before** each stage runs. **The standing acceptance sequence for every stage**, unchanged from `FABRIC-3.8.md`: - a boot to `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, faults and frame corruption; - the `dict_hash` set identical across ISAs; - the Stage G interleave (two workers, `A`/`B`), byte-identical across ISAs; - mint → WIREBIND → `USE` → `1 2 + .` → `3`; - 0 bytes of idle log growth over 20 s. ## 1. Staged plan | Stage | Change | Evidence (named in advance) | |---|---|---| | **O-A** | Observer role (`FABRIC-3.9.0.md` §2.1). The absolute-heat-share charge is discarded (ruling 5). Observers are left out of the decision's candidates, the work-heat charge and the floor, and (O-2) the absolute charge. Observers with events go ahead at clean boundaries, among themselves by wait stamp (§2.4, per O-1). | *(Corrected before starting.)* The standard sequence, with the G interleave unchanged. **Console responsive under load:** `1 2 + .` typed while `wa`/`wb` run prints `3` in the middle of the A/B stream. **Observer precedence:** a message to Hestia sent in the same line as the two worker jobs runs before any `A`/`B`. **Not run: the busy-wait DoE driver.** Under O-1, Hera running `DOE` is an observer acting on an event, and nothing displaces her, so `MU-WAIT-ALL` would wait forever. The originally named evidence ("Hera's share falls on the unchanged busy-wait driver") contradicts the design. `DOE` stays unrun until O-C replaces the driver. | | **M-0** *(first; a defect against the standing no-fixed-bounds rule)* | Kernel-Hermes arena, per-VM queues and channel member lists grow on demand; `Q_SLOT` decoupled from arena size (`FABRIC-3.9.0.md` §7.0). | The standard sequence. The existing kernel-Hermes boot self-tests (allocator, publish, drain, chunk, negotiation) still PASS, with `expected_n` now set by the reservoir, not the arena. A probe (reverted) admits **more than 32** messages system-wide, spread over senders with reservoir to pay, and more than 32 to one queue, with conservation and the ledger holding throughout. A channel takes more than 64 members. Nothing moves or corrupts across a grow: messages queued before a grow drain intact after it. | | **M-A** *(ahead of O-B, per Captain Bob: "close the gap first")* | Manual messaging words over the existing C API (`FABRIC-3.9.0.md` §7.2, names per M-1). | The standard sequence. At the console: `CHANNEL`, then `JOIN-VM` of `wa` and `wb`, then `PUBLISH` of a marker payload delivers to both (`n = 2`, both print). `LEAVE-VM` then `PUBLISH` delivers to one. `CHANNEL-CLOSE` succeeds, and refuses channel 0. `INJECT` lands in a queue that `MAIL?`/`.MAIL` show before the recipient runs. `.CHANNELS` lists what was made. The `dict_hash` set changes by the new words and is identical across ISAs. | | **M-B** | Autonomous events (§7.3): event channels created at boot; `EV-DONE`/`EV-BORN`/`EV-DIED` fired from the existing kernel points, for participants, funded per M-5, dropped and counted when full (M-6). | The standard sequence. A VM that `JOIN`s `EV-BORN`/`EV-DONE`/`EV-DIED` and defines the `ON-…` words sees one event for each `WORKER-BIRTH`, each job finished, and each `KILL`, with **no `SEND` anywhere in the job text**. A VM that didn't join sees nothing. The dropped-event counter reads 0 in normal use; a probe that floods the arena shows events dropped and counted, never blocking. | | **O-B** | Hera's own mail drained at `sk_vm_idle_point()`, with the readline `n > 0` gate; the timer-paced pump drain removed (§2.5). | The standard sequence, plus: a message sent to Hera while she sits idle at the prompt is acted on **without waiting for the idle beat**. A probe records the delay in beats from admission to drain; it must be well under `SK_IDLE_BEAT_INTERVAL`. The relay and BLK-ATTACH flows are unchanged. | | **O-C** | An event-driven DoE driver (§3, as revised by §7.4 once M-B lands): `DOE` starts trial 1 and returns to the prompt; Hera's `ON-DONE` (fed by `EV-DONE`) counts, ends trials and starts the next; `MU-WAIT-ALL` and the job text's own completion `SEND` are deleted. Capsule only. | A 1-rep smoke campaign on amd64 runs all 6 cells to `MU-CAMPAIGN: complete`, with every trial `fail=0` and N/N `KILL … dead`. Hera's rows during trials are only event handling. The prompt answers typed input while trials run. Then the standard sequence on all three ISAs. | | **O-D** | The DoE relaunch: `12345 3 S" " DOE`, amd64, then aarch64, then riscv64, with the ring extracted between ISAs and `artemis.img` **not** reset in between. | A fresh time estimate from O-C's measured row rate, **before** launch. Then per ISA: 18 `MU-TRIAL` markers, `fail=0`, `MU-CAMPAIGN: complete`. | | **Merge** | This branch to `master`, once all three ISAs have completed O-D. | Per this project's own merge practice, at Captain Bob's word. | **Stop conditions** (re-plan rather than push on): - `dict_hash` diverges across ISAs; - conservation (`vm_physics_conserved()`, `stadium_conserved()`) goes false; - an observer is ever returned as a heat candidate; - a switch storm: log growth at an idle prompt, with QEMU pinned; - a trial that never produces its marker. ## 2. Execution log - [x] **Stage O-A — the observer role (`FABRIC-3.9.0.md` §2).** 2026-09-24. Captain Bob: *"all as proposed, go ahead with O-A"* (O-1 to O-4 and the discard, all as proposed). - **Discarded:** the uncommitted absolute-heat-share charge (`git checkout` of `capsule_vm_physics.c/.h`), per ruling 5. - **`capsule_vm_switch_signal.c/.h`:** - Each slot carries an `observer` flag, with `sk_vm_switch_signal_set_observer()` and `is_observer()`. - `sk_vm_switch_decide()` now works in tiers: 1. An observer that is itself in progress (a live non-IDLE frame) or holding an event keeps the CPU. 2. Otherwise, an observer with an event, meaning mail (or console input, for Hera), goes ahead of every participant. Among observers, the oldest wait stamp wins. 3. Otherwise, participants are compared by work heat exactly as in FABRIC-3.8. Observers are never heat candidates. - `note_work_gained()` never floors an observer and leaves observers out of the floor set. The running VM counts only if it is a participant. - **`capsule_birth.c`:** `is_fleet_foundation` is hoisted to function scope. Registration marks the Tripod as observers from that same birth-time fact. - **`kernel_main.c`:** Hera is marked an observer where she registers. - **`vm_runtime.c`:** observers are not charged at all (O-2). - Nothing a VM executes can set or clear the role. **Evidence, as corrected before starting (§1).** Ordinary words only, typed at `[zuse@Hera] ok>`, byte-identical on all three ISAs (`logs/20260924-084904/amd64/`, `-090107/aarch64/`, `-091424/riscv64/`): - **G interleave unchanged:** A = 40, B = 40, 48 runs. - **Observer precedence:** `S" LA" S" wa" SEND . S" LB" S" wb" SEND . S" 72 EMIT 72 EMIT 72 EMIT" S" Hestia" SEND .` gives `HHHABBAABBAA…`. Hestia's mail ran before any participant output once Hera went idle. - **Console responsive under load:** with `wa`/`wb` on 200 markers × 20,000-iteration loops, `1 2 + .` typed mid-run appears as `…ABABAB1 2 + .` / `3 ok` / `ok> ABABBA…`. Hera, an observer with console input, got the CPU at the next worker boundary, ran the line, and the workers resumed. This is the C-4 (α) starvation recorded at C-b, fixed. *(The first amd64 attempt at this check used loops that finished before the line was typed, so it proved nothing. It was re-run with the heavy loops, and the script was fixed for the other two ISAs.)* **Acceptance** (same boots): - All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, faults and frame corruption. - The `dict_hash` set is unchanged from Stage I (word_count 531, M7.1a `0x572daac83b797332`), since no words were added or removed. - Mint → WIREBIND → `USE` → `1 2 + .` → `3`. - Idle log growth: 0 bytes. - Clean build on all three plus hosted, no new warnings. **Not run, by design:** the busy-wait DoE driver, which would deadlock under O-1 (§1). Don't run `DOE` until O-C. - [x] **Stack-convention fix: out-of-bounds stack writes in four vendored word files** (found during S-1a, 2026-09-24). Captain Bob: *"Fix now, own commit."* **The defect.** The VM's `dsp` is the index of the top item (−1 = empty): `vm_pop` reads `data_stack[dsp--]`. Four vendored files instead treated `dsp` as a count, pushing with `data_stack[vm->dsp++] = x` (11 sites) and popping with `data_stack[--vm->dsp]` (17 sites): - `physics_freeze_words.c`: 7 pushes, 12 pops; - `dictionary_heat_diagnostic_words.c`: 4 pushes, 2 pops; - `physics_diagnostic_words.c`: 1 pop; - `physics_benchmark_words.c`: 1 pop. **The consequences:** - On an empty stack a push wrote `data_stack[-1]`, **8 bytes before the VM struct**. POST's own `DECAY-RATE@.basic` test does exactly that on every boot. - Otherwise a push overwrote the top item and left `dsp` one slot off. - The pops read the item *below* the top, and on the last pop, `data_stack[-1]`. - Four one-item words also guarded with `dsp < 1`, which demands two items, so `LOOKUP-STRATEGY!`, `COMPARE-LOOKUPS`, `PHYSICS-BURN` and `BENCH-DICT-LOOKUP` always errored on valid input. **How it was found.** The S-1a parity probe's incomplete counter read one too many on aarch64 only. - A per-test hook in the test runner named `DECAY-RATE@.basic` as the test after which the counter moved (`logs/20260924-104424/aarch64/`). - Writing a marker into `mama->data_stack[-1]` changed the counter to the marker (`-105120`). - Printing runtime addresses (`-105729`) showed why aarch64 only: the aarch64 kernel runs inside the `lld-link` PE, whose `.bss` puts `parity.c`'s counter directly before `sk_mama_vm`. - On amd64 and riscv64 the same write lands in the last cell of the POST block-RAM buffer, silently. - An interim hypothesis of memory aliasing, drawn from ELF symbol addresses that don't describe the aarch64 runtime image, was wrong and is withdrawn. **The fix:** - Every site now uses the VM's own `vm_pop(vm)` / `vm_push(vm, x)`, which check bounds. - The four one-item guards are now `dsp < 0`. - The two- and three-item guards (`dsp < 1`, `dsp < 2`) were already correct for the top-index convention and are unchanged. **Evidence.** A probe (reverted) compared the 8 bytes before the VM struct across POST, plus console checks, on all three ISAs (`logs/20260924-111124/amd64/`, `-112508/aarch64/`, `-113949/riscv64/`): - `STACKFIX PROBE: … UNCHANGED across POST`; - `DECAY-RATE@ DEPTH . .` gives `1 1`; - `S" DUP" FROZEN? DEPTH . .` gives `1 0`; - `HEAT@` leaves 1 item, and `HEAT-PERCENTILES` leaves 3; - `1 LOOKUP-STRATEGY! LOOKUP-STRATEGY@ .` gives `1`, depth 0, where it used to error; and `0 …` gives `0`. Acceptance in the same boots: - the O-A checks: G interleave A = 40/B = 40/48 runs, `HHH` precedence, and the typed line running mid-stream; - relay `3`, idle growth 0; - `dict_hash` unchanged (531, `0x572daac83b797332`); - POST passes; - clean kernel and hosted builds.