/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 Robert A. James All rights reserved. This file is part of the StarForth project. Licensed under the StarForth License, Version 1.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at: https://github.com/star.4th@proton.me/StarForth/LICENSE.txt This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement. See the License for the specific language governing permissions and limitations under the License. */ /** * capsule_vm_physics.h - Dynamic VM Fleet Physics * * Structural parallel to the word-level physics engine (execution heat, * decay, rolling-window history, regression-inferred slope) applied to * VMs instead of dictionary words. Kernel-only: no hosted-build * equivalent, since the hosted build has no multi-VM fleet. * * sum(execution_heat_q48 for all LIVE VMs) == Q.1 is a conservation * invariant, held by construction: every state change is a balanced * transfer (see design doc VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md). * * This is a passive observer of VM activity, never a driver of it — * there is no scheduler here. Recording happens only at existing * dispatch points (BIRTH, KILL, VM-EXEC, VM-CALL, VM-STEP). */ #ifndef STARKERNEL_CAPSULE_VM_PHYSICS_H #define STARKERNEL_CAPSULE_VM_PHYSICS_H #include #include "starkernel/vm_uuid.h" /* VMUuid -- FABRIC-0.md item 3.8 */ #ifdef __cplusplus extern "C" { #endif /** * vm_physics_init - Register a newly-born VM at zero heat * * Called from mama_word_birth once a VM reaches VM_STATE_LIVE. Starts * at execution_heat_q48 = 0, so conservation holds trivially — no * fan-out from existing VMs is needed. * * @param vm_id Registry VM ID assigned at birth */ void vm_physics_init(VMUuid vm_id); /** * vm_physics_retire - Remove a killed VM, returning its heat to Hera * * Called by vm_physics_remove() (FABRIC-3.10.8 K-9) before the registry * entry is torn down; mama_word_kill used to call it directly. * The dying VM's entire remaining heat is transferred up its * parent_vm_id chain (VMRegistryEntry, capsule_run.h) to the fleet's * single structural root, Hera -- no division, no per-survivor * weighting. The chain walk tolerates already-dead intermediate * parents (a parent's own parent_vm_id was set once at its birth and * never rewritten, so the walk continues through it). * * @param vm_id Registry VM ID of the VM being killed */ void vm_physics_retire(VMUuid vm_id); /** * vm_physics_remove - Retire a dead VM, then drop its node * * FABRIC-3.10.8 K-9: called from capsule_vm_kill() and * capsule_vm_force_reap(), while the registry entry is still readable * (retirement walks parent_vm_id). Heat goes to the root exactly as * vm_physics_retire(); the node is then unlinked and freed, so a dead VM * leaves nothing in the fleet list. Hera is refused. * * @param vm_id Registry VM ID of the VM being torn down */ void vm_physics_remove(VMUuid vm_id); /** * vm_physics_touch - Record real dispatch activity against a VM * * Called from the three existing dispatch primitives (VM-EXEC, VM-CALL, * VM-STEP) at the point they've already confirmed the target VM is * live. Pulls heat from the rest of the fleet toward the touched VM * (amount = elapsed_ticks * fleet_transfer_slope_q48 >> 16) and appends * vm_id to the fleet's rolling touch-history window. * * Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): no * longer takes a wall-clock timestamp. Reads fleet_heartbeat_tick_count * internally, which is execution-paced (advanced once per vm_tick() * call, see vm_physics_heartbeat_tick), so the transfer this produces is * a deterministic function of the execution stream, not of wall time. * * @param vm_id Registry VM ID being dispatched to */ void vm_physics_touch(VMUuid vm_id); /** * vm_physics_charge - Charge the running VM for one beat of its own work * * FABRIC-3.8.md §3.13.2 (Stage C-a, 2026-09-23). The work law, beside * vm_physics_touch()'s dispatch law: called once per beat a VM produces * while it is the switch-current VM (vm_tick()), so a VM running its own * kernel-Hermes mail on its own stack heats up as it works -- before this, * nothing did (G9). Adds one beat's slope to a per-VM carry, and pulls the * carry's whole units from the rest of the live fleet in proportion to * each VM's heat, through the same conservative transfer as touch. The * fraction below one unit, and any whole units lost to the proportional * split's own rounding, stay in the carry instead of being discarded. * Records NO fleet_window sample, so the slope estimator and the existing * DoE heat columns keep their meaning (§3.13.2 "Frozen slope"). * * @param vm_id Registry VM ID of the running VM */ void vm_physics_charge(VMUuid vm_id); /** * vm_physics_work_heat_of - Cumulative heat a VM has pulled in by working * * FABRIC-3.8.md §3.13.7, ruling C-4 (β), Captain Bob 2026-09-23: heat * earned by the VM's own work -- the charge vm_physics_charge() assessed at * each of its beats (beats x slope, in the charge carry's sub-unit scale), * whether or not the rest of the fleet held heat to move -- independent of * any structural endowment (Hera's initial Q48_ONE). This is the quantity * the switch decision compares ("coolest VM with work" = least work heat). * Never decreases, except that it may be raised by * vm_physics_work_heat_floor(). 0 for an unknown or dead VM. */ uint64_t vm_physics_work_heat_of(VMUuid vm_id); /** * vm_physics_work_heat_floor - "Floor on contact" (Captain Bob, Stage H) * * Raise vm_id's work heat to floor_q48 if it is below it; never lowers it. * Called when a VM that had no work gains some (sk_vm_switch_signal_note_ * work_gained()), with the lowest work heat among the VMs that currently * have work -- so a newly-busy VM joins the running set level with its * floor instead of at whatever it last had (0 for a new VM), which would * otherwise let it run alone until it "caught up." */ void vm_physics_work_heat_floor(VMUuid vm_id, uint64_t floor_q48); /** * vm_physics_tick - Heartbeat-gated inference pass * * Mirrors vm_tick_inference_engine: when the fleet touch-history window * is warm, replays it to re-fit fleet_transfer_slope_q48 via the same * closed-form log-linear OLS regression used for word-level decay. * Skips the update (does not substitute a default) when unwarmed or * when fit quality is not usable. * * @param now_ns Current monotonic time in nanoseconds */ void vm_physics_tick(uint64_t now_ns); /** * vm_physics_heartbeat_tick - Fleet-wide heartbeat, gates vm_physics_tick() * * Call from every VM's own vm_tick(), not just Hera's. Increments a * single fleet-wide tick counter (distinct from any VM's own per-VM * HeartbeatState.tick_count) and calls vm_physics_tick() once that * shared counter has advanced by HEARTBEAT_INFERENCE_FREQUENCY since * the last fit. * * Fixes a real defect (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev k): * gating this on Hera's own tick_count meant the inference pass almost * never fired, because Hera-as-orchestrator mostly blocks on VM-EXEC/ * VM-CALL dispatch -- work that accrues to the *target* VM's own tick * count, not hers. VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md already * specified the fix's direction: fleet_transfer_slope_q48 must be * inferred "from aggregate fleet statistics... not one per VM" -- the * same principle applies to the readiness signal that gates computing * it, not just the regression's own input trajectory. * * @param now_ns Current monotonic time in nanoseconds (of whichever * VM is calling; unused by vm_physics_tick() today) */ void vm_physics_heartbeat_tick(uint64_t now_ns); /** * vm_physics_fleet_heat_sum - Sum of execution_heat_q48 over all LIVE VMs * * Diagnostic / verification primitive. Should always read Q.1 (65536) * if the conservation invariant holds. */ uint64_t vm_physics_fleet_heat_sum(void); /** * vm_physics_heat_of - Current execution_heat_q48 for one VM * * Transparency primitive (VM-FLEET-ATTRACTOR-DESIGN-20260705.md): the * fleet-wide conservation invariant always reads Q.1 by construction, so * it carries no information about how heat is actually distributed among * live VMs. This is the per-VM fact that sum alone can't provide. * * @param vm_id Registry VM ID * @return execution_heat_q48, or 0 if vm_id is unknown or not live */ uint64_t vm_physics_heat_of(VMUuid vm_id); /** * vm_physics_conserved - Conservation check * * @return 1 if |vm_physics_fleet_heat_sum() - Q.1| < epsilon, else 0 */ int vm_physics_conserved(void); /** * vm_physics_status - Print a diagnostic status report * * Replaces K-STATUS/VM-STATUS. Reports fleet heat sum, conservation * verdict, and the current fleet-wide inferred slope / fit quality / * warm-up state -- the fleet-level analog of K-STATUS's per-VM report, * adapted to a mechanism with no fixed VM count to enumerate. */ void vm_physics_status(void); #ifdef __cplusplus } #endif #endif /* STARKERNEL_CAPSULE_VM_PHYSICS_H */