Stage G: SEND, and two workers interleave; decision compares work heat (FABRIC-3.8 §3.8, §3.13.7)

SEND ( paddr plen name-c name-u -- ok? ): general kernel-Hermes send to a
named VM, from = calling VM, one block max (refused, not truncated), every
VM. New SK_HERMES_MSG_TYPE_SEND (30).

Ruling C-4 (beta), Captain Bob: the switch decision compares work heat --
the heat each VM pulled in by its own per-beat charges -- not absolute
heat, so Hera's structural endowment no longer makes her hottest. Before
it, the two-worker test ran serial (AAAA...BBBB); after it, byte-identical
on amd64/aarch64/riscv64:
AAABBBBBBBBAAAAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAAABB
(40 A, 40 B, 16 runs; ordinary words only, nothing yields). Acceptance:
relay 3, idle growth 0; dict_hash set changed by the new word
(word_count 533) and identical across ISAs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-23 21:01:44 -04:00
co-authored by Claude Opus 5.5
parent c7185facf0
commit 81def7b4a5
18 changed files with 46044 additions and 7 deletions
@@ -67,6 +67,16 @@ typedef struct {
* FABRIC-3.8.md §3.13.2: kept, never truncated
* 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
* 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). */
} VMPhysics;
typedef struct vm_physics_node {
@@ -208,6 +218,7 @@ void vm_physics_init(VMUuid vm_id)
node->physics.touched = 0;
node->physics.is_live = 1;
node->physics.charge_carry = 0;
node->physics.work_heat_q48 = 0;
return;
}
@@ -220,6 +231,7 @@ void vm_physics_init(VMUuid vm_id)
node->physics.touched = 0;
node->physics.is_live = 1;
node->physics.charge_carry = 0;
node->physics.work_heat_q48 = 0;
node->next = vm_physics_head;
vm_physics_head = node;
}
@@ -407,6 +419,16 @@ 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)
{
vm_physics_node_t *n = vm_physics_find(vm_id);
if (!n || !n->physics.is_live) return 0;
return n->physics.work_heat_q48;
}
/**
@@ -34,7 +34,7 @@
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/capsule_birth.h" /* registry, capsule_vm_force_reap() */
#include "starkernel/capsule_vm_physics.h" /* vm_physics_heat_of() -- the decision's heat */
#include "starkernel/capsule_vm_physics.h" /* vm_physics_work_heat_of() -- the decision's heat (C-4 β) */
#include "starkernel/vm/kernel_hermes.h" /* sk_hermes_pending_peek() -- eligibility is the queue */
#include "starkernel/vm/vm_frame.h" /* top frame kind -- in progress vs parked idle */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm() */
@@ -222,7 +222,10 @@ static int candidate_standing(VM *v, VM *mama, uint64_t parked_stamp,
}
}
if (!eligible) return 0;
*heat_out = vm_physics_heat_of(v->stadium_vm_id);
/* §3.13.7, ruling C-4 (β): work heat -- heat earned by this VM's own
* work -- not absolute heat, so Hera's structural endowment never
* makes her the "hottest" VM by construction. */
*heat_out = vm_physics_work_heat_of(v->stadium_vm_id);
*stamp_out = stamp;
return 1;
}
@@ -260,7 +263,7 @@ VM *sk_vm_switch_decide(VM *self, int self_competes) {
/* A running VM that still has work keeps the CPU unless the best
* candidate is strictly cooler: a tie with the incumbent is "no reason
* to move," not a coin toss (ruling C-3). */
if (best && self_competes && !(best_heat < vm_physics_heat_of(self->stadium_vm_id))) {
if (best && self_competes && !(best_heat < vm_physics_work_heat_of(self->stadium_vm_id))) {
return (VM *)0;
}
return best;
+63
View File
@@ -489,10 +489,73 @@ static void sk_word_kh_elevate_send(VM *vm) {
vm_push(vm, rc == 0 ? 1 : 0);
}
/* FABRIC-3.8.md §3.8 (Stage G, 2026-09-23; name ruled by Captain Bob:
* "SEND"). SEND ( paddr plen name-c name-u -- ok? ): the general
* FORTH-callable kernel-Hermes send. Delivers a copy of the payload text
* to the VM named name-c/name-u, where it is interpreted -- on that VM's own
* native stack, by a switch, once the compudynamic decision chooses it.
* `from` is the calling VM, derived here, never taken from the stack (the
* anti-spoof rule KH-ELEVATE-SEND established). Bounded to one block of
* text (the payload is sent NUL-terminated, so at most
* SK_HERMES_CHUNK_MAX_PAYLOAD - 1 characters); an oversized payload is
* refused, not truncated -- truncated code would run as different code.
* A plain, unconditional primitive: restricting who may SEND is ACL.4th's
* job, never a C check. Returns 1 if admitted, 0 if refused (no such VM,
* dead, oversized, bad address, or kernel-Hermes refused: queue full or the
* sender's reservoir can't fund it). */
static char g_send_buf[SK_HERMES_CHUNK_MAX_PAYLOAD];
static void sk_word_send(VM *vm) {
cell_t name_u, name_c, plen, paddr;
char name[VM_NAME_MAX];
VMRegistryEntry target;
const char *src;
int i, rc;
if (vm->dsp < 3) {
console_println("SEND: expects paddr plen S\" name\" SEND");
vm->error = 1;
return;
}
name_u = vm_pop(vm);
name_c = vm_pop(vm);
plen = vm_pop(vm);
paddr = vm_pop(vm);
if (name_u <= 0 || (uint32_t)name_u >= VM_NAME_MAX ||
plen < 0 || plen > SK_HERMES_CHUNK_MAX_PAYLOAD - 1) {
vm_push(vm, 0);
return;
}
src = (const char *)vm_ptr(vm, (vaddr_t)name_c);
if (!src) { vm_push(vm, 0); return; }
for (i = 0; i < (int)name_u; i++) name[i] = src[i];
name[name_u] = '\0';
if (capsule_vm_find_by_name_nocase(name, &target) != 0 || !target.vm_ptr ||
target.state == VM_STATE_DEAD || target.state == VM_STATE_STILLBORN) {
vm_push(vm, 0);
return;
}
if (plen > 0) {
src = (const char *)vm_ptr(vm, (vaddr_t)paddr);
if (!src) { vm_push(vm, 0); return; }
for (i = 0; i < (int)plen; i++) g_send_buf[i] = src[i];
}
g_send_buf[plen] = '\0';
rc = sk_hermes_send_one(vm->stadium_vm_id, target.vm_id,
SK_HERMES_MSG_TYPE_SEND, 0,
g_send_buf, (uint32_t)(plen + 1));
vm_push(vm, rc == 0 ? 1 : 0);
}
void sk_repl_register_words(VM *vm) {
register_word(vm, "BLK-ATTACH-ACK", sk_word_blk_attach_ack);
register_word(vm, "KH-BLK-ATTACH-SEND", sk_word_kh_blk_attach_send);
register_word(vm, "KH-ELEVATE-SEND", sk_word_kh_elevate_send);
register_word(vm, "SEND", sk_word_send); /* FABRIC-3.8.md §3.8 */
}
/*===========================================================================