Each shown dead three ways (FABRIC-3.5 §XXII): the pump loop by its condition, by construction (birth cap == switch table capacity, every birth registers) and at run time (probe silent through boot, G interleave, an 8-worker DoE trial and the relay); SWITCH-MARK-WORK by callers, capsules (source and baked), experiments/tools/logs; the depth counter and back_to field by having no readers. Kept: Hera's own pump drain (last timer-paced delivery, Hera only) and the always-0 readiness CSV columns (a DoE schema call). Acceptance on amd64/ aarch64/riscv64: G interleave unchanged, cfg-0 trial fail=0 2/2 dead, relay 3, idle growth 0; dict_hash set changed by the removed word (word_count 531) and identical across ISAs. FABRIC-3.8 stages A-I done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1374 lines
96 KiB
Markdown
1374 lines
96 KiB
Markdown
# FABRIC-3.8.md — Real concurrency: finishing what the reshuffle started
|
||
|
||
**Status: OPEN — Stages A, B and FR (VM frames) done (§6); C–I not started. §3.11 ruled
|
||
(coolest VM with pending work, by physical comparison; no scheduler).** 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.
|
||
|
||
**RULED 2026-09-23, Captain Bob:** *"VM frames first, then coolest VM with work goes next"* —
|
||
and, immediately after: ***"no scheduler allowed."*** So the switch decision at Stage C is a
|
||
**physical comparison made at the ownership-gated checkpoint**. Among VMs with pending work, the
|
||
one with the lowest heat is favored over the one running, the same least-dense judgment
|
||
`stadium_admit()` already makes for eviction and the §XIX turn-attractor argument. It is **not**
|
||
a scheduler: no run queue, no priority list, no ordering data structure, no component that owns
|
||
or computes an order, nothing that runs on a timer. The comparison is re-evaluated only at the
|
||
moments the physics already exposes (checkpoints, the trampoline's between-message point, idle
|
||
points), and the heat it reads is the heat the fleet already conserves. The per-tick readiness
|
||
accrual and the fixed `50` threshold are removed at Stage C, not tuned.
|
||
|
||
~~**This is a design question for Captain Bob before Stage C**~~ (settled above) (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.
|
||
|
||
**RULED 2026-09-23: VM frames land first, as Stage FR, before Stage C.** Concrete design:
|
||
|
||
- **`include/starkernel/vm/vm_frame.h`**, new: `typedef struct VMFrame { int interpret_depth;
|
||
int hermes_draining; int parked_idle; struct VM *attrib_vm; } VMFrame;`. `VM` gets one field,
|
||
`VMFrame frame;`. The three loose fields Stages A and B added (`interpret_depth`,
|
||
`hermes_draining`, `parked_idle`) **move into it**, since they are exactly this cluster. All
|
||
their uses are renamed; behavior is unchanged.
|
||
- **Stack-scoped log attribution.** `attrib_vm` lives in the frame of the VM that **owns the
|
||
physical stack** (`sk_vm_switch_current_vm()`), and means "the innermost VM interpreting on
|
||
this stack." `vm_interpret(vm, …)` saves and restores `owner->frame.attrib_vm` instead of the
|
||
global. A nested `VM-EXEC` into Artemis on Hera's stack still attributes to Artemis, exactly
|
||
as today. `vm_log_attributed_vm()` returns `current->frame.attrib_vm` (or `current` itself when
|
||
no interpret is live on its stack). Because each stack carries its own chain, a switch can
|
||
never leave attribution pointing at the VM that just switched out. Before the switch
|
||
mechanism is seeded at boot (no current VM), the existing global path is kept as the fallback.
|
||
- **Not in the frame this stage:** `g_prng_state` (vendored `starforth_words.c`, shared with
|
||
the hosted build, open ruling §5 item 5), the compile-time statics (covered by the Stage A
|
||
`MODE_COMPILE` gate; `FABRIC-4.md` scope), and `g_vm_interpret_depth` (retired in Stage I).
|
||
|
||
**REVISED 2026-09-23, Captain Bob: *"give frames IDs and call by ID"*; chose *"frames stack per
|
||
VM, each with an ID."*** This supersedes the single-struct shape above (written, never booted):
|
||
|
||
- **`VMFrameEntry { VMFrameID id; VMUuid vm_id; uint32_t kind; }`**. `id` is a system-wide
|
||
monotonic 64-bit frame ID (0 = none, never reused). `vm_id` is the VM executing at that level,
|
||
**held as an ID, never a pointer**, so nothing can dangle after a VM dies. `kind` is
|
||
`VM_FRAME_INTERPRET` or `VM_FRAME_DRAIN`.
|
||
- **`VMFrameStack { entries, count, cap, max }`**, one per VM, embedded as `VM.frames`. It holds the
|
||
nested interpret levels running **on that VM's own native stack** (a nested `VM-EXEC` into
|
||
Artemis on Hera's stack is a frame on *Hera's* stack whose `vm_id` is Artemis). Growable like
|
||
`VMCallState`, but grown by allocate-copy-free rather than `sf_realloc` (which orphans the old
|
||
block). Before the switch mechanism is seeded, a boot-time stack in `vm_core.c` stands in.
|
||
- **`vm_interpret()` pushes one frame and pops it by ID.** It keeps its own frame's ID and, at
|
||
exit, checks the top is that ID before popping. A mismatch is logged as frame corruption.
|
||
That's the "call by ID" integrity check across switches.
|
||
- **Everything the loose fields held is now derived from the stack:**
|
||
- ownership gate (§3.1): `current == vm`, the stack holds exactly one frame, and its `vm_id` is
|
||
`vm`;
|
||
- drain guard (§3.3): a `VM_FRAME_DRAIN` frame for `vm` is already on the current stack (the
|
||
drain pushes its interpret as `DRAIN` kind);
|
||
- parked-idle (§3.5): `VM_STATE_SWITCHED_OUT` **and the top frame is `VM_FRAME_IDLE`**. The
|
||
trampoline pushes that frame just around its idle yield and pops it on resume, *before*
|
||
draining. An empty stack alone is not enough: Hera parked at an idle point (Stage F) has zero
|
||
interpret frames but her whole REPL call chain live on her stack. The frame records *where*
|
||
the VM is parked, which the flag used to;
|
||
- log attribution: the top frame's `vm_id` on the current owner's stack, **resolved by ID**
|
||
through the VM registry. An empty stack means NULL, which maps to `HADES`.
|
||
- **Failure rules.** A frame push that can't grow **fails closed**: log it, set `vm->error`,
|
||
don't interpret. Interpreting without a frame would silently break the gate and attribution.
|
||
An initial capacity is allocated at birth so the hot path almost never allocates. A
|
||
**pop-by-ID mismatch** means corruption (every stack is LIFO, since resume restores the owner as
|
||
current): log both IDs, count it, and remove *our own* ID wherever it sits rather than popping
|
||
someone else's frame. No code holds a `VMFrameEntry *` across an interpret body (growth
|
||
copies), only the ID. The frame-ID counter has a single writer: `vm_interpret()`, mainline
|
||
only, with no interrupt-context caller (checked by grep).
|
||
|
||
**Evidence, named in advance (probe, reverted after capture).** Two temporary C words, `SB-GO`
|
||
(switch to Hestia) and `SB-BACK` (switch back to whoever switched in), plus `SB-ATTRIB` (record
|
||
`vm_log_attributed_vm()`'s name). These are *probe-only* control words, never committed; they
|
||
exist to force the interleaving that Stage C will produce naturally. Hestia is queued a
|
||
message `SB-ATTRIB SB-BACK SB-ATTRIB`, then Hera runs `SB-ATTRIB SB-GO SB-ATTRIB SB-GO`. The
|
||
recorded sequence must be **Hera, Hestia, Hera, Hestia**. The third entry is the discriminator:
|
||
Hera resuming while Hestia is still parked mid-payload. **Baseline first:** the same probe on
|
||
the pre-frame code must *fail* on that third entry (reading Hestia), proving the test detects
|
||
the defect before the fix is claimed.
|
||
|
||
### 3.13 Stage C design: heat under self-driven execution, and the switch decision (FOR REVIEW, 2026-09-23)
|
||
|
||
§3.11 ruled *coolest VM with pending work goes next, no scheduler*. Before designing the
|
||
comparison, I traced (read-only) what that heat actually does once a VM runs its own mail on its
|
||
own stack. **The finding blocks Stage C as written.** Everything below is traced unless marked as
|
||
a proposal.
|
||
|
||
#### 3.13.1 What the trace found
|
||
|
||
**G9: when a VM runs its own mail, VM-physics heat doesn't move.**
|
||
|
||
- `vm_physics_touch()` (`capsule_vm_physics.c:284`) is the only thing that moves
|
||
`execution_heat_q48` between live VMs (apart from `retire`, which moves heat to the root on
|
||
kill). Its only callers are Hera's three orchestration verbs: `VM-STEP`, `VM-EXEC` and
|
||
`VM-CALL` (`mama_forth_words.c:723/811/946`). Each one touches the *target* at dispatch.
|
||
- `sk_hermes_drain_checkpoint()` and the trampoline (`switch.c`) call nothing in physics. A
|
||
VM that runs a kernel-Hermes payload on its own stack, which is the whole point of Stages
|
||
C–H, leaves every heat value exactly where it was.
|
||
- Starting state (`vm_physics_init:188`): Hera holds all of `Q48_ONE` (0x10000 raw) and every
|
||
other VM starts at 0.
|
||
- **Consequence:** once the DoE is rebuilt on `SEND` (§3.9), nothing ever calls `VM-EXEC`. Every
|
||
worker stays at 0 and Hera stays at `Q48_ONE` forever. "Coolest with work" becomes a permanent
|
||
tie among all workers, and whatever breaks that tie is the scheduler in all but name. That
|
||
violates *"no scheduler allowed."*
|
||
|
||
**The transfer law barely moves heat at switch granularity** (`vm_physics_touch`, lines
|
||
301–343):
|
||
|
||
- The formula is `amount = elapsed_beats × slope >> 16`, with slope seeded at `65536/3` = 21845
|
||
raw.
|
||
- `elapsed_beats` counts fleet beats since *this VM's own last touch*. Every touch resets it
|
||
(`last_active_tick = now`), and a VM's first-ever touch moves nothing (`touched == 0`).
|
||
- Worked out: elapsed 1, 2 or 3 moves **0**. 3 × 21845 = 65535, and 65535 >> 16 = 0. Elapsed 4
|
||
moves 1 raw unit (1/65536 of the fleet). Elapsed 100 moves 33 raw units, about 0.05%.
|
||
- The truncated remainder is thrown away, not carried. So touching often moves *nothing*, which
|
||
rules out the obvious "touch the running VM at each of its beats": elapsed would always be 1.
|
||
- The seed's own comment (lines 138–144) already says the slope is **"UNVALIDATED after the
|
||
ns-to-tick restatement… a real re-tune is DoE work (item 5.1)."**
|
||
|
||
**What a beat is, and that every VM produces them** (checked because Bob ruled out the timer):
|
||
|
||
- The fleet beat (`fleet_heartbeat_tick_count`) advances once per `vm_heartbeat_run_cycle()` →
|
||
`vm_tick()` → `vm_physics_heartbeat_tick()`.
|
||
- That runs every `HEARTBEAT_CHECK_FREQUENCY` (256, Kconfig) words of **a VM's own execution**.
|
||
The per-VM `check_counter` is bumped at both `vm_core.c:909` (each colon word) and `:1286`
|
||
(each word parsed by `vm_interpret`).
|
||
- `heartbeat_enabled = 1` and `worker = NULL` are set for every VM in `vm_init`
|
||
(`vm_bootstrap.c:305/308`). The pthread worker path is hosted-only.
|
||
- **So the beat is execution-paced, produced by whichever VM is running, and not the timer.**
|
||
The *timer* ISR is a different function, `heartbeat_tick()` in `heartbeat.c`. Today it calls
|
||
`sk_vm_switch_signal_tick()` (`heartbeat.c:179`), which is the readiness path Bob ruled out.
|
||
In this doc, "beat" always means the execution-paced one.
|
||
- Each VM also keeps its own beat count, `vm->heartbeat.tick_count` (`vm_runtime.c:118`), so
|
||
the beats a VM produced during one turn can be read directly.
|
||
|
||
**Stadium heat doesn't answer "whose turn" either.**
|
||
|
||
- A message's Stadium heat is pulled from the **sender's** reservoir at `sk_hermes_alloc()`, and
|
||
the cell's owner is the sender. On release it goes back to the **sender** (`stadium_evict()`).
|
||
- The recipient's Stadium state never changes when it receives or drains.
|
||
- `sk_hermes_decay()` is only called from the boot self-tests (`kernel_main.c:969/1006/1033`).
|
||
- So Stadium heat measures a VM's *outstanding sent mail*. It says nothing about how much CPU a
|
||
recipient has had. It's listed for completeness, not proposed.
|
||
|
||
**Stage C reverses a recorded decision.** `capsule_vm_switch_signal.h:28–34` deliberately did
|
||
*not* use VM-physics heat for switching, on timescale grounds: word-level dispatch over millions
|
||
of executions versus instant-by-instant readiness. Bob's §3.11 ruling overrides it. The
|
||
truncation above is that timescale gap in concrete form, and §3.13.2 is how it would be closed.
|
||
|
||
**The existing law measures age, not work** (derived from the code, worth stating because it
|
||
changes what "coolest" means):
|
||
|
||
- A touch pulls in proportion to beats *since the last touch*.
|
||
- Over a VM's life, the sum of its pulls is its age in beats × slope, however often or rarely it
|
||
actually ran. How often it ran only changes *when* it gets charged.
|
||
- It loses heat when others are touched, in proportion to its share.
|
||
- So under the current law, "coolest" ranks VMs by charge timing and share, not by CPU used.
|
||
|
||
#### 3.13.2 Proposal: charge a VM for the beats it produced (ruling C-1)
|
||
|
||
**Proposed: charge the running VM once at each of its own beats, keeping a carry.**
|
||
|
||
- At every beat a VM produces while it is the switch-current VM, it is charged one beat's worth:
|
||
`slope` raw units, plus the carry left over from its previous beats.
|
||
- The whole units are pulled from the rest of the live fleet in proportion to each VM's heat,
|
||
through the existing conservative `vm_physics_transfer()`. Nothing is created, and fleet K is
|
||
still conserved.
|
||
- The fraction below one unit stays in a per-VM carry. That carry is what makes a per-beat charge
|
||
work where a per-beat *touch* can't (see row (d)).
|
||
- It would be one new entry point in `capsule_vm_physics.c`, e.g. `vm_physics_charge(vm_id)`,
|
||
called from the beat path (`vm_physics_heartbeat_tick()`'s caller) for the running VM.
|
||
- It is kernel-side and invisible to anything a VM executes.
|
||
- Result: **the VM that has done the most work is the hottest,** and its heat rises *while it
|
||
runs*. That matches the plain meaning of "coolest VM with work goes next."
|
||
|
||
**Why per beat and not at switch-out (corrected before review).** An earlier draft of this
|
||
section charged only at switch-out. That design is **purely cooperative**, which contradicts
|
||
§3.11:
|
||
|
||
- During a turn nothing would change. The running VM's stored heat stays where it was at
|
||
switch-in, and the waiting VMs are untouched.
|
||
- So every decision taken during the turn repeats the answer from switch-in. The running VM is
|
||
never displaced, and a long payload keeps the CPU until it finishes.
|
||
- Charging per beat is what makes the running VM's own execution displace it, which is the
|
||
preemptive effect.
|
||
|
||
**Worked example.** Two workers start at 0 with equal work.
|
||
|
||
- The running worker gains 21845/65536 of a unit per beat, so a whole unit moves every 3 beats.
|
||
- At that point the other worker is strictly cooler and takes over at the next beat decision.
|
||
- They alternate every ~3–6 beats. The turn length comes out of the physics; nobody chose it.
|
||
|
||
| Option | Moves heat under self-driven execution? | Preemptive in effect? | Side effects |
|
||
|---|---|---|---|
|
||
| **(a) Charge at each of the running VM's own beats, with carry** (proposed) | Yes, in proportion to work | **Yes**, because heat rises during the turn | New entry point. Proposed: records **no** `fleet_window` samples (see "Frozen slope" below). |
|
||
| (a′) Charge at switch-out by `tick_count` delta | Yes, in proportion to work | **No**, unless decisions compare *projected* heat: stored heat plus the charge for the beats produced so far this turn. With projection it is equivalent to (a), but without any mid-turn transfer. | Same as (a). |
|
||
| (b) `vm_physics_touch()` on switch-in (the `VM-EXEC` analogue) | Yes, but it charges *time away*, not work (see "age, not work"), and needs ≥4 beats away before anything moves | No | Writes `fleet_window` samples, which changes slope adaptation and comparability with existing DoE heat columns. |
|
||
| (d) `vm_physics_touch()` at each beat | **No.** Elapsed is always 1 and the remainder is discarded, so 0 moves, forever | — | Rejected on the arithmetic. Row (a) differs only by keeping the carry and not resetting the elapsed count. |
|
||
|
||
**Truncation (ruling C-2).** With the carry, nothing is lost to rounding under (a). The other
|
||
choices:
|
||
|
||
- (i) no carry: then (a) moves nothing, the same as (d);
|
||
- (iii) refit the slope at beat scale. That is DoE item 5.1 and **out of scope** here.
|
||
|
||
The carry applies to the new charge only. `vm_physics_touch()` is untouched, so `VM-EXEC`
|
||
transfers stay byte-for-byte as today.
|
||
|
||
**Frozen slope (part of C-1).**
|
||
|
||
- With no samples recorded by the charge, and no `VM-EXEC` in the rebuilt DoE, the adaptive
|
||
`fleet_transfer_slope_q48` **never updates under self-driven load**. It stays at the
|
||
unvalidated seed.
|
||
- Recording samples would let it adapt. It would also change what the existing heat columns mean
|
||
compared with past campaigns.
|
||
- Bob should choose this knowingly.
|
||
|
||
**Two laws on one ledger.** Artemis, and any participant that Hera also `VM-EXEC`s, is both
|
||
*touched* (the dispatch law) and *charged* (the work law). Both go through `vm_physics_transfer()`
|
||
on the same heat, so conservation holds, but the VM's heat then mixes two meanings. Proposed:
|
||
accept this and document it. `VM-EXEC` into a participant becomes rare once §3.8's `SEND` exists.
|
||
|
||
**Where the charge starts.** Each VM's charge baseline is its beats at the moment it becomes
|
||
switch-current. Hera's is seeded at `sk_vm_switch_set_current()`, so her first charge doesn't
|
||
cover every beat since boot. The clamp to `others_total` would make that harmless anyway, but it
|
||
should be defined rather than left to the clamp.
|
||
|
||
**What a beat counts** (corrected at the start of C-a, from reading `vm_core.c:909` in context).
|
||
|
||
- A beat is every 256 words a VM executes. That counts each word dispatched inside a colon body,
|
||
primitives included (the `check_counter` bump in `execute_colon_word`'s inner loop), plus each
|
||
word parsed at top level by `vm_interpret` (`:1286`).
|
||
- The earlier draft said primitives inside colon bodies weren't counted. That was wrong.
|
||
- The "words" figures below are therefore beat counts × 256 executed words.
|
||
|
||
**Hera's reservoir mass (ruling C-4, open, arithmetic shown).**
|
||
|
||
- Hera starts with all of `Q48_ONE` as the fleet's structural root (`vm_physics_init`'s "cold mass
|
||
added to a closed system").
|
||
- At slope 1/3 raw per beat, the workers together have to produce about `65536 × 3 × (N/(N+1))`
|
||
beats before Hera cools to a worker's level. For N = 8 that is ≈175k beats, ≈45M
|
||
executed words (see "What a beat counts" above).
|
||
- Until then, Hera is never the coolest while any worker has work, **so the console gets no turn
|
||
under load.** This doesn't block Stage C's relay evidence, because the relay runs while the
|
||
workers are idle. It does block Stage F/H usability.
|
||
- Options for Bob:
|
||
- (α) accept it;
|
||
- (β) compare the heat each VM has been *charged* (its turn heat) rather than its absolute
|
||
heat, so the root's structural mass doesn't count against it;
|
||
- (γ) something else Bob rules.
|
||
- I have no recommendation yet. (β) is a different physical quantity, and Bob should choose it
|
||
knowingly.
|
||
|
||
#### 3.13.3 Proposal: the decision (ruling C-3 on ties)
|
||
|
||
**Eligible** means the VM is a registered participant, is not the one running, is not dead or
|
||
stillborn, and **either** of the following holds:
|
||
|
||
- its kernel-Hermes pending count is > 0, read directly from the queue
|
||
(`sk_hermes_pending_count`), not from a `has_work` flag. The queue *is* the eligibility, so
|
||
`SWITCH-MARK-WORK` and G1's missing mark become unnecessary;
|
||
- it is switched out, its **frame stack is non-empty, and its top frame is not
|
||
`VM_FRAME_IDLE`**. That means it was displaced mid-task, or it is Hera with her REPL chain live.
|
||
- Without this clause, a VM preempted in the middle of a payload never gets picked again. This
|
||
is what the frame stacks were for.
|
||
- A never-entered VM has an empty stack, so it is not eligible unless it has mail.
|
||
- Hera parked at an idle point pushes an IDLE frame (§3.12, and §3.13.5 below), so she is not
|
||
eligible there unless she has mail or input.
|
||
|
||
**Switch when** some eligible VM is **strictly** cooler than the one running. If it is equal, the
|
||
running VM stays: a tie with the incumbent is "no reason to move," not a coin toss.
|
||
|
||
**Ties among several equally cool candidates (ruling C-3).**
|
||
|
||
- **Proposed:** the candidate that has been *waiting longest*, measured by one single-writer
|
||
event count.
|
||
- Each message is stamped with it when it is admitted (a new `SkHermesMessage` field).
|
||
- Each VM is stamped with it when it is displaced mid-task.
|
||
- A candidate's wait stamp is the minimum of its oldest pending mail and its displacement.
|
||
- Every stamp is unique, so the result is unique.
|
||
- This is the arrival order of the mail itself, the kernel's FIFO. It is a fact about the
|
||
messages, not a ranking of VMs.
|
||
- **Bob to rule** whether arrival order counts as "an ordering" under *"no scheduler allowed."*
|
||
- Rejected outright: slot index, UUID and registry order. Each of those is a fixed order of VMs.
|
||
|
||
**Testable "not a scheduler" criterion.**
|
||
|
||
- The decision is a pure function of physical state: heat, pending mail, frames and wait stamps.
|
||
It keeps no queue of its own and remembers nothing between decisions.
|
||
- It must return the same VM whichever order the participant table is walked.
|
||
- This is Stage C-b's evidence (below).
|
||
|
||
**When the decision is made** (only at moments the physics exposes, never on a clock):
|
||
|
||
1. **The running VM's own beat.** The beat sets a per-VM "decide" flag. The next *owned*
|
||
checkpoint consumes it (`sk_vm_owns_outermost_frame`). There are two checkpoint sites:
|
||
- the existing one in `execute_colon_word`;
|
||
- the between-words point in `vm_interpret_kind`'s loop, which needs a new gated checkpoint
|
||
so that a payload made only of primitives can be displaced.
|
||
2. **Mail arrival** to a VM that would then win.
|
||
3. **The trampoline's between-message point**, as already noted in `sk_vm_switch_entry()`.
|
||
4. **Idle points** (Stage F).
|
||
|
||
**Why not every word.** Under (a), heat changes only at beats, and eligibility changes only when
|
||
mail arrives or a queue empties. Between those events every decision would repeat the last one,
|
||
so deciding every word is pure overhead. Deciding at the beat means a turn lasts at least one beat
|
||
unless the VM runs out of work first.
|
||
|
||
**Flagged honestly:** that minimum is the existing Loop #7 cadence constant
|
||
(`HEARTBEAT_CHECK_FREQUENCY`). It is not new, but it does act as a floor on turn length.
|
||
|
||
**The switch target on an empty queue.**
|
||
|
||
- Today the trampoline yields to whoever switched in (`native_switch_back_to`).
|
||
- **Proposed:** it asks the decision instead, and falls back to `native_switch_back_to` only when
|
||
no VM is eligible.
|
||
|
||
#### 3.13.4 What removing the timer path changes
|
||
|
||
- `heartbeat.c:179`'s `sk_vm_switch_signal_tick()` call is removed. The ISR returns to its
|
||
pre-§XXVIII minimal shape, so the timer no longer decides anything.
|
||
- These are removed:
|
||
- the readiness accrual;
|
||
- `SK_SWITCH_READINESS_THRESHOLD` (50);
|
||
- `has_work`;
|
||
- `take_pending`.
|
||
- `note_switch_performed` is kept only as the counter.
|
||
- **Effect on the DoE CSV** (`doe_log.c:205–230`):
|
||
- `switch_count` and `switch_current_slot` survive.
|
||
- `switch_ticks_since` loses its meaning. Proposed: it becomes beats since the last switch.
|
||
- The three `*_readiness` columns die.
|
||
- Found while tracing, *not fixed*: those columns and the heat columns still look up
|
||
`"Hermes"`, which no longer exists as a VM since the reshuffle, so those values are always 0
|
||
today.
|
||
|
||
#### 3.13.5 The relay needs the idle point in the same stage (traced)
|
||
|
||
Stage C-b's own evidence, the WIREBIND relay delivered by a real switch, **can't be reached
|
||
without §3.7's idle point** (Stage F):
|
||
|
||
- The console line is forwarded from C. `sk_hermes_send_one(vm->stadium_vm_id, paired.vm_id,
|
||
CONSOLE_CMD, …)` at `repl.c:1510`, then `goto out`. Hera interprets nothing and goes straight
|
||
back to polling.
|
||
- Today the `~user` VM's mail is delivered by the idle pump (`repl.c:~909–928`), which drains
|
||
every live VM *on Hera's stack*. That is G3.
|
||
- Once the pump skips participants (C-b), the only places a switch can happen are owned word
|
||
checkpoints and beats. Hera at her poll loop reaches neither: she is running C, not words.
|
||
- So nothing switches, the `~user` VM's `CONSOLE-CMD-EVENT` sits in its queue, and the relay
|
||
breaks.
|
||
|
||
**Proposed:** fold §3.7's idle point into C-b.
|
||
|
||
- The pump loop's own location (the idle point, Hera between lines) becomes a decision point.
|
||
Hera pushes an IDLE frame, consults the decision, and switches to the coolest eligible VM
|
||
instead of draining participants on her stack. The pump then shrinks to Hera's own queue.
|
||
- Stage F then only has to show that work proceeds with the console idle.
|
||
- The alternative is to keep the pump draining participants until F. That keeps G3's
|
||
borrowed-stack delivery alive through C-b, so C-b's relay evidence would prove nothing about
|
||
switching.
|
||
|
||
**Hera's eligibility at the idle point.** Keyboard or serial input pending is Hera's mail. At
|
||
the idle point she is eligible when input is pending (the REPL's own input poll), exactly as a
|
||
worker is eligible when its kernel-Hermes queue is non-empty. **Ruling C-5**, below.
|
||
|
||
#### 3.13.7 Finding at Stage G: under C-4 (α), a sender can't dispatch a batch (2026-09-23)
|
||
|
||
`SEND` is built (§3.8). The naive interleave test, typed at `[zuse@Hera] ok>` with ordinary
|
||
words only (`logs/20260923-201602/amd64/`):
|
||
|
||
```
|
||
S" workload-1-lite.4th" S" wa" WORKER-BIRTH . → 1
|
||
S" workload-1-lite.4th" S" wb" WORKER-BIRTH . → 1
|
||
S" : LA 40 0 DO 65 EMIT 300 0 DO LOOP LOOP ; LA" S" wa" SEND . S" : LB 40 0 DO 66 EMIT 300 0 DO LOOP LOOP ; LB" S" wb" SEND .
|
||
→ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1 BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB1 ok
|
||
```
|
||
|
||
`SEND` works: each payload is delivered by a switch and runs on the worker's own stack. But the
|
||
run is **serial**, and the mechanism is exactly C-4 (α):
|
||
|
||
- The first `SEND` makes `wa` eligible at heat 0.
|
||
- Hera (heat ≈ `Q48_ONE`) is displaced at her next owned checkpoint, before she even prints that
|
||
`SEND`'s `1`.
|
||
- She is eligible only as "parked mid-task" and is the hottest VM, so she gets the CPU back only
|
||
once `wa` has no work left. Then she sends to `wb`.
|
||
|
||
The rebuilt DoE driver (Stage H) would serialize the same way: it is Hera sending to N workers in
|
||
a loop. So would the console under load (already recorded under C-b).
|
||
|
||
**Needs Captain Bob's ruling (C-4 reopened):**
|
||
|
||
- **(β) compare work heat, not absolute heat.** Work heat is each VM's cumulative heat pulled in
|
||
by its own per-beat charges (Stage C-a). It is conserved-transfer bookkeeping already happening,
|
||
so no new physics, but it is a different quantity than the one "coolest" has meant so far.
|
||
Hera's structural endowment doesn't count.
|
||
- Predicted: Hera keeps the CPU while she has work, because on a tie the incumbent stays and the
|
||
workers' heat is still 0. She sends both messages, and the workers then alternate by work
|
||
heat.
|
||
- Hera with pending console input is cooler than busy workers, so the console gets turns under
|
||
load.
|
||
- **(δ) mail arrival stops triggering a decision for the running sender.** Decisions happen only
|
||
at beats, the trampoline and idle points.
|
||
- Batches sent within one beat all land before anyone runs.
|
||
- Where the beat boundary falls decides whether a given batch lands intact. It's deterministic,
|
||
not guaranteed.
|
||
- It doesn't address the console starvation.
|
||
- **(ε) keep (α)** and prove interleaving with a chain instead: worker A's payload `SEND`s B's.
|
||
That satisfies Stage G's evidence, but leaves the DoE driver and the console serialized.
|
||
|
||
**RULED 2026-09-23, Captain Bob: (β) work heat.** The decision compares
|
||
`vm_physics_work_heat_of()`, not `vm_physics_heat_of()`. Absolute heat still moves, is still
|
||
conserved, and still appears in the DoE heat columns; it just no longer decides turns. The
|
||
prediction held on all three ISAs: see the Stage G entry in §6.
|
||
|
||
**Refined at Stage H, RULED 2026-09-23, Captain Bob: "floor on contact."** Two changes to what
|
||
work heat is, both forced by a live stall (Stage H entry, §6):
|
||
|
||
- **Assessed, not moved.** Work heat accrues each beat's assessed charge (the slope, in the
|
||
carry's sub-unit scale), whether or not the rest of the fleet held heat to move.
|
||
- As first built, it counted only heat actually moved.
|
||
- A busy-waiting Hera, with nobody else holding absolute heat, then stayed at 0 forever, tied
|
||
the workers, and never yielded.
|
||
- **Floor on contact.** When a VM with no work gains some (its kernel-Hermes queue goes from
|
||
empty to one message, and it is neither running nor parked mid-task), its work heat is raised
|
||
to the lowest work heat among the VMs that have work right now: the running one, and every
|
||
eligible participant.
|
||
- Without the floor, a fresh worker at 0 would run alone until it had done as much work as
|
||
Hera has since boot. The batch would start serially.
|
||
- No tuning constant.
|
||
- Rejected alternatives: decay/dissipation (needs a rate constant), and assessed charge
|
||
without the floor.
|
||
|
||
#### 3.13.6 Stage C, split
|
||
|
||
This splits the single C row in §4 in two:
|
||
|
||
- **C-a** is physics only: heat starts moving and nothing switches yet.
|
||
- **C-b** is the decision: pump skip, eligibility read from the queue, the ISR path removed, and
|
||
the idle point (§3.13.5).
|
||
|
||
C-a is observable on its own, and doing it first proves the heat signal is alive before anything
|
||
depends on it. The §4 table below is updated to show this split, **pending rulings C-1 to C-4.**
|
||
|
||
---
|
||
|
||
## 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. |
|
||
| **FR** | §3.12 VM frames: `VMFrame` header; Stage A/B fields move into it; stack-scoped log attribution. | Probe `SB-ATTRIB`/`SB-GO`/`SB-BACK` (reverted): attribution sequence Hera, Hestia, **Hera**, Hestia on all three ISAs, and the **same probe fails on the pre-frame baseline** (third entry reads Hestia). Then formal acceptance: `dict_hash` unchanged, relay works. |
|
||
| ~~**C**~~ | ~~§3.4 eligibility marked + pump skips participants (one commit).~~ Split into C-a/C-b by §3.13 (pending rulings C-1–C-4). | (see C-a, C-b) |
|
||
| **C-a** | §3.13.2 the running VM is charged at each of its own beats, with carry. Physics only; switching unchanged. | Temporary probe (reverted), which switches Hestia in with the Stage FR probe's `SB-GO`/`SB-BACK` words, because C-a itself switches nothing. Hestia runs a colon-word payload that produces ≥ 8 beats. Her `VM-HEAT` is sampled **at a beat during the turn** and again after it, and must rise from 0 by `beats × slope` (whole units, remainder kept in carry). The rest of the live fleet must fall by the same total. `vm_physics_conserved()` holds, and the `fleet_window` sample count is unchanged. **Baseline first:** the same probe on current code shows heat unchanged (proves G9). Then acceptance: `dict_hash` unchanged and the relay works. |
|
||
| **C-b** | §3.13.3 decision at owned checkpoints, beats, the trampoline and **the idle point** (§3.13.5, which absorbs Stage F's §3.7). Eligibility comes from the queue or a non-IDLE top frame. The pump skips participants. The ISR switch path and readiness are removed (§3.13.4). | The WIREBIND relay still works, now delivered by a real switch: `switch_count` increments across the relay and the `~user` VM's own drain line appears. Ledger and `stadium_conserved()` hold. `heartbeat_tick()` contains no switch call (source). **Walk-order probe** (temporary): every decision is also computed with the participant table walked in reverse; disagreements = 0. No `native_stack_saved_sp` ever inside Hera's stack range (temporary probe). No switch storm (log growth at idle `ok>`, QMP `send-key` computes a value). |
|
||
| **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. *(If C-5 is ruled yes, the mechanism lands in C-b and F keeps only this evidence.)* | 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) — **RULED 2026-09-23: `SEND`.**
|
||
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.
|
||
|
||
6. **C-1 (§3.13.2):** how heat is charged under self-driven execution.
|
||
- Proposed (a): the running VM is charged at each of its own beats. Switch-out-only charging
|
||
is purely cooperative, unless decisions compare projected heat (a′).
|
||
- Also: charge records no `fleet_window` samples, which means the slope stays frozen at its
|
||
unvalidated seed under self-driven load. Or record them and let it adapt.
|
||
7. **C-2 (§3.13.2):** a per-VM carry on the new charge only. Without it, (a) moves nothing.
|
||
8. **C-3 (§3.13.3):** ties. The incumbent stays on a tie with itself. Among several candidates:
|
||
whether "waiting longest," by a single-writer arrival stamp, is acceptable under *"no scheduler
|
||
allowed,"* or whether Bob wants something else. Slot, UUID and registry order are rejected.
|
||
9. **C-4 (§3.13.2) — RULED 2026-09-23: (β) work heat** (§3.13.7; first taken as (α) at C-b,
|
||
reopened at Stage G). Hera's reservoir mass means the console gets no turn under worker load.
|
||
Options are (α) accept it, (β) compare charged heat instead of absolute heat, or (γ) something
|
||
else. No recommendation.
|
||
10. **C-5 (§3.13.5):** fold the idle point (Stage F's §3.7) into C-b, since the relay can't be
|
||
delivered by a switch without it. Also: Hera's pending console input counts as her mail for
|
||
eligibility.
|
||
|
||
**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`.
|
||
- [x] **Stage FR — VM frames: a frame stack per VM, each frame with an ID (§3.12).** 2026-09-23.
|
||
Captain Bob: *"give frames IDs and call by ID"*, *"frames stack per VM, each with an ID."*
|
||
New `include/starkernel/vm/vm_frame.h` / `src/starkernel/vm/vm_frame.c`: `VMFrameEntry`
|
||
`{ VMFrameID id; VMUuid vm_id; kind }` (kinds `INTERPRET`/`DRAIN`/`IDLE`); `VMFrameStack`
|
||
embedded as `VM.frames`, reserved at birth (`vm_init_with_host`), freed in `vm_cleanup`,
|
||
grown by allocate-copy-free. `vm_interpret()` → `vm_interpret_kind()`: pushes one frame onto
|
||
the running stack's owner (`vm_frame_current_stack()`; a boot stack before seeding), holds
|
||
only its ID, **fails closed** if the push can't grow, and **pops by ID** at exit (a mismatch
|
||
is printed as `VM FRAME CORRUPTION` and only our own frame is removed). The Stage A/B loose
|
||
fields are gone, all derived now: the ownership gate is "the owner's stack holds exactly one
|
||
frame and it's `vm`'s"; the drain guard is "a `DRAIN` frame for `vm` is on the running
|
||
stack"; parked-idle is a `VM_FRAME_IDLE` frame the trampoline pushes around its yield and
|
||
pops before draining; log attribution is the top frame's `vm_id`, **resolved by ID** through
|
||
the VM registry (empty stack → NULL → `HADES`). The attribution global is removed.
|
||
**Evidence, as named in §4:**
|
||
- **Baseline (pre-frame code, probe only):** `logs/20260923-150901/amd64/` →
|
||
`attribution=HSSAS want=HSHAS FAIL`. The old global named Hestia while Hera resumed,
|
||
which is exactly the defect.
|
||
- **With frames, probe on all three ISAs:** `logs/20260923-152109/amd64/`,
|
||
`-152424/aarch64/`, `-152828/riscv64/` → `attribution=HSHAS hestia_idle_top=1
|
||
hera_frames=0 PASS`. That covers interleaved attribution, nested `VM-EXEC` into Artemis
|
||
still attributed to Artemis, Hestia parked with an `IDLE` top frame, and Hera's frames
|
||
back to zero. The probe (`SB-GO`/`SB-BACK`/`SB-ATTRIB`, probe-only control words) was
|
||
reverted (`git checkout -- src/starkernel/kernel_main.c`).
|
||
- **Formal acceptance without the probe, coupled images reset before each:**
|
||
`logs/20260923-153227/amd64/`, `-153644/aarch64/`, `-154208/riscv64/`. All reach
|
||
`[zuse@Hera] ok>`, with zero `UNKNOWN WORD`/failures/faults, **zero `VM FRAME CORRUPTION`
|
||
and zero refused interprets**. The `dict_hash` set is identical across ISAs and unchanged
|
||
from Stages A/B. The full mint → WIREBIND → `USE` → `1 2 + .` → `3` relay works (the path
|
||
with a nested frame on Hera's stack). Clean build on all three plus hosted, no new warnings.
|
||
- [x] **Stage C-a — the work law: the running VM is charged at each of its own beats, with carry
|
||
(§3.13.2).** 2026-09-23. Captain Bob: *"commit it and start stage C-a"*, which accepts
|
||
§3.13.2's proposals C-1(a) and C-2 as written: charge at each beat, keep a carry, record no
|
||
`fleet_window` samples. C-3, C-4 and C-5 are C-b's rulings and are still open.
|
||
Changes:
|
||
- `capsule_vm_physics.c` gets a new `vm_physics_charge(vm_id)`. It adds one beat's slope
|
||
to a new per-VM `charge_carry`, then pulls the whole units from the rest of the live
|
||
fleet in proportion to each VM's heat, through `vm_physics_transfer()`.
|
||
- Clamped to what the others hold. That shortfall is dropped, as in touch, so the carry
|
||
can't grow without bound while one VM runs alone.
|
||
- Units lost to the proportional split's rounding go back into the carry.
|
||
- No `fleet_window` sample and no `last_active_tick` change, so `vm_physics_touch()` and
|
||
`VM-EXEC` behave byte-for-byte as before.
|
||
- `vm_runtime.c`'s `vm_tick()` calls the charge right after `vm_physics_heartbeat_tick()`,
|
||
only when the VM producing the beat is `sk_vm_switch_current_vm()`. A VM running nested
|
||
inside someone else's `VM-EXEC` stays on the dispatch law.
|
||
- §3.13.2 "What a beat counts" is corrected. `vm_core.c:909` is in `execute_colon_word`'s
|
||
inner loop, so primitives inside colon bodies *are* counted.
|
||
|
||
**Evidence, as named in §4.** A temporary probe (`kernel_main.c`: `SB-GO`/`SB-BACK` plus
|
||
`SB-HEAT`, which records Hestia's heat, the rest of the fleet's heat, her beat count and
|
||
`vm_physics_conserved()`; reverted with `git checkout`). It sends Hestia `: SBW ; : SBL 3000
|
||
0 DO SBW LOOP ; SB-HEAT SBL SB-HEAT SBL SB-HEAT SB-BACK`, and Hera runs `SB-GO SB-GO`.
|
||
- **Baseline (pre-change code, probe only):** `logs/20260923-160929/amd64/` gives
|
||
`hestia_heat 0 → 0 → 0` over beats 15 → 51 → 86, and **FAIL**. Hestia ran 71 beats of
|
||
her own mail on her own stack and gained nothing, which **proves G9**.
|
||
- **With the charge, all three ISAs:** `logs/20260923-161448/amd64/`,
|
||
`-161959/aarch64/` and `-162543/riscv64/` give, byte-identically, `hestia_heat 0 → 11 →
|
||
23` at beats 15 → 51 → 86, `rest 65536 → 65525 → 65513`, conserved at every sample, and
|
||
**PASS**. The heat rises **mid-turn** at each in-turn sample.
|
||
- Exact to the arithmetic: switched in at beat 15, so ⌊36·21845/65536⌋ = 11 and
|
||
⌊71·21845/65536⌋ = 23. Every unit Hestia gained came out of the rest of the fleet (all
|
||
from Hera; Artemis held 0).
|
||
- `fleet_window_warm=NO` and `slope 21845` are unchanged. By source, the charge never
|
||
writes `fleet_window`.
|
||
|
||
**Formal acceptance with the probe gone,** coupled images reset before each:
|
||
`logs/20260923-163215/amd64/`, `-163757/aarch64/`, `-164359/riscv64/`.
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures, faults, `VM FRAME
|
||
CORRUPTION` and refused interprets.
|
||
- The `dict_hash` set is identical across ISAs and unchanged from Stages A/B/FR.
|
||
- The full mint → WIREBIND → `USE` → `1 2 + .` → `3` relay works, with
|
||
`stadium_conserved(self)=true`.
|
||
- Clean build on all three plus hosted, no new warnings.
|
||
|
||
**Consequence to watch in C-b:** heat now moves at boot too. Hera is switch-current from
|
||
her Stage 3 registration onward and is charged for her own beats. She holds nearly all the
|
||
heat, so her charges pull almost nothing; that is the C-4 arithmetic playing out as
|
||
expected. The per-beat DoE heat columns will read differently from pre-C-a runs.
|
||
- [x] **Stage C-b — the compudynamic switch decision, the idle point, the timer path removed
|
||
(§3.13.3–§3.13.5).** 2026-09-23. Captain Bob: *"go ahead with C-b as proposed."*
|
||
- Rulings: **C-3** as proposed (the incumbent stays on a tie; among candidates, the one
|
||
waiting longest, by unique wait stamp). **C-5** as proposed (the idle point folded in;
|
||
Hera's pending console input is her mail).
|
||
- **C-4** had no proposal, so I took **(α) accept**. No special case for Hera; the
|
||
consequence is recorded below.
|
||
|
||
Changes:
|
||
- `capsule_vm_switch_signal.c/.h`:
|
||
- Removed: readiness, `has_work`, the 50-tick threshold, `tick()`, `take_pending()` and
|
||
the readiness getters.
|
||
- New: `sk_vm_switch_decide(self, self_competes)`. It considers participants other
|
||
than `self` that are alive, not `pending_reap`, and have a native stack. A candidate
|
||
is eligible with pending kernel-Hermes mail, or when parked mid-task (frame stack
|
||
non-empty, top frame not IDLE), or for Hera, pending console input. The lowest
|
||
VM-physics heat wins; on equal heat, the lowest wait stamp. Stamps are unique:
|
||
either a message's admission or the last time the VM was parked. With
|
||
`self_competes` set, it returns a candidate only if that candidate is **strictly**
|
||
cooler than `self`. It keeps no state between calls.
|
||
- Also new: wait stamps (`next_stamp`, `note_parked`), the "decision due" flag (set by
|
||
`note_beat` and `note_mail`, consumed at the next owned checkpoint), `is_participant`,
|
||
and `sk_vm_switch_reap_pending()`. The latter is the Stage 4 `pending_reap` handling,
|
||
now run at every safe point, one reap per call.
|
||
- `ticks_since_switch` now counts beats.
|
||
- `heartbeat.c`: the ISR's `sk_vm_switch_signal_tick()` call is gone. The timer decides
|
||
nothing.
|
||
- `kernel_hermes.c/.h`: each admitted message carries `wait_stamp`, and a send sets
|
||
decision-due.
|
||
- `vm_runtime.c`: the running VM's beat also calls `note_beat()`, right after its C-a
|
||
charge.
|
||
- `switch.c/.h`:
|
||
- `sk_vm_context_switch()` stamps `from` as parked.
|
||
- `sk_vm_switch_checkpoint(vm)` re-decides only when a decision is due.
|
||
- The trampoline decides after every drain: with mail left, it switches only if
|
||
someone is strictly cooler; with no mail, it goes to the coolest VM with work, or to
|
||
Hera when nobody has any. That fallback isn't `native_switch_back_to`, because two
|
||
idle trampolines would bounce into each other forever. A drain that leaves mail
|
||
unconsumed panics rather than spins.
|
||
- `sk_vm_idle_point()`: Hera, running with no frame live (between lines), parks with an
|
||
IDLE frame and switches to the coolest VM with work.
|
||
- `vm_core.c`:
|
||
- The `execute_colon_word` checkpoint calls `sk_vm_switch_checkpoint()`.
|
||
- **New checkpoint between top-level words** in `vm_interpret_kind`, with the same
|
||
ownership gate, so a primitives-only payload can be displaced.
|
||
- `repl.c`:
|
||
- The idle pump skips participants. Their mail goes on their own stacks now; this
|
||
closes G3 for participants.
|
||
- `sk_vm_idle_point()` is called on every no-input pass of all three console poll
|
||
loops (`sk_console_getkey`, `sk_repl_headless_wait`, `sk_console_readline`), **not**
|
||
at the `SK_IDLE_BEAT_INTERVAL` cadence. That cadence comes from the timer.
|
||
- `doe_log.c`: the three readiness columns are kept and always 0, so the CSV shape
|
||
doesn't change. Stage I decides whether to drop them.
|
||
|
||
**Evidence, as named in §4.** A temporary probe, restored afterwards from a pre-probe
|
||
snapshot and verified free of probe code:
|
||
- a walk-order check that recomputes every decision walking the slot table in reverse;
|
||
- a line per switch (names, heats, decision and disagreement counts);
|
||
- a check that each parked VM's `native_stack_saved_sp` lies inside its own native stack
|
||
(guard, top).
|
||
|
||
Probe runs with the full relay: `logs/20260923-171359/amd64/`, `-172010/aarch64/`,
|
||
`-172635/riscv64/` (plus an earlier amd64 run, `-170740`, before the stack check could
|
||
print). On every ISA:
|
||
- **`switch 1 Hera(heat 65536) → rajames~user(heat 0)`** happens at Hera's idle point
|
||
after she forwards the console line. `rajames~user` drains `CONSOLE-CMD-EVENT` **on its
|
||
own stack** (`stadium_conserved(self)=true`) and prints `3`.
|
||
- **`switch 2 rajames~user → Hera`** is the trampoline's no-work fallback.
|
||
- `switch_count` reached 2.
|
||
- **`parked rajames~user saved_sp inside own stack (checked=1 bad=0)`**.
|
||
- **`disagree=0` over 40,246,330 (amd64), 4,182,971 (aarch64) and 25,030,081 (riscv64)
|
||
decisions.** The decision doesn't depend on walk order.
|
||
- **Idle log growth over 20 s after the relay: 0 bytes.** No switch storm.
|
||
- Nothing switched during boot. No participant has mail then, and Hera is hottest.
|
||
|
||
**Formal acceptance with the probe gone:** `logs/20260923-173341/amd64/`,
|
||
`-173947/aarch64/`, `-174610/riscv64/`.
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures, faults, frame corruption
|
||
and refused interprets.
|
||
- The `dict_hash` set is identical and unchanged.
|
||
- Mint → WIREBIND → `USE` → `1 2 + .` → **`3`**, delivered by a real switch.
|
||
- Idle growth: 0 bytes.
|
||
- Clean build on all three plus hosted, no new warnings.
|
||
|
||
**Observed and recorded, not changed:**
|
||
- The relayed line's output now follows Hera's `ok>` on the same line
|
||
(`[rajames@rajames] ok> Kernel-Hermes CONSOLE-CMD-EVENT …`), because delivery happens
|
||
at her idle point after she prints the prompt. This is cosmetic.
|
||
- **Idle cost:** the idle point runs the decision on every poll pass (tens of millions of
|
||
decisions per boot while idle). Each walks the participant table, but the CPU was
|
||
spinning there anyway.
|
||
- **C-4 (α) consequence, now concrete:** Hera's other idle work, including xHCI event
|
||
servicing, USB attach and detach, and block sync, runs from her idle beat. Under
|
||
sustained worker load Hera is the hottest VM with work and gets no turn, so those
|
||
services wait until the workers go idle. This doesn't affect the relay. It will matter
|
||
for Stage G/H, and for any USB keyboard input under load (serial input still makes
|
||
her eligible).
|
||
- Stage F's mechanism is now in C-b. Its remaining content is evidence only: work
|
||
proceeds with the console idle, which this relay already shows for one message.
|
||
- [x] **Stage D — kill of a parked-idle VM, and unregister on kill (§3.5, G6).** 2026-09-23.
|
||
Captain Bob: *"go ahead with stage D."* §3.5's `parked_idle` flag is the frame predicate
|
||
Stage FR already made possible, not a new field.
|
||
|
||
Changes:
|
||
- `switch.c/.h`: new `sk_vm_parked_idle(vm)`. The VM has a saved context, and its frame
|
||
stack is exactly one frame: its own `VM_FRAME_IDLE`. That describes a VM sitting at its
|
||
trampoline with nothing in progress.
|
||
- `capsule_birth.c/.h`: new `capsule_vm_kill_refusal(entry)`, the one place the rule
|
||
lives. It refuses:
|
||
- a `SWITCHED_OUT` VM that isn't parked idle (mid-task: a live frame awaits resuming);
|
||
- one parked idle but with mail still pending (those messages hold their senders'
|
||
Stadium heat, and nothing would drain them);
|
||
- **the VM physically running**. A participant can now be the one executing `KILL`, and
|
||
it can't free its own stack.
|
||
|
||
`capsule_vm_kill()` uses it and now calls `sk_vm_switch_signal_unregister()`, as
|
||
`capsule_vm_force_reap()` already did. Before this, an ordinary `KILL` left the slot
|
||
behind.
|
||
- `mama_forth_words.c`: the `KILL` word used its own copy of the old blanket
|
||
`SWITCHED_OUT` refusal. It now asks `capsule_vm_kill_refusal()`, and runs its heat
|
||
retire and active-VM cleanup for any VM that will actually be killed, parked-idle ones
|
||
included.
|
||
|
||
**Finding, fixed here: a C-b regression.**
|
||
- `vm_registry_alloc()` never initialized `VMRegistryEntry.pending_reap`, and `kmalloc`
|
||
doesn't zero memory, so every born VM started with whatever the allocation held.
|
||
- This was latent since `FABRIC-3.md` §XXVIII Stage 4 (2026-09-14), because only the old
|
||
checkpoint read it, on a timer-picked target.
|
||
- C-b's `sk_vm_switch_reap_pending()` walks **every participant at every safe point**, so
|
||
a garbage non-zero value reaps a healthy VM. Seen live in the first Stage D baseline
|
||
(`logs/20260923-182435/amd64/`): `REAP: dprobe tombstone reclaimed` appears before any
|
||
`KILL`.
|
||
- Fixed at the root: `pending_reap = 0` in `vm_registry_alloc()`. It was the only
|
||
`VMRegistryEntry` field left uninitialized.
|
||
- The Tripod and `rajames~user` weren't reaped in C-b's runs only because their bytes
|
||
happened to be 0.
|
||
|
||
**Evidence, as named in §4.** A temporary probe (`kernel_main.c`, reverted with
|
||
`git checkout`):
|
||
- It births `dprobe` from `workload-1-lite.4th` with `WORKER-BIRTH` and registers it by
|
||
hand, standing in for Stage E.
|
||
- It sends it `1 2 + DROP`. Hera runs `: DPX 1 DROP ; DPX`; that colon-word checkpoint,
|
||
with a decision due from the mail, switches to `dprobe`. `dprobe` drains the message,
|
||
finds nobody with work, and falls back to Hera.
|
||
- It then checks that `dprobe` is parked idle, runs `S" dprobe" KILL`, and checks the
|
||
slot count and registry state.
|
||
|
||
Runs:
|
||
- **Baseline**, with the init fix but no Stage D, amd64 (`logs/20260923-182940/amd64/`):
|
||
`KILL: refused -- switched-out`, `slots before=3 registered=4 after_kill=4
|
||
parked_idle=1 dead=0`, **FAIL**. That proves G6.
|
||
- A first run with Stage D (`-183452`) still failed identically. That is how the `KILL`
|
||
word's own copy of the refusal was found; after that, the shared rule.
|
||
- **With Stage D:** `logs/20260923-184026/amd64/`, `-184502/aarch64/` and
|
||
`-184946/riscv64/` give `KILL: dprobe dead`, `slots before=3 registered=4 after_kill=3
|
||
parked_idle=1 dead=1`, **PASS** on all three.
|
||
|
||
**Formal acceptance with the probe gone:** `logs/20260923-185515/amd64/`,
|
||
`-190119/aarch64/`, `-190745/riscv64/`.
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD` and zero failures or 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.
|
||
|
||
**Reported, not changed (pre-existing):**
|
||
- Killing a **LIVE** VM that still has kernel-Hermes mail queued leaves those messages in
|
||
its queue. They hold their senders' Stadium heat forever, because nothing drains a dead
|
||
VM's queue. Stage D refuses the parked case for exactly this reason, but the LIVE path
|
||
has always had it.
|
||
- 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
|
||
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.
|
||
- [x] **Stage G — `SEND`, and two workers interleave (§3.8, §3.13.7).** 2026-09-23. Captain
|
||
Bob: *"SEND, go ahead with stage G"*, then ruling C-4 (β), work heat, when the first run
|
||
came out serial.
|
||
|
||
Changes:
|
||
- `repl.c`: new `SEND ( paddr plen name-c name-u -- ok? )`, registered for every VM
|
||
through the standard word set (`sk_repl_register_words`).
|
||
- It resolves the target by registry name, copies the payload (at most 1023 chars plus
|
||
the NUL; oversized payloads are **refused, not truncated**), and calls
|
||
`sk_hermes_send_one()`. That stamps the message and sets decision-due (C-b).
|
||
- `from` is the calling VM, derived in C, never taken from the stack.
|
||
- It returns 1 if admitted, 0 if refused.
|
||
- No C-side restriction; ACL is `ACL.4th`'s job.
|
||
- `kernel_hermes.h`: new `SK_HERMES_MSG_TYPE_SEND` (30), clear of every older type space.
|
||
- **C-4 (β):**
|
||
- `capsule_vm_physics.c/.h`: a per-VM `work_heat_q48` accumulates what each of the VM's
|
||
own per-beat charges actually moved in. Read with `vm_physics_work_heat_of()`.
|
||
- `sk_vm_switch_decide()` compares work heat for candidates and for the incumbent.
|
||
|
||
**First run, before (β):** `logs/20260923-201602/amd64/`, the §3.13.7 finding. It came
|
||
out `AAAA…A1 BBBB…B1`, serial.
|
||
|
||
**Evidence, as named in §4.** Only ordinary words, typed at `[zuse@Hera] ok>`, no probe:
|
||
```
|
||
S" workload-1-lite.4th" S" wa" WORKER-BIRTH .
|
||
S" workload-1-lite.4th" S" wb" WORKER-BIRTH .
|
||
S" : LA 40 0 DO 65 EMIT 300 0 DO LOOP LOOP ; LA" S" wa" SEND . S" : LB 40 0 DO 66 EMIT 300 0 DO LOOP LOOP ; LB" S" wb" SEND .
|
||
```
|
||
Result, **byte-identical on all three ISAs** (`logs/20260923-202403/amd64/`,
|
||
`-204054/aarch64/`, `-205150/riscv64/`):
|
||
```
|
||
[zuse@Hera] 1 1 ok
|
||
[zuse@Hera] ok> AAABBBBBBBBAAAAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAABB
|
||
```
|
||
- **A = 40, B = 40, in 16 alternating runs.**
|
||
- Hera kept the CPU while she had work (work heat tied at ~0, so the incumbent stays), and
|
||
both sends landed.
|
||
- From her idle point, `wa` and `wb` took turns. Each switch came from the per-beat charge
|
||
making the runner strictly hotter by work heat.
|
||
- Nothing in either payload asks to yield.
|
||
- The sequence is deterministic across ISAs, as the execution-paced beat implies.
|
||
|
||
**Acceptance,** in the same three boots, after the interleave test with both workers still
|
||
alive:
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures and faults.
|
||
- Mint → WIREBIND → `USE` → `1 2 + .` → `3`, with `stadium_conserved(self)=true`.
|
||
- Idle log growth: 0 bytes.
|
||
- **The `dict_hash` set changes, as expected, because `SEND` is a new word in every VM's
|
||
dictionary, and is identical across all three ISAs.**
|
||
|
||
| | New | Stages A–E |
|
||
|---|---|---|
|
||
| `word_count` | 533 | 532 |
|
||
| M7.1a | `0x330b28ce0f4e1c75` | `0x3933d76939ef66c6` |
|
||
| Hera | `0x0bf15066a73f6e32` | `0x27ba08011349842b` |
|
||
| Hestia | `0x9116e19b34d2cb0b` | `0xdd1aa879b3a6c5df` |
|
||
| Artemis | `0x165a9a66eafce131` | `0xe9d0408b1119d51f` |
|
||
|
||
- Clean build on all three plus hosted, no new warnings.
|
||
|
||
**Harness note (not a kernel change):** the first aarch64 attempt
|
||
(`logs/20260923-203226/aarch64/`, kept) typed the next line while Hera was still birthing
|
||
`wa`. Serial input received while Hera isn't polling is dropped, so the line arrived
|
||
truncated (`S" workload-1-li…`) and gave `UNKNOWN WORD`. My test script now waits for each
|
||
prompt before typing. Serial input is lost while the console isn't reading, which is the
|
||
pre-existing behaviour.
|
||
|
||
**Consequences of (β) to know:**
|
||
- Work heat only grows. A long-lived VM that has done a lot of work loses ties to newer
|
||
ones until they catch up.
|
||
- Hera's work heat grows only when other VMs hold absolute heat for her charges to pull
|
||
from. She runs cheaply while they hold ~0.
|
||
- Both are properties of the ruled quantity, recorded here, not changed.
|
||
- *(Superseded at Stage H: both consequences above turned out to matter, and the "floor on
|
||
contact" refinement replaced them. See §3.13.7 and the Stage H entry.)*
|
||
- [x] **Stage H — the DoE rebuilt on ordinary words; "floor on contact" (§3.9, §3.13.7).**
|
||
2026-09-23. Captain Bob: *"defs-only, no users factor yet, accept PRNG, go ahead with H"*,
|
||
and then ruled **floor on contact** when the first trial stalled.
|
||
|
||
**Changes, capsules:**
|
||
- The ten workload capsules the DoE births (`workload-0`, `-1-lite`, `-2`, `-3`, `-4`,
|
||
`-5-lite`, `-6`, `-7`, `-8`, `-9`) are now **definitions only**.
|
||
- Every top-level line that executed at birth is removed. That includes the heavy
|
||
self-exec tails and the mid-file `SEED`/wave/`RUN-ASYM`/`RUN-DUAL` runs in 2, 3 and 9.
|
||
- Each gains `: WL-RUN <exactly what it used to run at birth> <its entry word, twice> ;`.
|
||
That is the old trial's birth work plus its two `VM-EXEC` passes, now one job.
|
||
- Each also gains `: WL-DONE-ARGS S" MU-DONE" S" Hera" ;`.
|
||
- `workload-4` keeps its `4:dopi-ok` print inside `WL-RUN`.
|
||
- `workload-7` needed a second block for the two definitions. Block 2151 was unclaimed;
|
||
the conflict gate and `mkcapsule --lint` pass (37 files, 0 violations).
|
||
- `workload-1` and `-5` (full versions) are untouched, since the DoE doesn't birth them.
|
||
- `multiuser-doe.4th`:
|
||
- `WL-ENTRY`, `MU-TOUCH-WORKER/ALL` (the serialized `VM-EXEC` round-robin) and
|
||
`MU-CHECK-WORKER/ALL` are gone.
|
||
- New: `MU-DONE-COUNT`/`MU-DONE`, `MU-JOB` (the constant job text `WL-RUN WL-DONE-ARGS
|
||
SEND`), `MU-SEND-WORKER/ALL`, and `MU-WAIT-ALL` (an ordinary `BEGIN … WHILE REPEAT`;
|
||
Hera's own checkpoints drain the `MU-DONE`s).
|
||
- `MU-RUN-TRIAL` is now birth all, send all, wait, kill all, marker.
|
||
- A refused `SEND` counts as a failure **and** as done, so the wait can't hang on it.
|
||
|
||
**Changes, kernel:**
|
||
- `vm_core.c`: the between-top-level-words checkpoint (C-b) doesn't fire after the
|
||
**last** word (`vm_input_exhausted()`). The job's final word is the `SEND` to Hera, which
|
||
makes a decision due. Switching the worker out right there would park it "mid-task" with
|
||
nothing left, and `KILL` would refuse it. The trampoline decides a moment later anyway.
|
||
- **Floor on contact** (§3.13.7):
|
||
- `vm_physics_charge()` accrues the assessed slope into `work_heat_q48`.
|
||
- New `vm_physics_work_heat_floor()`.
|
||
- New `sk_vm_switch_signal_note_work_gained()`, called by `sk_hermes_send_one()` when the
|
||
recipient's queue goes from empty to one message.
|
||
|
||
**First trial, before the floor** (`logs/20260923-211253/amd64/`, kept): stalled.
|
||
- After 8,200+ beats the rows showed `switch_count_cumulative=0` and
|
||
`switch_current_slot=2` (Hera) throughout.
|
||
- Hera sat in `MU-WAIT-ALL`, with work heat 0 against workers at 0: a tie, so the
|
||
incumbent stays, forever.
|
||
|
||
**Evidence, as named in §4.**
|
||
- **Rows** (`logs/20260923-212353/amd64/`, cfg 4 with `HB-ON`, stopped deliberately once
|
||
captured because per-VM rows at every beat make a full trial take hours):
|
||
- `switch_count_cumulative` reached 44,297.
|
||
- Over the first ~7,600 rows, `switch_current_slot` was spread **evenly** across Hera
|
||
(slot 2: 851) and all eight workers (slots 5–12: 844–846 each).
|
||
- **Full trials** at cfg 4 (8 workers, uniform `workload-8`), rows off, one per ISA:
|
||
`logs/20260923-220439/amd64/`, `-222512/aarch64/`, `-231553/riscv64/`.
|
||
- Each ends with **`MU-TRIAL isa=<isa>run=999 cfg=4 rep=0 nw=8 mode=0 fail=0`**.
|
||
- **All 8 `KILL: muN dead`, 0 refused.** Every worker ended parked idle and killable.
|
||
- Times: amd64 ≈ 13 min, aarch64 ≈ 43 min, riscv64 ≈ 23 min.
|
||
- **Stage G's test, re-run on each boot under the refined rule:** byte-identical on all
|
||
three, `ABBAABBABAABBAABBABAABBA…` (A = 40, B = 40, **48** runs). It is finer than
|
||
Stage G's 16 runs because work heat now moves by sub-unit steps every beat.
|
||
|
||
**Acceptance,** in the same three boots after the trial:
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures and faults.
|
||
- The `dict_hash` set is unchanged from Stage G (word_count 533, M7.1a
|
||
`0x330b28ce0f4e1c75`).
|
||
- Mint → WIREBIND → `USE` → `1 2 + .` → `3`.
|
||
- Idle log growth: 0 bytes.
|
||
- Clean build on all three plus hosted, no new warnings.
|
||
|
||
**Known limits, recorded, not changed:**
|
||
- **A worker whose job errors never sends `MU-DONE`,** so `MU-WAIT-ALL` waits forever.
|
||
The error prints, but the trial produces no marker.
|
||
- **Worker execution errors are no longer counted.** `VM-ERROR?` can't see them, because
|
||
`sk_hermes_drain_checkpoint()` restores the VM's error flag after each payload (by
|
||
design: a bad message must not abort the enclosing context). `fail` now counts only
|
||
refused `SEND`s.
|
||
- **Hera's wait loop is a busy loop.** She takes a fair share of the CPU
|
||
(1/(N+1)), as the row tally shows.
|
||
- Per-VM, per-beat `HB-ON` rows make trials slow. One row in that capture shows a negative
|
||
(wrapped) `tick_interval_ns` for a VM a beat behind the others. That's a DoE-row
|
||
observation, not investigated.
|
||
- Stage F's remaining evidence (work proceeds with the console idle) is shown by Stage G's
|
||
interleave and by these trials.
|
||
- [x] **Stage I — strip the dead code (§4 row I).** 2026-09-24. Captain Bob: *"go ahead with
|
||
stage I."* Per `FABRIC-3.5.md` §XXII's discipline, each removal's reachability was shown
|
||
three ways, not by grep alone.
|
||
|
||
**Removed:**
|
||
|
||
| Item | Why dead |
|
||
|---|---|
|
||
| **The idle pump's drain loop** (`repl.c`, `sk_repl_idle()`), which drained every non-Hera live VM on Hera's stack (G3) | (1) **Static:** since C-b it acted only on a live **non-participant**. (2) **Construction:** births are refused at `stadium_max_vm_count()` live VMs (`capsule_birth.c:731`), the same capacity as the switch-signal table. Every birth registers (Stage E), stillborn VMs never go live, and `KILL`/reap unregister, so registration can't fail and no live non-participant can exist. (3) **Runtime:** a probe line in that branch (temporary, removed) printed **nothing** through a boot, the Stage G interleave, a full 8-worker cfg-4 trial (`fail=0`, 8/8 dead) and the WIREBIND relay (`logs/20260924-051657/amd64/`). |
|
||
| **`SWITCH-MARK-WORK`** (the word, `mama_word_switch_mark_work`, and `sk_vm_switch_signal_mark_work()`) | (1) Its only caller, `MSG-SEND` in `capsules/common/messaging.4th`, was deleted in `FABRIC-3.6.md` Phase 4. (2) No capsule source, no baked capsule (`capsule_generated.c`: 0), no experiment, tool or committed log invokes it; only the design docs mention it. (3) Since C-b, eligibility **is** the queue, so the word had nothing left to mark. |
|
||
| **The global interpret depth** (`g_vm_interpret_depth`, `sk_vm_at_outermost_interpret()`) | No caller since Stage A replaced it with the per-VM ownership gate. Only comments referred to it; they're updated. |
|
||
| **`VM.native_switch_back_to`** (`vm.h`) | Written by `sk_vm_context_switch()`, read by nothing since C-b's trampoline stopped yielding to "whoever switched in" (the Stage B/FR probes, which read it, are long reverted). |
|
||
|
||
**Kept, deliberately:**
|
||
- **Hera's own drain in the idle pump.** She is the one VM never switched to for her mail,
|
||
since she's the one running there. It still runs at the `SK_IDLE_BEAT_INTERVAL`
|
||
(timer-paced) cadence, and at her own word checkpoints. **This is the last
|
||
timer-paced delivery left, and it applies only to Hera's own queue.** Moving it to
|
||
`sk_vm_idle_point()` would need care with the readline `n > 0` mid-edit gate. Noted for
|
||
Captain Bob.
|
||
- **The three `*_readiness` DoE CSV columns** (always 0 since C-b). Dropping them changes
|
||
the CSV layout the extractor and analysis read. That's a DoE-schema decision for the
|
||
campaign relaunch, not a dead-code strip.
|
||
|
||
**Acceptance** (on every ISA: the Stage G interleave, a cfg-0 DoE trial, then the relay):
|
||
`logs/20260924-054146/amd64/`, `-055249/aarch64/`, `-061024/riscv64/`.
|
||
- The G interleave is byte-identical to Stage H on all three (A = 40, B = 40, 48 runs).
|
||
- `MU-TRIAL … cfg=0 … nw=2 … fail=0`, with 2/2 `KILL … dead` and 0 refused.
|
||
- All reach `[zuse@Hera] ok>` with zero `UNKNOWN WORD`, failures and faults.
|
||
- Mint → WIREBIND → `USE` → `1 2 + .` → `3`.
|
||
- Idle log growth: 0 bytes.
|
||
- Clean build on all three plus hosted (`vm.h` is shared), no new warnings.
|
||
- **The `dict_hash` set changes, as expected, because `SWITCH-MARK-WORK` is gone, and is
|
||
identical across ISAs.** `word_count` drops by 2 because the word was registered twice
|
||
in Hera's dictionary.
|
||
|
||
| | New | Stages G/H |
|
||
|---|---|---|
|
||
| `word_count` | 531 | 533 |
|
||
| M7.1a | `0x572daac83b797332` | `0x330b28ce0f4e1c75` |
|
||
| Hera | `0xda9742ce9c3376a2` | |
|
||
| Hestia | `0x413e7d086f342d8b` | |
|
||
| Artemis | `0xdfd68316d5f5ba85` | |
|
||
|
||
**FABRIC-3.8's staged plan (A–I) is complete.** What's next is the per-ISA DoE relaunch
|
||
(seed 12345, 3 reps) on the rebuilt driver, then the merge to master.
|