Files
StarForth/src/starkernel/capsule/capsule_vm_physics.c
T
Robert Allan JamesandClaude Sonnet 5 89d8c08582 stadium: wire STADIUM_CAPACITY_TICK in as a flat threshold, not a scheduler
Closes FABRIC-2.md's last open §12 Q5 question. fleet_heartbeat_tick_count
is fed by every live VM's own vm_tick(), not one VM's, so it was reaching
HEARTBEAT_INFERENCE_FREQUENCY (shared/borrowed from the per-VM inference
gate) several times faster than intended with more than one VM live -
backwards from FABRIC.md §22.4's required ~1000:1 separation.

What's actually gated turned out to be low-stakes: vm_physics_tick()
(capsule_vm_physics.c:397) is a passive statistics refit - re-sorts a
window of past heat-transfer samples and recomputes a median rate
estimate. It doesn't move heat or arbitrate capacity. Firing too often
just meant a noisier statistic recomputed more frequently than planned,
not incorrect behavior.

Considered and explicitly rejected: scaling the threshold by live VM
count at the check site. That's the first brick of a scheduler - reading
fleet state to adjust a rate dynamically - which this project has
deliberately avoided building. Implemented instead: STADIUM_CAPACITY_TICK
(existing Kconfig symbol, defined but never read by any code path) now
gates vm_physics_heartbeat_tick()'s call directly, replacing the borrowed
HEARTBEAT_INFERENCE_FREQUENCY. Default bumped 1000 -> 4000, a flat
constant picked once for Tripod's known 4-VM topology, same kind of
placeholder as every other frequency knob in Kconfig.kernel - not
computed from anything at runtime. Renamed fleet_last_inference_tick ->
fleet_last_capacity_tick to match. Still one clock, one counter
(fleet_heartbeat_tick_count) - just a bigger flat divisor on it.

Three-arch QEMU acceptance: all clean to ok>, identical Stadium
conservation invariant on all three (resident_sum=43691 reservoir=21845
sum=65536). logs/20260815-093425/amd64, logs/20260815-093521/aarch64,
logs/20260815-093641/riscv64.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 09:37:58 -04:00

