diff --git a/FABRIC-3.9.0.md b/FABRIC-3.9.0.md index b60ff897..b553019b 100644 --- a/FABRIC-3.9.0.md +++ b/FABRIC-3.9.0.md @@ -192,3 +192,286 @@ queue, all of that should be manual, but also able to operate autonomously."* **Relation to the O stages:** independent of O-B to O-D. The event-driven DoE driver (O-C) is one concrete case: a worker's completion notice is today a `SEND` in the job text, and could become an OS event ("worker parked idle after its job") if autonomous flow lands first. + +**Captain Bob, 2026-09-24:** *"Let's go and close the gap first."* The messaging gap goes ahead of +O-B to O-D. The design follows, **for review; nothing here authorizes an edit.** + +### 7.0 First: kernel-Hermes gets no fixed small-scale bounds (defect, M-0) + +**The rule already exists.** +- `FABRIC-3.5.md` §XLV.2 (B2, 2026-09-21) ruled the switch table dynamic, *"not to be fixed and + static"*. §XLVI 3.0(c) sized it from RAM at boot, like Stadium's `stadium_max_vm_count_val`. + `capsule_vm_switch_signal.c` cites the same principle as `feedback_no_hardcoded_small_scale_ + bounds`. +- **Kernel-Hermes was never brought under it.** Four fixed bounds remain (traced): + +| Bound | Where | Today | +|---|---|---| +| Message arena | `sk_hermes_msgs[SK_HERMES_MSG_MAX]`, a static array | 32 messages, system-wide | +| Per-VM pending queue | `SkHermesPendingQueue.slots[SK_HERMES_PENDING_MAX]` | 32 per VM | +| Channel membership | `SkHermesMembership.members[SK_HERMES_MEMBER_MAX]` | 64 per channel | +| Per-message heat | `SK_HERMES_Q_SLOT = Q48_ONE / SK_HERMES_MSG_MAX` | 1/32 of a reservoir. **Tied to the arena size.** `FABRIC-3.6.md` task 2.2 already noted: *"worth remembering if `SK_HERMES_MSG_MAX` is ever resized independently of `Q_SLOT`'s divisor."* | + +The reassembly scratch array (`present[SK_HERMES_MSG_MAX]`, `sk_hermes_reassemble()`) sizes +from the same constant. + +**Proposed:** +- **The arena, the queues and the member lists grow on demand** from the kernel heap (kmalloc, + grow by doubling, never shrink). This is the same allocate-copy-free pattern `vm_frame.c` + already uses, so there's no boot-time cap to choose, and the only limit is RAM, reported when + hit. *(Alternative: size them once at boot from RAM, like the switch table. Simpler, but still + a ceiling.)* +- **Message identity moves off the array index.** Growing the arena moves messages, so a queue + holds message ids resolved through the arena, never pointers. Today `idx = msg - + sk_hermes_msgs` already makes it an index; it stays an index into a table that can grow. +- **Per-message heat is decoupled from arena size** (ruling M-0b). How many messages a VM can + have in flight is a **heat** question: its reservoir, `Q_SLOT` per message. Proposed: `Q_SLOT` + becomes its own named constant with its current value (1/32 of a reservoir), **no longer + derived from the arena**. The heat budget stays a physics limit per VM, while capacity stops + being a memory limit. The alternative is to derive `Q_SLOT` from something else, which is Bob's + call. + +`M-0` lands **before** the manual and autonomous work (M-A/M-B), since both add traffic. + +### 7.1 Constraints the design has to live with (traced) + +- **One arena of 32 messages system-wide** (`SK_HERMES_MSG_MAX`). Each queue is capped at the same + 32 (`SK_HERMES_PENDING_MAX`), and each channel at 64 members (`SK_HERMES_MEMBER_MAX`). Autonomous + events share this arena with every `SEND`. **Captain Bob, 2026-09-24: "The arena should be + dynamic. We talked about that earlier and it wasn't done."** This is a defect against a standing + rule, not a design option; see §7.0. +- **Every message is funded from its sender's Stadium reservoir.** `sk_hermes_alloc()` pulls + `SK_HERMES_Q_SLOT` = `Q48_ONE`/32 and refuses if the sender can't pay. So an OS-originated + message still needs an identity to be paid from, and conservation must still hold. +- **A payload is FORTH text the recipient interprets** on its own stack. An event arrives as text, + so the recipient must have defined whatever words that text calls. Otherwise it gets + `UNKNOWN WORD`. +- **Channel 0 (common) is permanent.** Every born VM joins it (task 3.2). Channel slots number + `stadium_max_vm_count()`. + +### 7.2 Manual control: FORTH words over the existing C API (proposed) + +These are registered for every VM. No C-side restriction: who may use which word is `ACL.4th`'s +job. Names are placeholders for ruling M-1. + +| Word | Stack | Does | +|---|---|---| +| `CHANNEL` | `( -- ch \| -1 )` | `sk_hermes_channel_create()` | +| `CHANNEL-CLOSE` | `( ch -- ok? )` | `sk_hermes_channel_destroy()`; refuses channel 0 | +| `JOIN` / `LEAVE` | `( ch -- ok? )` | subscribe or unsubscribe the **calling** VM | +| `JOIN-VM` / `LEAVE-VM` | `( ch name-c name-u -- ok? )` | subscribe or unsubscribe a **named** VM (administration) | +| `PUBLISH` | `( paddr plen ch -- n )` | send a copy to every member; returns how many were delivered | +| `SEND` | `( paddr plen name-c name-u -- ok? )` | exists (Stage G) | +| `INJECT` | `( paddr plen name-c name-u -- ok? )` | put a message in a VM's queue **as if from the kernel**: sender recorded as the root (Hera) and funded from her reservoir. For administration and testing. | +| `MAIL?` | `( name-c name-u -- n )` | a VM's pending count | +| `.MAIL` | `( name-c name-u -- )` | list a VM's pending messages: type, sender, length, first bytes | +| `.CHANNELS` | `( -- )` | list channels in use, with their members | + +`from` is always derived in C, never taken from the stack, except that `INJECT` deliberately +records the root. That is exactly why it's ACL-gated. + +### 7.3 Autonomous flow: the OS publishes what it already knows (proposed) + +The kernel already sees the moments worth telling someone about. The proposal is that it **publishes +them itself, on well-known event channels, to whoever has joined.** Nothing is sent to a VM that +didn't join, so a VM never receives text it has no words for. + +| Event channel | Fired by (existing kernel point) | Payload text | +|---|---|---| +| `EV-DONE` | a **participant** goes from having work to having none: its trampoline finds its queue empty after draining, and it parks idle | `S" " ON-DONE` | +| `EV-BORN` | `capsule_birth_baby()` reaches LIVE | `S" " ON-BORN` | +| `EV-DIED` | `capsule_vm_kill()` / `capsule_vm_force_reap()` succeed | `S" " ON-DIED` | +| *(later)* `EV-DEVICE` | WIREBIND/USB attach or detach | *(ruling M-3)* | + +The handler convention is: **joining an event channel is the promise to define its `ON-…` word.** +The event channels are created at boot, beside channel 0. Their ids are fixed and published as FORTH +constants, so both `JOIN` and the ACL can name them. + +- **Who pays** (ruling M-5). The VM the event is about is the natural sender: it "emits" its own + completion or birth, just without its code deciding to. A dying VM can't pay after its reservoir + is gone, so `EV-DIED` comes from the root. +- **Which VMs** (ruling M-4). Events are fired for **participants only**. Observers already act on + events; they don't need to announce their own idleness, and that also rules out any loop of + observers reacting to each other's events. +- **When the arena is full** (ruling M-6). An event that can't be admitted is **dropped, counted + and logged, never blocking**, since a kernel event point must not wait on a queue. Dropped events + are visible in the ledger and in a counter the DoE can check. + +### 7.4 What this does for the DoE (O-C, redone) + +- Hera `JOIN`s `EV-DONE` once and defines `ON-DONE ( c-addr u -- )`, which counts. +- Each worker's job becomes just `WL-RUN`: no `WL-DONE-ARGS`, and no `SEND` in the job text. +- A worker's completion reaches Hera autonomously, as the OS's `EV-DONE`. +- `WL-DONE-ARGS` comes out of the workload capsules again. +- The event-driven driver of §3 stays: the last `ON-DONE` ends the trial and starts the next. + +### 7.5 Rulings needed from Captain Bob + +1. **M-1:** the word names in §7.2 (placeholders). +2. **M-2:** `INJECT`, sent as the root and ACL-gated (proposed), or sent as the caller (which would + make it just `SEND`). +3. **M-3:** the first event set: `EV-DONE`, `EV-BORN`, `EV-DIED` now, with device events later + (proposed). +4. **M-4:** events for participants only (proposed), or for every VM. +5. **M-5:** funding: the VM the event is about, with `EV-DIED` from the root (proposed). +6. **M-6:** when a message can't be admitted, drop, count and log, never block (proposed). After + M-0 that happens only when a sender's **reservoir** can't fund it, or RAM runs out; the arena + itself is no longer a limit. +7. **M-0 (defect, ahead of everything):** the arena, queues and member lists grow on demand + (proposed), or are sized at boot from RAM. +8. **M-0b:** `Q_SLOT` decoupled from arena size and kept at 1/32 of a reservoir as its own + constant (proposed), or derived some other way. + +## 8. Audit: every fixed-size structure in the kernel (2026-09-24) + +**Captain Bob, verbatim:** *"completely audit everything and find out where we're using static data +structures … no absolutely dedicated sized memory slots … if you're slotting, we can only do X +number of VMs or Y number of messages or whatever … None of that should exist. No arrays, in other +words. It should all be fairly dynamic."* + +**Method** (read-only, 2026-09-24). +- Searched `src/`, `include/` and `src/starkernel/` (test runner excluded) for: + - every `#define` whose name carries `MAX`, `CAP`, `SLOTS`, `COUNT`, `DEPTH`, `LIMIT`, + `ENTRIES`, `SIZE` or `LEN`; + - every static or global array with a constant size; + - every constant-sized array inside a struct; + - every table sized once at boot. +- For each capacity limit, checked **what happens at the limit** by reading its use sites. +- Grouped by what the size actually limits. Classes C and D are the cases Captain Bob named; + classes E–H are listed so that nothing is hidden, with a recommendation. + +### 8.1 Class C — silent truncation (worse than a limit; fix first) + +| Where | Today | At the limit | +|---|---|---| +| `parity.c:498/646`, `MAX_DICT_ENTRIES` 2048 | parity walks at most 2048 dictionary entries | **Hashing stops.** Beyond 2048 words, `dict_hash` silently ignores the rest, so the determinism evidence would be computed over part of the dictionary. | +| `vm.h` `DICTIONARY_SIZE` 1024: `word_id_map[1024]`, `recycled_word_ids[1024]`; `dictionary_management.c:87` | word ids 0–1023 | **Word 1025 onward gets `WORD_ID_INVALID`,** and the physics hooks skip it (`physics_execution_hooks.c:95`). Execution heat, pipelining and transitions silently stop for later words. Hera is at 531. | +| `string_words.c` `WORD` scratch, `WORD_SCRATCH_CAP` 64 | one **static** `word_scratch_addr`, allotted in the first caller's VM space | Not a count limit, but a static shared across VMs: a second VM calling `WORD` gets an address in someone else's space. Found at Stage G. | + +**Captain Bob, 2026-09-24:** *"both of those failure modes that you've noted need to be handled +properly as well."* Removing the limit is not enough. **Every place something can fail to grow +must fail properly:** + +1. **Never silently.** Every failure is logged (`log_message` at `LOG_ERROR`, which the log + region persists), and counted in a counter that a FORTH word or DoE row can read. +2. **Never partially.** The operation is refused whole, with a return value the caller checks. + Nothing is done halfway. +3. **With the right consequence for what it guards:** + - **Parity (`dict_hash`)** must cover the whole dictionary. If the walk can't be completed, for + example the entry list can't be allocated, the parity record is marked **incomplete** + (`PARITY:… INCOMPLETE`) rather than printing a hash that looks valid. It becomes a hard, + visible failure of the determinism evidence, not a quiet wrong value. + - **Word ids:** the id map grows with the dictionary. If growth fails, defining the word + **fails with an error** (`vm->error`, same as any other dictionary-full condition), rather + than defining a word with `WORD_ID_INVALID` that silently runs outside the physics. + +This applies to every item in §8.2–§8.4 as it becomes dynamic: each grow path gets a checked +failure, a log line, a counter and a clean refusal. The evidence for each stage includes a probe +that **forces** the failure (an injected allocation failure, reverted) and shows the refusal, the +log line and the counter. + +### 8.2 Class D — fixed counts of things (must become dynamic) + +| Where | Limits | At the limit | +|---|---|---| +| `kernel_hermes.c` `sk_hermes_msgs[32]` (`SK_HERMES_MSG_MAX`) | messages in flight, **system-wide** | send refused (§7.0, M-0) | +| `kernel_hermes.c` queue `slots[32]` (`SK_HERMES_PENDING_MAX`) | pending messages per VM | send refused | +| `kernel_hermes.h` `members[64]` (`SK_HERMES_MEMBER_MAX`) | VMs per channel | subscribe refused | +| `kernel_hermes.c:698` `present[32]`; `kernel_main.c:886` `msgs[32]` | chunks per reassembly; a self-test | follows `SK_HERMES_MSG_MAX` | +| `capsule_wirebind.c:90` `g_wirebind_live[13]` (`WIREBIND_MAX_LIVE`) | **identities attached at once: 13** | attach/detach tracking degraded (message printed) | +| `block_subsystem.c:199` `reloc[500]` (`BLK_RELOC_MAX_ENTRIES`) | block relocations | `BLK_ENOMEM` | +| `hal.c:173` `hal_exec_regions[8]` | executable regions registered | refused | +| `capsule_run.c:57` `run_log[1024]` | run records kept | wraps; oldest records lost | +| `kernel_main.c`/`capsule_loader.c` `KRD_MAX_BLOCKS` 1024, `KRD_BUF_SIZE` 1 MiB | the kernel RAM disk | fixed size | +| `vm.h` `CF_STACK_MAX` 64: `cf_stack`, `leave_addrs`, `endof_addrs`, … | control-flow nesting while compiling | compile error past 64 | +| `include/starkernel/capsule.h` `CAPSULE_MAX_COUNT` 256 | *(nothing: defined, never used)* | dead; remove | + +### 8.3 Class D′ — tables sized once at boot (a ceiling, just a RAM-derived one) + +The B2 precedent (`FABRIC-3.5.md` §XLV.2) made these "dynamic" by sizing them from RAM at boot. By +the rule stated today that is still a slot count: *"we can only do X number of VMs."* + +| Where | Sized from | Limits | +|---|---|---| +| `stadium.c` `stadium_max_vm_count_val` | `STADIUM_VM_MEMORY_PERCENT` of free heap ÷ `VM_MEMORY_SIZE` | **the VM population**: `capsule_birth.c:731` refuses births past it | +| `stadium.c` cells, bitmap, owner, resident lists | `STADIUM_MEMORY_PERCENT` of free heap ÷ cell size | Stadium cells (patrons, messages) | +| `stadium.c` `owner` is `uint16_t` | type width | ≤ 65,535 quota slots, whatever the RAM | +| `capsule_vm_switch_signal.c` slots; `kernel_hermes.c` channel table and queue table; `session.c` sessions; `stadium_blocks.c` counters; Stadium quota table | `stadium_max_vm_count()` | participants, channels, queues, sessions | + +**Proposed:** all of these grow on demand (kmalloc, doubling, as `vm_frame.c` does), and the VM +population is bounded only by whether a birth can actually allocate. Stadium's cell pool is the +one to discuss: it is the physics' own substrate, and whether it grows or stays a fixed "stadium" +is a physics question (ruling S-2). + +### 8.4 Class E — sized by hardware or firmware (size from what the device or table reports) + +| Where | Today | Proposed | +|---|---|---| +| `pmm.c:61` `pmm_bitmap[2 MiB]` (static) | tracks at most ~64 GiB of RAM | allocate from the UEFI memory map's actual extent | +| `ioapic.c:123` `g_redir_low/valid[24]` | 24 redirection entries | the IOAPIC reports its own count (up to 240); size from it | +| `ioapic.c:116` `g_overrides[16]` | MADT interrupt-source overrides | size from the MADT | +| `xhci_driver.h:197` `port_slot_id[32]`; `XHCI_MAX_TRACKED_PORTS` | 32 root ports | the controller reports `MaxPorts`; size from it | +| `xhci.h` `XHCI_RING_TRB_COUNT` 256; virtio `VQUEUE_SIZE` 4/1, `EVENTQ_SIZE` 8 | ring and queue depths | devices negotiate their queue size; could size from it. Behaves as a throughput limit, not a count of things. | +| `rpi5_native_boot.c` memmap/RAM/reserved-range arrays | firmware regions | size from the device tree | +| `interrupts.c` `idt[256]` | **architecture-defined** (x86 has exactly 256 vectors) | keep | +| `uefi_loader.c` `KERNEL_MAX_SIZE` 8 MiB | kernel image size the loader accepts | read the image size from the file instead | + +### 8.5 Class F — the per-VM model (a design decision, not a slot count) + +| Where | Today | +|---|---| +| `vm.h` `VM_MEMORY_SIZE` / `arena.c` `SK_VM_MEMORY_SIZE` | 5 MiB address space per VM (dictionary plus blocks) | +| `vm.h` `STACK_SIZE` | 1024 cells, data and return stacks | +| `arena.c` `SK_VM_STACK_SIZE` | 2 MiB native stack per VM | +| `vm.h` `INPUT_BUFFER_SIZE` 1025 | one block per line, **already ruled** (`.claude/CLAUDE.md`, §XLV.3) | +| `vm.h` `WORD_NAME_MAX` 31; `capsule_run.h` `VM_NAME_MAX` 64; `USER_IDENTITY_*_MAX` | name lengths | +| `kernel_args.h` default heap 2 GiB | kernel heap | + +These bound how big one VM or one name can be, not how many of anything there are. Growable VM +memory and stacks are possible but reach deep into the vendored VM (`VM_ADDR`/`vaddr_t` addressing). +Proposed: leave for a separate decision (ruling S-4). + +### 8.6 Class G — on-disk formats and persistence rings + +- `log_region.h`: `LOG_SLOT_SIZE` 1024, and `LOG_REGION_MAX_DEVBLOCKS` 32. +- `doe_region.h`: `DOE_SLOT_SIZE` 64, `DOE_REGION_MAX_DEVBLOCKS` 4, and `DOE_SLOT_ISA_MAX` 8. +- `capsule_wirebind.c`: `WIREBIND_CERT_MAX_DEVBLOCKS` 4. + +Slot sizes are the persistence **format**. Region sizes are ring capacities on the disk image +(older entries are overwritten). Changing either is a format change with a migration question. +Proposed: keep the formats, and make region sizes a disk-layout decision (ruling S-5). + +### 8.7 Class H — not capacity: caches, statistical windows, tables of constants + +- **Caches:** `HOTWORDS_CACHE_SIZE`, the TTF caches, the vt100 scrollback (1000/4096 lines), + `DISK_CACHE_SLOTS` 8, `BLK_VM_SLOTS` 4, and FORTH-79 `BUFFER_COUNT` 4. More would be faster; none + refuses anything. +- **Statistical windows:** `ROLLING_WINDOW_SIZE`, `TRANSITION_WINDOW_SIZE`, + `VM_FLEET_WINDOW_DEPTH`, `TIME_WINDOW_SIZE`, `CD_MAX_CLASSIFY_DEPTH`, and `HEARTBEAT_TICK_BUFFER_ + SIZE`. These are measurement parameters, Kconfig-tunable, and part of the DoE's definition. +- **Scratch and staging buffers** sized to one payload or line (`g_send_buf`, `g_kh_*_buf`, + `DOE_BUF_SIZE`, `LOG_MSG_LINE_MAX`), and device rings (`i8042` 32, virtio-input diagnostics 16). +- **Constant tables:** crypto constants, font bitmaps, keymaps, and the TTF parser's per-glyph + bounds (a font-format limit, refused cleanly). +- **Pools that refill:** `vm_uuid_pool[16]`, which is a batch, not a limit. + +Proposed: leave class H as it is. + +### 8.8 Rulings needed + +1. **S-1:** fix class C first, each on its own: parity hashing, dictionary word ids, `WORD` scratch + (proposed). Each gets the failure handling ruled in §8.1: visible, counted, refused whole, and + parity marked incomplete rather than quietly wrong. +2. **S-2:** class D′, Stadium's cell pool. Grow on demand like everything else, or stay a fixed + "stadium" sized at boot, since it is the physics' substrate? +3. **S-3:** classes D and D′ (except S-2) grow on demand, in this order: kernel-Hermes (M-0); + WIREBIND identities; the VM population and its per-VM tables (switch slots, sessions, queues, + channels, quotas); relocations, exec regions, run log and control-flow nesting (proposed). +4. **S-4:** class F left for a separate decision (proposed). +5. **S-5:** class G formats kept; region sizes are a disk-layout decision (proposed). +6. **S-6:** class E sized from what the hardware or firmware reports (proposed), IDT excepted. + +**RULED 2026-09-24, Captain Bob: "all as proposed, commit and start S-1."** S-1, S-3, S-4, S-5 +and S-6 go as proposed. So do §7's M-0, M-0b and M-1 to M-6, including the §7.2 word names as +written. **S-2 stays open:** it was posed as a question with no proposal (does Stadium's cell pool +grow, or stay a fixed stadium?). It needs its own answer before S-3 reaches Stadium. diff --git a/FABRIC-3.9.8.md b/FABRIC-3.9.8.md index e932a05f..0b620644 100644 --- a/FABRIC-3.9.8.md +++ b/FABRIC-3.9.8.md @@ -23,8 +23,11 @@ Evidence is named **before** each stage runs. | 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): `DOE` starts trial 1 and returns to the prompt; `MU-DONE` counts, ends trials and starts the next; `MU-WAIT-ALL` is 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-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. |