FABRIC-3.9.0/3.9.8: messaging design (§7: dynamic kernel-Hermes M-0, manual words M-A, autonomous events M-B) and the fixed-size audit (§8), rulings recorded

Captain Bob: close the messaging gap first; the arena must be dynamic;
audit every fixed-size structure ("no arrays ... it should all be fairly
dynamic"); failures must be handled properly (never silent, never
partial, logged + counted + refused, parity marked INCOMPLETE). All
rulings as proposed; S-2 (Stadium cell pool) left open -- it was a
question, not a proposal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 09:41:04 -04:00
co-authored by Claude Opus 5.5
parent 6e933dac38
commit d6c90a6839
2 changed files with 287 additions and 1 deletions
+283
View File
@@ -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 **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 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. 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" <name>" ON-DONE` |
| `EV-BORN` | `capsule_birth_baby()` reaches LIVE | `S" <name>" ON-BORN` |
| `EV-DIED` | `capsule_vm_kill()` / `capsule_vm_force_reap()` succeed | `S" <name>" 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.
+4 -1
View File
@@ -23,8 +23,11 @@ Evidence is named **before** each stage runs.
| Stage | Change | Evidence (named in advance) | | 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. | | **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-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" <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`. | | **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. | | **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. |