Stage E: every born VM is a switch participant (FABRIC-3.8 §3.6, G4)

capsule_birth_baby() registers each VM at its LIVE completion point, beside
the common-channel subscription. register() is idempotent. The Artemis/
Hestia registrations in kernel_main.c and WIREBIND's own call are removed
(Hera, never born through capsule_birth_baby, stays with set_current).

Probe (restored away): Tripod registered once each (3 slots), a
WORKER-BIRTH VM becomes a participant with no hand registration (4) and
KILL frees it (3), WIREBIND registers exactly console+user, zero
duplicates, on amd64/aarch64/riscv64. Acceptance: dict_hash unchanged,
relay 3, idle growth 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 20:07:22 -04:00
co-authored by Claude Opus 5.5
parent 604ad7aadc
commit c7185facf0
19 changed files with 55115 additions and 22 deletions
+51
View File
@@ -1052,3 +1052,54 @@ explicit instruction.
- WIREBIND's detach path calls `capsule_vm_kill()` directly, without the `KILL` word's - WIREBIND's detach path calls `capsule_vm_kill()` directly, without the `KILL` word's
`vm_physics_retire()`. I have not checked whether a detached identity's VM-physics heat `vm_physics_retire()`. I have not checked whether a detached identity's VM-physics heat
is retired elsewhere. is retired elsewhere.
- [x] **Stage E — every born VM is a switch participant (§3.6, G4).** 2026-09-23. Captain Bob:
*"yes"* (go ahead with Stage E).
Changes:
- `capsule_birth.c`: `capsule_birth_baby()` registers the new VM right after its task 3.2
common-channel subscription, at its LIVE completion point. The capsule code has already
run to completion by then. A full table is logged and doesn't fail the birth.
- `capsule_vm_switch_signal.c/.h`: `sk_vm_switch_signal_register()` is now
**idempotent**. An already-registered VM gets its existing slot back, not a duplicate.
- `kernel_main.c`: the Artemis/Hestia registrations are removed. Hera is still registered
there (she's the one VM never born through `capsule_birth_baby()`), together with
`sk_vm_switch_set_current(mama)`. Nothing can switch before that: every switch point
requires a current VM.
- `capsule_wirebind.c`: the user VM's `sk_vm_switch_signal_register()` call becomes a
participation lookup. `switch_slot` was only ever used as a ≥ 0 flag and is now
documented as one, since slot indices shift when the table compacts.
- The WIREBIND console VM is now a participant too. That's harmless: it never has mail,
so it is never chosen. It is unregistered when it's killed at detach (Stage D).
- Registration order changes: Hestia, then Artemis at their births, then Hera. Decisions
don't depend on order (C-b's walk-order evidence), but the DoE's `switch_current_slot`
column now numbers them in that order.
- Boot order checked: `sk_vm_switch_signal_boot_init()` (`kernel_main.c:536`) runs before
the Tripod births (`:1124`/`:1142`).
**Evidence, as named in §4.** A temporary probe, restored away from a pre-probe snapshot
and verified free of probe code:
- `register()` prints every call, and flags any `DUPLICATE`;
- `kernel_main.c` checks Tripod participation, then `WORKER-BIRTH`s `eprobe` **without
registering it by hand**, checks it is a participant, and `KILL`s it.
Probe runs with the full relay: `logs/20260923-192830/amd64/`, `-193439/aarch64/`,
`-194103/riscv64/`. Identical on all three:
- `register` lines for Hestia (count 0 → 1), Artemis (1 → 2), Hera (2 → 3), eprobe
(3 → 4);
- `STAGE-E PROBE: tripod hera=1 artemis=1 hestia=1 slots=3; WORKER-BIRTH eprobe
participant=1 slots=4; after KILL slots=3`, **PASS**;
- at WIREBIND attach, exactly **two** registrations (the console VM and `rajames~user`);
- **zero `DUPLICATE`** lines anywhere;
- the relay still gives `3`.
**Formal acceptance with the probe gone:** `logs/20260923-194803/amd64/`,
`-195401/aarch64/`, `-200026/riscv64/`.
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures and faults.
- The `dict_hash` set is unchanged.
- The relay gives `3` through a real switch.
- Idle log growth: 0 bytes.
- Clean build on all three plus hosted, no new warnings.
**What this makes possible (not exercised yet):** every `WORKER-BIRTH`ed DoE worker is now
a participant. Mail sent to a worker is delivered by a switch onto its own stack, and it
competes by heat. Stage G's `SEND` word and interleave test are next.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-23T23:07:10Z --> <!-- Generated by mkcapsule --manifest 2026-09-23T23:59:50Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. --> <!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live --> <!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. --> <!-- in MANIFEST.md alongside this auto-generated index. -->
@@ -85,7 +85,10 @@ int sk_vm_switch_signal_boot_init(void);
* or -1 if the slot table is full (or sk_vm_switch_signal_boot_init() was * or -1 if the slot table is full (or sk_vm_switch_signal_boot_init() was
* never called / failed). Call once per participating VM, after that VM is * never called / failed). Call once per participating VM, after that VM is
* fully born (never mid-birth -- this stage has no critical-section * fully born (never mid-birth -- this stage has no critical-section
* protection against being switched away mid-setup). */ * protection against being switched away mid-setup). FABRIC-3.8.md §3.6
* (Stage E): capsule_birth_baby() is now the one caller for every born VM
* (Hera, never born through it, is registered in kernel_main.c).
* Idempotent: registering an already-registered VM returns its slot. */
int sk_vm_switch_signal_register(VMUuid vm_id); int sk_vm_switch_signal_register(VMUuid vm_id);
/* Remove a switch-signal participant (FABRIC-3.md §XXVIII Stage 4, /* Remove a switch-signal participant (FABRIC-3.md §XXVIII Stage 4,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14
View File
@@ -920,6 +920,20 @@ CapsuleRunResult capsule_birth_baby(
* and does not fail the birth itself. */ * and does not fail the birth itself. */
(void)sk_hermes_channel_subscribe(SK_HERMES_CHANNEL_COMMON, vm_id); (void)sk_hermes_channel_subscribe(SK_HERMES_CHANNEL_COMMON, vm_id);
/* FABRIC-3.8.md §3.6 (Stage E, G4): every born VM is a switch
* participant -- same choke point, same "only once birth has fully
* completed" rule as the subscription above (its capsule code has
* already run to completion on the birthing VM's stack). BIRTH,
* WORKER-BIRTH, UNATTENDED-BIRTH and WIREBIND identities all pass
* through here, so none of them registers on its own any more. Soft
* failure, same posture: a full table leaves this VM runnable only by
* VM-EXEC, not a failed birth. A VM with no native stack is registered
* but never chosen (sk_vm_switch_decide() skips it). */
if (sk_vm_switch_signal_register(vm_id) < 0) {
log_message(LOG_WARN, "capsule_birth_baby: switch-signal table full -- %s won't be switched to",
capsule_name);
}
capsule_parity_log_birth(vm_id, cap->capsule_id, cap->content_hash, dict_hash); capsule_parity_log_birth(vm_id, cap->capsule_id, cap->content_hash, dict_hash);
/* §H.12 step 11: applied AFTER dict_hash/parity logging above, not /* §H.12 step 11: applied AFTER dict_hash/parity logging above, not
@@ -143,6 +143,11 @@ int sk_vm_switch_signal_boot_init(void) {
} }
int sk_vm_switch_signal_register(VMUuid vm_id) { int sk_vm_switch_signal_register(VMUuid vm_id) {
/* FABRIC-3.8.md §3.6 (Stage E): idempotent. Registration now happens
* once, at birth; a second call returns the existing slot rather than
* allocating a duplicate that the decision would walk twice. */
int existing = slot_for_vm_id(vm_id);
if (existing >= 0) return existing;
if (g_slot_count >= g_slot_capacity) return -1; if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id; g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].parked_stamp = 0; g_slots[g_slot_count].parked_stamp = 0;
+11 -4
View File
@@ -80,9 +80,11 @@ typedef struct {
struct blkio_dev *dev; struct blkio_dev *dev;
VMUuid user_vm_id; VMUuid user_vm_id;
char username[USER_IDENTITY_USERNAME_MAX]; char username[USER_IDENTITY_USERNAME_MAX];
int switch_slot; /* Stage 4: index into the switch-signal int switch_slot; /* Stage 4: >= 0 if the user VM is a
* table, or -1 if not yet registered * switch-signal participant, -1 if not
* (wired in the next increment). */ * (table full). FABRIC-3.8.md Stage E:
* a flag, not a slot index -- indices
* shift when the table compacts. */
} wirebind_live_entry_t; } wirebind_live_entry_t;
static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE]; static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE];
@@ -313,7 +315,12 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
* failed attach -- the identity is still fully usable via * failed attach -- the identity is still fully usable via
* ordinary VM-EXEC/console-relay dispatch, same as before * ordinary VM-EXEC/console-relay dispatch, same as before
* Stage 4 existed. */ * Stage 4 existed. */
live->switch_slot = sk_vm_switch_signal_register(user_id); /* FABRIC-3.8.md §3.6 (Stage E): no longer registered here --
* capsule_birth_baby() registered the user VM (and, now, the
* console VM too: harmless, it never has mail, so it is never
* chosen) at birth. This only records whether that succeeded,
* which is all switch_slot has ever been used for (>= 0). */
live->switch_slot = sk_vm_switch_signal_is_participant(user_id) ? 0 : -1;
memcpy(live->username, username, sizeof(live->username)); memcpy(live->username, username, sizeof(live->username));
if (live->switch_slot < 0) { if (live->switch_slot < 0) {
console_println("WIREBIND: switch-signal table full -- this identity won't be preempted"); console_println("WIREBIND: switch-signal table full -- this identity won't be preempted");
+8 -16
View File
@@ -1255,8 +1255,15 @@ static void kernel_main_deep(BootInfo *boot_info) {
* Artemis's own birth sequencing. FABRIC-3.6.md Phase 4 (Stage E), * Artemis's own birth sequencing. FABRIC-3.6.md Phase 4 (Stage E),
* 2026-09-22: Hermes's own registration here is retired along with * 2026-09-22: Hermes's own registration here is retired along with
* her birth above. */ * her birth above. */
/* FABRIC-3.8.md §3.6 (Stage E, 2026-09-23): Artemis and Hestia are no
* longer registered here -- capsule_birth_baby() registers every born
* VM at its own LIVE completion point, which they already passed. Hera
* is the one VM never born through it, so she is still registered
* here, and this is still where the switch mechanism learns she is the
* one running. Nothing can switch before sk_vm_switch_set_current():
* every switch point requires a current VM. */
{ {
VMRegistryEntry hera_entry, artemis_entry; VMRegistryEntry hera_entry;
if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) { if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) {
sk_vm_switch_signal_register(hera_entry.vm_id); sk_vm_switch_signal_register(hera_entry.vm_id);
/* Seed the switch mechanism's own "who is running" tracker /* Seed the switch mechanism's own "who is running" tracker
@@ -1265,21 +1272,6 @@ static void kernel_main_deep(BootInfo *boot_info) {
* ever happened. */ * ever happened. */
sk_vm_switch_set_current(mama); sk_vm_switch_set_current(mama);
} }
if (capsule_vm_find_by_name_nocase("Artemis", &artemis_entry) == 0 &&
artemis_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(artemis_entry.vm_id);
}
/* Hestia is the third fleet member through Phase 4 (FABRIC-3.5.md
* SXXXIV.4) -- FABRIC-3.6.md task 1.5, same registration shape as
* Artemis above, added here rather than earlier for the
* identical reason the comment above this block already gives. */
{
VMRegistryEntry hestia_entry;
if (capsule_vm_find_by_name_nocase("Hestia", &hestia_entry) == 0 &&
hestia_entry.state == VM_STATE_LIVE) {
sk_vm_switch_signal_register(hestia_entry.vm_id);
}
}
} }
/* FABRIC-3.6.md task 3.2 (B1) self-test: confirm every live fleet /* FABRIC-3.6.md task 3.2 (B1) self-test: confirm every live fleet