Stage M-B: autonomous events -- EV-DONE / EV-BORN / EV-DIED published by the kernel itself (FABRIC-3.9.0 §7.3, M-3..M-6)

- Permanent event channels 1/2/3 created at boot beside channel 0, exposed
  as FORTH constants EV-DONE / EV-BORN / EV-DIED; CHANNEL-CLOSE refuses 0..3.
- sk_hermes_event(): `S" <name>" ON-...` as type 32 to every member except
  the VM it's about; never blocks -- undeliverable copies are dropped,
  counted (EV-DROPPED), printed and logged.
- Fired for participants only: EV-DONE from the switch trampoline when real
  work runs dry (funded by the VM), EV-BORN at a LIVE VM's first naming
  (funded by the newborn), EV-DIED on KILL/reap after departure (funded by
  the root). VM.ev_real_work keeps event handling from firing EV-DONE, so
  mutual subscribers can't storm.

Console evidence identical on amd64/aarch64/riscv64: one BORN per birth,
one DONE per job run dry (2 queued -> 1), one DIED per KILL, no SEND in any
job text; storm guard: exactly one reaction; channels 1-3 unclosable;
non-joined VMs see nothing; EV-DROPPED 0. Flood probe (reverted): 32
delivered, 7 dropped and counted, never blocked, all heat returned.
dict_hash set changes by the 4 words (546), identical across ISAs;
acceptance passes with held=2048.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-09-24 21:12:51 -04:00
co-authored by Claude Opus 5.5
parent b562e16ccf
commit fe55038e9a
19 changed files with 55239 additions and 7 deletions
+32 -5
View File
@@ -345,13 +345,21 @@ void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
* released (heat back to each sender). Found at FABRIC-3.9.8 Stage M-B --
* before this, KILL/reap left it a member everywhere, and anything
* published to it afterwards held heat forever. */
static void capsule_vm_hermes_depart(VMUuid vm_id, const char *name) {
static void capsule_vm_hermes_depart(VMUuid vm_id, const char *name, int was_observer) {
int left = 0, released = 0;
char line[96];
sk_hermes_vm_departed(vm_id, &left, &released);
snprintf(line, sizeof line, "HERMES: %s departed -- left %d channel(s), released %d pending",
(name && name[0]) ? name : "(unnamed)", left, released);
console_println(line);
/* FABRIC-3.9.8 Stage M-B (§7.3): EV-DIED, after the departure above so
* the dead VM is already off every channel. Funded by the root -- a
* dead VM can't pay (M-5); participants only (M-4). was_observer is
* read by the caller before sk_vm_switch_signal_unregister() erases it. */
if (!was_observer && name && name[0]) {
(void)sk_hermes_event(SK_HERMES_CHANNEL_EV_DIED, vm_id, vm_uuid_hera(), name);
}
}
void capsule_vm_force_reap(VMUuid vm_id) {
@@ -391,8 +399,11 @@ void capsule_vm_force_reap(VMUuid vm_id) {
/* Generic cleanup, independent of whatever subsystem set
* pending_reap -- a reaped VM is no longer a switch-signal
* participant either way. */
sk_vm_switch_signal_unregister(vm_id);
capsule_vm_hermes_depart(vm_id, entry->name);
{
int was_observer = sk_vm_switch_signal_is_observer(vm_id);
sk_vm_switch_signal_unregister(vm_id);
capsule_vm_hermes_depart(vm_id, entry->name, was_observer);
}
capsule_parity_log_kill(vm_id, entry->name);
@@ -408,10 +419,23 @@ void capsule_vm_force_reap(VMUuid vm_id) {
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
int first_naming;
if (!name) return;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
first_naming = (entry->name[0] == '\0');
vm_name_copy(entry->name, name);
/* FABRIC-3.9.8 Stage M-B (§7.3): EV-BORN. capsule_birth_baby() leaves
* a VM unnamed; every birth path (BIRTH, WORKER-BIRTH, identity births,
* console, runcap) names it here right after it reaches LIVE -- so a
* first naming of a LIVE VM is the moment it's born, under the name
* its subscribers know it by. Funded by the newborn (M-5), whose quota
* is granted during birth; participants only (M-4). */
if (first_naming && entry->state == VM_STATE_LIVE &&
!sk_vm_switch_signal_is_observer(vm_id)) {
(void)sk_hermes_event(SK_HERMES_CHANNEL_EV_BORN, vm_id, vm_id, entry->name);
}
}
/*===========================================================================
@@ -542,8 +566,11 @@ int capsule_vm_kill(const char *name) {
* does -- a dead VM is no longer a switch participant. Before this,
* an ordinary KILL left its slot behind (G6), to be walked by every
* decision and never freed. No-op for a VM that never registered. */
sk_vm_switch_signal_unregister(vm_id);
capsule_vm_hermes_depart(vm_id, name);
{
int was_observer = sk_vm_switch_signal_is_observer(vm_id);
sk_vm_switch_signal_unregister(vm_id);
capsule_vm_hermes_depart(vm_id, entry->name, was_observer);
}
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;