Every lock carries a rank (hermes < switch < registry < VM dict_lock <
VM tuning_lock < stadium < slot < doe < log < block < console < heap <
vmm < pmm); each core counts held locks per rank and an acquisition not
above the highest held rank counts an order violation, reported in full
at the idle point. Probe inversion reported exactly once; O-A, a 28-VM
churn and the standard sequence show 0 violations on all three ISAs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CONNECT-HERMES (birthed the removed hermes:init.4th; always failed) and
ARTEMIS-ANNOUNCE-READY (no callers) are gone; DoE CSV column 17 is now
hestia_heat_q48, same position. New baseline on all three ISAs:
word_count 544, M7.1a hash 0x927513aabd543433, Hera 0x4953a2e2d11fff34,
Artemis 0xc5a8cff4b805b6a0, Hestia unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vm_physics_remove() retires the node (heat to the root) and frees it;
capsule_vm_kill() and capsule_vm_force_reap() call it, so WIREBIND/EJECT
kills and reaps now retire too (before, only the KILL word did). A dead
VM now leaves only its registry node (168 B). 3-ISA acceptance clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ~3.4 KB/VM heap residual left after K-4 was each dead VM's CONTEXT
and FORTH first-character bucket arrays (84 blocks, 3,400 bytes per
worker), plus last_inference_outputs. New vocabulary_free_indexes();
both vm_cleanup()s call it and free the outputs. Per-VM residual is now
232 bytes: the registry node (kept by design) and the vm_physics node
(open finding). 3-ISA acceptance clean, hash 0x99b01134c3d3049c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Investigation (riscv64): completions are slow, not lost -- up to 15 ms,
while the ~1M-iteration bound was ~14 ms of guest time. Each timeout
left the request in the device while the driver forgot it; the next
request took its used entry as its own and returned before its own I/O
finished, and so did every later one (394 in one boot), rewriting
descriptors the device was still reading.
- Fix: VBLK_TIMEOUT_MS (2 s) on the raw hardware counter; each request
waits for exactly one more used entry; a timed-out request stays
outstanding and its completion is drained before anything is reused,
or the next request is refused untouched. Counters for timeouts, late
drains and busy refusals.
- After the fix, all three ISAs: 0 timeouts, 0 unaccounted completions
over 1,912 I/Os; riscv64 slowest completion 7.7 ms (was 15.1).
- Standard sequence on amd64/aarch64/riscv64: hash, G, ledger unchanged;
0 timeouts in the riscv64 log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New XHCI_INPUT_CTX_BYTES (control ctx + slot + 31 EP contexts).
Address Device allocates the Input Context at that size once per slot;
Configure Endpoint reuses it (bounds-checked). It allocated a fresh one
on every attach and never freed the old.
- Probe (removed), identical on all three ISAs: after the standard
sequence's attach/detach/re-attach, input-context allocations=2 (one
per xHCI slot) against configure-endpoint=3.
- Standard sequence on amd64/aarch64/riscv64: hash, G, ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stadium_release_quota(): evict every patron but pending messages, mark
the slot retiring; stadium_retire_sweep() releases a retiring slot once
nothing is resident -- free cells to the best donor (ruled), word row
and block-table entries cleared, slot free for reuse. Swept at release
and after evictions while any slot is retiring.
- Called on KILL and reap, and for the nine synthetic self-test ids after
the boot self-tests. Hera's slot never released.
- Admission fallback skips DELIVER (message) cells.
- Probe (removed): slots in use == live VMs after boot and after 56-64
churned VMs on all three ISAs; forced fallback kept the message cell
resident and intact; conserved everywhere; K-1's post-churn Hermes
refusal gone (1 2 + . -> 3 on the churned system). Heap growth per
28-VM churn halved (~3.4 KB/VM still retained -- open as K-8).
- Standard sequence on amd64/aarch64/riscv64: hash, G, ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Both looked up the retired Hermes VM and printed SKIPPED on every boot.
Drain test: target Hestia, nested VM-EXEC of "1 DROP". Policy test:
Hestia is the no-policy VM (never loads ACL.4th). A missing target now
prints FAIL, not SKIPPED.
- Both PASS on amd64/aarch64/riscv64; standard sequence passes (hash, G,
MINT/WIREBIND/USE, conserved, 0 idle growth). Ledger baseline now
pulled 262144 / returned 259359 (the drain test's real message).
- Found, not fixed: ARTEMIS-ANNOUNCE-READY, doe_log's "Hermes" heat
column and CONNECT-HERMES still reference the retired Hermes VM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Defect 1: COLD now forgets every word since the fence exactly as FORGET
does (new vm_dictionary_forget_to_fence(): untrack, metrics, entry) and
rewinds HERE to the fence. It rewound HERE alone to 1024, leaving live
words to be compiled over.
- Defect 2: PAD = BLK_VM_WINDOW_BASE - 512 (was inside the window's top
slot, shared with BLOCK/BUFFER).
- Probe (plain FORTH, all three ISAs identical): PAD..PAD+511 =
5238272..5238783 below the window (5238784); on a worker KA/KC found,
COLD, then KA/KB/KC not found, HERE at the fence (1424), new word works.
- Standard sequence on amd64/aarch64/riscv64: hash and G unchanged,
MINT/WIREBIND/USE ok, conserved, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Kernel sf_realloc() is a real realloc (copy min(old,new) via new
kmalloc_usable_size(), free old, NULL-on-0 frees). It allocated fresh
and nothing else: every growth leaked, and vm_state_push()'s call-state
stack lost every saved state past depth 8 (found here).
- Shim realloc() copies min(old,new) (over-read when growing).
- Defect 9: FC bucket growth no longer leaks or patches a copy; no
sf_xrealloc() (freed a block the index still held on failure).
- Defect 5: heat snapshot heap-sized (was heats[1024], truncating).
- Defect 6: word refused whole when its metrics can't be allocated.
- Defect 7: vm_cleanup() (both) frees metrics' inner arrays.
- Defect 8: most-likely-next kept incrementally; no full id scan.
- Probe (removed): realloc keeps contents, heap returns exactly; call
stack depth 20 intact; all three ISAs. Churn criterion NOT met: ~7 KB
retained per churned VM (quota slots/word rows -- K-4 re-measures); a
post-churn CONSOLE-CMD refusal attributed to the quota leak, K-4 retests.
- Standard sequence on amd64/aarch64/riscv64: hashes, G, ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ruling (Captain Bob): per-slot locks, not "own path needs no lock" --
a lock only on the cross-slot side can't exclude an unlocked owner
(Hermes release evicting a sender's cell while the sender admits).
FABRIC-3.10.0 §3.6 amended.
- StadiumVMQuota.lock covers the slot's lists, reservoir, consumed, word
row and its cells. Word dispatch holds only its own slot lock for the
whole dispatch (*_in_slot forms); peek-then-pull is one step.
- g_lock_stadium: grant slot allocation (new slot published with release
in_use) and the shared block table (ruled: the one shared-lock Stadium
path). Taken before a slot lock.
- Bitmap bytes set/cleared atomically (8 cells, possibly 8 slots).
- Cross-slot writers moved under the owner's lock: Hermes decay
(stadium_cell_decay), Hermes release ledger (stadium_evict_credit),
session pin (stadium_cell_set_pin). stadium_conserved() one hold.
- Word and block cell lookups check the cell is still this slot's.
- Probe (removed), identical on all three ISAs: 10,746,599 of 10,746,616
word dispatches took exactly one lock (own slot), the 17 others all row
growths; every live VM conserved; Hermes audit failures 0.
- Findings reported: births/USE run a VM's words while another VM is the
core's current VM (MC-3 claim must cover it); quota slots never
released; owner admission can evict a live message cell (latent, 0).
- Standard sequence on amd64/aarch64/riscv64: dict_hash set and G
byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Dictionary lookup index is per VM (vm->fc_index, struct SfFcIndex):
was one global index rebuilt whenever a different VM was searched, and
heat reorganization could reorder another VM's buckets. Allocated on
first lookup, freed by vm_cleanup(); allocation failure counted/logged,
lookup falls back to a newest-first dictionary walk.
- PRNG: still one stream, stepped by compare-and-swap; SEED one store.
- state_variable removed (write-only; STATE is vm->mode).
- Sweep additions (Captain Bob's ruling): heartbeat_export.c delta
last-values moved into each VM's HeartbeatState (were statics shared by
every VM -- wrong across VMs today); TOP-WORDS list heap-sized per call
(was static [1000]); inference_outputs_to_string() takes a caller
buffer; three failure counters atomic; crc64 table published with
release/acquire.
- Confirmed, unchanged: g_runtime compiled out of the kernel,
g_hotwords_cache unreferenced, ttf cache core-0-only.
- Probe (removed): every VM has its own index; two VMs searched
alternately 2x5000 lookups: 0 rebuilds, 0 wrong-VM results, all three
ISAs.
- Standard sequence on amd64/aarch64/riscv64: dict_hash set unchanged,
G byte-identical, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- block_subsystem.c (vendored): every public function's body becomes a
static *_locked function; bodies call each other only through the
_locked forms, breaking the internal cycles (relocate, meta relocate,
migration scan, firsttouch, the meta accessors). Thin public wrappers
take g_lock_block under __STARKERNEL__; hosted build has no lock and
still builds clean.
- Lock order: hermes, switch, registry, doe, log, block, console, heap,
vmm, pmm (Stadium, MC-0d, above block).
- Documented, not coverable by this lock: blk_get_buffer() pointers are
used after release -- block-buffer ownership across cores is open for
MC-3.
- Probe (removed): block lock balances on all three ISAs (2,057-2,379
acquisitions), 0 recursions, 0 held at switch; vblk_io timeouts
(0/0/4 probe, 0/0/2 acceptance) within the pre-lock riscv64 baseline
of 0-6.
- Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical
to MC-0b, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
- FABRIC-3.10.8 §3.2 wording fix; lock order noted as checked by
inspection only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New spinlock primitive (spinlock.h/.c): CAS on an owner word (core+1),
interrupts masked while any lock is held (state kept in the per-core
block, so release order doesn't matter). Recursion, release-not-held,
held-at-switch and destroyed-while-held are counted and logged, never
silent. Locks are no-ops until the boot core's block is installed
(riscv64's monolithic early boot prints before sk_percpu_boot_init()).
- Kernel locks in a documented order: hermes, switch, registry, doe, log,
console, heap, vmm, pmm. sf_mutex (dict_lock/tuning_lock) uses the same
lock word.
- Hermes: public functions over *_locked internals; never held across the
drain's vm_interpret_kind(); switch notifications after dropping the
lock; event payload moved off static storage. Switch table and registry
never call out while held (slot/entry copied under the lock).
- No lock crosses a switch: checked at context switch (leave, resume) and
the idle point.
- Probe (removed): every lock balances on all three ISAs; 0 held at
switch over 7.5M-51M checks; 0 recursions, 0 bad releases.
- Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical
to MC-0a, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
- Block subsystem split out as MC-0b2 (Captain Bob's ruling).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New sk_percpu_t { self, core_id, current_vm }, reached via GS base
(amd64, %gs:0), TPIDR_EL1 (aarch64), tp (riscv64). Boot core's block is
one static object installed right after arch_early_init(); secondary
cores' blocks come with MC-1.
- switch.c's global g_switch_current_vm is gone; current VM is
sk_percpu()->current_vm, re-read after resume in the context switch.
- Probe (removed): normal path and every heartbeat ISR found core 0's
block on all three ISAs (ISR ok 889/1877/1625, bad 0).
- Standard sequence on amd64/aarch64/riscv64: hashes unchanged from O-C,
G A=40 B=40, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xhci_cmd_address_device() sized the Output Device Context for Slot + EP0
(64 bytes); xhci_cmd_configure_endpoint() then allocated a bigger copy and
repointed DCBAA[slot_id]. The controller keeps the address it was given at
Address Device (QEMU caches it), so every bulk Endpoint Context was written
past the 64-byte block into the next kmalloc header -- on every USB
mass-storage attach, every ISA. After O-C's smoke campaign reshuffled the
heap, a kfree at thumbdrive unplug walked into it: #GP in coalesce_neighbors().
Found with temporary instrumentation (load base, code bytes at RIP, event
crumbs, a heap-list checker): the corrupt word was an EP Context (Bulk IN,
Running, MPS 1024) exactly DCI 3 x 32 bytes into the 64-byte context.
Now: XHCI_DEVICE_CTX_BYTES (32 contexts x 32 bytes) allocated once per
slot at Address Device, zeroed whole, never reallocated; Configure
Endpoint just fills it. Also ends the per-attach leak of the old context.
Verified: heap checker clean from boot; campaign -> O-A -> MINT -> unplug
-> re-attach passes with 0 exceptions; 3-ISA acceptance passes, parity and
dict_hash set unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sk_vm_idle_point(may_drain_self): with no frame live and mail pending,
Hera drains one message and returns to the poll loop. Readline passes
n == 0 (the mid-edit gate), headless 1, the key read 0.
- Removed sk_repl_idle()'s timer-paced drain of Hera's queue and the two
flags that guarded it (g_mama_interpreting, g_idle_pump_active) -- the
idle point's frames check already covers that re-entry hazard.
Probe (reverted), all three ISAs: admission -> drain for mail to an idle
Hera 0-1 beats (was up to 100); a message arriving mid-edit is held until
the line is entered (2080-3587 beats), never printed into typed text; an
event queued during a busy line drains when it ends; BLK-ATTACH at boot 1
beat. Acceptance passes; parity, dict_hash set and relay ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
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>
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>
Each shown dead three ways (FABRIC-3.5 §XXII): the pump loop by its
condition, by construction (birth cap == switch table capacity, every birth
registers) and at run time (probe silent through boot, G interleave, an
8-worker DoE trial and the relay); SWITCH-MARK-WORK by callers, capsules
(source and baked), experiments/tools/logs; the depth counter and back_to
field by having no readers.
Kept: Hera's own pump drain (last timer-paced delivery, Hera only) and the
always-0 readiness CSV columns (a DoE schema call). Acceptance on amd64/
aarch64/riscv64: G interleave unchanged, cfg-0 trial fail=0 2/2 dead,
relay 3, idle growth 0; dict_hash set changed by the removed word
(word_count 531) and identical across ISAs. FABRIC-3.8 stages A-I done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
capsule_birth_baby() registers each VM at its LIVE completion point, beside
the common-channel subscription. register() is idempotent. The Artemis/
Hestia registrations in kernel_main.c and WIREBIND's own call are removed
(Hera, never born through capsule_birth_baby, stays with set_current).
Probe (restored away): Tripod registered once each (3 slots), a
WORKER-BIRTH VM becomes a participant with no hand registration (4) and
KILL frees it (3), WIREBIND registers exactly console+user, zero
duplicates, on amd64/aarch64/riscv64. Acceptance: dict_hash unchanged,
relay 3, idle growth 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sk_vm_parked_idle(): saved context + exactly its own IDLE frame.
capsule_vm_kill_refusal() is the single kill rule, shared by
capsule_vm_kill() and the KILL word: refuse SWITCHED_OUT mid-task, parked
with mail pending, or the VM running; allow parked-idle. capsule_vm_kill()
now unregisters the switch slot (G6).
Finding fixed: vm_registry_alloc() never initialized pending_reap (kmalloc
does not zero); C-b's reap_pending walks every participant, so garbage
reaped a healthy VM (seen live before any KILL). Now zeroed at alloc.
Probe (reverted): baseline KILL refused, slots 3->4->4 (FAIL, proves G6);
with Stage D dprobe parked idle, KILL dead, slots 3->4->3, PASS on amd64/
aarch64/riscv64. Acceptance: dict_hash unchanged, relay 3, idle growth 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Who runs next is now sk_vm_switch_decide(): among participants with work
(pending kernel-Hermes mail, parked mid-task, or Hera with console input),
the coolest by VM-physics heat; ties with the incumbent keep the incumbent,
ties among candidates go to the longest waiting (unique wait stamps). No
scheduler state, no timer: heartbeat_tick() no longer touches switching,
readiness/threshold/has_work/take_pending removed. Decided at owned word
checkpoints (colon words and, new, between top-level words) after a beat
or new mail, at the trampoline between messages, and at Hera's idle point
in the console poll loops. The idle pump skips participants (G3 closed for
them): the WIREBIND relay is delivered by a real switch to rajames~user.
Probe (restored away): relay via switch Hera->rajames~user->Hera on all
three ISAs, parked saved_sp inside own stack, walk-order disagreements 0
over tens of millions of decisions, 0 bytes idle log growth. Acceptance:
dict_hash unchanged, relay 3, no faults. Rulings: C-3, C-5 as proposed;
C-4 taken as (alpha), consequence recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vm_physics_charge(): per-beat slope into a per-VM carry, whole units pulled
from the rest of the live fleet in proportion to heat (conservative
vm_physics_transfer), rounding shortfall kept in the carry, no fleet_window
sample. Called from vm_tick() only for the switch-current VM. Closes G9:
a VM running its own mail on its own stack now heats up while it works.
Probe (reverted): baseline Hestia heat 0->0->0 over 71 beats (FAIL, proves
G9); with charge 0->11->23 at beats 15->51->86, identical on amd64/aarch64/
riscv64, conserved, PASS. Acceptance: dict_hash unchanged, WIREBIND relay 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captain Bob: "give frames IDs and call by ID" -- frames stack per VM, each
with an ID. New vm_frame.h/.c: VMFrameEntry { VMFrameID id; VMUuid vm_id;
kind (INTERPRET/DRAIN/IDLE) }, a growable VMFrameStack embedded as VM.frames
(reserved at birth, freed in vm_cleanup, grown by allocate-copy-free).
vm_interpret() -> vm_interpret_kind() pushes a frame onto the running stack's
owner, holds only its ID, fails closed if it cannot grow, and pops by ID
(mismatch reported as VM FRAME CORRUPTION).
The Stage A/B loose fields are gone and everything is derived from the stack:
- ownership gate: one frame, and it is vm's own
- drain guard: a DRAIN frame for vm already on the running stack
- parked-idle: an IDLE frame the trampoline pushes around its yield
- log attribution: the top frame's vm_id, resolved by ID through the registry
The single saved-and-restored attribution global is removed.
Evidence: the probe baseline on the pre-frame code read HSSAS (the global
named Hestia while Hera resumed). With frames it reads HSHAS on all three
ISAs, with Hestia's top frame IDLE and Hera's frames back to 0; the probe is
reverted. Formal acceptance on all three ISAs: dict_hash identical and
unchanged, zero UNKNOWN WORD, faults, frame corruption or refused interprets,
and the WIREBIND relay 1 2 + . -> 3 works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sk_vm_switch_entry() stops being the Stage-2 placeholder bounce and becomes
every switch participant's own idle loop: it drains one of its own
kernel-Hermes messages per pass, on its own native stack (driven by the drain's
return value, so a non-consuming drain cannot spin), and when nothing is
drained it marks the VM parked_idle and yields back. It is entirely OS-driven,
with no VM-side yield word. sk_hermes_drain_checkpoint() gains a per-VM
hermes_draining guard so a payload's own in-word checkpoint cannot drain a
second message inside the first. Still inert in normal operation: nothing
marks work until Stage C.
Finding fixed here: amd64 sk_vm_switch_prime built the first-entry frame at
top-56, entering with rsp 16-aligned instead of the SysV-required 8 mod 16, so
the first aligned SSE spill in a callee took #GP (logs/20260923-142526, kept).
It had been latent since SXXVIII Stage 2 because the placeholder never called
anything. The frame is now at top-64 with a zeroed pad slot. aarch64 and
riscv64 were already correct.
Evidence: a temporary kernel_main probe (reverted, not in this commit) queued
two messages to Hestia, the first calling a colon word, and switched into her.
It passed on all three ISAs: 0 pending, parked_idle, depth 0, context saved on
her own stack, SB-SEQ=12 (drained one at a time, not nested), ledger and
stadium_conserved restored. Formal acceptance without the probe on all three
ISAs: dict_hash identical and unchanged, zero UNKNOWN WORD or faults, and the
live WIREBIND mint -> USE -> 1 2 + . -> 3 relay.
FABRIC-3.8.md also records Captain Bob's rulings that no VM-side word ever gives
up control and that multitasking is a compudynamic hybrid, and opens SS3.12
(VM frames for per-VM execution state).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the global g_vm_interpret_depth test at both execute_colon_word()
checkpoints (kernel-Hermes drain, switch-signal take) with
sk_vm_owns_outermost_frame(vm): the VM physically owns the stack
(sk_vm_switch_current_vm() == vm), is at its own outermost vm_interpret()
frame (new per-VM VM.interpret_depth), and is not compiling. Once VMs
interleave on their own native stacks, a global depth count across all VMs
no longer means "this VM owns this stack"; this is the invariant it was
approximating. The global and the old function stay until Stage I.
Acceptance, all three ISAs, foreground with the framebuffer visible, coupled
disk images reset before each: zero UNKNOWN WORD, zero self-test failures,
dict_hash identical across ISAs and unchanged from the pre-Stage-A boot, and
the live WIREBIND mint -> attach -> USE -> 1 2 + . -> 3 relay with
stadium_conserved(self)=true. Logs: 20260923-140120 (amd64), -140654
(aarch64), -141303 (riscv64).
FABRIC-3.8.md also records Captain Bob's ruling that preemption comes from
the compudynamics, not the timer: principle 5 is corrected and the design
question is opened as SS3.11, to be ruled before Stage C.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Local master had 9 unpushed commits building block-backed DoE persistence on
Artemis's own disk (FABRIC-3.md SXXXV.3/.4/.12, designed and built 2026-09-17,
never pushed) that this branch never inherited after the Tripod/kernel
reshuffle diverged history. Ports the mechanism cleanly onto the current
Hera/Artemis/Hestia shape:
- doe_region.h/.c: growable, CRC-protected trial-summary ring in Artemis's
top-of-device metadata fence, immediately past log_region.c's own ceiling
(devblock_from_top 98-102). Same accessor/CRC discipline as log_region.c.
- log_words.c: DOE-PERSIST-TRIAL and DOE-RESUME-COUNT kernel-only FORTH words.
- multiuser-doe.4th: per-ISA tagging (MU-ISA-TAG!) and campaign resume support
(MU-EXEC-CAMPAIGN now takes a start-idx).
- per-isa-doe.4th (new): RUN-AMD64-CAMPAIGN/RUN-AARCH64-CAMPAIGN/
RUN-RISCV64-CAMPAIGN entry points, resuming from DOE-RESUME-COUNT.
- extract_doe_region.py (new): host-side reader, CRC-verified, no filesystem
assumptions -- reads the ring directly off a raw disk image.
Deliberately not ported: the R/LaTeX report-generation pipeline
(analyse_multiuser_doe.R, multiuser_doe_report.tex, build_multiuser_doe_report.sh)
-- not needed to prove persistence works, and unscoped for this pass.
Verified live on all three architectures: clean boot, word_count 530->532,
zero UNKNOWN WORD. amd64 and riscv64 both live-exercised end-to-end (write via
DOE-PERSIST-TRIAL, count via DOE-RESUME-COUNT, clean QEMU exit, then read back
correctly with extract_doe_region.py against the raw disk image -- no host
serial dependency, matching the real bare-metal requirement this campaign is
rehearsing for).
riscv64 finding (not a defect in this port, no fix applied): DOE-PERSIST-TRIAL
failed 3/3 on a first attempt against a fresh disk image with
"virtio_blk: vblk_io timed out". Root cause: disk/artemis.img is a sparse file
(180KB allocated of a 1GB apparent size); the new DOE region (devblock_from_top
98-102) is genuinely virgin territory on a fresh image, and riscv64's slower
TCG emulation missed the existing virtio_blk spin-bound budget (already
documented as fragile, FABRIC-3.md 2026-09-09) on that first cold touch.
Confirmed by isolation: a plain BLOCK/UPDATE at an already-warm LBN succeeded
instantly, the pre-existing LOG-ERROR" (same meta-zone mechanism, an
already-warmed offset range) also succeeded, and a reboot reusing the
now-partially-warmed disk let DOE-PERSIST-TRIAL succeed immediately and
verify correctly on read-back. Pre-existing environmental fragility, not a
logic bug in the ported address computation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two research passes confirmed the identity record itself (seed/pubkey/
cert) is already unreachable from any FORTH primitive -- only C-level
read_devblock/write_devblock touch it. But a device's ordinary user block
content CAN be copied between two attached devices today, using only
stock, unpinned words: <src> BLOCK <dst> BUFFER 1024 MOVE UPDATE
SAVE-BUFFERS, or the dedicated RELOCATE-BLOCK word (whose own doc comment
already admits "performs no policy validation of its own"). Checked
whether the existing per-block owner_fp/BLK-ACL-ALLOW@ metadata already
solves this -- it doesn't: owner_fp encodes who (a VM identity pubkey),
never where (physical device), and blk_get_buffer()/blk_update() never
consult acl_allow/acl_ttl at all -- those fields are completely inert.
Small, targeted fix, no rearchitecture:
- blk_subsys_relocate_block() (block_subsystem.c): same-device check.
Its own documented purpose is wear-leveling (relocate on the SAME
device) -- never stated as cross-device, and nothing enforced that
until now.
- New public blk_lbn_device_handle() (block_subsystem.c/.h): the missing
LBN-to-device direction (blk_get_device_range() already goes the other
way). Opaque, stable, == comparable.
- MOVE (memory_words.c) and CMOVE/CMOVE> (string_words.c): refuse when
both addresses are block-window addresses backed by two different
devices -- the exact shape of the composed attack. A copy where either
end is ordinary VM memory (the overwhelming common case: staging text
from PAD, editing a block in place) is untouched.
- blk_vm_check_epoch()/blk_vm_slot_for_addr() exposed (block_words.h) so
the two new call sites share the same window-slot invalidation contract
rather than a second, divergent copy of it.
Verified live on all three architectures, not just boot-clean: same-
device MOVE/RELOCATE-BLOCK still succeed exactly as before; cross-device
MOVE/CMOVE/RELOCATE-BLOCK all refused. Zero UNKNOWN WORD, identical
dict_hash across all three (this change adds no FORTH-visible word, only
internal refusal conditions, as predicted).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All FORTH-owned message types were cut over to kernel-Hermes in Phase 3
(tasks 3.8-3.10). This removes the now-dead FORTH messaging layer and
the Hermes VM itself: capsules/common/messaging.4th, capsules/hermes/init.4th,
the slot-3 VM-NAME-REG pairing convention, and every load-site/birth-site
reference across capsules/init.4th, artemis/init.4th, hestia/init.4th,
doe-campaign.4th (Artemis-only now), capsule_console.c, capsule_mint.c,
capsule_wirebind.c, capsule_birth.c, and kernel_main.c.
Verified on all three architectures: clean boot, mkcapsule --lint clean
(36 files, 0 violations), zero UNKNOWN WORD, identical dict_hash across
amd64/aarch64/riscv64 for every VM, and a full mint -> WIREBIND-attach ->
USE -> relay round-trip exercising the two highest-risk edits
(capsule_console.c/capsule_mint.c).
Found, not fixed: deleting messaging.4th removes SEND-ELEVATE-REQUEST,
which was the only caller of KH-ELEVATE-SEND and the only path to
ELEVATE-GRANT (zuse-eligibility.4th, still loaded at boot) -- Phase 8
PKI's own elevation entrypoint. Needs a decision before Phase 5 close-out.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reachability verified live before writing any code, per this
project's own standing rule (grep cannot establish reachability
alone): FIND SEND-ELEVATE-REQUEST / FIND ELEVATE-GRANT / FIND
CH-REQUEST all resolve on a live Hera boot, though grep across
capsules/experiments/docs found zero callers of SEND-ELEVATE-REQUEST
-- a real, complete, directly-callable entrypoint (H.5/H.8's own
design) with no current automatic trigger, not dead code.
Correction to a prior finding, made in the course of this check: task
3.8's write-up claimed "Hera's own pre-existing inability to load
common:messaging.4th" -- false. capsules/init.4th (Hera's own
MAMA_INIT capsule) loads it directly, and SEND-ELEVATE-REQUEST lives
and works in her dictionary right now. Task 3.8's own actual scope is
unaffected by this correction.
Cutover: SEND-ELEVATE-REQUEST (messaging.4th) no longer ends in
CH-REQUEST's COMMON-CH/MSG-SEND path; it now calls KH-ELEVATE-SEND
(repl.c), a new C word wrapping sk_hermes_send_one(), registered
unconditionally for every VM. from/to are derived from the calling VM
and sk_get_mama_vm() directly in C, never taken from the stack -- a
real correctness improvement over CH-REQUEST's own initiator-only
gate, which only existed because a caller COULD pass the wrong from
value; deriving it in C makes that spoof structurally impossible.
SK_HERMES_MSG_TYPE_ELEVATE_REQUEST reuses ELEVATE-REQUEST's own value
(8), same partition-rule reasoning as tasks 3.8/3.9. Delivery is
unchanged task 3.4 machinery. No new static-buffer lifetime caveat --
the payload-aliasing fix landed before this task started.
CH-REQUEST (messaging.4th) is now dead code, its one real caller just
removed -- found, not fixed, per Captain Bob's Law.
Verified live on all three architectures: 0 0 0 0 S" DUP"
SEND-ELEVATE-REQUEST (deliberately-invalid pubkey, so ELEVATE-GRANT
correctly refuses -- the check is the pipeline running, not a grant
succeeding) fires the evidence line and completes cleanly, DUP
unaffected afterward. Zero UNKNOWN WORD, dict_hash identical across
all three architectures (changed uniformly from prior runs -- one new
word registered -- not diverged, matching SXXXIV.6's own rule).
This closes task 3.11 (Phase 3 gate): all of messaging.4th's live
FORTH-owned message types (BLK-ATTACH-EVENT, CONSOLE-CMD-EVENT,
ELEVATE-REQUEST) are now real kernel-Hermes cutovers. Phase 4 may
begin.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two separate fixes, found and closed together after task 3.9/prompt-
format landed (Captain Bob: "finish the work first then we'll do
cleanup before 3.10 begins").
1. Logging noise (confirmed live via QMP screendump): three sources
were cluttering ordinary interactive console sessions.
- INFERENCE "Output validation failed, ignoring results"
(vm_runtime.c, kernel; vm_time.c, hosted mirror) and xhci "CSW
status = FAILED"/"unit not ready -- retrying" (xhci.c) were
already log_message(LOG_WARN/LOG_ERROR, ...) calls, just visible
at the default runtime LOG_WARN level -- downgraded to LOG_INFO,
all three fire routinely and self-resolve (xhci.c's own existing
comment already documents the retry as expected SCSI UNIT
ATTENTION behavior, not a driver defect).
- Stadium: dispatch cell=... (stadium.c's stadium_dispatch()) was a
genuine defect: an unconditional console_puts()/console_println()
sequence with no level gating at all, printing on every single
dispatch. Rewritten through log_message(LOG_DEBUG, ...).
2. CRLF double-submit (found while investigating why the cleaned-up
noise still didn't look like a normal single-VM session):
sk_console_readline() (repl.c) breaks on '\r' OR '\n' as independent
terminators, so a line sent as both bytes submits twice -- the real
line, then an immediate empty-line submit on the second byte, each
printing its own " ok". Pre-dates Stage D entirely, not an async-
relay artifact. Fixed with a single non-blocking peek-and-discard
for the paired byte right where the line terminates.
Verified live on all three architectures for both fixes: zero UNKNOWN
WORD, dict_hash identical to every prior acceptance run in this
document (both changes are display/interaction-only).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sk_hermes_send_one() -- the single funnel every sender, including
sk_hermes_publish(), already goes through -- used to store the
caller's own payload_addr pointer as-is. Two sends before either
drains meant both messages pointed at the same caller-owned buffer,
whichever send wrote last silently winning: real, confirmed live (a
second identity thumbdrive attached at boot alongside Zuse's own left
its WIREBIND pairing silently never happening).
SkHermesMessage gains an inline payload_buf[SK_HERMES_CHUNK_MAX_
PAYLOAD] field; sk_hermes_send_one() now memcpy()s the caller's
payload into it and points payload_addr at that copy instead. No
sender or reader call site needed to change -- every existing reader
already only ever reads through payload_addr, which still points at
valid bytes of the same length, now message-owned. g_kh_blk_attach_buf/
g_kh_console_cmd_buf (repl.c, tasks 3.8/3.9) no longer need to survive
past their own send call; their doc comments, which had claimed the
old aliasing shape was benign, are corrected.
Verified the original bug is actually gone: reproduced the exact
original scenario (a real, sequentially-minted rajames identity
attached at boot alongside Zuse's own, two simultaneous BLK-ATTACH-
EVENT sends in one idle-loop pass) -- WIREBIND pairing, USE, and the
Stage D relay all now work where WIREBIND previously silently failed.
Standard three-ISA acceptance also clean: zero UNKNOWN WORD, dict_hash
identical across all three and matching every prior acceptance run in
this document.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sk_console_user_prefix() (repl.c) previously always returned "zuse"
(or the WIREBIND-attached username) as the left side of the bracket
prefix, regardless of which VM the console was actually pointed at --
"[zuse@rajames]" after USE rajames, always showing the authenticating
superuser rather than the active identity.
Changed on Captain Bob's direct instruction: once the console is
redirected into a WIREBIND identity's own console VM
(console_get_vm_name() != "Hera"), show that same name on both sides
-- "[rajames@rajames]" -- since WIREBIND births the console VM
literally named after the identity, so the identity IS that VM, not a
separate label. At the top level (still on Hera, nothing has
redirected yet), the original zuse_session/WIREBIND-username logic is
unchanged.
An earlier, more ambitious attempt (separate identity/machine tracked
state across every console_set_vm_name() call site) regressed live to
a wrong [zuse@Artemis] prompt and was fully reverted before reaching
any acceptance run -- the landed fix needed none of that new state,
just this one function.
Verified live on all three architectures: [zuse@Hera] at the top
level and after a live WIREBIND attach (before USE), [rajames@rajames]
after USE rajames, with task 3.9's Stage D relay still firing
correctly on top of it. Zero UNKNOWN WORD, dict_hash unaffected
(display-only change).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Send-side cutover: sk_repl_dispatch_line()'s FORTH-string
"CONSOLE-CMD-EVENT 0 3 S\" ...\" 0 MSG-SEND" interpret is replaced with
a direct sk_hermes_send_one() call (SK_HERMES_MSG_TYPE_CONSOLE_CMD,
kernel_hermes.h, deliberately reusing CONSOLE-CMD-EVENT's own value 7,
same partition-rule reasoning as task 3.8's BLK-ATTACH-EVENT cutover).
Real finding along the way: kernel-Hermes's drain only ever runs as a
side effect of vm_interpret() being called on the target VM. Task
3.8's target (Hera) is always being interpreted via the interactive
REPL loop; task 3.9's target is a WIREBIND identity's own ~user VM, a
passive receiver nothing else drives. The existing idle-loop pump only
ticked VMs with the old FORTH MSG-TICK word ACL-allowed -- a VM minted
with the STD79-lockdown personality never has it, so the pump silently
skipped it forever and queued messages never delivered. Fixed by
adding an unconditional, direct sk_hermes_drain_checkpoint() call in
the same pump loop, independent of the MSG-TICK gate.
Verified live on all three architectures: WIREBIND-attach a real
identity, USE into it, type a plain console line, confirm the Stage D
evidence line and correct relayed execution result. Zero UNKNOWN WORD,
dict_hash identical across all three ISAs and matching task 3.8's own
baseline. The FABRIC-3.md-documented USE/BINDSTEP crash did not
reproduce in any of these live sessions (recorded as a finding, not
chased further).
Depends on the zuse_root_pubkey_known fix already landed in ac4d431 --
without it, WIREBIND cannot attach any identity on a fresh boot at
all, which blocked this task's own verification until found and fixed
separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
capsule_zuse_boot_load_root_pubkey() is the only writer of
zuse_root_pubkey_known, and it only ever runs once, synchronously, at
Artemis's boot-time virtio-blk attach -- before genesis mint has
happened on a fresh artemis.img, so it finds no marker yet and leaves
the flag 0. Nothing re-triggers it after genesis mint completes.
Silent result: capsule_wirebind_try_attach()'s own
`if (!mama_vm->zuse_root_pubkey_known) return;` gate then refuses
every identity attach for the rest of that boot, with no message at
all -- meaning WIREBIND was silently dead on any boot that reset
artemis.img fresh, which is exactly what this project's own
acceptance convention does before every run.
Found live verifying FABRIC-3.6.md task 3.9's console-session
acceptance. Fixed at the source: install_and_activate() already has
the root pubkey in hand (from genesis mint or an already-Zuse
re-attach) -- activate it directly there instead of relying on a disk
re-read that may never happen in-session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The reply leg (Artemis -> Hera ack) that used to flow through
common:messaging.4th's MSG-SEND/MSG-TICK now goes through
kernel-Hermes's sk_hermes_send_one()/sk_hermes_drain_checkpoint()
instead -- FORTH Hermes never sees a BLK-ATTACH-EVENT message again
(SXXXIV.2's partition rule). The request leg was never real FORTH
messaging traffic to begin with (a direct VM-EXEC, no type tag,
forced by Hera's own inability to load common:messaging.4th), so it
is untouched.
New KH-BLK-ATTACH-SEND (repl.c) wraps sk_hermes_send_one(), reached
from capsules/artemis/init.4th's HERA-BLK-ATTACH-REQ. Delivery reuses
task 3.4's already-wired sk_hermes_drain_checkpoint(); BLK-ATTACH-ACK
itself is unchanged, just reached by a different layer.
SK_HERMES_MSG_TYPE_BLK_ATTACH deliberately reuses BLK-ATTACH-EVENT's
own value (9) to document this as a cutover of the same message, not
a new one.
Two real bugs found on the way, both recorded in FABRIC-3.6.md's
findings log:
- A popped FORTH CREATE-buffer address was raw-cast to a host pointer
instead of going through vm_ptr() -- silently read all-zero memory,
no crash, no error, just a message that arrived and did nothing.
Fixed; the rule and its exception (repl.c's own dev-addr is
legitimately a raw pointer, formatted that way by its own pushing
code) are written up for the next FORTH-facing C word.
- A separate, genuine hang on the very first live exercise of this
path, never reproduced across ten subsequent boots. Reported, not
chased -- not blocking, per the task's own check being otherwise
fully satisfied.
Also found live: log_message() is invisible in this build's actual
serial-log capture at every level -- settled on a single
console_println in the real drain target instead, one line per real
USB attach, not a hot-path.
Final acceptance (logs/20260922-105501, -105758, -110304, disk images
reset before each): dict_hash identical across all three
architectures for every VM, zero UNKNOWN WORD, mkcapsule --lint
clean, real ledger+stadium_conserved(Artemis)=true evidence on every
boot.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Added HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? ) at
capsules/ACL.4th block 4008 (default: approve everything) -- the one
word policy authors edit. sk_hermes_channel_open_policy(VM*, VMUuid)
(kernel_hermes.h/.c) is the C-side query that calls it via plain
word-dispatch against the target VM's own dictionary/stack, never
vm_interpret() (avoids task 3.4's input-buffer cursor hazard entirely)
and never decides the answer itself. Fails closed: no policy word,
a policy error, or stack underflow all deny, matching CLAUDE.md's
posture that absence of policy must never mean "always allow."
Two real bugs found and fixed before this was called done:
missing current_executing_entry assignment before calling the word's
func pointer (colon words silently no-op without it, vm_core.c:730 --
no crash, just a wrong answer); and a second FAIL with debug
instrumentation still in place whose precise cause isn't
reconstructable, since no intermediate commit exists for that attempt.
Self-test proves the task's check four ways against the same
unchanged C function: default approve, live redefinition to deny
(zero C change), restore, and a VM with no ACL.4th loaded at all
(fail closed). A fifth check wires the result into task 3.6's
sk_hermes_channel_respond() end to end: a denied policy produces a
NACK and no channel, ledger/stadium_conserved() holding throughout.
Scope, per Captain Bob's ruling: closes with the query built and
proven; sk_hermes_channel_respond() still takes a caller-supplied
approved bool rather than calling the policy internally. Wiring a
real channel-open call site to only this query is deferred to
whichever later task first needs a live decision.
dict_hash identical across amd64/aarch64/riscv64 for every VM, zero
UNKNOWN WORD, mkcapsule --lint clean (38 files, 0 violations).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracted sk_hermes_send_one() from sk_hermes_publish()'s own
per-subscriber body -- one code path for both point-to-point and
fan-out delivery, so the ledger can never diverge between them.
Point-to-point addressing turned out to be load-bearing, not
incidental: sk_hermes_publish()'s fan-out sets msg->to to whichever
member it is iterating, so a negotiation message "published" to the
common channel would spuriously reach every member, not just the real
target (checked with advisor() before building the naive version).
"Over the common channel" means every VM is reachable from birth (task
3.2), not that the exchange itself fans out -- messaging.4th's own
CH-REQUEST carried an explicit `to` for the same reason.
sk_hermes_channel_request/respond/close build the mechanics: request ->
grant (creates a private channel, subscribes both parties, sends
CH_GRANT + one ACK) or NACK ("a deny is a NACK", SXLV.1 -- no separate
type); close authorized by membership alone. The grant/deny decision is
a plain caller-supplied `approved` bool -- task 3.7 replaces the call
site that produces it with a real ACL.4th query, not this signature.
Self-test covers the task's own three checks plus a sibling advisor()
flagged: an approved respond() whose channel creation itself fails
(table exhausted) must still fall through to NACK, not a silent false
grant or half-open channel -- verified by exhausting the whole channel
table and confirming the fallback.
Bug found and fixed before this was called done: the first draft
dropped a message via pending_pop() alone, without releasing it first,
leaking its Stadium heat and failing the self-test's own ledger
baseline check (logs/20260922-065946/amd64/, kept as audit trail).
Fixed and re-verified PASS on all three architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sk_hermes_publish() now enforces SK_HERMES_CHUNK_MAX_PAYLOAD (1024) on
every message's payload_len uniformly, chunked or not -- closing the
gap task 3.3 explicitly parked. A chunk carrier is
[SkHermesChunkHeader][content slice], slice capped at
1024 - sizeof(header) rather than 1024 itself, so every message on the
wire satisfies the same one-block bound vm_interpret()'s own drain
limit already requires -- a future chunk-aware drain never has to
special-case a carrier that can't be handed to vm_interpret() as-is.
Deliberately no chunking-sender API: building one would need
kernel-Hermes to own chunk-buffer memory with a real lifetime it has no
way to track (kept alive until every subscriber drains it). Sending is
a loop pattern a caller writes with sk_hermes_chunk_count() +
sk_hermes_publish(), demonstrated by this task's own self-test.
sk_hermes_reassemble() is pure and memory-agnostic: validates msg_id
agreement, exact seq coverage, and per-chunk slice sizes before a
single memcpy, with the total length computed once and checked against
the caller's buffer once -- never order-dependent on which chunk
happens to overflow.
Verified live on all three architectures: a 1024-byte payload as one
message, a 1025-byte send refused outright with the ledger untouched,
and a 3000-byte payload split into 3 chunks, drained, and reassembled
byte-exact against the original. dict_hash unmoved and identical
across architectures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>