Files
LithosAnanake/FABRIC-3.9.0.md
T
Robert Allan JamesandClaude Opus 5.5 879d00dcf9 Stage M-A: manual messaging words -- CHANNEL, CHANNEL-CLOSE, JOIN/UNJOIN, JOIN-VM/LEAVE-VM, PUBLISH, INJECT, MAIL?, .MAIL, .CHANNELS (FABRIC-3.9.0 §7.2)
- Eleven words in repl.c over the existing kernel-Hermes C API, registered
  for every VM; `from` always derived in C. Names as ruled (M-1) except
  UNJOIN for LEAVE, which is FORTH-79's DO-loop exit (Captain Bob).
- INJECT (M-2) sends as the root, funded from Hera's reservoir, as new
  type 31; pinned STRICT in kernel_main.c beside BIRTH.
- Read-only accessors sk_hermes_pending_at() / sk_hermes_channel_member_at()
  for .MAIL / .CHANNELS.

Console evidence identical on amd64/aarch64/riscv64: PUBLISH delivers 2
(both print), LEAVE-VM then PUBLISH delivers 1, INJECT shows in MAIL?/.MAIL
as type 31 from Hera before the recipient runs, CHANNEL-CLOSE succeeds and
refuses channel 0, JOIN/UNJOIN, refusals 0/-1. dict_hash set changes by the
11 words (542 words) and is identical across ISAs; acceptance passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 19:24:07 -04:00

31 KiB
Raw Blame History

FABRIC-3.9.0.md — Observers and participants: the Tripod steps outside the compudynamics

Status: DESIGN, for Captain Bob's review. Nothing here authorizes an edit. The execution plan and log are in FABRIC-3.9.8.md.

