capsule_vm_kill() and capsule_vm_force_reap() never touched kernel-Hermes:
a dead VM stayed a member of channel 0 and every channel it joined, its
undelivered mail was never released, and anything published to it
afterwards held heat forever. M-B's autonomous events would leak on every
dead subscriber. Captain Bob: "Fix first, own commit."
- sk_hermes_vm_departed(): unsubscribe from every channel, release + pop
the pending queue (heat back to each funder, ledger exact), free the
queue entry. Called from both kill and reap, with a HERMES departure line.
Console evidence identical on amd64/aarch64/riscv64: wa with 2 pending ->
"left 2 channel(s), released 2 pending", gone from .CHANNELS, PUBLISH to
its channel delivers 0; acceptance ledger held=2048 (6144 without the
release). Boot parity/dict_hash unchanged; acceptance passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Eleven words in repl.c over the existing kernel-Hermes C API, registered
for every VM; `from` always derived in C. Names as ruled (M-1) except
UNJOIN for LEAVE, which is FORTH-79's DO-loop exit (Captain Bob).
- INJECT (M-2) sends as the root, funded from Hera's reservoir, as new
type 31; pinned STRICT in kernel_main.c beside BIRTH.
- Read-only accessors sk_hermes_pending_at() / sk_hermes_channel_member_at()
for .MAIL / .CHANNELS.
Console evidence identical on amd64/aarch64/riscv64: PUBLISH delivers 2
(both print), LEAVE-VM then PUBLISH delivers 1, INJECT shows in MAIL?/.MAIL
as type 31 from Hera before the recipient runs, CHANNEL-CLOSE succeeds and
refuses channel 0, JOIN/UNJOIN, refusals 0/-1. dict_hash set changes by the
11 words (542 words) and is identical across ISAs; acceptance passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The static sk_hermes_msgs[32] is replaced by a directory of fixed-size
segments (SK_HERMES_ARENA_SEGMENT, growth granularity only). Messages
never move -- callers, including the drain across vm_interpret_kind(),
hold message pointers across later allocations, so copy-on-grow (as
§7.0 first proposed) would dangle them. Each message carries its index;
numbering unchanged.
- Pending-queue rings and channel member lists heap-grown (doubling);
member lists freed on destroy; reassembly scratch sized per call.
SK_HERMES_MSG_MAX / _PENDING_MAX / _MEMBER_MAX are gone.
- Q_SLOT = Q48_ONE / 32 as its own constant (M-0b), value unchanged.
- Every grow failure is printed, logged at LOG_ERROR, counted
(sk_hermes_grow_failures()) and refused whole; a send whose queue can't
grow releases its message.
Probe (reverted), identical on amd64/aarch64/riscv64: 70 messages in one
queue (arena 32 -> 96), sender refused by its reservoir, 72 popped intact
in order, 100 members; a live Hera drain on a full arena grew it mid-drain
and released/popped the right message; each grow path's injected failure
refused whole and counted. 3-ISA acceptance passes; boot parity, dict_hash
set and the live ledger line byte-identical to before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The static word_scratch_addr shared by every VM is gone: WORD builds in
vm->word_buf (257 bytes: count + 255 + NUL). A token over 255 chars is
refused whole (delimiter restored, >IN unconsumed, vm->error), logged at
LOG_ERROR and counted -- no more silent cut to 62 chars.
- Pre-existing corruption found by the probe: TIB/>IN/SPAN (and WORD's
scratch) were allotted lazily at HERE; POST's restore_dict_state(),
FORGET and COLD rolled HERE below them and compiled the dictionary over
them (Hera's TIB lay under ACL-RECHECK's body; QUERY/WORD wrote into
code). Per Captain Bob, all four are now allotted first in both
vm_init()s (offsets 0-1305); COLD's clamp never goes below them.
Probe (reverted), identical on amd64/aarch64/riscv64: 0 colon bodies inside
the buffers; per-VM isolation; 62/100/255-char tokens full length; 300
refused and counted; interpreter sane after full-width TIB writes.
3-ISA acceptance passes; M7.1a hash and dict_hash set unchanged (here
0xd8 -> 0x5f8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vm_create_word()'s S-1b refusal path now calls transition_metrics_cleanup()
before freeing the metrics struct (its context_window leaked).
- FABRIC-3.9.8: the transition_heat grow is untested -- ENABLE_PIPELINING=0
in the kernel AND the default hosted build; the doc wrongly said hosted.
- Recorded the review: moving id tables have no unlocked or interrupt
readers (heartbeat lookups hold dict_lock, as does the grow); sweep for
other word-id-indexed arrays found none unbounded. Status line updated.
3-ISA acceptance passes; boot parity and dict_hash set unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- word_id_map / recycled_word_ids are heap tables (one shared capacity,
64 doubling), grown all-or-nothing by allocate-copy-free. Numbering is
unchanged, so boot parity is unchanged.
- vm_create_word() takes the id before linking; on failure it frees the
entry, sets vm->error, logs LOG_ERROR and counts
(vm_dictionary_word_id_refusals()). No word runs with WORD_ID_INVALID.
- Readers of the id space bound by vm->next_word_id, not DICTIONARY_SIZE.
- transition_heat gets its own capacity and grows on record (was sized
once -- a heap overflow under a dynamic bound); every reader bounds by it.
- Stadium id->cell rows start empty and grow on dispatch; a failed grow is
logged, counted and not admitted. S-2 (the cell pool) stays open.
Probe (reverted), identical on amd64/aarch64/riscv64: +2000 words get real
ids (capacity 4096); a word at id 2738 lands 200x in the rolling window and
is Stadium-admitted; FORGET recycles across the old boundary; an injected
grow failure refuses the word whole (NULL, error, counter, dictionary
unchanged) and the retry succeeds. 3-ISA acceptance passes; boot parity
(531, 0x572daac83b797332) and the dict_hash set unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- MAX_DICT_ENTRIES (2048) removed: the entry list is allocated to the
dictionary's size; a cyclic chain is detected (Floyd) instead of capped.
- Colon-body walk bounded by vm->here, not a 1024-cell cap.
- Every failure prints PARITY:INCOMPLETE #n with its full reason, logs at
LOG_ERROR, bumps sk_parity_incomplete_count(), and the record reads
hash=INCOMPLETE / PARITY:FAIL code=4 rather than a plausible hash.
- Pre-existing #GP fixed: the walker dereferenced inline body data (S"
bytes, offsets); it now follows only known dictionary entries.
- LIT operands that are entry pointers are hashed by word_id, not address,
so a mature dictionary hashes identically across ISAs.
Probe (reverted): 563 -> 2563 words hashed, identical on amd64/aarch64/
riscv64; cyclic chain -> INCOMPLETE, counter 2. Boot parity unchanged
(531, 0x572daac83b797332). 3-ISA acceptance passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
physics_freeze_words.c, dictionary_heat_diagnostic_words.c,
physics_diagnostic_words.c, physics_benchmark_words.c treated dsp as a
count (data_stack[dsp++] push, data_stack[--dsp] pop) while the VM's dsp
is the top index. On an empty stack a push wrote data_stack[-1], 8 bytes
before the VM struct -- POST's DECAY-RATE@ test did it every boot (on
aarch64 it hit parity.c's counter, elsewhere the POST block RAM tail).
All 28 sites now use vm_pop/vm_push; four one-item guards fixed (dsp<0).
Evidence on amd64/aarch64/riscv64: bytes before the VM struct unchanged
across POST; DECAY-RATE@ leaves 1 item; LOOKUP-STRATEGY! now works;
G interleave, precedence, console under load, relay 3, dict_hash unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captain Bob: close the messaging gap first; the arena must be dynamic;
audit every fixed-size structure ("no arrays ... it should all be fairly
dynamic"); failures must be handled properly (never silent, never
partial, logged + counted + refused, parity marked INCOMPLETE). All
rulings as proposed; S-2 (Stadium cell pool) left open -- it was a
question, not a proposal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Observer flag on switch-signal slots, set by the kernel for the Tripod
(is_fleet_foundation, hoisted; Hera in kernel_main). The decision: an
observer in progress or holding an event keeps the CPU; observers with
events (mail, Hera's console input) go ahead of participants, oldest wait
stamp first; participants compared by work heat as before. Observers are
never charged (O-2) nor floored. Uncommitted heat-share charge discarded.
Evidence, byte-identical amd64/aarch64/riscv64: G interleave unchanged;
Hestia's mail runs before any worker output (HHHABB...); a line typed
while two workers run is read and executed mid-stream (...ABAB1 2 + . /
3 ok / ABAB...) -- console responsive under load. Relay 3, dict_hash
unchanged. Busy-wait DOE deliberately not run (deadlocks by design until
O-C). FABRIC-3.9.0 records rulings and Captain Bob's messaging note (§7).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captain Bob: Hera, Artemis and Hestia no longer participate in the
calculations; they observe them and act on events. Design (3.9.0):
observer role from is_fleet_foundation, excluded from decision/work
heat/floor, event-driven (mail, console input, device events), Hera's
own mail at the idle point, event-driven DoE driver replacing the busy
wait. Rulings O-1..O-4 + discard of the uncommitted heat-share charge
open. Plan (3.9.8): stages O-A..O-D then merge, evidence named in advance.
FABRIC-3.8 gets a continuation pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>