Stage M-0: kernel-Hermes arena, pending queues and member lists grow on demand; Q_SLOT its own constant (FABRIC-3.9.0 §7.0, M-0/M-0b)

- 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>
This commit is contained in:
Robert Allan James
2026-09-24 17:52:43 -04:00
co-authored by Claude Opus 5.5
parent 316be9fc4b
commit d8358a3011
16 changed files with 55512 additions and 69 deletions
+49 -23
View File
@@ -64,14 +64,22 @@
#include "starkernel/q48_16.h" /* Q48_ONE */
/*
* SK_HERMES_MSG_MAX / SK_HERMES_MEMBER_MAX - sizing. Mirrors
* messaging.4th's own MSG-MAX (32) and MBR-MAX (64) as a starting point --
* kernel-Hermes is a single central pool rather than N per-VM arenas, so
* these may need revisiting once real traffic exists to size against. Not
* a ruling, just where the FORTH precedent already was.
* FABRIC-3.9.0.md §7.0 (Stage M-0): kernel-Hermes has no fixed counts any
* more. SK_HERMES_MSG_MAX (32 messages system-wide), SK_HERMES_PENDING_MAX
* (32 per queue) and SK_HERMES_MEMBER_MAX (64 per channel) are gone: the
* message arena, each pending queue and each channel's member list grow on
* demand from the kernel heap, and a grow that fails is logged, counted
* (sk_hermes_grow_failures()) and refused whole.
*
* SK_HERMES_ARENA_SEGMENT - the arena's GROWTH GRANULARITY, not a limit.
* The arena is a directory of fixed-size segments; running out adds a
* segment. Messages never move once allocated -- callers hold
* SkHermesMessage pointers across later allocations (the drain holds one
* across interpreting a payload that may itself SEND), so a copy-on-grow
* arena would leave them dangling. Only the segment directory is
* allocate-copy-free, and nothing holds pointers into it.
*/
#define SK_HERMES_MSG_MAX 32
#define SK_HERMES_MEMBER_MAX 64
#define SK_HERMES_ARENA_SEGMENT 32
/* SK_HERMES_CHUNK_MAX_PAYLOAD - one block (1024 bytes, 64x16, SXLIV.1),
* moved up here (was originally defined much further down, next to
@@ -91,8 +99,15 @@
* floor to protect. Deliberately simpler here rather than carrying the
* old formula's now-unmotivated term forward: reservoir split evenly
* across message slots only.
*
* FABRIC-3.9.0.md §7.0, ruling M-0b (Stage M-0): Q_SLOT is its own
* constant now, 1/32 of a reservoir -- the same value as before, no longer
* derived from the arena size (which no longer exists as a number). How
* many messages a VM can have in flight is a heat question (its reservoir
* over Q_SLOT), not a memory one.
*/
#define SK_HERMES_Q_SLOT ((uint64_t)Q48_ONE / SK_HERMES_MSG_MAX)
#define SK_HERMES_Q_SLOT_DIVISOR 32u
#define SK_HERMES_Q_SLOT ((uint64_t)Q48_ONE / SK_HERMES_Q_SLOT_DIVISOR)
/*
* SkHermesMessage - one message slot, field-for-field mirror of
@@ -169,6 +184,8 @@ typedef struct {
* admission stamp (sk_vm_switch_signal_next_stamp())
* -- how long the recipient has been waiting,
* for the switch decision's tie-break */
uint32_t index; /* Stage M-0: this message's arena index (its
* identity -- queues hold indices, never pointers) */
} SkHermesMessage;
/*
@@ -176,12 +193,16 @@ typedef struct {
* has joined, no per-member state beyond identity. Replaces the 28-word
* channel abstraction; see this file's own top comment for why.
*
* @field members Member VM identities, valid for indices < count.
* @field members Member VM identities, valid for indices < count. Stage
* M-0: heap-grown on subscribe (was a fixed [64]); freed by
* sk_hermes_channel_destroy().
* @field count Number of valid entries in members[].
* @field cap Allocated length of members[].
*/
typedef struct {
VMUuid members[SK_HERMES_MEMBER_MAX];
size_t count;
VMUuid *members;
size_t count;
size_t cap;
} SkHermesMembership;
/*
@@ -302,7 +323,7 @@ uint64_t sk_hermes_audit_failure_count(void);
/*
* Task 2.8 (item 28), SXXXVII.4: the scan that verifies the counters
* themselves. DIAGNOSTIC ONLY -- O(SK_HERMES_MSG_MAX), never called from
* themselves. DIAGNOSTIC ONLY -- O(arena capacity), never called from
* alloc/release/decay.
*
* sk_hermes_scan_held - walks the message arena and sums the live Stadium
@@ -385,8 +406,8 @@ int sk_hermes_channel_destroy(int channel_id);
/* sk_hermes_channel_subscribe - add vm_id to channel_id's membership.
* Refuses (-1, no effect) if channel_id is invalid/not in use, vm_id is
* already a member, or the channel's membership is already at
* SK_HERMES_MEMBER_MAX. Idempotent in effect (a second call with the
* already a member, or the member list can't grow (Stage M-0: logged and
* counted, sk_hermes_grow_failures()). Idempotent in effect (a second call with the
* same args refuses rather than duplicating), not in return value. */
int sk_hermes_channel_subscribe(int channel_id, VMUuid vm_id);
@@ -506,12 +527,17 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
int sk_hermes_publish(VMUuid from, int channel_id, uint32_t type,
void *payload_addr, uint32_t payload_len);
/* SK_HERMES_PENDING_MAX - per-subscriber pending-queue depth. The global
* message arena (SK_HERMES_MSG_MAX) is the real ceiling on how many
* messages can ever be in flight system-wide, so sizing each VM's own
* queue to that same bound is a safe, simple upper limit rather than a
* new number to justify. */
#define SK_HERMES_PENDING_MAX SK_HERMES_MSG_MAX
/* Stage M-0: each subscriber's pending queue grows on demand (was a fixed
* SK_HERMES_PENDING_MAX = 32). A queue that can't grow refuses the send,
* which rolls the message back (released, ledger exact). */
/* sk_hermes_grow_failures - arena, queue, member-list and reassembly-scratch
* grows that failed (each logged at LOG_ERROR and refused whole). */
uint64_t sk_hermes_grow_failures(void);
/* sk_hermes_arena_capacity - messages the arena can currently hold without
* growing (diagnostic; grows in SK_HERMES_ARENA_SEGMENT steps). */
size_t sk_hermes_arena_capacity(void);
/* sk_hermes_pending_count - number of messages currently queued for
* vm_id (0 if vm_id has no queue yet -- never having received a message
@@ -664,9 +690,9 @@ uint32_t sk_hermes_chunk_count(uint32_t len);
* seq order regardless of the order chunks[] itself is passed in.
*
* Refuses (-1, *out_len untouched, no partial copy ever lands in
* out_buf) if: n_chunks is 0 or exceeds SK_HERMES_MSG_MAX (the system-
* wide message arena size -- chunking can never need more chunk-carrier
* messages than the whole arena holds); any chunk is NULL, has no
* out_buf) if: n_chunks is 0; its per-call seq scratch can't be allocated
* (Stage M-0: was a fixed [SK_HERMES_MSG_MAX] array; now sized to n_chunks,
* a failure logged and counted); any chunk is NULL, has no
* payload, or its payload is shorter than sizeof(SkHermesChunkHeader);
* the chunks' msg_ids disagree; seq values are not exactly
* {0..n_chunks-1} (a duplicate or a gap); is_last is not set on exactly