Files
LithosAnanake/FABRIC-3.8.md
T
Robert Allan JamesandClaude Opus 5.5 56da55aba9 FABRIC-3.8.md Stage B: kernel-side idle loop in the switch trampoline
sk_vm_switch_entry() stops being the Stage-2 placeholder bounce and becomes
every switch participant's own idle loop: it drains one of its own
kernel-Hermes messages per pass, on its own native stack (driven by the drain's
return value, so a non-consuming drain cannot spin), and when nothing is
drained it marks the VM parked_idle and yields back. It is entirely OS-driven,
with no VM-side yield word. sk_hermes_drain_checkpoint() gains a per-VM
hermes_draining guard so a payload's own in-word checkpoint cannot drain a
second message inside the first. Still inert in normal operation: nothing
marks work until Stage C.

Finding fixed here: amd64 sk_vm_switch_prime built the first-entry frame at
top-56, entering with rsp 16-aligned instead of the SysV-required 8 mod 16, so
the first aligned SSE spill in a callee took #GP (logs/20260923-142526, kept).
It had been latent since SXXVIII Stage 2 because the placeholder never called
anything. The frame is now at top-64 with a zeroed pad slot. aarch64 and
riscv64 were already correct.

Evidence: a temporary kernel_main probe (reverted, not in this commit) queued
two messages to Hestia, the first calling a colon word, and switched into her.
It passed on all three ISAs: 0 pending, parked_idle, depth 0, context saved on
her own stack, SB-SEQ=12 (drained one at a time, not nested), ledger and
stadium_conserved restored. Formal acceptance without the probe on all three
ISAs: dict_hash identical and unchanged, zero UNKNOWN WORD or faults, and the
live WIREBIND mint -> USE -> 1 2 + . -> 3 relay.

FABRIC-3.8.md also records Captain Bob's rulings that no VM-side word ever gives
up control and that multitasking is a compudynamic hybrid, and opens SS3.12
(VM frames for per-VM execution state).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 14:57:51 -04:00