574 lines
24 KiB
C
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.
/*
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.
*/
/**
* capsule_vm_physics.c - Dynamic VM Fleet Physics Implementation
*
* See capsule_vm_physics.h and
* docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
* for the full design rationale.
*
* Concurrency: no locking. The only interrupt-driven kernel path
* (heartbeat_tick, from the timer ISR) touches only its own isolated
* TimeTrustState and never calls into mama_word_*, vm_interpret, or any
* capsule/dictionary state. HEARTBEAT_THREAD_ENABLED=0 means no separate
* OS thread runs physics logic either. BIRTH/KILL/VM-EXEC/VM-CALL/
* VM-STEP all execute synchronously in Hera's single interpreter
* context with nothing able to preempt them mid-transfer.
*
* L8 coupling (rev q): the only place this file reaches outside its own
* VMPhysics/VMFleetWindow state is l8_regime_modulation_q16(), a
* read-only look at Hera's own ssm_l8_state via sk_get_mama_vm() --
* modulating the fleet's empirically-recovered transfer rate uniformly,
* never deciding which VM receives heat. Still a passive observer: this
* changes how fast the fleet's own rate estimate responds, not who runs
* or who gets touched.
*/
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h"
#include "starkernel/console.h"
#include "starforth_config.h" /* STADIUM_CAPACITY_TICK */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
#include "vm.h" /* VM, vm->ssm_l8_state */
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
/* Word-level ROLLING_WINDOW_SIZE defaults to 4096 because word executions
* number in the millions. Fleet touches are real VM-EXEC/VM-CALL/VM-STEP
* dispatches -- dozens to low hundreds over a full boot + acceptance run.
* 64 gives the regression enough samples to stabilize without being
* wastefully oversized for a 3-8 VM fleet. Compile-time constant, same
* tuning-knob convention as ROLLING_WINDOW_SIZE. */
#define VM_FLEET_WINDOW_DEPTH 64
/* |fleet_heat_sum - Q48_ONE| < 5% of Q48_ONE, mirrors fleet-k.4th's
* K-EPSILON (3277). */
#define VM_PHYSICS_EPSILON_Q48 3277ULL
typedef struct {
uint64_t execution_heat_q48;
uint64_t last_active_tick; /* fleet_heartbeat_tick_count value at last touch */
int touched; /* 0 until the first touch; explicit rather than
* overloading last_active_tick == 0, which is a
* legitimate tick value a first touch can land on */
int is_live;
} VMPhysics;
typedef struct vm_physics_node {
VMUuid vm_id;
VMPhysics physics;
struct vm_physics_node *next;
} vm_physics_node_t;
/* kmalloc-backed linked list, same pattern as capsule_birth.c's
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
static vm_physics_node_t *vm_physics_head = (void *)0;
/* One recorded touch's inputs to the transfer law (amount = elapsed_ticks *
* slope >> 16, clamped to available heat) -- not a derived heat value.
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
* heat-snapshot design (rev m) with this: since the transfer law is known
* exactly, the true rate can be recovered directly from any touch that
* wasn't clamped (rate = amount / elapsed_ticks), rather than curve-fitting
* a reconstructed trajectory against a synthetic time axis.
*
* Restated on the virtual tick (FABRIC.md item 2.1, 2026-08-04): this used
* to be elapsed_us, wall-clock microseconds -- non-reproducible under TCG
* per GAP-A1 (FABRIC.md §25.7.1). Now counts fleet_heartbeat_tick_count
* ticks, which is execution-paced (incremented once per vm_tick() call,
* confirmed at vm_runtime.c:143). */
typedef struct {
uint64_t elapsed_ticks; /* since this vm_id's previous touch; 0 if there
* was no previous touch to compare against */
uint64_t amount; /* elapsed_ticks * slope >> 16 at touch time,
* i.e. the rate-implied transfer before any
* clamp; 0 if not computed (first touch, zero
* slope, or non-monotonic tick) */
int clamped; /* 1 if the heat actually moved was capped below
* `amount` by what the rest of the fleet held --
* carries no rate information, excluded from
* the estimator rather than treated as a zero */
} VMFleetTouchSample;
typedef struct {
VMFleetTouchSample touch_samples[VM_FLEET_WINDOW_DEPTH];
uint32_t head;
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
int is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
} VMFleetWindow;
/* One instance, fleet-wide -- not per-VM state. */
static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
/* Below this many informative (unclamped) samples in a full window, skip
* the fit and keep the current slope rather than trust a handful of
* points -- same "skip, don't substitute a degenerate value" philosophy
* as the is_warm gate. Arbitrary but conservative relative to the
* 64-deep window: needs an eighth of the window to be informative. */
#define VM_PHYSICS_MIN_INFORMATIVE_SAMPLES 8
/* Shared adaptive slope, fit from aggregate fleet statistics (one slope
* for the whole fleet), exactly mirroring how the word engine infers one
* decay_slope_q48 per VM instance rather than one per word.
*
* Seeded at 65536/3 (Q48.16 for 1/3), matching the word engine's actual
* bootstrap value (src/vm_bootstrap.c / src/starkernel/vm/vm_bootstrap.c:
* "vm->decay_slope_q48 = (1ULL << 16) / 3"). The old comment on that field
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
* here. This was previously seeded at literal 0, which cannot bootstrap:
* vm_physics_touch's transfer amount is elapsed_ticks * slope >> 16, so a
* zero slope moves zero heat on every touch forever, which starves
* vm_physics_tick's regression of the very heat-trajectory signal it
* needs to infer a better slope -- a closed loop with no way out. Found
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f).
*
* UNVALIDATED after the ns-to-tick restatement (FABRIC.md item 2.1,
* 2026-08-04): this value was calibrated against elapsed wall-clock
* microseconds. Elapsed fleet-heartbeat *ticks* between touches is a
* different quantity at a different scale, and the seed has not been
* re-fit against it. Left as-is rather than guessed at -- the acceptance
* boot test below only checks determinism, not transfer magnitude; a real
* re-tune is DoE work (item 5.1), not something to invent here. */
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
static uint64_t slope_fit_quality_q48 = 0;
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
* HeartbeatState.tick_count -- see the rationale in the header. Advances
* from whichever VM's own heartbeat cycle happens to fire, so the
* readiness signal reflects the fleet's aggregate activity rather than
* any one VM's (in practice, Hera's) personal word-execution rate.
* Declared here (rather than just above vm_physics_heartbeat_tick, where
* it used to live) so vm_physics_touch() can read the current tick
* without a forward declaration -- it is now this counter's second
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
static uint64_t fleet_heartbeat_tick_count = 0;
static uint64_t fleet_last_capacity_tick = 0; /* renamed 2026-08-15 from
fleet_last_inference_tick -- gated on
STADIUM_CAPACITY_TICK now, not HEARTBEAT_
INFERENCE_FREQUENCY, see vm_physics_heartbeat_tick() */
static vm_physics_node_t *vm_physics_find(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_head;
while (n) {
if (vm_uuid_equal(n->vm_id, vm_id)) return n;
n = n->next;
}
return (void *)0;
}
/**
* vm_physics_transfer - The one conservative primitive everything else
* is a special case of. Subtracts amount from from (clamped at 0) and
* adds the same amount to to. Nothing is created or destroyed:
* sum(execution_heat for all LIVE VMs) is invariant across any call.
*/
static 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;
}
void vm_physics_init(VMUuid vm_id)
{
vm_physics_node_t *node = vm_physics_find(vm_id);
/* Hera (vm_uuid_hera()) is the fleet's single structural root -- the one
* entry capsule_vm_kill refuses to ever kill, and the same fixed
* point vm_physics_find_root_id below walks every parent chain up
* to. She is where the fleet's whole Q48_ONE initially resides;
* every other VM joins an already-nonzero fleet and starts at 0,
* per the "cold mass added to a closed system" reasoning -- which
* requires a nonzero sum to already exist, so Hera's own
* registration is where that sum is seeded. */
int is_root = vm_uuid_is_hera(vm_id);
if (node) {
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
return;
}
node = (vm_physics_node_t *)kmalloc(sizeof(vm_physics_node_t));
if (!node) return;
node->vm_id = vm_id;
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
node->next = vm_physics_head;
vm_physics_head = node;
}
/**
* vm_physics_find_root_id - Walk parent_vm_id edges up to Hera
*
* Every VM's parent_vm_id (VMRegistryEntry, capsule_run.h) is set once
* at its own birth and never rewritten -- including a dead VM's, so the
* walk continues correctly through an already-killed intermediate
* parent without any special dead-parent handling. Hera's own entry is
* self-referential (parent_vm_id == vm_id == vm_uuid_hera()), which is
* what stops the walk: she is the only node with no outbound edge.
*
* The 64-hop guard is defensive only -- a well-formed chain terminates
* in O(fleet depth) hops, nowhere near 64. It exists so a corrupted
* chain (which should never happen) returns the starting vm_id instead
* of spinning.
*/
static VMUuid vm_physics_find_root_id(VMUuid vm_id)
{
VMUuid cursor = vm_id;
uint32_t guard;
for (guard = 0; guard < 64; guard++) {
VMRegistryEntry entry;
if (capsule_vm_registry_get(cursor, &entry) != 0) break;
if (vm_uuid_equal(entry.parent_vm_id, cursor)) break; /* self-referential: root */
cursor = entry.parent_vm_id;
}
return cursor;
}
void vm_physics_retire(VMUuid vm_id)
{
vm_physics_node_t *dying = vm_physics_find(vm_id);
vm_physics_node_t *root;
VMUuid root_id;
if (!dying || !dying->physics.is_live) return;
root_id = vm_physics_find_root_id(vm_id);
root = !vm_uuid_equal(root_id, vm_id) ? vm_physics_find(root_id) : (void *)0;
if (root) {
/* The dying VM's entire remaining heat flows to the root it
* chains up to (in practice always Hera today) -- no division,
* no weighting policy, no "no survivors" case, since a root is
* always there by construction. This is also what makes
* TRIPOD-TEST's kill-then-rebirth "K soak" check meaningful: a
* root VM (e.g. Hermes) being killed and later respawned by
* Hera doesn't lose heat across the gap, it just parks at Hera
* in the meantime. */
vm_physics_transfer(&dying->physics, &root->physics,
dying->physics.execution_heat_q48);
}
/* root_id == vm_id means the dying VM IS the root itself (Hera --
* shouldn't happen, she can't be killed) or the chain was broken
* (shouldn't happen, see vm_physics_find_root_id). Either way
* there's nowhere conservation-preserving to send the remainder;
* it is dropped along with the dying entry. */
dying->physics.execution_heat_q48 = 0;
dying->physics.is_live = 0;
}
void vm_physics_touch(VMUuid vm_id)
{
vm_physics_node_t *target = vm_physics_find(vm_id);
VMFleetTouchSample sample = { 0, 0, 0 };
uint64_t now_tick = fleet_heartbeat_tick_count;
if (!target || !target->physics.is_live) return;
/* Restated on the virtual tick (FABRIC.md item 2.1, 2026-08-04): this
* used to gate on target->physics.last_active_ns > 0 and scale by
* elapsed wall-clock microseconds -- non-reproducible run to run under
* TCG (GAP-A1, FABRIC.md §25.7.1). fleet_heartbeat_tick_count is
* execution-paced (vm_runtime.c:143), so elapsed_ticks is now a pure
* function of the execution stream. `touched` replaces the old
* `> 0` sentinel: tick 0 is a legitimate value for a genuine first
* touch to land on, so "never touched" needs its own flag rather than
* overloading the tick value. */
if (fleet_transfer_slope_q48 > 0 &&
target->physics.touched &&
now_tick > target->physics.last_active_tick) {
uint64_t elapsed_ticks = now_tick - target->physics.last_active_tick;
uint64_t amount = (elapsed_ticks * fleet_transfer_slope_q48) >> 16;
sample.elapsed_ticks = elapsed_ticks;
sample.amount = amount;
if (amount > 0) {
/* Pull toward the touched VM, proportional to each other LIVE
* VM's current heat -- same fan-out shape as retire, mirror
* direction. Clamped to what the rest of the fleet actually
* holds, so this can never manufacture heat. */
vm_physics_node_t *n;
uint64_t others_total = 0;
uint64_t moved_total;
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
others_total += n->physics.execution_heat_q48;
}
}
/* moved_total == 0 when others_total == 0 (nothing to pull --
* the fleet is already fully concentrated elsewhere), handled
* uniformly by the same clamp as a partial cap: either way
* the actual transfer fell short of what the rate implied, so
* this touch carries no usable rate information (rev o). */
moved_total = (amount > others_total) ? others_total : amount;
sample.clamped = (moved_total != amount);
if (moved_total > 0) {
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
uint64_t share = (moved_total * n->physics.execution_heat_q48)
/ others_total;
vm_physics_transfer(&n->physics, &target->physics, share);
}
}
}
}
}
target->physics.last_active_tick = now_tick;
target->physics.touched = 1;
/* Record this touch's transfer-law inputs, not a derived heat value
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
* VMFleetTouchSample for why. */
fleet_window.touch_samples[fleet_window.head] = sample;
fleet_window.head = (fleet_window.head + 1) % VM_FLEET_WINDOW_DEPTH;
if (fleet_window.count < VM_FLEET_WINDOW_DEPTH) {
fleet_window.count++;
}
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
}
/**
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
*
* Gives L8 a real, passive-observer-respecting causal channel into the
* fleet's transfer rate (rev q, VM-FLEET-ATTRACTOR-DESIGN-20260705.md):
* previously nothing L8 did could affect fleet dynamics at all (rev j's
* architectural diagnosis for its null DoE result). Reuses Hera's
* adaptive table's current_regime -- the same 3-bit entropy/cv/temporal
* classification the bandit already stratifies its own scores by,
* rather than inventing a new signal -- as a proxy for fleet-wide
* dispatch activity, since all VM-EXEC/VM-CALL flows through her.
*
* modulation = (popcount(regime) + 1) * 0.5, i.e. 0.5x-2.0x: more of the
* three high-signal bits active scales the fleet's already-recovered
* rate up (track change more aggressively), fewer scales it down (damp
* toward the raw empirical rate). No per-VM favoritism -- this scales
* whatever vm_physics_tick() already computed uniformly, it doesn't
* decide who receives heat.
*
* Returns Q16 65536 (neutral 1.0x) if Hera or her adaptive table isn't
* available, so a missing signal never blocks or corrupts the estimate
* this multiplies.
*/
static uint32_t l8_regime_modulation_q16(void)
{
VM *mama = (VM*)sk_get_mama_vm();
ssm_l8_state_t *l8;
uint32_t popcount;
if (!mama || !mama->ssm_l8_state) return 65536u;
l8 = (ssm_l8_state_t*)mama->ssm_l8_state;
if (!l8->table) return 65536u; /* legacy path tracks no regime */
popcount = (uint32_t)(((l8->table->current_regime >> 2) & 1u) +
((l8->table->current_regime >> 1) & 1u) +
(l8->table->current_regime & 1u));
return (popcount + 1u) * 32768u;
}
void vm_physics_tick(uint64_t now_ns)
{
uint64_t rates[VM_FLEET_WINDOW_DEPTH];
uint32_t rate_count = 0;
uint32_t i;
(void)now_ns; /* Unlike the log-linear regression this replaced, the
* estimator below needs no external time reference --
* each sample already carries its own elapsed_ticks. */
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
* substitute a default slope. Expected to stay unwarmed (or warm
* with a low-quality fit) at the current 3-VM Tripod scale -- there
* simply aren't enough distinct touch events yet. */
if (!fleet_window.is_warm) return;
/* Direct rate recovery, not curve-fitting (rev o,
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
* OLS approach of rev b-n): the transfer law is known exactly
* (amount = elapsed_ticks * slope >> 16), so any unclamped touch can
* be inverted directly for an exact per-sample rate. Order doesn't
* matter here -- a median over the observation window, not a
* time-indexed fit -- so this walks the fixed-size array directly
* rather than replaying ring order from fleet_window.head. */
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
if (s->clamped || s->amount == 0 || s->elapsed_ticks == 0) continue;
rates[rate_count++] = (s->amount << 16) / s->elapsed_ticks;
}
/* slope_fit_quality_q48 as the informative fraction of the window --
* a real measure now, not a fixed placeholder. Reported even when
* there's too little to fit, same as a partial-sample regression
* would report low confidence rather than nothing at all. */
slope_fit_quality_q48 = ((uint64_t)rate_count << 16) / VM_FLEET_WINDOW_DEPTH;
/* Too few informative samples to trust: keep the current slope
* rather than overwrite it with an estimate from a handful of
* points (or zero) -- the same "skip, don't substitute a degenerate
* value" philosophy as the is_warm gate above. */
if (rate_count < VM_PHYSICS_MIN_INFORMATIVE_SAMPLES) return;
/* Insertion sort for the median -- rate_count <= VM_FLEET_WINDOW_DEPTH
* (64), no library qsort in this freestanding kernel-only file. */
for (i = 1; i < rate_count; i++) {
uint64_t key = rates[i];
uint32_t j = i;
while (j > 0 && rates[j - 1] > key) {
rates[j] = rates[j - 1];
j--;
}
rates[j] = key;
}
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
}
/* Gated on STADIUM_CAPACITY_TICK, not HEARTBEAT_INFERENCE_FREQUENCY (fixed
* 2026-08-15, FABRIC-2.md §12 Q5): fleet_heartbeat_tick_count is fed by
* EVERY live VM's own vm_tick() (see the comment above this counter's
* declaration), so it advances several times faster in wall-clock terms
* than a single VM's own tick_count -- comparing it against
* HEARTBEAT_INFERENCE_FREQUENCY (written assuming a single VM's stream)
* meant this fired far more often than §22.4's required ~1000:1 separation
* from a VM's own heat-inference loop. Still one clock, one counter --
* this is a bigger flat threshold on the same counter, not a second tick
* source or anything that reads live VM count to adjust itself. */
void vm_physics_heartbeat_tick(uint64_t now_ns)
{
fleet_heartbeat_tick_count++;
if ((fleet_heartbeat_tick_count - fleet_last_capacity_tick) >= STADIUM_CAPACITY_TICK) {
vm_physics_tick(now_ns);
fleet_last_capacity_tick = fleet_heartbeat_tick_count;
}
}
uint64_t vm_physics_fleet_heat_sum(void)
{
uint64_t total = 0;
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
if (n->physics.is_live) {
total += n->physics.execution_heat_q48;
}
}
return total;
}
int vm_physics_conserved(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
return diff < VM_PHYSICS_EPSILON_Q48;
}
uint64_t vm_physics_heat_of(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_find(vm_id);
if (!n || !n->physics.is_live) return 0;
return n->physics.execution_heat_q48;
}
/* Decimal print, same shape as parity.c's parity_put_u64 -- freestanding,
* no snprintf dependency. */
static void console_put_u64(uint64_t val)
{
char buf[21];
int i = 20;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = (char)('0' + (val % 10));
val /= 10;
}
}
console_puts(&buf[i + 1]);
}
void vm_physics_status(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
console_puts("VM-PHYSICS: fleet_heat_sum=");
console_put_u64(sum);
console_println("");
console_puts("VM-PHYSICS: conserved=");
console_println(vm_physics_conserved() ? "CONSERVED" : "DRIFTED");
console_puts("VM-PHYSICS: fleet_transfer_slope_q48=");
console_put_u64(fleet_transfer_slope_q48);
console_println("");
console_puts("VM-PHYSICS: slope_fit_quality_q48=");
console_put_u64(slope_fit_quality_q48);
console_println("");
console_puts("VM-PHYSICS: fleet_window_warm=");
console_println(fleet_window.is_warm ? "YES" : "NO");
/* Per-VM breakdown (VM-FLEET-ATTRACTOR-DESIGN-20260705.md transparency
* requirement): the aggregate above always reads Q.1 when conserved,
* which says nothing about how heat is actually split among live VMs. */
{
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
VMRegistryEntry entry;
if (!n->physics.is_live) continue;
console_puts("VM-PHYSICS: vm_id=");
{
char uuid_buf[37];
vm_uuid_format(n->vm_id, uuid_buf);
console_puts(uuid_buf);
}
if (capsule_vm_registry_get(n->vm_id, &entry) == 0) {
console_puts(" name=");
console_puts(entry.name);
}
console_puts(" heat_q48=");
console_put_u64(n->physics.execution_heat_q48);
console_println("");
}
}
}