Stage H: DoE rebuilt on BIRTH/SEND/KILL, defs-only workloads; work heat floor on contact (FABRIC-3.8 §3.9, §3.13.7)

Workload capsules the DoE births are definitions only; each defines WL-RUN
(its old birth-time work plus its entry word twice) and WL-DONE-ARGS. The
driver births N workers, SENDs each 'WL-RUN WL-DONE-ARGS SEND', waits in an
ordinary loop for N MU-DONE messages, KILLs them (parked idle), and emits
the marker. The serialized VM-EXEC round-robin is gone.

Kernel: no switch checkpoint after a payload's last word; work heat is the
assessed per-beat charge, and a VM gaining work is raised to the busy set's
floor (Captain Bob's ruling) -- fixes a live stall where a busy-waiting Hera
never yielded, without serializing batch starts.

Evidence: cfg-4 rows spread evenly over Hera and all 8 worker slots (44k
switches); full cfg-4 trials on amd64/aarch64/riscv64 end fail=0 with 8/8
KILL dead; Stage G interleave byte-identical on all three. Acceptance:
dict_hash unchanged, relay 3, idle growth 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 23:48:16 -04:00
co-authored by Claude Opus 5.5
parent 81def7b4a5
commit f152a91099
29 changed files with 165584 additions and 153 deletions
+25 -11
View File
@@ -68,15 +68,16 @@ typedef struct {
* away, so a one-beat charge (slope < 1 unit)
* still moves heat once enough beats accrue */
uint64_t work_heat_q48; /* FABRIC-3.8.md §3.13.7, ruling C-4 (β), Captain
* Bob 2026-09-23: cumulative heat this VM has
* pulled in through its OWN per-beat charges --
* heat earned by working. The switch decision
* Bob 2026-09-23: heat earned by working -- the
* charge assessed at each of this VM's own
* beats (sub-unit scale), whether or not it
* could be moved; raised to the busy set's
* floor when the VM gains work (Stage H,
* "floor on contact"). The switch decision
* compares this, not execution_heat_q48, so
* Hera's structural endowment (all of Q48_ONE
* at init) never counts against her. Only ever
* grows; a record of transfers that already
* happened, not heat of its own (fleet K is
* unaffected). */
* Hera's structural endowment never counts
* against her. Bookkeeping, not heat of its
* own: fleet K is unaffected. */
} VMPhysics;
typedef struct vm_physics_node {
@@ -388,6 +389,15 @@ void vm_physics_charge(VMUuid vm_id)
if (!target || !target->physics.is_live) return;
/* §3.13.7 (β), refined at Stage H (Captain Bob, "floor on contact"):
* work heat accrues the charge each beat was ASSESSED, in the carry's
* own sub-unit scale -- not what could actually be moved. Counting
* only moved heat left a busy VM at 0 whenever the rest of the fleet
* held nothing to pull (seen live: Hera busy-waiting in the DoE never
* yielded). This is work done, beats x slope, independent of who holds
* heat. */
target->physics.work_heat_q48 += fleet_transfer_slope_q48;
target->physics.charge_carry += fleet_transfer_slope_q48;
amount = target->physics.charge_carry >> 16;
if (amount == 0) return;
@@ -419,9 +429,6 @@ void vm_physics_charge(VMUuid vm_id)
* vanishing. Conservation is unaffected either way -- only what
* vm_physics_transfer() actually moved ever changed any heat. */
target->physics.charge_carry += (moved_total - moved) << 16;
/* §3.13.7 (β): what this VM's own work actually pulled in. */
target->physics.work_heat_q48 += moved;
}
uint64_t vm_physics_work_heat_of(VMUuid vm_id)
@@ -431,6 +438,13 @@ uint64_t vm_physics_work_heat_of(VMUuid vm_id)
return n->physics.work_heat_q48;
}
void vm_physics_work_heat_floor(VMUuid vm_id, uint64_t floor_q48)
{
vm_physics_node_t *n = vm_physics_find(vm_id);
if (!n || !n->physics.is_live) return;
if (n->physics.work_heat_q48 < floor_q48) n->physics.work_heat_q48 = floor_q48;
}
/**
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
*
@@ -269,6 +269,46 @@ VM *sk_vm_switch_decide(VM *self, int self_competes) {
return best;
}
/* Stage H, "floor on contact" (Captain Bob, 2026-09-23). See the header. */
void sk_vm_switch_signal_note_work_gained(VMUuid vm_id) {
VM *current = sk_vm_switch_current_vm();
VM *mama = (VM *)sk_get_mama_vm();
VMRegistryEntry e;
VM *v;
uint64_t floor_q48 = 0;
int have = 0, i;
if (capsule_vm_registry_get(vm_id, &e) != 0 || !e.vm_ptr) return;
v = (VM *)e.vm_ptr;
/* Only a VM that had NO work gains some here: not the one running, and
* not one parked mid-task (either already had work, and keeps its own
* work heat). */
if (v == current) return;
if (v->native_stack_saved_sp != 0 && v->frames.count > 0 &&
v->frames.entries[v->frames.count - 1].kind != VM_FRAME_IDLE) return;
/* The floor: the lowest work heat among the VMs that have work right
* now -- the one running, and every other eligible participant. */
if (current) {
floor_q48 = vm_physics_work_heat_of(current->stadium_vm_id);
have = 1;
}
for (i = 0; i < g_slot_count; i++) {
VMRegistryEntry o;
VM *ov;
uint64_t heat, stamp;
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) continue;
if (capsule_vm_registry_get(g_slots[i].vm_id, &o) != 0) continue;
ov = (VM *)o.vm_ptr;
if (!ov || ov == current || o.pending_reap) continue;
if (o.state == VM_STATE_DEAD || o.state == VM_STATE_STILLBORN) continue;
if (!candidate_standing(ov, mama, g_slots[i].parked_stamp, &heat, &stamp)) continue;
if (!have || heat < floor_q48) { floor_q48 = heat; have = 1; }
}
if (have) vm_physics_work_heat_floor(vm_id, floor_q48);
}
void sk_vm_switch_reap_pending(void) {
VM *current = sk_vm_switch_current_vm();
int i;
+1
View File
@@ -536,6 +536,7 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
* the switch decision told something changed -- the recipient may now
* be the coolest VM with work. */
msg->wait_stamp = sk_vm_switch_signal_next_stamp();
if (q->count == 1) sk_vm_switch_signal_note_work_gained(to); /* Stage H floor */
sk_vm_switch_signal_note_mail();
return 0;
+22 -1
View File
@@ -1213,6 +1213,19 @@ int sk_vm_owns_outermost_frame(VM *vm) {
vm->mode != MODE_COMPILE;
}
/* Stage H (FABRIC-3.8.md §3.9): 1 if the rest of the input holds no more
* words -- the same whitespace set vm_parse_word() skips, so "exhausted"
* means exactly "the next vm_parse_word() would return 0". */
static int vm_input_exhausted(const VM *vm) {
size_t p = vm->input_pos;
while (p < vm->input_length) {
char c = vm->input_buffer[p];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') return 0;
p++;
}
return 1;
}
void vm_interpret(VM* vm, const char* input)
{
vm_interpret_kind(vm, input, VM_FRAME_INTERPRET);
@@ -1276,7 +1289,15 @@ void vm_interpret_kind(VM* vm, const char* input, uint32_t kind)
* is a clean switch point too -- without it, a payload made only
* of primitives (no colon word, so no execute_colon_word()
* checkpoint) could never be displaced. Same ownership gate. */
if (!vm->error && !vm->abort_requested && sk_vm_owns_outermost_frame(vm)) {
/* Stage H (2026-09-23): not after the LAST word. A VM with nothing
* left to interpret isn't mid-task -- its trampoline decides the
* moment this returns -- and switching it out here would park it
* "mid-task" with no work left, which KILL (rightly) refuses. Seen
* in design for the DoE's completion notice: a payload whose final
* word is a SEND to Hera sets decision-due, and this checkpoint
* would otherwise act on it one word too late to matter. */
if (!vm->error && !vm->abort_requested && sk_vm_owns_outermost_frame(vm) &&
!vm_input_exhausted(vm)) {
sk_vm_switch_checkpoint(vm);
}
#endif