Provenance. Opened 2026-09-24. This design follows FABRIC-3.8.md, whose Stages A–I are all done. The per-ISA DoE relaunch on the rebuilt driver (FABRIC-3.8.md §6) was stopped once a progress check showed:

  • Hera's MU-WAIT-ALL busy loop took an equal share of the CPU as workers finished;
  • it wrote 43% of all heartbeat rows so far (Hera 75,117 against mu0's 75,114).

Two fixes were tried and set aside:

  • A capsule-level 100 WAIT, reverted. Captain Bob: "not liking this much … redistribute work when hera is the major consumer." The application must not steer its own scheduling.
  • A kernel-side absolute-heat-share charge. Built and tested (a cfg-0 trial took about 150 s against about 210 s), but never committed. It is superseded by this design (§5).

Captain Bob, 2026-09-24:

  • "Hera, Hestia, and Artemis no longer participate in the calculations. However, they observe them and act on them."
  • "let's do it."

1. The idea

The fleet splits into two roles.

  • Participants are every VM that does the work being done and measured: DoE workers, WIREBIND user and console VMs, and anything BIRTH-ed. For these, FABRIC-3.8's compudynamics stays exactly as built:
    • per-beat charge;
    • work heat, with floor on contact;
    • coolest VM with work goes next;
    • ties to the incumbent, then to the one waiting longest.
  • Observers are Hera, Artemis and Hestia, the Tripod. They are not compared, not charged work heat, and not counted in any floor. They observe the participants' physics and act on events: mail, console input, device events.

In thermodynamic terms, the observers are the environment (the heat bath) and the participants are the system under study.

  • Hera already behaves like a reservoir: she starts holding all of Q48_ONE, as the root that vm_physics_retire() returns heat to.
  • Every problem since Stage G came from making the reservoir compete as if it were one more particle:
    • her endowment made her "hottest";
    • her busy-wait took a fair share of the CPU and polluted the data;
    • two fixes had to push her back out.

2. What changes for observers

2.1 Who is an observer. It's a structural fact, set by the kernel, not a name test in the switcher.

  • capsule_birth_baby() already computes is_fleet_foundation (Hera/Artemis/Hestia) for session pinning. The observer role reuses that.
  • Hera, born through capsule_birth_mama(), is marked where kernel_main.c registers her.
  • Nothing a VM executes can set or clear the role.

2.2 Not in the calculation.

  • sk_vm_switch_decide() never offers an observer as a heat candidate.
  • vm_physics_charge() doesn't accrue observers' work heat.
  • sk_vm_switch_signal_note_work_gained() leaves observers out of the floor.
  • Proposed (ruling O-2): observers aren't charged absolute heat either. The reservoir doesn't heat itself by running. Participants still pull absolute heat off it through their own charges, and retire returns it, so fleet K stays conserved.

2.3 Observers act on events. An observer is runnable only when it has an event:

  • Mail: its kernel-Hermes queue is non-empty (MU-DONE, BLK-ATTACH, ELEVATE-REQUEST, a relayed console line).
  • Console input, for Hera: sk_console_key_available(), as today.
  • A device event, for Hera: xHCI and USB attach or detach. See §4, ruling O-4.

2.4 When an observer runs (ruling O-1: the central question). Proposed: at the next clean boundary, an observer with an event goes ahead of the participants.

  • The boundaries are the same owned checkpoints, trampoline points and idle point FABRIC-3.8 built.
  • Among several observers with events, the one waiting longest (the existing unique wait stamps) goes first, with no heat.
  • Once it has no events left it parks, and the participants' physics resumes.

Why this isn't a scheduler:

  • There is no run queue, no priority number, and no ordering computed or kept.
  • It is a role (environment versus system), not a rank among peers.
  • Observers never compete with participants on the same scale; they respond to what happens.
  • The honest risk: an observer with a long event (a heavy command typed at Hera's own prompt) holds the CPU until it finishes, because nothing displaces an observer. §4 lists this. Work meant to share the CPU belongs in a participant. A user's commands already go to their own WIREBIND user VM, which is a participant.

The alternative, observers run only when no participant has work, is pure but brings back C-4 (α)'s starvation: under load, the console, USB and storage wait until every worker is idle.

2.5 Hera's own mail is delivered at the idle point, not on the timer.

  • Today her queue is drained by sk_repl_idle()'s pump at the SK_IDLE_BEAT_INTERVAL cadence (timer-paced; the last such delivery left, FABRIC-3.8.md Stage I).
  • As an observer that acts on events, her mail must be acted on promptly, so her drain moves to sk_vm_idle_point().
  • It keeps the readline n > 0 mid-edit gate, so a drained payload never prints into a line being typed.

3. What this lets the DoE become

The driver becomes event-driven. Hera never waits in a loop.

  • DOE sets up the campaign and starts trial 1 (birth all, send all), then returns to the prompt.
  • Each worker's final SEND delivers MU-DONE to Hera as mail.
  • She acts on it at her idle point: increments the count, and on the last one ends the trial (KILL all, marker, persist) and starts the next.
  • After the last trial she prints MU-CAMPAIGN: complete.
  • MU-WAIT-ALL is deleted. This is the §3.9 design's own "ordinary words" rule, applied: Hera reacts to messages, which is what messages are for.

The heartbeat rows then measure the system, not the environment.

  • An observer produces rows only while acting on an event.
  • Proposed (ruling O-3): its rows stay in the CSV, tagged by vm_name as today, and the analysis filters by role. This keeps the CSV layout unchanged. The alternative is to suppress them.

4. Known limits and open questions

  • O-4: device servicing. xHCI event polling and USB attach/detach run in sk_repl_idle() at the timer-paced SK_IDLE_BEAT_INTERVAL, and only when Hera reaches her idle point.
    • Under participant load, Hera reaches it only when she has another event, so USB goes unserviced. A USB keyboard is a device event that can only be seen by polling.
    • Options: (a) treat "the device-poll interval elapsed" as a Hera event; that's I/O housekeeping triggering a switch, not a scheduling decision, but it is timer-paced. (b) Move xHCI polling to an interrupt path. (c) Accept it for now; the DoE doesn't need USB mid-run.
    • Proposed: (c) for the relaunch, (b) as later work.
  • A long event on an observer holds the CPU (§2.4). Accepted by the role design: heavy work belongs in participants.
  • Artemis and Hestia become observers too.
    • Artemis's storage work (attach, block sync) and Hestia's framebuffer are responses to events, which fits.
    • If a future Tripod duty is compute-heavy, it would have to move to a participant.
  • Stage G's evidence (two participants interleave) and C-b's walk-order property are unaffected: observers aren't candidates.
  • The WIREBIND relay stays participant-delivered: the user VM gets the console line as mail and is switched to by heat among participants. Only Hera's own forwarding is observer work.

5. What this supersedes

  • Absolute-heat-share charge (uncommitted: capsule_vm_physics.c/.h working-tree changes on 2026-09-24). No longer needed once Hera isn't compared. Proposed: discard it.
  • MU-WAIT-ALL, and every busy-wait in the DoE driver.
  • The C-4 (α) consequence recorded at C-b, where the console got no turn under worker load. An observer with console input goes ahead of the participants at the next boundary.

6. Rulings needed from Captain Bob

  1. O-1: observers with an event go ahead of participants at the next clean boundary (proposed), or observers run only when no participant has work.
  2. O-2: observers aren't charged absolute heat either (proposed), or they keep the C-a absolute charge and are only left out of work heat.
  3. O-3: observer heartbeat rows kept and tagged, filtered in analysis (proposed), or suppressed.
  4. O-4: device servicing under load: (c) accept for the relaunch and (b) interrupt-driven later (proposed), or (a) treat the device-poll interval as a Hera event.
  5. Discard the uncommitted absolute-heat-share charge (proposed), or keep it as a safety net.

RULED 2026-09-24, Captain Bob: "all as proposed." O-1 through O-4 and the discard (5) all go as proposed above.

7. Raised 2026-09-24: messaging should flow on its own (OPEN, not yet designed)

Captain Bob, verbatim: "Another potential defect is perhaps in messaging itself. We shouldn't have to actually specifically decide to send a message … that should be autonomous by the operating system itself … The messages should just flow. … However, I would still have a manual method of sending and receiving messages … I should be able to create channels manually, I should be able to destroy channels manually, I should be able to inject messages into the queue, all of that should be manual, but also able to operate autonomously."

What exists today (traced, not proposed):

  • The kernel side is complete in C: sk_hermes_channel_create/destroy/subscribe/unsubscribe/ is_member/member_count, sk_hermes_publish, sk_hermes_send_one, and the negotiation calls (channel_request/respond/close/open_policy), in kernel_hermes.c.
  • Only SEND reaches FORTH. There are also the fixed-purpose KH-BLK-ATTACH-SEND and KH-ELEVATE-SEND. There is no FORTH word to create or destroy a channel, subscribe, publish, or inspect a queue.
  • Every message today is sent because some code decided to send it: the console relay, BLK-ATTACH, SEND in a job. Nothing is routed by the OS on its own.

Two parts, which separate cleanly:

  • Manual. FORTH words over the existing C API: create/destroy a channel, subscribe and unsubscribe, publish, inject a message into a queue, and inspect queues. Mechanical: mostly exposure, not new mechanism. The ACL decides who may use them (ACL.4th, never C).

  • Autonomous. What should flow without anyone deciding, and on what trigger, is a real design question. Candidates to discuss:

    • events the OS already knows about (a VM born, killed, parked idle; a device attached);
    • results a VM leaves on its stack or in a known place, routed to whoever subscribed;
    • heat/physics events routed to observers, which fits "observers act on events".

    Not designed. It needs Captain Bob's framing first.

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 / UNJOIN ( ch -- ok? ) subscribe or unsubscribe the calling VM. (Was LEAVE, which is FORTH-79's DO-loop exit and would shadow it. Captain Bob, 2026-09-24, at M-A: "UNJOIN".)
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 JOINs 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 stop for later words. Hera is at 531. (Corrected at S-1a: not strictly silent. vm_dictionary_track_entry logs a LOG_WARN, "out of stable IDs, disabling metrics for '…'", per word. But nothing counts it and nothing refuses: the word is still defined and runs outside the physics, so §8.1's rules are still unmet.)
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.