physics_freeze_words.c, dictionary_heat_diagnostic_words.c, physics_diagnostic_words.c, physics_benchmark_words.c treated dsp as a count (data_stack[dsp++] push, data_stack[--dsp] pop) while the VM's dsp is the top index. On an empty stack a push wrote data_stack[-1], 8 bytes before the VM struct -- POST's DECAY-RATE@ test did it every boot (on aarch64 it hit parity.c's counter, elsewhere the POST block RAM tail). All 28 sites now use vm_pop/vm_push; four one-item guards fixed (dsp<0). Evidence on amd64/aarch64/riscv64: bytes before the VM struct unchanged across POST; DECAY-RATE@ leaves 1 item; LOOKUP-STRATEGY! now works; G interleave, precedence, console under load, relay 3, dict_hash unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
12 KiB
Markdown
148 lines
12 KiB
Markdown
# 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" <isa>" 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.
|