Files
LithosAnanake/docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
T

256 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Dynamic VM Fleet Physics — Design Doc
**Date:** 2026-07-05 (rev 2026-07-05g)
**Branch:** `lithosananke`
**Status:** Core mechanism implemented and three-arch accepted (amd64/aarch64/riscv64 all boot clean, full `TRIPOD-TEST` pass, matching `dict_hash` across all three). `compudynamics.4th`/`fleet-k.4th` fully deleted; every consumer capsule migrated. Fold-in question settled: Hermes (messages/channels) and Artemis (blocks) get their own sibling instances of this same mechanism in separate follow-on docs — not folded into `VMPhysics`, not generalized in this doc. Kill's heat-return resolved (rev g) as a parent-pointer walk to Hera, replacing the earlier proportional-fan-out proposal. Remaining work: `MANIFEST.md` doc debt, `doe-campaign.4th` migration, Hermes/Artemis sibling docs, DoE rewrite, BIRTH-by-any-VM-with-permission (all detailed in `docs/working/archive/session-logs/2026-07-05-worklog.md`).
**Author:** Captain Bob / Claude Code
---
## Execution rules (rev 2026-07-05f)
Binding for all implementation work under this doc, from this point forward:
1. **Small units.** Implement one piece at a time (one new file, one function, one call-site migration, one deletion) — not the whole design in a single pass. Each unit should be small enough to review and verify in isolation.
2. **Clean + make at checkpoints.** After each unit that touches buildable code, `make -f Makefile.starkernel clean` followed by the build, before moving to the next unit. Never stack multiple unverified units on top of each other.
3. **QEMU when applicable.** Once a unit is buildable and the change is substantive enough to affect boot behavior (new capsule wiring, deleted words, new primitives reachable from FORTH), run the QEMU acceptance boot per the existing acceptance rule below — not skipped, not deferred to "do it all at the end."
4. **One ISA at a time.** Never run more than one QEMU instance concurrently (already a hard rule in `CLAUDE.md`). During incremental development, it is not necessary to run all three architectures after every single checkpoint — run one (amd64, being the fastest under TCG) for fast iteration, and reserve the full three-arch pass for milestones where the doc's Verification approach section calls for acceptance (e.g. after the new primitives are wired end-to-end, after the deletions land, before declaring the design implemented).
---
## Problem
`VM-COUNT` is hardcoded to 3 across the Tripod fleet, in two different ways:
1. **Static array capacity.** `compudynamics.4th`'s `VM-HEATS`/`VM-TRANS` and `fleet-k.4th`'s `K-WINDOWS`/`K-HEADS`/`K-LOCAL`/`K-TARGET` are all `CREATE ... VM-COUNT ... ALLOT`ed once at boot. This FORTH has no realloc — `ALLOT` only ever advances `HERE` — so whatever size is picked at creation is permanent for that boot.
2. **Hand-unrolled loops.** `K-REBALANCE`, `K-STATUS`, `VM-HOTTEST`, `CD-TICK` all spell out exactly three VM indices in the source rather than looping over a count.
Both are downstream of a deeper issue this session already found and fixed: a kernel C primitive and a FORTH capsule constant both happen to be named `VM-COUNT`, and which one wins a dictionary lookup was heat-timing-dependent (see `K-PUSH-DICTIONARY-SHADOWING-BUG-20260704.md`). That fix made the constant deterministically win — but a fixed constant is exactly wrong if the goal is a genuinely dynamic fleet size.
On top of the sizing problem, `K-FLEET` special-cases Hermes: Hera and Artemis's thermal contribution is read locally (`K-LOCAL@`), Hermes's is fetched via a remote `VM-CALL` to `HERMES-K`, because Hermes tracks its own thermal mass differently. A fourth VM has no defined answer for "measured locally or remotely" — that's hand-written policy, not something a loop-bound fix generalizes away.
**Also found during this investigation:** `compudynamics.4th` already contains a heat-based VM scheduler (`VM-TICK`/`VM-RUN`/`VM-STEP-IDX` — finds the hottest VM via `VM-HOTTEST`, drives it via `VM-STEP`) that is defined but **never called** from any boot path (`init.4th`, `process.4th`, `doe-campaign.4th`). It's dead code, and it's exactly the kind of heat-decides-execution-order mechanism this design deliberately does not build. It gets deleted along with everything else in that file, not revived.
## Goal
Eliminate the special-casing and the hardcoding together, by making VM-level heat governance a genuine structural parallel to the word-level physics engine that already exists in C — not a shared implementation, not a scheduler, but the same mechanics (heat accrual, decay, rolling-window history, regression-based adaptive tuning) applied to a different kind of entity.
**Explicitly not wanted:** a VM scheduler. Nothing gets built that decides whose turn it is to execute. Execution stays exactly as state-driven as it is today — explicit `VM-EXEC`/`VM-CALL` calls from capsule FORTH code, or a VM's own internal state (e.g. Hermes's message queue) determining whether it does anything. The physics is a passive observer of real activity, never a driver of it.
---
## What's already dynamic vs. what isn't
**Already dynamic, no work needed:**
- The kernel VM registry (`capsule_birth.c`) is a `kmalloc`-backed linked list (`vm_registry_head`/`vm_node_t`), unbounded except by heap memory — confirmed no hardcoded `MAX_VM` anywhere in the kernel.
- `ACTIVE-VMS` (`fleet-k.4th`) is a `VARIABLE`, already correctly incremented/decremented by `K-SPAWN-HOOK`/`K-KILL-HOOK`, which are already wired into `process.4th`'s birth/kill flow — though under the new design these hooks are removed entirely (see Consumer code migration) since birth/kill heat bookkeeping moves into the C hook points themselves.
**Hardcoded, needs replacing (not generalizing — see below):** everything listed under "Problem" above.
---
## The mechanism
### 1. `VMPhysics` — new struct, file-scope state in `capsule_vm_physics.c`
Mirrors `DictPhysics`'s *shape* (`include/vm.h:257`) — not reused, not shared code:
```c
typedef struct {
uint64_t execution_heat_q48; /* Q48.16 fixed point */
uint64_t last_active_ns;
int is_live;
} VMPhysics;
```
Word-specific fields that don't map (`mass_bytes` — header+body footprint; `pubsub_mask`) are dropped. `avg_latency_ns` is a candidate addition if VM-EXEC/VM-CALL dispatch latency is ever wanted, but isn't required for the core mechanism. Per the resolved file-organization decision (see "File organization and new primitive signatures"), records live in a `kmalloc`-backed linked list in `capsule_vm_physics.c` — the same pattern `capsule_birth.c` already uses for `vm_registry_head`/`vm_registry_count` — not embedded in `VMRegistryEntry` as a public field.
**Parent lineage lives in the registry, not here.** `VMRegistryEntry` (`include/starkernel/capsule_run.h:92`) already carried an unused `reserved` field and `capsule_birth.c` already carried a `parent_vm_id` on its internal registry node, hardcoded to 0 everywhere (correct today, since only Hera calls `BIRTH`) but never read anywhere. `reserved` is renamed to `parent_vm_id` and exposed on `VMRegistryEntry` itself (the redundant registry-node-level copy is removed) — this is what kill's heat-return walks (see section 5).
### 2. `VMFleetWindow` — new struct, one instance, fleet-wide
Mirrors `RollingWindowOfTruth` (`include/vm.h:95`) structurally: a circular buffer, but of `vm_id`s instead of `word_id`s, plus the same warm-up gate:
```c
typedef struct {
uint32_t touch_history[VM_FLEET_WINDOW_DEPTH];
uint32_t head;
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
bool is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
} VMFleetWindow;
```
This is fleet state, not per-VM state — one instance, living alongside the registry (`capsule_birth.c`), not inside `VMRegistryEntry`. The shared-slope decision (below) is what makes this fleet-wide rather than per-VM: the word engine infers *one* `decay_slope_q48` per VM instance from aggregate dictionary statistics, not one slope per word; the fleet analog infers *one* `fleet_transfer_slope_q48` from aggregate fleet statistics, not one per VM.
**`VM_FLEET_WINDOW_DEPTH` default: 64.** The word engine's `ROLLING_WINDOW_SIZE` defaults to 4096 because word executions number in the millions. Fleet touches are real `VM-EXEC`/`VM-CALL`/`VM-STEP` dispatches from capsule FORTH code — dozens to low hundreds over a full boot + acceptance run, not millions. 64 gives the regression enough samples to stabilize without being wastefully oversized for a 38 VM fleet, and it's a compile-time constant, trivially bumped if experience says otherwise — same tuning-knob convention as `ROLLING_WINDOW_SIZE`.
**Small-fleet warm-up.** At current Tripod scale (3 VMs), `VMFleetWindow` should expect to spend most of its life with `is_warm == false`, or with a low-quality fit once warm — there simply aren't enough distinct touch events to make the regression meaningful with so few actors. This is expected behavior, not a bug, and any code consuming `fleet_transfer_slope_q48` must treat an unwarmed or low-quality-fit fleet the same way `vm_tick_inference_engine` already treats an unwarmed dictionary window: skip the update, don't substitute a default slope. The 4-VM acceptance test below is the first configuration where the fit is expected to mean anything.
### 3. Recording — passive, hooked at existing dispatch points
No new call sites beyond where VMs are already actually driven. `mama_word_vm_exec` (`mama_forth_words.c:584`), `mama_word_vm_call` (`:664`), and `mama_word_vm_step` (`:526`) each already resolve the target VM and confirm it's live before dispatching. A new `vm_physics_touch(vm_id, now_ns)`, called right at that point in each of the three, does two things in one step — exactly like a word's dispatch simultaneously bumps `execution_heat` *and* gets recorded into the rolling window:
- Updates that VM's own `VMPhysics.execution_heat` / `last_active_ns`.
- Appends `vm_id` to `VMFleetWindow.touch_history`.
`mama_word_birth` (`:214`) calls a new `vm_physics_init()` the moment a VM reaches `VM_STATE_LIVE`. `mama_word_kill` (`:486`) retires the entry's physics on death.
**Mutation access pattern:** `capsule_vm_find_by_name_nocase()` (used by `VM-EXEC`/`VM-CALL`/`VM-STEP` to resolve a VM by name) returns a **copy** of the `VMRegistryEntry`, not a live pointer — confirmed by reading its implementation (`capsule_birth.c:212`, `*out = node->entry;`). The existing precedent for mutating the *live* entry is `capsule_vm_set_state(vm_id, state)` (`capsule_birth.c:226`), which reaches it via the internal `vm_find_entry_ptr(vm_id)` helper. The new physics functions follow the same pattern: `capsule_vm_physics_touch(vm_id, now_ns)` as a public function resolving the live pointer internally, called from the three dispatch primitives after they already have `entry.vm_id` in hand.
**Concurrency — resolved, no locking needed.** `vm_physics_transfer` (below) reads and writes two live `VMPhysics` structs across a subtract-then-add sequence, which would need protection if anything could preempt it mid-operation. Checked directly: the only interrupt-driven code path in the kernel is the timer ISR (`isr_common_handler`, `interrupts.c:327`, `APIC_TIMER_VECTOR` on amd64; the riscv64/aarch64 equivalents follow the same shape), which calls `heartbeat_tick()` — and that function touches only its own isolated `TimeTrustState` (TSC/`rdcycle` delta bookkeeping, `timer.c:202`). It never calls into `mama_word_*`, `vm_interpret`, or any capsule/dictionary state. Combined with `HEARTBEAT_THREAD_ENABLED=0` in the kernel build (confirmed in `Makefile.starkernel`; no separate OS thread runs physics logic), `BIRTH`/`KILL`/`VM-EXEC`/`VM-CALL`/`VM-STEP` all execute synchronously in Hera's single interpreter context with nothing else able to preempt them mid-operation. `vm_physics_transfer` needs no lock, and the implementation should say so explicitly in a comment so one isn't added speculatively later.
### 4. Inference — heartbeat-gated, same closed-form regression, different input series
`infer_decay_slope_q48` (`inference_engine.c:561`) fits `heat(t) = h₀·e^(slope·t)` via closed-form log-linear OLS: `slope = (n·Σ(t·ln h) Σt·Σln h) / (n·Σt² (Σt)²)`, all in Q48.16 integer-only arithmetic. `extract_heat_trajectory` (`:139`) builds its input by replaying the recorded `word_id` history and looking up each entry's **current** heat — not a stored historical value — exploiting the correlation between ring position (recency) and how much a word has since cooled.
A new function does the identical thing for the fleet window: replay `VMFleetWindow.touch_history`, look up each `vm_id`'s **current** `VMPhysics.execution_heat`, feed that trajectory into the same regression formula (new code, same math — not a call into the word-level function). Output: `fleet_transfer_slope_q48` plus a `slope_fit_quality_q48` bounded `[0.0, 1.0]` (mirroring the validated bound in `inference_outputs_validate`, `inference_engine.c:824`).
This runs on the same cadence as `vm_tick_inference_engine` (`vm_runtime.c:553`, gated by `HEARTBEAT_INFERENCE_FREQUENCY`, requiring `is_warm`) — a new fleet-side counterpart, not a modification of the word-side one.
**Dead `vm_id` in trajectory.** Replay may encounter a `vm_id` in `touch_history` that has since been killed and had its registry entry removed. The lookup must treat this the same way a word-level lookup would treat a since-removed dictionary entry: skip that sample rather than fault, and let `slope_fit_quality_q48` reflect the reduced effective sample count. This needs an explicit test (see Verification approach) rather than being left to whatever the lookup happens to do.
### 5. Application — one conservative primitive, everything else is a special case of it
```c
void vm_physics_transfer(VMPhysics *from, VMPhysics *to, uint64_t amount_q48) {
uint64_t moved = (amount_q48 > from->execution_heat_q48)
? from->execution_heat_q48
: amount_q48;
from->execution_heat_q48 -= moved;
to->execution_heat_q48 += moved;
}
```
Subtracts `amount` from `from->execution_heat` (clamped at 0) and adds the same amount to `to->execution_heat`. Nothing is created or destroyed by this operation — by construction, `sum(execution_heat for all LIVE VMs)` is invariant across any call to it.
Every other VM-heat operation is this primitive applied to a specific pair:
- **Touch** (real dispatch): `amount = elapsed_since_last_touch * fleet_transfer_slope_q48 >> 16` — same formula shape as `physics_metadata_apply_linear_decay` (`physics_metadata.c:305`), but a two-sided move (pull from the rest of the fleet, credit the active VM) instead of a one-sided subtraction.
- **Decay** (lazy, computed at the next touch or query — never on a separate schedule): the mirror transfer, idle VM's heat flows back to the fleet.
**Idle-VM resolution is intentional.** Because decay is lazy, a VM that is birthed and then never touched or queried again simply holds its heat share indefinitely — there is no background sweep that forces resolution. This should be stated as explicit intended behavior in the implementation comments so it isn't later "fixed" into a periodic sweep, which would reintroduce exactly the kind of driver-not-observer behavior the Goal section rules out.
- **Birth — resolved, no fan-out needed.** A new VM starts at `execution_heat_q48 = 0`. No transfer out of existing VMs is required at all: a zero-heat entry joining the live set doesn't change `sum(execution_heat)`, so conservation holds trivially, by construction, with no proportional-split logic to write or reason about. This is the "cold mass added to a closed system" reading of the metaphor — the new VM only starts accumulating heat once something actually touches it, exactly like any other VM. (The earlier draft of this doc proposed a proportional fan-out transfer for birth, symmetric with kill; that's dropped — birth and kill are not actually symmetric, and inventing a `s_new` initial-share constant to make them symmetric would have been exactly the kind of arbitrary hand-tuned parameter this design is trying to avoid.) The one exception is Hera herself: she never goes through `BIRTH` (she's the bootstrap VM, registered directly at kernel startup), and her registration is where the fleet's whole `Q48_ONE` is seeded — every VM born after her joins an already-nonzero fleet.
- **Kill — resolved (rev f): a parent-pointer walk to Hera, not proportional fan-out.** Every VM has exactly one outbound edge, `parent_vm_id` (`VMRegistryEntry`, set once at birth, never rewritten afterward — this is the renamed `reserved` field described in section 1). Hera is the fleet's single structural root: the one entry `capsule_vm_kill` refuses to ever kill, and her own `parent_vm_id` is self-referential (`parent_vm_id == vm_id == 0`) — the sentinel a walk up the chain stops at. `vm_physics_retire` resolves the dying VM's root by following `parent_vm_id` edges (via the existing `capsule_vm_registry_get`, no new registry accessor needed) until landing on a self-referential entry, then transfers the *entire* remaining heat there in one `vm_physics_transfer` call.
This replaces an earlier proportional-to-current-heat fan-out across all LIVE survivors, which had three problems at once: it needed division (an all-cold survivor set makes the split meaningless), it needed a "no survivors" fallback (nowhere to send heat if the fleet drops to one VM), and it invited an argument about the weighting formula itself. The parent-pointer walk has none of these — no division, a root is always there by construction (Hera can't be killed), and there is exactly one place heat can go, not a policy choice.
**Dead intermediate parents need no special handling.** If the immediate parent has already been killed, its own `parent_vm_id` was set once at *its* birth and never rewritten — so the walk continues through the dead node's registry entry (still present, just `state == VM_STATE_DEAD`) rather than needing to detect and route around a dead link. The walk only ever needs one termination check: has it reached a self-referential entry.
**This is why `TRIPOD-TEST`'s kill-then-rebirth matters, not just an edge case to wave off.** `init.4th`'s acceptance test (block 2051) deliberately kills Hermes — a root, in the sense that Hermes has no `BIRTH`-time parent other than Hera — then immediately rebirths her and re-checks `K-CONSERVED?` (the `"K soak"` check). Investigated directly: this isn't leftover boot cruft, it's a deliberate resilience test proving the fleet survives a Tripod member dying. Under the parent-pointer model, killing Hermes walks straight to Hera (one hop) and parks the heat there; when Hermes is reborn she starts at 0 as any birth does, and the heat that was hers is now sitting with Hera rather than lost. This matters concretely: Hera is expected to eventually gain the ability to respawn a failed Hermes or Artemis, and this is exactly the mechanism that keeps conservation holding across that gap.
### Conservation is provable, not asserted
Because every operation is a balanced `vm_physics_transfer` call (or, for kill, a composition of N balanced calls), `sum(execution_heat for i in LIVE VMs) == Q.1` holds by induction: show the primitive alone preserves the sum, and any composition of touches/decays/births/kills — any execution history at all — preserves it automatically. This is the same proof shape as the existing `Loop3_Decay.thy`-style Isabelle theories, but the claim is now a genuine conservation law (heat moves, never appears or vanishes) rather than an enforced constant (the old `K-REBALANCE`'s explicit `Q.1 / ACTIVE-VMS` reset). This was the deciding factor over the alternative (normalizing each VM's share by the fleet total, which sums to 1 by tautology regardless of the underlying dynamics and proves nothing).
---
## Consumer code migration
Every existing call site of a to-be-deleted word was traced. **Correction (rev d):** the first pass of this audit only grepped the flat `capsules/*.4th` and missed `capsules/hermes/init.4th` and `capsules/artemis/init.4th` entirely — Hermes turns out to be a real consumer. This is what changes at each:
**`hermes/init.4th` `HERA-NOTIFY-SPAWN`/`HERA-NOTIFY-KILL`** (block 4118) — call the doomed hooks *remotely*, from inside Hermes, not from Hera's own capsule code:
```forth
: HERA-NOTIFY-SPAWN ( -- ) S" K-SPAWN-HOOK" S" Hera" VM-EXEC ;
: HERA-NOTIFY-KILL ( -- ) S" K-KILL-HOOK" S" Hera" VM-EXEC ;
```
Triggered by `EVENT-EMIT` whenever a `SPAWN-EVENT`/`KILL-EVENT` message arrives at Hermes (`process.4th`'s `SPAWN`/`KILL-VM` emit these via `EVENT-EMIT` before calling `BIRTH`/`KILL`). Both bodies are deleted outright — since birth/kill heat bookkeeping now happens automatically inside the C `BIRTH`/`KILL` primitives (section 3), there's nothing left for Hermes to notify Hera *of*. `EVENT-EMIT`'s dispatch on `SPAWN-EVENT`/`KILL-EVENT` becomes a no-op case (or is removed if nothing else depends on the event codes existing).
**`hermes/init.4th` also has its own independent hardcoded `VM-COUNT=3`**, unrelated to `fleet-k.4th`'s arrays — a literal `3 /` in the `COMMON-CH` channel-heat floor:
```forth
( COMMON floor = Q.1/3: Hermes's fair share, VM-COUNT=3 )
Q.1 3 / OVER CH-HEAT! ( COMMON-INIT, block 4116 )
Q.1 3 / COMMON-CH @ CH-HEAT! ( HERMES-TICK, block 4116 )
```
This is a second, independent instance of the same disease this whole design exists to cure, in a completely different capsule. It is **not resolved by this doc as written** — see "Fold-in question" below; whatever mechanism ends up governing message/channel heat needs to answer what `COMMON-CH`'s "fair share" means when the fleet size isn't fixed at 3, and it isn't simply "replace `3` with `VM-COUNT`" if messages/channels end up under their own independent physics rather than reading the fleet's VM count directly.
The remaining capsules (`init.4th`, `process.4th`, `doe-campaign.4th`):
**`init.4th` `TRIPOD-TEST`** (block 2051) — the core acceptance test currently drives conservation checks by *artificially pumping* fake ticks:
```forth
K-INIT 8 0 DO CD-TICK LOOP K-STATUS
K-CONSERVED? IF ... THEN
```
There is no `CD-TICK`/`K-INIT` equivalent under the new design — there's nothing to artificially pump, because heat only moves in response to real dispatch. `TRIPOD-TEST` needs rewriting, not a 1:1 word swap: replace the fake-tick loop with repeated *real* `VM-EXEC`/`VM-CALL` touches (the test already does real Hermes/Artemis calls elsewhere in the same word — `HERMES-TICK`, `ART-STATUS` — so this is duplicating an existing pattern, not inventing one), then check the new `VM-CONSERVED?` primitive. `K-STATUS` is replaced 1:1 by a new `VM-PHYSICS-STATUS` primitive.
**`init.4th` lines 42 and 44** — `S" Hermes" KILL-VM K-KILL-HOOK` and `S" Hermes" BIRTH K-SPAWN-HOOK ...` — the `K-KILL-HOOK`/`K-SPAWN-HOOK` calls are dropped outright, no replacement. Heat rebalancing on birth/kill now happens automatically inside the C `BIRTH`/`KILL` primitives themselves (`vm_physics_init`/kill-side transfer, section 3), so nothing needs to be called from FORTH afterward.
**`process.4th` `SPAWN`** (block 4300) — drops its internal `K-SPAWN-HOOK` call (line 12) for the same reason. **`KILL-VM`** (block 4301) doesn't call `K-KILL-HOOK` itself (the caller in `init.4th` does), so it needs no change. **`CD-PHASE@`** (block 4301, `COLD`/`WARM`/`HOT` classifier reading `VM-HEAT@`/`K-TARGET` directly) is dead code — confirmed via search, nothing calls it anywhere — and is deleted along with its dependencies rather than migrated.
**`doe-campaign.4th` — flagged, not resolved here.** This is a separate, opt-in experimental capsule, not part of the standard Tripod boot (it's never `EXEC`'d from `init.4th`). It redefines its own `CD-TICK` (shadowing `fleet-k.4th`'s, identical body) and calls `VM-STATUS`, `K-BUMP`, and the `VM-HERA`/`VM-HERMES`/`VM-ARTEMIS` index constants directly; it also defines `CD-WORK` (dead code — never called) which dispatches DoE work to a caller-supplied VM index, a pattern presumably meant to pair with `VM-HOTTEST` for heat-driven work distribution. Whether DoE experiments *should* use heat to pick which VM does work is a legitimately different question from whether the core Tripod fleet should — it's a research-harness decision, not a fleet-conservation one. This capsule breaks under the deletions below and needs its own explicit decision (migrate it to the new primitives, deprecate it, or leave it broken until someone needs it) before or separately from this implementation.
---
## What gets deleted
Not generalized, not left in place — deleted outright, replaced by new C primitives reading `VMPhysics`/`VMFleetWindow` directly:
- **`compudynamics.4th` — the entire file.** `VM-HERA`/`VM-HERMES`/`VM-ARTEMIS`/`VM-COUNT` (constants), `VM-HEATS`, `VM-TRANS`, `VM-LAST`, `VM-HOT`, `VM-BEST-IDX`, `VM-BEST-HEAT`, `Q-DECAY`, `VM-HEAT@`, `VM-HEAT!`, `VM-DECAY-ONE`, `VM-DECAY-ALL`, `VM-BUMP`, `VM-HOTTEST`, `VM-STEP-IDX`, `VM-TICK`, `VM-RUN` (the dead scheduler noted above), `VM-STATUS`, `VM-INIT`.
- **`fleet-k.4th`**: `K-WINDOW-DEPTH`, `K-WINDOWS`, `K-HEADS`, `K-SLOT`, `K-PUSH`, `K-LOCAL@`, `K-LOCAL`, `K-TARGET`, `ACTIVE-VMS`, `K-FLEET`, `K-REBALANCE`, `K-SPAWN-HOOK`, `K-KILL-HOOK`, `K-EPSILON`, `K-CONSERVED?`, `K-BUMP`, `K-STATUS`, `K-INIT`, `CD-TICK`.
- **`process.4th`**: `CD-PHASE@` (dead code, depended on doomed words).
New primitives replacing them (FORTH-visible, backed by `capsule_vm_physics.c`): `VM-PHYSICS-STATUS` (replaces `K-STATUS`/`VM-STATUS`), `VM-CONSERVED?` (replaces `K-CONSERVED?`).
`K-FLEET`'s Hermes special-case disappears because there's no longer a "local vs. remote" distinction to make — every VM, however many, is a `VMPhysics` entry updated identically by the same three dispatch hooks.
---
## File organization and new primitive signatures
New kernel-only file pair, matching `capsule_birth.c`'s own scope — this is genuinely kernel-only code with no hosted-build equivalent (the hosted build has no multi-VM fleet at all), unlike the word physics engine which is shared:
- `src/starkernel/capsule/capsule_vm_physics.c`
- `include/starkernel/capsule_vm_physics.h`
```c
void vm_physics_init(uint32_t vm_id); /* called from mama_word_birth on VM_STATE_LIVE, and once for Hera at kernel bootstrap */
void vm_physics_retire(uint32_t vm_id); /* called from mama_word_kill; walks parent_vm_id up to Hera and transfers remaining heat there */
void vm_physics_touch(uint32_t vm_id, uint64_t now_ns); /* called from vm_exec/vm_call/vm_step */
void vm_physics_tick(uint64_t now_ns); /* heartbeat-gated inference pass, mirrors vm_tick_inference_engine */
uint64_t vm_physics_fleet_heat_sum(void); /* diagnostic: should always read Q.1 */
int vm_physics_conserved(void); /* |fleet_heat_sum - Q.1| < epsilon; backs VM-CONSERVED? */
void vm_physics_status(void); /* diagnostic report; backs VM-PHYSICS-STATUS, replaces K-STATUS/VM-STATUS */
```
`fleet_transfer_slope_q48`, `slope_fit_quality_q48`, the `VMFleetWindow` instance, and the per-VM `VMPhysics` records are all static file-scope state in `capsule_vm_physics.c` — the same pattern `capsule_birth.c` already uses for `vm_registry_head`/`vm_registry_count`, not embedded in `VMRegistryEntry` as a public field. `vm_physics_retire`'s root walk (`vm_physics_find_root_id`, a static internal helper) reads `parent_vm_id` off the *registry*, via the existing `capsule_vm_registry_get` — it does not duplicate parent lineage inside `capsule_vm_physics.c`.
---
## Verification approach
- Three-arch acceptance as usual (this is shared VM code — `#ifdef __STARKERNEL__` gates apply per `CLAUDE.md`).
- A targeted test birthing a 4th throwaway VM mid-boot and confirming `vm_physics_fleet_heat_sum()` still equals `Q.1` after arbitrary touch/decay/birth/kill sequences, with no capsule-side code aware of the count.
- **Kill-during-warm-up test.** Birth a VM, touch it a few times (fewer than `VM_FLEET_WINDOW_DEPTH`, so `is_warm` is still false), then kill it, then trigger an inference pass. Confirms the trajectory replay's dead-`vm_id` handling (section 4) skips the missing entry cleanly rather than faulting, and that `slope_fit_quality_q48` reflects the reduced sample rather than the pass silently no-oping in a way that masks a real fault.
- The conservation proof (primitive preserves the sum → induction over any history) is a candidate for a new Isabelle theory alongside the existing Loop theories, if formal verification of this specific claim is wanted.
## Explicitly out of scope
- **Fleet composition/boot order** — `init.4th` still hand-scripts `BIRTH`ing Artemis then Hermes in a specific sequence relative to capsule loads (this is exactly the load-order dependency that caused the original K-PUSH bug). Making *that* dynamic is a separate, larger question about capsule-driven fleet composition, not a physics question.
- **A VM scheduler** — deliberately not built. See "Goal" above.
- **`doe-campaign.4th`'s migration** — flagged under Consumer code migration; needs its own decision before or separately from this implementation.
- **`BIRTH` called by any VM, with permission.** Today `BIRTH` is a MAMA-only word (only Hera's VM has it registered), so `parent_vm_id` being hardcoded to `0` at every allocation site (`capsule_birth.c`) is correct as written, not a stand-in for something smarter. Captain Bob's note: any VM should eventually be able to birth another, gated by a permission ask — not by which VM happens to hold the `BIRTH` word. That's a distinct question (an ACL-shaped gate on `BIRTH` itself, plus threading "who is actually calling" into `vm_registry_alloc` instead of the current hardcoded `0`) deliberately deferred to when messaging and OS-glue utilities exist — `parent_vm_id` is exactly the field that question would need, so this is the reminder that it's still there, unresolved, waiting on that later work.
## Fold-in question — settled
Hermes and Artemis each already have their own independent, hand-rolled Q48.16 heat economy — Hermes's `MSG-HEAT`/`CH-HEAT`/`HERMES-K` (message and channel heat), Artemis's `BLK-HEAT`/`ART-K-TOTAL` (block heat, from this session's earlier BAM work). Neither is wired into `VMPhysics`, into each other, or into any DoE/heartbeat metrics today.
**Decision: no fold-in. Sibling instances, not a merge.** A Hermes message-scan and an Artemis block-touch do not become fleet-level `VM-EXEC` activity credited to `VMPhysics.execution_heat`. Each level — words, VMs, messages, blocks — keeps its own independent heat quantity.
**What does scale identically across all four is the mechanism, not the data:** passive accrual on real activity, lazy decay, a rolling-window trajectory, and a regression-inferred decay slope (the same closed-form log-linear OLS this doc already specifies for `fleet_transfer_slope_q48`) replacing every hand-picked constant. Confirmed concretely while settling this: `Q-DECAY = 65208` is independently re-declared as its own `CONSTANT` in three separate files — `compudynamics.4th` (VM heat), `hermes/init.4th` (message/channel heat), `artemis/init.4th` (block heat) — the identical hand-picked number, copy-pasted three times, uncoordinated, with no regression anywhere. That's the literal "before" picture. Words already have the "after" (Loop #6's inferred slope). This doc gives VMs the "after". Messages and blocks get their own siblings — `msg_decay_slope_q48` (Hermes), `blk_decay_slope_q48` (Artemis) — each independently fit from its own rolling window, in separate follow-on design docs, not derived from or merged with the VM-level slope.
**Conservation (sum = `Q.1`) does not generalize to messages or blocks.** That invariant was a deliberate, specific choice for VMs, justified by `K≡1.0` — a claim that pre-existed this design and is already a formal-verification target in `TRIPOD.md`. Messages and blocks carry no equivalent pre-existing "sum stays constant" claim; today each message/block is independently born hot and decays independently, exactly like words. So they get the word-shaped treatment (independent regression-tuned decay, no zero-sum transfer primitive needed), not the VM-shaped one. Inventing a conservation law for messages or blocks would be adding a new invariant nobody asked for — the word-level mechanism this all traces back to was never conservative either.
**This resolves `hermes/init.4th`'s hardcoded `VM-COUNT=3` `COMMON-CH` floor as a direct consequence, not a separate patch.** `Q.1 / VM-COUNT` as "Hermes's fair share" only ever made sense because the old design conflated channel-heat and fleet-heat into one shared `Q.1` budget — exactly the conflation this decision rules out. Under the settled model, `COMMON-CH` is an ordinary channel: born hot, decays via Hermes's own `msg_decay_slope_q48` like every other channel, with no `VM-COUNT` dependency and no fleet-relative floor concept at all. The hardcoded `3` disappears because the quantity it computed (a fleet-relative share) was never coherent for a channel to have, not because a replacement formula was found for it.
This unblocks the DoE rewrite scoping: it's now clear the new campaign observes (at least) four independent metric spaces — word, VM-fleet, message, block — each with its own rolling window and inferred slope, none merged into a single number.
## Resolved items
1. ~~Confirm the single-threaded (or otherwise race-free) assumption for the dispatch/birth/kill path.~~ **Resolved** — see the Concurrency note under section 3. No locking needed.
2. ~~Confirm the birth/kill proportional-weighting formula matches intent.~~ **Resolved by simplification, then superseded (rev g)** — birth needs no weighting formula at all (starts at zero, see section 5). Kill's proportional-to-current-share redistribution, initially thought settled, was revisited during implementation and replaced outright — see item 7.
3. ~~New function signatures / file organization.~~ **Resolved** — see File organization and new primitive signatures.
4. ~~`VM_FLEET_WINDOW_DEPTH` value.~~ **Resolved** — 64, see section 2.
5. ~~What replaces deleted words at call sites.~~ **Resolved** — see Consumer code migration, except `doe-campaign.4th`, which is explicitly deferred as its own decision.
6. ~~The Fold-in question — whether Hermes/Artemis's heat economies merge into `VMPhysics` or stand as their own sibling instances of the same mechanism.~~ **Resolved (rev e)** — sibling instances, no merge; see "Fold-in question — settled" above. Governs the Hermes `COMMON-CH` floor fix (resolved as a direct consequence) and the scope of the DoE rewrite (four independent metric spaces).
7. ~~Kill's proportional-fan-out formula (item 2) turned out to have real problems: division, an all-cold edge case, and an arguable weighting policy.~~ **Resolved (rev g)** — replaced with a parent-pointer walk to Hera (single structural root, self-referential `parent_vm_id`), reusing a previously-dead `parent_vm_id` field already scaffolded in the registry. See the rewritten Kill bullet under section 5. Implemented and amd64-accepted; three-arch acceptance still pending.