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

35 KiB
Raw Blame History

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 ... ALLOTed 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:

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_ids instead of word_ids, plus the same warm-up gate:

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

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:

: 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:

( 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:

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 44S" 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
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 orderinit.4th still hand-scripts BIRTHing 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.