413 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FABRIC-3.8.md — Real concurrency: finishing what the reshuffle started
**Status: OPEN — Stages A and B done (§6); Stages C–I not started. §3.11 (compudynamic preemption) needs
a ruling before Stage C.** Each stage below needs Captain
Bob's explicit go, gets its own commit, and gets its own three-architecture acceptance (`clean
qemu`, amd64 → aarch64 → riscv64, foreground, visible framebuffer, one at a time).
**Provenance.** Opened 2026-09-23. The multiuser/multitasking DoE (`FABRIC-3.md` §XXXIII–§XXXV)
was launched on amd64 (`12345 3 S" amd64" DOE`, `logs/20260923-125936/`) and stopped by
direct instruction about 49 minutes in, with zero trials complete, once it was established that
the harness measures a **cooperative, serialized round-robin**, not concurrency (log and CSV
kept). Captain Bob: *"that was the point of this branch"* — the `v2.1.0` Tripod/kernel reshuffle
(`FABRIC-3.5.md`/`-3.6.md`, now on `master`) was meant to deliver real concurrent execution.
This document records why it doesn't yet, and the design that finishes it.
**The governing objective, Captain Bob's words:** *"the object: doe contains zero dependence on
special execution states."* Concurrency must be a property of the OS for **every** VM. The DoE
may only use ordinary words (birth a VM, send it a message, kill it). No DoE-specific
registration, no DoE-specific dispatch path, no special mode. If the DoE needs something the
OS doesn't give every VM, that's an OS defect to fix here, not a harness workaround.
Successor-in-topic to `FABRIC-3.5.md`/`FABRIC-3.6.md` (both CLOSED/ARCHIVAL at `v2.1.0`), whose
close headers say further fleet work gets its own document. `FABRIC-3.md` stays open and
authoritative for bare-metal boot; this doesn't supersede it.
---
## 1. What the reshuffle ruled, and what actually landed
**Ruled** (`FABRIC-3.5.md`):
- §XXXII: *kernel-Hermes publishes eligibility; it does not dispatch.* The switcher is **the sole
mover of control**. The MSG-TICK idle pump is retired.
- §XLIII: *the target drains its own queue, in its own context, at its own outermost
checkpoint.* Nobody `VM-EXEC`s into anybody.
- §XV.3: nobody owns turn order. It's compudynamic.
**Landed** (`FABRIC-3.6.md` Phases 0–5, `v2.1.0`): the message allocator and heat ledger,
per-VM pending queues, the drain function, channels, chunking, per-VM native stacks (§XXVIII
Stage 1), the switch primitive (Stage 2), the timer-driven switch signal (Stage 3), and WIREBIND
participation (Stage 4).
**Not landed, traced 2026-09-23 against this branch (`e40cafc`, same code as `origin/master`
for every file below):**
| # | Gap | Evidence |
|---|---|---|
| G1 | **Kernel-Hermes never publishes eligibility.** `sk_hermes_send_one()` doesn't call `sk_vm_switch_signal_mark_work()`. `has_work` is never set, so the switcher never switches. | `kernel_hermes.c:485–537` — zero `mark_work` references in the file. The only caller left is the `SWITCH-MARK-WORK` word (`mama_forth_words.c:867`), which had one caller: FORTH `MSG-SEND`, deleted with `messaging.4th` in Phase 4. |
| G2 | **A switched-to VM does nothing.** First entry lands in the Stage-2 placeholder, which yields straight back forever. | `switch.c:100–105`, `sk_vm_switch_entry()`. Its own comment: "no production behavior defined yet." |
| G3 | **Delivery to passive VMs is still the pump.** Task 3.9 put the kernel-Hermes drain into `sk_repl_idle()`'s registry walk: Hera calls `vm_interpret()` on each live VM, **on Hera's own physical stack**, once per idle beat. | `repl.c:909–928`. §XXXII ruled this pump retired. |
| G4 | **Only the Tripod and WIREBIND user VMs are switch participants.** VMs born through `capsule_birth_baby()` (`BIRTH`, `CAPSULE-BIRTH`, `WORKER-BIRTH`, `UNATTENDED-BIRTH`) never register. | `kernel_main.c:1261/1270/1280`, `capsule_wirebind.c:316` — the only `sk_vm_switch_signal_register()` call sites. |
| G5 | **The stack-ownership gate is a single global.** `g_vm_interpret_depth` counts *all* `vm_interpret()` nesting across all VMs, and the gate is `depth <= 1`. Once VMs genuinely interleave on their own stacks, that global stops meaning "this VM owns this stack at its outermost frame." Same for `g_log_attrib_vm`. | `vm_core.c:1164–1178`, `:1183–1220`. |
| G6 | **Kill vs. parked VMs.** `capsule_vm_kill()` refuses any `VM_STATE_SWITCHED_OUT` VM, and the ordinary kill path never unregisters from the switcher (only `capsule_vm_force_reap()` does). A worker that finished and parked could never be killed; one killed some other way would leave a stale switch slot. | `capsule_birth.c:412–` (refusal at the function's `SWITCHED_OUT` check); `capsule_birth.c:371` (the only unregister in that file, in the reap path). |
| G7 | **Switches only happen inside word dispatch.** The checkpoint lives in `execute_colon_word()`. A VM sitting at an idle point (Hera at her REPL, blocked in `sk_console_readline()`) never takes a pending switch. | `vm_core.c:913–990`. `sk_repl_idle()` never calls `sk_vm_switch_signal_take_pending()`. |
| G8 | **Shared file-scope state.** `RANDOM`/`SEED` share one `g_prng_state` across every VM. Under interleaving, one VM's draws perturb another's stream, and reproducibility of any seeded sequence becomes timing-dependent. | `starforth_words.c:71`, `:657`. The Isabelle sweep memory lists more of this class (control-flow compile stack, vocabulary globals). |
**Latent hazard created by combining G1 with G3.** Wiring G1 alone would wake the switcher for
every message recipient, WIREBIND `~user` VMs included, and those are already participants. The
pump (G3) runs their relayed console commands via `vm_interpret()` on **Hera's** stack at
global depth 1, so the gate (G5) passes. The first pending switch would then save Hera's stack
pointer under the `~user` VM's `native_stack_saved_sp`. That's `FABRIC-3.md` §XXVIII.1's
stack-ownership corruption, reborn on the live console path. **G1 must not land before G5 and
G3 are fixed** (§4).
**Why the DoE couldn't see any of this.** `multiuser-doe.4th`'s `MU-TOUCH-ALL` calls `VM-EXEC` into
`mu0…muN` in index order. `VM-EXEC` runs `vm_interpret()` synchronously on Hera's stack at
depth 2, where the gate defers anyway. So the "concurrency" factor (2/4/8) counted resident
VMs; execution was strictly serialized. That's `FABRIC-3.md` §XIX's own named null hypothesis.
---
## 2. Design principles
1. **One mover.** Only `sk_vm_context_switch()`, driven by the switch signal, moves control
between VMs. Nothing else runs a VM's code on another VM's stack. (§XXXII)
2. **Work runs on its owner's stack.** A VM's payloads and words execute only while that VM
physically owns the CPU stack it's standing on. This is the property the depth gate was
approximating.
3. **Kernel-Hermes publishes facts.** It enqueues, and marks the recipient as having work. It never
dispatches or orders anything. (§XXXII, §XV.3)
4. **Generic, not DoE-shaped.** Every VM born through `capsule_birth_baby()` is a participant,
drained the same way, killable the same way. The DoE gets concurrency by using `BIRTH` and a
message send, exactly like any future program would.
5. ~~**Nothing new decides turn order.** The existing readiness/threshold signal stays as it is. Its
hardcoded `50` threshold remains the flagged seam (`FABRIC-3.5.md` §XV.3), not something this
pass tunes.~~ **CORRECTED 2026-09-23, Captain Bob: *"the timer is NOT the source of
preemption, compudynamics is."*** Today's switch decision is timer-driven:
`heartbeat_tick()` (`heartbeat.c:179`, ISR context) calls `sk_vm_switch_signal_tick()`, which
adds one unit of readiness per timer tick to every non-current participant and requests a
switch once readiness passes a fixed `50` (`capsule_vm_switch_signal.c:58`, `:191–210`).
That's a time-slice scheduler with a threshold, the opposite of `FABRIC-3.5.md` §XV.3 ("turn
order is compudynamic"). **Principle 5 is replaced by:** *the decision to preempt, and whom
to run, must come from the compudynamics (the physics the fleet already computes), not from
elapsed timer ticks.* §3.11 holds the design question; nothing is decided there yet. Stage A
(the ownership gate, §3.1) is unaffected: it governs *where* a switch may safely happen,
not *whether* one should.
---
## 3. Mechanism
### 3.1 The ownership gate becomes per-VM (G5)
Replace the global depth test with the real invariant:
> A checkpoint may switch or drain only when **`sk_vm_switch_current_vm() == vm`** (this VM
> physically owns the stack) **and `vm->interpret_depth == 1`** (at its own outermost
> interpret frame).
- New per-VM field `interpret_depth`, incremented and decremented in `vm_interpret()` beside the
existing global. The global stays for log attribution until §3.7 retires it.
- `sk_vm_switch_current_vm()` (`switch.c:68–72`) already tracks the physical owner, seeded at boot
for Hera and updated on every switch. It becomes load-bearing here, which is what it was built for.
- A nested `VM-EXEC` into another VM fails the first clause (that VM isn't the stack owner), so the
existing defer-if-nested behavior is preserved exactly, now by construction.
- **Also gate on `vm->mode != MODE_COMPILE`**: never switch or drain mid-definition (G8's compile
statics).
### 3.2 The trampoline becomes every participant's idle loop (G2)
`sk_vm_switch_entry()` stops being a pure bounce. On each entry or resume:
1. If this VM has a pending message: set `vm->hermes_draining = 1`, drain **one** message
(`sk_hermes_drain_checkpoint()`, ruled one per checkpoint, `FABRIC-3.5.md` §XLVI 3.0e), clear
the flag.
2. Otherwise, mark this VM **parked-idle** (§3.5) and yield back to `native_switch_back_to`.
The payload runs via `vm_interpret()` at this VM's depth 1, on its own stack. So its own word
checkpoints can preempt it (§3.1 passes), and preemption now happens inside real work, not just
at message boundaries.
### 3.3 One drain at a time per VM (nested drain)
`FABRIC-3.5.md` §XLIII.5's "recursive drain prevented for free" assumed every checkpoint sits under
some outer `vm_interpret()`. With the trampoline as a base, a payload runs at depth 1, and the
existing in-word drain (`vm_core.c:923–925`) would then drain a *second* message **inside** the
first's execution. **Rule:** a per-VM `hermes_draining` flag. No drain site drains while it's
set. The in-word drain site stays: it's how Hera and any VM busy running a long word still
receive mail. It just can't nest.
### 3.4 Eligibility is published, and the pump stops draining participants (G1 + G3, one commit)
- `sk_hermes_send_one()` calls `sk_vm_switch_signal_mark_work(to)` after a successful enqueue.
That's the §XXXII ruling, one line.
- `sk_repl_idle()`'s registry walk (`repl.c:909–920`) **skips switch participants.** Their queues
are drained only by themselves (§3.2). Non-participants, if any remain, keep today's behavior.
Once §3.6 makes every born VM a participant, the loop becomes dead code, and it's stripped in
its own commit per this series' strip discipline.
- These two changes **must land together**. Either alone is either inert (mark without trampoline
behavior) or the §1 latent hazard.
### 3.5 Parked-idle is a killable state (G6)
`VM_STATE_SWITCHED_OUT` currently means "a live frame is parked here, don't free," and that's
correct mid-work. A VM that yielded from its trampoline's idle branch (§3.2 step 2) has nothing
borrowed on its stack and nothing in progress. **New per-VM flag `parked_idle`**, set only on
that branch and cleared on every switch-in. `capsule_vm_kill()` allows a `SWITCHED_OUT` VM whose
`parked_idle` is set. The same function gains the `sk_vm_switch_signal_unregister()` call its
reap sibling already has (`capsule_birth.c:371`). A plain flag, not a new `VMState`, following
the `pending_reap` precedent (`FABRIC-3.md` §XXX: `VM_STATE_DEAD` is treated as idempotent
success and would swallow it).
### 3.6 Every born VM participates (G4)
`capsule_birth_baby()` registers the new VM with the switch signal at its `VM_STATE_LIVE`
completion point. That's the same choke point task 3.2 already uses to subscribe every VM to the
common channel (`FABRIC-3.6.md` task 3.2). Registration happens **after** birth completes, never
before, the same rule `kernel_main.c`'s Tripod block states. `WORKER-BIRTH`/`UNATTENDED-BIRTH`/
`BIRTH` gain nothing of their own. `kernel_main.c`'s explicit Artemis/Hestia calls and
`capsule_wirebind.c:316` become duplicates. Remove or guard them (registering twice would
double-allocate a slot), in the same commit.
**Birth itself stays synchronous.** `capsule_birth_baby()` runs the capsule's code to completion
before returning. Any workload a capsule self-executes at load (every `workload-N.4th` does,
`FABRIC-3.md` §XXXIII.4) therefore still runs serially at birth. That's addressed in §3.9 as a DoE
ruling, not an OS change.
### 3.7 An idle point is a switch point (G7)
`sk_repl_idle()` runs on Hera's own stack at depth 0. That's a clean switch point by §3.1's
invariant. It checks `sk_vm_switch_signal_take_pending()` and performs the switch, exactly as the
word-dispatch checkpoint does. Without this, concurrency stops whenever Hera sits at her prompt,
which is exactly when a human has launched background work and is waiting.
**`sk_repl_headless_wait()` gets the same treatment**, since it's also an idle loop on Hera's
stack. Neither may switch while `g_mama_interpreting`/`g_idle_pump_active` is set (the existing
reentrancy guards stay until proven dead).
### 3.8 A generic send word (needed by any program, not just the DoE)
There's no general FORTH-callable kernel-Hermes send today, only the fixed-purpose
`KH-BLK-ATTACH-SEND`/`KH-ELEVATE-SEND` (`repl.c:385–488`), each hardwired to send to Hera.
**New word `SEND ( paddr plen name-c name-u -- ok? )`** (name is Bob's call, see §5): resolves the
target by registry name, sends a copy of the payload via `sk_hermes_send_one()` (so G1's
eligibility mark fires), and returns 0/1. `from` is the calling VM, derived in C, never taken from
the stack, the same anti-spoof rule `KH-ELEVATE-SEND` established. Payload bound is one block
(1024), per `FABRIC-3.5.md` §XLV.3. Registered for every VM. A plain unconditional primitive:
restricting it, if ever wanted, is `ACL.4th`, never a C check.
### 3.9 The DoE, rebuilt on ordinary words (after §3.1–§3.8 land)
Using only `BIRTH`-family words, `SEND`, and `KILL`:
1. **Birth** N workers (serial, as all birth is).
2. **Send** each worker its workload command as a message. Workers now run **concurrently**,
each on its own stack, preempted by the switch signal.
3. Each workload payload ends by `SEND`ing a completion line back to Hera (e.g. `" <n> MU-DONE"`),
which runs in Hera's own context when she drains it and increments a counter.
4. Hera **waits with an ordinary loop** (`BEGIN … MU-DONE-COUNT @ N = UNTIL`). Her word checkpoints
drain her mail and let the switcher preempt her. No special wait state.
5. Check errors, `KILL` each worker (legal now they're parked-idle, §3.5), and print and persist
the trial marker as today.
**DoE ruling needed (§5):** workload capsules self-execute at load, so the heavy work happens
serially at birth. For the concurrent phase to be the measured phase, workers should be born from
**definitions-only** capsules, with the workload arriving as the first message.
### 3.10 Shared state that interleaving exposes (G8), stated rather than fixed
- `g_prng_state`: the DoE's Fisher-Yates shuffle runs before any worker exists, so the **run
order** stays seed-deterministic across ISAs. Any `RANDOM` a workload calls mid-trial is not.
Whether that matters depends on which workloads draw from it (not yet checked).
- The compile-time control-flow and vocabulary statics named by the Isabelle sweep: protected for
switches by §3.1's `MODE_COMPILE` gate. A payload that *itself* compiles (`:` … `;`) is protected
as long as it can't be preempted mid-definition, and the gate guarantees that.
- **Not fixed in this pass.** Making these per-VM is real work in vendored shared source
(`src/word_source/`), which is `FABRIC-4.md`'s declared scope.
### 3.11 Preemption comes from the compudynamics, not the timer (OPEN, Captain Bob's ruling of principle)
**Ruled:** the timer isn't the source of preemption; the compudynamics is. **Not yet
designed:** which physical quantity decides "switch now, to whom." Material already in the tree,
listed so the design starts from what exists (traced, not proposed):
- **VM-physics heat** (`execution_heat_q48`, `capsule_vm_physics.c`; FORTH `VM-HEAT`). Moves
when a VM does real work (`vm_physics_touch()`) and is conserved fleet-wide (fleet K).
`FABRIC-3.md` §XIX already argued the turn-attractor from this: *the coolest live VM with
pending work goes next*, the same least-dense judgment `stadium_admit()` uses for eviction,
applied to "whose turn."
- **Stadium heat/reservoir** (per-VM quota conservation, `stadium_conserved()`). A message's heat
is the Stadium cell it occupies (`FABRIC-3.5.md` §XL.4), so pending mail already has a physical
weight.
- **Loop #7's adaptive heartbeat** (`HeartbeatState`, per VM). This project's "one and only
clock" (memory `project_acl_ttl_overhead_final`), and adaptive rather than a fixed period.
Distinct from the hardware timer ISR (`heartbeat_ticks()`), which is what currently drives
readiness.
**The shape this implies, for ruling (not decided):** the switch decision moves out of ISR
readiness counting and into the ownership-gated checkpoint (§3.1), where it compares physics.
Switch when some VM with pending work (`has_work`) is compudynamically favored over the current
one (e.g. cooler per §XIX), and never merely because N timer ticks have elapsed. The timer ISR
would then keep only what physics needs from it, and nothing that decides turns. The fixed `50`
threshold and the per-tick readiness accrual go away.
**Clarified 2026-09-23, Captain Bob:** *"this is kind of like a mixture of preemptive and
cooperative multitasking… It's the compudynamics that drives the whole thing. Not necessarily a
preemptive timer. Not necessarily cooperatively, as in 'when I finish my task, I move on'. But
it's a mixture of both."* Read as:
- **Where a switch may happen is cooperative in shape.** Only at a VM's own clean boundary, the
ownership-gated word checkpoint (§3.1): stack owned, own outermost frame, not compiling. Never
at an arbitrary instruction, never because an interrupt fired.
- **Whether, and to whom, is preemptive in effect.** A running VM doesn't hold the CPU until it
finishes. At any such boundary it can be displaced mid-task when the compudynamics favors
another VM with pending work.
- **Running out of work parks a VM.** That's the plain cooperative case (§3.2's idle loop).
- **Neither a clock nor task completion decides. The physics does.**
**Constraint, Captain Bob 2026-09-23:** *"make sure nothing inside of a given execution state or
VM controls this. There are no special words saying I'm giving up control or anything like
that. This is strictly all operating system and strictly all compudynamically driven."* No yield
word, no `PAUSE`, no "I'm done" word, anywhere. Parking happens in the kernel's trampoline when
the OS sees an empty queue (§3.2), and every switch decision is kernel-side.
**Also a Stage C input:** the trampoline's point between two messages is a clean switch point
(own stack, depth 0). The compudynamic decision has to be consulted there too, or a VM whose queue
holds only checkpoint-free payloads (primitive words, no colon calls) never yields. Noted in
`sk_vm_switch_entry()`'s own comment; not built in Stage B.
**This is a design question for Captain Bob before Stage C** (Stage C is the first stage where
anything actually switches). Stages A and B don't depend on it.
### 3.12 VM frames: per-VM execution state that travels with the VM (OPEN, raised by Captain Bob)
**Captain Bob, 2026-09-23:** *"I'm thinking we may need VM frames for state."* Once VMs genuinely
interleave, any execution context that isn't already inside the `VM` struct or on the VM's own
native stack is shared across VMs, and it goes wrong at the first switch that lands inside it.
The Stage B probe hit an instance of this class on its first run (§6, the amd64 alignment finding
is the same family: state the first-entry frame should have carried and didn't).
**Known members of that class today, traced:**
| State | Where | Why it breaks under interleaving |
|---|---|---|
| `g_log_attrib_vm` | `vm_core.c` (save/restore in `vm_interpret()`) | Save/restore assumes last-in-first-out nesting. Hera switches out mid-interpret; Hestia's `vm_interpret()` saves `prev=Hera`; Hestia switches back mid-payload → attribution stays "Hestia" while Hera runs. `(LOG-APPEND-RAW)` takes its source from this, so persisted log records name the wrong VM (defeating `FABRIC-3.md` §XXVII's anti-spoof rule). |
| `g_vm_interpret_depth` | `vm_core.c` | Same LIFO assumption; superseded by per-VM `interpret_depth` (Stage A) and retired in Stage I. |
| `g_switch_entry_vm` | `switch.c` | Read once at first entry; safe only while first entries can't overlap. |
| `g_prng_state` | `starforth_words.c:71` | One `RANDOM` stream for every VM (G8). |
| Compile-time control-flow and vocabulary statics | vendored `src/word_source/` (Isabelle sweep memory) | Shared across VMs; kept safe for switches by the Stage A `MODE_COMPILE` gate, not by ownership. |
**Shape, for ruling (not decided):** a `VMFrame` struct in its own header (per
`feedback_dedicated_headers_not_inline_fields`: related state gets a header, not loose `VM`
fields), embedded in `VM`, holding the per-VM execution context that is global today. The
switch primitive then needs to swap nothing extra: whatever lives in the frame moves with the VM
by construction. Log attribution becomes "the frame of `sk_vm_switch_current_vm()`" rather than a
saved global. The vendored-source statics are `FABRIC-4.md`'s scope, but the frame is where they'd
go. **Should land before or with Stage C**, since Stage C is the first stage where switches
happen for real.
---
## 4. Staged plan
Each stage is its own commit and its own three-arch acceptance, and each names its evidence
**before** it runs (`FABRIC-3.5.md` §XXXV.6). A green boot proves nothing here: a switch storm and a
healthy idle produce the same serial log (`FABRIC-3.md` §XXVIII.1).
| Stage | Change | Evidence that proves it (named in advance) |
|---|---|---|
| **A** | §3.1 per-VM ownership gate (+ `MODE_COMPILE` gate). Nothing else. | Byte-identical behavior: `dict_hash` triple unchanged, identical boot, WIREBIND mint→attach→`USE`→relay (the `FABRIC-3.6.md` Phase 4 acceptance sequence) still `1 2 + .` → `3`. |
| **B** | §3.2 trampoline idle loop + §3.3 `hermes_draining` + §3.5 parked-idle flag. Still inert (nothing marks work yet). | Same as A: nothing can reach the new trampoline branch yet, so behavior is identical. A synthetic self-test (kernel_main.c, the §XXVIII Stage 2 `SWITCH-TEST` pattern: write, capture, revert) switches into **Hestia** (not Artemis, whom the Zuse attach flow `VM-EXEC`s; a parked VM refuses `VM-EXEC`) with **two** messages queued, the first calling a colon word so the in-word drain checkpoint is actually reached. The ordering witness `SB-SEQ` must equal 12 (one-at-a-time drain), not 21 (nested). She must end parked-idle at depth 0 with her context saved on her own stack, current VM back to Hera, and the ledger and `stadium_conserved()` restored. |
| **C** | §3.4 eligibility marked + pump skips participants (one commit). | The WIREBIND relay still works, now delivered by a real switch: `switch_count_cumulative` increments across the relay and the `~user` VM's own drain line appears. The ledger and `stadium_conserved()` hold (`fleet_conserved` is not evidence). No `native_stack_saved_sp` ever equals an address inside Hera's stack range (temporary probe). |
| **D** | §3.5 kill of parked-idle + unregister in `capsule_vm_kill()`. | Birth, message, idle-park, `KILL` succeeds; the switch-signal slot count returns to its prior value. |
| **E** | §3.6 generic registration in `capsule_birth_baby()`, duplicate call sites removed. | The Tripod and WIREBIND VMs still register exactly once (slot count), and a `BIRTH`-ed VM appears as a participant. |
| **F** | §3.7 idle point is a switch point. | With Hera idle at `ok>`, a message sent to a worker **gets processed without typing anything**: the worker's output appears while the console is idle. |
| **G** | §3.8 `SEND`. | Two workers, each sent a payload that loops printing a tagged marker (`A` / `B`) N times: **the markers interleave** in the serial log. This is the one-line proof of concurrency, and it's the test this whole document exists to pass. |
| **H** | §3.9 DoE driver rebuilt (capsule change only). | A single trial at cfg 4 (8 workers): `switch_current_slot` in the per-beat DoE rows moves across **multiple worker slots within one trial**, and `MU-TRIAL … fail=0`. Then Bob's call on relaunching the campaign. |
| **I** | Strip dead code: the pump loop once every VM is a participant, `SWITCH-MARK-WORK` if nothing calls it, the global depth counter once attribution no longer needs it. | Per `FABRIC-3.5.md` §XXII's strip discipline: reachability shown three ways, not by grep alone. |
**Stop conditions** (re-plan, don't push on): `dict_hash` diverges across ISAs; the ledger or
`stadium_conserved()` goes false; any evidence of a stack saved under the wrong VM; a switch storm
(serial log frozen with QEMU pinned, checked by log line-count growth at an idle prompt and by
QMP `send-key` computing a value, per `FABRIC-3.6.md` task 1.5).
---
## 5. Rulings needed from Captain Bob before code
1. **Go/no-go on the staged plan (§4)**, one stage at a time.
2. **Workload capsules (§3.9):** keep self-execution at birth (the heavy phase stays serial), or
born definitions-only with the workload as the first message (recommended, so the measured
phase is the concurrent one). If the latter: new `-def` capsule variants, or strip the tail
call from the existing `workload-N.4th` files (check their other users first, per `FABRIC-3.5.md`
§XXII.2's three reachability routes)?
3. **The send word's name** (§3.8): `SEND`, `HERMES-SEND`, `KH-SEND`, something else.
4. **The multiuser factor.** The DoE as built varies concurrency × workload mode and runs once
per ISA. `FABRIC-3.md` §XXXIII.2 planned identity origin as a fixed arm, never built. Should
identities (`UNATTENDED-BIRTH` capsule identities, or WIREBIND thumbdrives) become a crossed
factor alongside concurrency and ISA, now that "users" is meant to be as important as the other
two? This decides the DoE's shape, not the OS work, and can wait until Stage H.
5. **§3.10's shared PRNG:** accept it for this pass (run order is still deterministic), or put
per-VM `RANDOM` state in scope.
**Nothing in this document authorizes an edit.** Per Captain Bob's Law, no stage starts without an
explicit instruction.
---
## 6. Execution log
- [x] **Stage A — per-VM ownership gate (§3.1).** 2026-09-23. `include/vm.h`: new
`VM.interpret_depth`. `vm_core.c`: `vm_interpret()` raises and lowers it beside the global
counter; new `sk_vm_owns_outermost_frame(vm)` (`sk_vm_switch_current_vm() == vm &&
vm->interpret_depth == 1 && vm->mode != MODE_COMPILE`) replaces `sk_vm_at_outermost_interpret()`
at both checkpoint sites in `execute_colon_word()` (drain and switch). The global counter and
the old function stay until Stage I.
**Evidence, as named in §4:** clean build on all three ISAs with no new warnings (only
the pre-existing `mkcapsule`/linker ones), hosted build clean. Foreground, visible-framebuffer
boots, with `disk/artemis.img` + `zuse-thumb-ident.img` + `bob-thumb-ident.img` reset together
before each: `logs/20260923-140120/amd64/`, `logs/20260923-140654/aarch64/`,
`logs/20260923-141303/riscv64/`. All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, zero
self-test FAIL/fault/panic, and a `dict_hash` set **identical across all three and unchanged
from the pre-Stage-A boot** (`logs/20260923-110505/amd64/`): M7.1a `0x3933d76939ef66c6`, Hera
`0x27ba08011349842b`, Hestia `0xdd1aa879b3a6c5df`, Artemis `0xe9d0408b1119d51f`. The full live
WIREBIND sequence on each ISA: hot-attach blank `bob` via QMP → `MINT` (`-1`,
restricted) → detach/reattach → `WIREBIND: rajames attached` → `S" rajames" USE` →
`1 2 + .` → `3`, delivered via `Kernel-Hermes CONSOLE-CMD-EVENT` with
`stadium_conserved(self)=true`. Clean QMP `quit` each time, no leaked processes.
note: this relay still runs through the idle pump on Hera's stack (G3, unchanged until
Stage C). With the new gate, checkpoints inside that relayed payload now correctly **fail**
the ownership test (Hera owns the stack, not `rajames~user`). Before Stage A they passed on
the global depth count, which was the latent §1 hazard.
- [x] **Stage B — trampoline idle loop, one drain at a time, parked-idle (§3.2/§3.3/§3.5).**
2026-09-23. `include/vm.h`: `VM.hermes_draining`, `VM.parked_idle`. `kernel_hermes.c`:
`sk_hermes_drain_checkpoint()` refuses while `hermes_draining` is set, and sets and clears it
around its `vm_interpret()` (set only once a message is in hand; single exit). `switch.c`:
`sk_vm_switch_entry()` is now each participant's kernel-side idle loop. It drains one of its
own messages per pass, driven by the drain's return value, not a separate pending probe
(so a non-consuming drain can't spin with no checkpoint to break it). When nothing is
drained, it sets `parked_idle` and yields to `native_switch_back_to`; a failed yield panics.
Inert in normal operation: nothing marks work yet (Stage C), so no real boot enters it.
**Finding, fixed in this stage: amd64 first-entry stack misalignment.** The first probe boot
(`logs/20260923-142526/amd64/`, kept) took **#GP (vector 13)** the moment Hestia's trampoline
called into `vm_interpret()`. `sk_vm_switch_prime` (amd64 `switch.S`) built its frame at
`top − 56`, so the entry function ran with `rsp == top` (16-aligned). SysV needs
`rsp ≡ 8 (mod 16)` at entry, so every compiler-emitted aligned SSE spill faulted. Latent since
§XXVIII Stage 2, because the old placeholder never called anything that spilled. Fixed:
frame at `top − 64` with a zeroed pad slot. aarch64 and riscv64 return through the link
register and already land 16-aligned; no change there.
**Evidence, as named in §4:** a temporary probe (kernel_main.c, written, captured,
**reverted**; `git diff` afterward shows only `vm.h`, `kernel_hermes.c`, `switch.c` and amd64
`switch.S`) queued two messages to Hestia, the first calling a colon word, and switched
Hera→Hestia. `STAGE-B PROBE: PASS` on all three: `logs/20260923-143121/amd64/`,
`-143435/aarch64/`, `-143838/riscv64/`. Sends ok, switch ok, 0 pending, `parked_idle=1`,
depth 0, context saved on her own stack, current VM back to Hera, **`SB-SEQ = 12`** (one at a
time; a nested drain gives 21), ledger `held` restored, `stadium_conserved(hera)` true. The
probe's printed line reads `seq=ld …` because this kernel's freestanding `snprintf` has no
`%lld` and shifted the arguments after it; the PASS test compares values directly and is
unaffected. Formal acceptance with the probe gone, coupled images reset before each:
`logs/20260923-144234/amd64/`, `-144701/aarch64/`, `-145224/riscv64/`. All reach
`[zuse@Hera] ok>` with zero `UNKNOWN WORD`/failures/faults, the `dict_hash` set identical
across ISAs and unchanged from Stage A, and the full mint → WIREBIND → `USE` → `1 2 + .` →
`3` relay with `stadium_conserved(self)=true`.