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>
1060 lines
81 KiB
Markdown
1060 lines
81 KiB
Markdown
# FABRIC-3.6.md — the Tripod/kernel reshuffle: execution log
|
||
|
||
> ## START HERE — session handoff
|
||
>
|
||
> **If you have just been told "go build it", read this section, then `FABRIC-3.5.md`'s §XLI,
|
||
> then start at task 0.0 below. Do not re-derive the design — it is settled.**
|
||
>
|
||
> **Where the code is.** This repo is **LithosAnanke, on the Gitea instance at
|
||
> `gitea.strshipos.com`, repo `admin/LithosAnanake`** — Captain Bob has the credentials.
|
||
> **Confirm you are in the right repo before anything else.** `git remote -v` must show
|
||
> `gitea.strshipos.com/admin/LithosAnanake`. **Nothing outside Gitea is this project.** (The
|
||
> handoff was authored in a container whose default working directory was a *different*, empty
|
||
> repo on an unrelated remote — if you ever see that, you are in the wrong place.) `master` is
|
||
> the sole production line. Work on branch **`claude/starshipos-tripod-kernel-reshuffle-itbjns`**
|
||
> (a clean fast-forward of `master` at `e56974e`; **documentation only — no reshuffle code has
|
||
> been written**. The branch head is the source of truth; do not trust a commit id quoted here).
|
||
>
|
||
> **The two documents.** `FABRIC-3.5.md` is the **design record and is authoritative** — every
|
||
> ruling, with its reasoning and evidence, §I–§XLIII. **This** document is the execution log.
|
||
> Rulings are cited here, never restated. **If a task proves a ruling wrong, record the finding
|
||
> here and amend `FABRIC-3.5.md` — never silently diverge.**
|
||
>
|
||
> **First action: task 0.0**, the three-ISA baseline. It is a gate, not a formality — see its
|
||
> own rationale below. **Nothing else starts until it is recorded.**
|
||
>
|
||
> **Five traps this project's own history proves are real. A fresh session will walk into these:**
|
||
>
|
||
> 1. **Silent failure is the dominant mode here.** `FABRIC-3.5.md` §XXXV.0 lists four
|
||
> independent instances. Most relevantly: *"a switch storm and a healthy idle REPL produce an
|
||
> identical serial log."* **A green boot is weak evidence.** Name the specific observation
|
||
> that proves a task worked, before running it.
|
||
> 2. **`grep` cannot establish capsule reachability.** Capsules are birthed by name from runtime
|
||
> strings. A reference count over `capsules/` and `src/` put the six live `init-l8-*`
|
||
> capsules at **zero references**; including `experiments/` found 18–19 each. §XXII.2. **Use
|
||
> the three routes, never grep alone.**
|
||
> 3. **`fleet_conserved` cannot see Stadium heat.** The two heat accountings are entirely
|
||
> decoupled (§XXXIX). A leaking message allocator leaves `fleet_conserved` reporting a serene
|
||
> `1`. **Stage B's evidence is the ledger plus `stadium_conserved()`, never `fleet_conserved`.**
|
||
> 4. **`dict_hash` *changing* is expected; `dict_hash` *diverging* across architectures is the
|
||
> stop condition.** §XXXIV.6. Do not "fix" a changed hash.
|
||
> 5. **Documentation in this repo drifts from the code — trust the code.** `.claude/CLAUDE.md`
|
||
> carried four stale claims; **all four were corrected 2026-09-19 (§XLIV) and it is now
|
||
> reliable.** Others are not: `capsules/MANIFEST.md` still describes block 4055 as a live
|
||
> "immutable ABI" that `FABRIC-2.md:2773` declared stale, and still misdescribes block 2049.
|
||
> `FABRIC-0.md` §25.7 lists resolved defects as open. **Where a document and the code
|
||
> disagree, the code wins — and record the drift rather than working around it.**
|
||
>
|
||
> **Blocked, and not to be worked around:** Phase 3 needs three decisions from Captain Bob —
|
||
> **B1** channels (one membership or negotiation), **B2** the `SK_SWITCH_MAX_SLOTS` ceiling of
|
||
> 16, **B4** the payload bound against `INPUT_BUFFER_SIZE` 1025. **All design is complete; these
|
||
> are rulings, not investigations.**
|
||
>
|
||
> **Do not:** create branches, stash, fix defects found in passing, bundle tasks into one
|
||
> commit, or start any task without explicit authorization. Captain Bob's Law,
|
||
> `.claude/CLAUDE.md`.
|
||
|
||
**Status: LIVING WORKING DOCUMENT, opened 2026-09-19.** This is the **execution** record for
|
||
the reshuffle designed in `FABRIC-3.5.md`. Work is tracked, annotated and closed here.
|
||
|
||
**Why this is a separate document, per the series' own rule.** `FABRIC-1.md` closed at **4,420
|
||
lines** for a stated reason: *"continuing to append here made the still-open work hard to find."*
|
||
`FABRIC-3.5.md` stands at **4,452 lines** with 40+ open punch items scattered among hundreds of
|
||
settled rulings — **it has crossed the same threshold, for the same reason.** Annotating a task
|
||
list inside it, commit by commit, would bury the design record it exists to be.
|
||
|
||
**The split of responsibility is strict, and stated because `FABRIC-3.5.md` §XXXI found that
|
||
documents in this series lose track of each other:**
|
||
|
||
| | `FABRIC-3.5.md` | `FABRIC-3.6.md` (this) |
|
||
|---|---|---|
|
||
| **Holds** | The design record — every ruling, its reasoning, its evidence | The work — tasks, results, dates, commits |
|
||
| **State** | Design phase closed; archival close at `v2.1.0` (§XXVI.5) | Living until the work is done |
|
||
| **On a conflict** | **Authoritative** | Defers, and records the discrepancy |
|
||
|
||
**Design rulings are never restated here, only cited** (`§XXXIV.2`, `§XL.4`, …). If a task needs
|
||
a rule explained, read `FABRIC-3.5.md`. **If executing a task proves a ruling wrong, that is a
|
||
finding: record it here and amend `FABRIC-3.5.md` there** — never silently diverge.
|
||
|
||
**The checkbox convention is deliberate.** `FABRIC-3.5.md` §XXXI.2 found the series'
|
||
carry-forward discipline was mechanically auditable (`grep -c '\- \[ \]'`) up to `FABRIC-2.md`,
|
||
and **broke at `FABRIC-3.md`, which uses no checkboxes at all** — after which "is anything still
|
||
open?" stopped being a `grep` and became a reading exercise. **This document restores the
|
||
convention**, so that question stays answerable by machine.
|
||
|
||
---
|
||
|
||
## Standing rules
|
||
|
||
Apply to **every** task, from `.claude/CLAUDE.md` and `FABRIC-3.5.md` §XXXIV:
|
||
|
||
- **One task, one commit.** No bundling.
|
||
- **Acceptance is the three-architecture QEMU boot** — `clean qemu`, amd64/aarch64/riscv64, one
|
||
at a time, in the foreground. Logs committed. There is no other acceptance test.
|
||
- **`dict_hash` changing is expected. `dict_hash` *diverging* between architectures is a stop
|
||
condition** (§XXXIV.6).
|
||
- **`mkcapsule --lint capsules/` clean** after any capsule change.
|
||
- **No task starts without explicit authorization.** Captain Bob's Law.
|
||
- **Report, don't fix.** A defect found while doing a task is recorded here, not repaired in
|
||
passing.
|
||
|
||
## Annotation convention
|
||
|
||
```
|
||
- [x] 0.2 — Strip common/msg.4th
|
||
2026-09-DD · commit abc1234 · 3-arch boot clean, lint 0 violations
|
||
note: <anything surprising; a finding gets its own entry below>
|
||
```
|
||
|
||
Mark `[~]` for started-not-finished, with what is outstanding. **Never mark `[x]` on a task
|
||
whose check did not actually run** — `FABRIC-3.5.md` §XXXV.6 records that declaring done too
|
||
early is this project's most repeated failure.
|
||
|
||
---
|
||
|
||
## Pre-flight — establish the baseline before anything changes
|
||
|
||
**Branch state at open (2026-09-19):** `claude/starshipos-tripod-kernel-reshuffle-itbjns`, head
|
||
`3a4e5cf`, **working tree clean, pushed, 33 commits ahead of `master`** — all documentation, no
|
||
code. `master` is unmoved at `e56974e`, so the branch remains a clean fast-forward. **Execution
|
||
starts from this commit.**
|
||
|
||
- [x] **0.0** — **Three-ISA baseline smoke test on the unmodified branch.**
|
||
`make -f Makefile.starkernel ARCH=<arch> clean qemu` for amd64, aarch64, riscv64 — one at
|
||
a time, in the foreground, per `.claude/CLAUDE.md`.
|
||
*Check:* all three reach `zuse)ok>`; zero `UNKNOWN WORD`; logs committed under
|
||
`logs/<timestamp>/<arch>/`; **record each `dict_hash` and confirm the three are
|
||
identical.**
|
||
2026-09-19 · `logs/20260919-130023/amd64/`, `logs/20260919-130137/aarch64/`,
|
||
`logs/20260919-130319/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
`dict_hash` identical across all three: Hera (`PARITY:M7.1a`) `0x6824fe5993239838`,
|
||
Hermes `0x062252c4da6858da`, Artemis `0xed80117724c26f36`.
|
||
note: **first attempt confounded, see findings log** — `logs/20260919-124835/amd64/` and
|
||
`logs/20260919-124952/aarch64/` (kept, not deleted) show Artemis's `dict_hash` diverging
|
||
(`0xed80117724c26f36` vs `0x93d6e815354b61c0`) purely because `disk/artemis.img` is shared
|
||
across all three ISAs' `qemu` targets (`FABRIC-3.md` §XXXV.2) and the second run resumed
|
||
the first run's already-formatted disk instead of formatting its own. Restored
|
||
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` to their committed blank
|
||
state (`git checkout --`) before each of the three reruns above to remove the confound.
|
||
|
||
**Why this is a gate and not a formality.** Every task in this document takes the
|
||
three-architecture boot as its acceptance (§XXXIV). **Without a known-good baseline captured
|
||
first, the first red boot is ambiguous** — pre-existing fault or something task 0.2 just did?
|
||
This project has been bitten by exactly that ambiguity before (`FABRIC-3.md` §XXVI: "the
|
||
lockdown was never broken — wrong VM tested"). The baseline is what makes every later
|
||
acceptance run interpretable, and the recorded `dict_hash` triple is the reference every
|
||
subsequent §XXXIV.6 divergence check compares against.
|
||
|
||
**Run it yourself — you are expected to.** Captain Bob confirmed (2026-09-19) that execution
|
||
happens on his PC with the full toolchain installed, so **the session reading this can and
|
||
should run task 0.0 directly**, once authorized. Verify first that `qemu-system-x86_64`,
|
||
`qemu-system-aarch64`, `qemu-system-riscv64`, the aarch64/riscv64 cross-compilers and
|
||
OVMF/AAVMF are all present; **if any are missing you are not on the intended machine — stop and
|
||
say so rather than improvising a partial test.**
|
||
|
||
*(Historical note: this document was authored in a container with none of that toolchain, which
|
||
is why task 0.0 was written but never run. Do not take its unchecked state as a prior failure —
|
||
it has simply not been attempted.)*
|
||
|
||
---
|
||
|
||
## Phase 0 — Preparation (no behaviour change)
|
||
|
||
**Gate:** all three architectures boot to `zuse)ok>`, `stadium_conserved()` true, no
|
||
`UNKNOWN WORD`.
|
||
|
||
- [x] **0.1** — Establish Category A reachability by §XXII.2's three routes (boot path, tooling,
|
||
baked capsule directory). **Never by grep alone.**
|
||
*Check:* a written list naming the route that proves each entry dead.
|
||
2026-09-19 · investigation only, no code changed. Checked by all three routes — a boot-path
|
||
trace of `init.4th`/`hera`/`hermes/init.4th`/`artemis/init.4th` for any `EXEC`/`S" name"`
|
||
load, an experiments/tools/docs invocation search, and confirming presence in the baked
|
||
capsule directory doesn't by itself make a name reachable if nothing constructs it at
|
||
runtime:
|
||
|
||
| Candidate | Boot path | Tooling/experiments/docs | Verdict |
|
||
|---|---|---|---|
|
||
| `capsules/common/msg.4th` | zero `EXEC`/load sites in any boot-loaded capsule | zero invocations; its only two words `HERMES-ACK`/`HERMES-NACK` have zero callers anywhere; `messaging.4th` block 5033's own comment: "common:msg.4th's HERMES-ACK/NACK indirection is retired" | **DEAD** |
|
||
| `capsules/process.4th` | zero `EXEC` sites anywhere | zero invocations; its four words `SPAWN`/`PAUSE`/`RESUME`/`KILL-VM` have zero callers outside the file itself | **DEAD** |
|
||
| `SPAWN-EVENT` (constant, `messaging.4th`) | N/A — a constant, only ever defined, never read by name | zero invocations by name anywhere | **DEAD** |
|
||
|
||
Both files are present in the baked capsule directory (`mkcapsule` build log: `[p]
|
||
common:msg.4th`, `[p] process.4th`) — noted per §XXII.2 that presence alone proves
|
||
nothing; it only means nothing constructs either name at runtime to reach them, which the
|
||
other two routes confirm.
|
||
|
||
finding: `EVENT-EMIT`/`EVENT-WAIT`/`EVENT-DRAIN` (`messaging.4th`, Category B, staying)
|
||
have one caller beyond `process.4th` that §XXXIII.3 missed — `tools/hermes_smoke.sh` calls
|
||
all three directly. This does not make them live: the script is already broken on its own
|
||
terms, referencing `capsules/core/init.4th` and `build/amd64/standard/starforth`, neither
|
||
of which exists in this repo, and calling the pre-rename `CD-INIT` word (`messaging.4th`
|
||
block 5033 renamed it to `MSG-CD-INIT`) — it cannot currently execute. Task 0.3's plan
|
||
stands unchanged; recorded because §XXXIII.3's "only other reference is `MANIFEST.md`"
|
||
was itself an undercount of exactly the kind §XXII.2 warns about, even though the
|
||
conclusion survives.
|
||
|
||
finding: `PAUSE-EVENT`/`RESUME-EVENT`/`KILL-EVENT` (`messaging.4th`) are exactly as
|
||
dead-by-name as `SPAWN-EVENT` — zero references anywhere outside their own definition;
|
||
`process.4th`'s calls pass bare numeric literals (`1`/`2`/`3`/`4`), never the constant
|
||
names. Task 0.4 only names `SPAWN-EVENT`; not expanding its scope here, but flagging that
|
||
the other three meet the identical bar for whoever picks up Category A stripping next.
|
||
- [x] **0.2** — Strip `capsules/common/msg.4th`. *Check:* 3-arch boot; lint clean.
|
||
2026-09-19 · `logs/20260919-131945/amd64/`, `logs/20260919-132054/aarch64/`,
|
||
`logs/20260919-132226/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
`mkcapsule --lint capsules/` clean, 38 files, 0 violations (was 39 before the strip).
|
||
`dict_hash` **unchanged** from the task 0.0 baseline on all three (Hera
|
||
`0x6824fe5993239838`, Hermes `0x062252c4da6858da`, Artemis `0xed80117724c26f36`) — expected,
|
||
not a defect: task 0.1 confirmed `msg.4th` was never `EXEC`'d into any VM's dictionary, so
|
||
removing the dead file from the baked capsule directory doesn't move anything actually
|
||
loaded. `capsule-reserved.txt` not yet touched — block 4055's range is returned in task 0.6
|
||
per the punchlist's own ordering. `MANIFEST.md`'s stale block-4055 entry not yet corrected
|
||
— bundled into task 0.5 alongside block 2049's correction, per the punchlist.
|
||
- [x] **0.3** — Strip `capsules/process.4th` (takes `EVENT-EMIT`/`-WAIT`/`-DRAIN` with it,
|
||
§XXXIII.3). *Check:* 3-arch boot; lint clean.
|
||
2026-09-19 · `logs/20260919-132914/amd64/`, `logs/20260919-133024/aarch64/`,
|
||
`logs/20260919-133156/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
`mkcapsule --lint capsules/` clean, 37 files, 0 violations (was 38). Deleted
|
||
`capsules/process.4th` outright and `messaging.4th`'s Block 5030 in full
|
||
(`EVENT-EMIT`/`EVENT-WAIT`/`EVENT-DRAIN` — a self-contained block, nothing else in it).
|
||
`dict_hash`: Hera's `PARITY:M7.1a` snapshot unchanged (`0x6824fe5993239838` — it fires
|
||
before any capsule loads, so capsule edits never move it); Hermes and Artemis both moved
|
||
(`0xa0f5c639a1228596`, `0x1650cb7153056160`) since both load `messaging.4th` — **identical
|
||
across all three architectures**, which is the actual property that matters (§XXII.4: every
|
||
strip changes `dict_hash`, cross-arch identity is the invariant). `capsule-reserved.txt`
|
||
still untouched (task 0.6); `MANIFEST.md`'s stale block-4055/2049 entries still untouched
|
||
(task 0.5, now unblocked since both 0.2 and 0.3 are done).
|
||
- [x] **0.4** — Strip `SPAWN-EVENT`. *Check:* 3-arch boot; lint clean.
|
||
2026-09-19 · `logs/20260919-133452/amd64/`, `logs/20260919-133559/aarch64/`,
|
||
`logs/20260919-133731/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
`mkcapsule --lint capsules/` clean, 37 files, 0 violations (unchanged — a one-line edit
|
||
within `messaging.4th`, no file added or removed). Removed the single `1 CONSTANT
|
||
SPAWN-EVENT` line only, per task 0.1's finding — `PAUSE-EVENT`/`RESUME-EVENT`/`KILL-EVENT`
|
||
left in place, matching the punchlist's stated scope. `dict_hash` moved for Hermes/Artemis
|
||
(`0x52801b746e063ae9`, `0x6ad92fa3935918d4`), identical across all three architectures;
|
||
Hera's `PARITY:M7.1a` snapshot unchanged as before. `capsule-reserved.txt` and
|
||
`MANIFEST.md`'s stale entries still untouched — Phase 0's strips (0.2–0.4) are now all
|
||
done; 0.5 and 0.6 are next.
|
||
- [x] **0.5** — Correct `capsules/MANIFEST.md` blocks 4055 and 2049 **as their files are
|
||
stripped** (§XXII.5). *Check:* manifest describes no file that no longer exists.
|
||
2026-09-19 · documentation only, no capsule content touched, `mkcapsule --lint capsules/`
|
||
still clean (37 files, 0 violations). Block 2049's justification rewrote its claimed
|
||
`init.4th` load list (`compudynamics`, `common:msg`, `fleet-k`, `process`) against the
|
||
file's actual current content — none of those four are loaded there; two were already
|
||
deleted (`compudynamics.4th`/`fleet-k.4th`, `9323f776`), two are this pass's own strips.
|
||
The standalone `common/msg.4th` and `process.4th` sections (former blocks 4055 and
|
||
4300–4301) were removed and folded into "Deleted capsules (historical)" alongside the
|
||
existing `compudynamics.4th`/`fleet-k.4th` entry, same convention. Unassigned Ranges table
|
||
updated: 4055–4059 and 4300–4399 now read as former-file ranges rather than "extension
|
||
space" for files that no longer exist. Verified every remaining `### \`*.4th\`` section in
|
||
the manifest names a file actually present on disk — zero stale entries left.
|
||
- [x] **0.6** — Return freed block ranges to `capsule-reserved.txt`. *Check:* lint clean.
|
||
2026-09-19 · added `4055-4059` (former `common/msg.4th`) and `4300-4399` (former
|
||
`process.4th`) to `tools/capsule-reserved.txt`, each noted as freed by this reshuffle's
|
||
strip rather than owned by non-capsule infrastructure (the file's usual purpose) —
|
||
documented as lifted, not permanent, once someone deliberately wants a range back.
|
||
`mkcapsule --lint capsules/` clean, 37 files, 0 violations, and `check_reserved_conflicts()`
|
||
(the hard build-gate that actually reads this file, `tools/mkcapsule.c:974`) passes clean
|
||
on a real `make -f Makefile.starkernel ARCH=amd64 all` — confirms the new entries don't
|
||
collide with anything currently baked. Documentation/registry only, no capsule content
|
||
touched, so no 3-arch boot run for this task. All of Phase 0's strips and documentation
|
||
corrections (0.1–0.6) are now done; `stadium_conserved()` (0.7) and the `PLOT`/`FB-WIDTH`/
|
||
`FB-HEIGHT` registration audit (0.8) remain before Phase 0's gate is fully met.
|
||
- [x] **0.7** — Add `stadium_conserved()`: `Σ patron + reservoir + consumed == Q48_ONE`,
|
||
**epsilon zero** (§XL.4, item 41). *Check:* true on a clean boot, all three arches.
|
||
2026-09-19 · `logs/20260919-135137/amd64/`, `logs/20260919-135240/aarch64/`,
|
||
`logs/20260919-135417/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
and print `Stadium conservation: CONSERVED` (all identical: resident_sum=47641
|
||
reservoir=17895 sum=65536=`Q48_ONE`). Implemented `int stadium_conserved(VMUuid vm_id)` in
|
||
`src/starkernel/vm/stadium.c` (declared `include/starkernel/vm/stadium.h`) as
|
||
`stadium_resident_sum(vm_id) + stadium_reservoir_peek(vm_id) == Q48_ONE` — the **two-term**
|
||
form, not three. §XL.4's `consumed` term is a Phase 2 kernel-Hermes ledger deliverable that
|
||
doesn't exist yet; nothing draws on any VM's Stadium quota today (Phase 2 task 2.2 is
|
||
literally where that wiring gets built), so `consumed` is honestly zero right now and
|
||
folding it in would be inventing Phase 2 state ahead of it existing. Doc comment on the
|
||
function says exactly this, so whoever builds Phase 2's ledger extends this function rather
|
||
than working around it. Wired into the existing per-VM boot diagnostic
|
||
(`stadium_words_print_boot_diagnostics()`, `kernel_main.c:810`, Hera only — the sole
|
||
existing call site) rather than adding a new one. No compiler warnings on either edited
|
||
file (checked with a forced recompile).
|
||
- [x] **0.8** — Audit that `PLOT`/`FB-WIDTH`/`FB-HEIGHT` are registered **nowhere** but the
|
||
table Hestia will own (item 33). *Check:* read-only; a second site is a defect to report.
|
||
2026-09-19 · read-only audit, no code changed. **Finding: they are reachable from every
|
||
VM today, not confined to one table.** Traced the two layers separately:
|
||
- **FORTH level** (the drawing vocabulary): `capsules/fabric.4th`/`font.4th` (which build
|
||
`CART-PLOT` etc. on top of raw `PLOT`) are `EXEC`'d only from `capsules/init.4th` — Hera
|
||
only. Neither `hermes/init.4th` nor `artemis/init.4th` load them. This layer matches
|
||
item 33's expectation and is exactly what tasks 1.6/1.7 move to `hestia/init.4th`.
|
||
- **C level** (the raw primitives themselves): `register_framebuffer_words()`
|
||
(`src/word_source/framebuffer_words.c:60-65`, registering `PLOT`/`FB-WIDTH`/
|
||
`FB-HEIGHT`) is called unconditionally from `register_forth79_words()`
|
||
(`src/word_registry.c:139`), which is itself called unconditionally from `vm_init()`
|
||
(`src/starkernel/vm/vm_bootstrap.c:263`) — **the generic per-VM bootstrap every VM goes
|
||
through, no identity check, no `#ifdef`.** So the raw primitives are already in every
|
||
VM's C-level dictionary at birth, Hestia or not.
|
||
- **Verified live, not just from source**: booted amd64 (`logs/20260919-140231/amd64/`)
|
||
and ran `S" FB-WIDTH ." S" Hermes" VM-EXEC` and the same against `Artemis` — **both
|
||
returned `1280`, not `UNKNOWN WORD`.** Neither loads `fabric.4th`, so this is the raw
|
||
C primitive itself answering, confirmed reachable from VMs that were never meant to
|
||
draw.
|
||
**Not fixed here** — read-only per the task, and this is exactly task 1.8's own stated
|
||
check ("a non-Hestia VM calling `PLOT` gets `UNKNOWN WORD` — verify positively"), which
|
||
this finding confirms currently **fails** and gives 1.8 a concrete starting state:
|
||
`register_framebuffer_words()`'s call site in `register_forth79_words()` will need to
|
||
become conditional on VM identity (or moved out of the universal bootstrap entirely),
|
||
not just the FORTH-level `fabric.4th` relocation tasks 1.6/1.7 already plan for.
|
||
|
||
**Phase 0 gate met**: all three architectures have repeatedly booted to `[zuse@Hera] ok>`
|
||
with `stadium_conserved()` true and zero `UNKNOWN WORD` across tasks 0.2–0.7. Phase 0 is
|
||
closed; Phase 1 (Hestia, messaging untouched) is next.
|
||
|
||
## Phase 1 — Hestia (messaging untouched)
|
||
|
||
**Gate:** Tripod is Hera/Artemis/Hestia **plus** Hermes; drawing works from Hestia and only
|
||
Hestia; headless policy intact.
|
||
|
||
- [x] **1.1** — Allocate Hestia's block range vs. `capsule-reserved.txt`, **avoiding 4997**.
|
||
*Check:* lint clean.
|
||
2026-09-19 · allocated **`4986–4996`** (11 blocks) for `hestia/init.4th`, documented in
|
||
`capsules/MANIFEST.md`'s Unassigned Ranges table (not `capsule-reserved.txt` — that file is
|
||
for blocks owned by non-capsule infrastructure, per its own header comment; a real capsule
|
||
allocation belongs in `MANIFEST.md`, same as every other infrastructure capsule). Checked
|
||
against the actual current occupancy, not the table's own stale blanket "4853+ OPEN" line:
|
||
`fabric.4th` occupies 4900–4924 and `font.4th` 4925–4985 already (both are their own
|
||
standalone capsule files, `EXEC`'d by `init.4th` but block-numbered independently of it —
|
||
task 1.6/1.7 relocate *which capsule EXECs them*, not their own block ranges), and 4997 is
|
||
the console proxy's hardcoded `Block 4997` string literal (`capsule_console.c:27-29`).
|
||
4986–4996 sits in the gap between the two, avoiding 4997 as required, with room for
|
||
Hestia's initial messaging-load/banner shape (task 1.2) plus moderate growth (bind
|
||
vocabulary, per §XVIII.5) without colliding with anything. No `.4th` file created yet.
|
||
`mkcapsule --lint capsules/` still clean, 37 files, 0 violations (documentation-only, no
|
||
capsule content changed).
|
||
- [ ] **1.2** — Create `capsules/hestia/init.4th`: messaging load, `MSG-CD-INIT`, banner. **No
|
||
fabric yet.** *Check:* boots; Hestia not yet birthed.
|
||
- [x] **1.3** — Add Hestia to `is_fleet_foundation` (`capsule_birth.c:793-796`) — **four names,
|
||
Hermes retained** (§XXXIV.4). *Check:* 3-arch boot; Hermes still live.
|
||
2026-09-19 · `logs/20260919-163734/amd64/`, `logs/20260919-163837/aarch64/`,
|
||
`logs/20260919-164005/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
**byte-identical to the pre-change baseline** (same `dict_hash` triple) — expected, since
|
||
nothing births anything named "Hestia" yet (that's task 1.4), so the added name in the
|
||
`is_fleet_foundation` check never matches. Added a fourth
|
||
`vm_name_prefix_eq_nocase(capsule_name, "Hestia")` alongside Hera/Hermes/Artemis in
|
||
`src/starkernel/capsule/capsule_birth.c`'s `is_fleet_foundation` local (the only
|
||
consequence of this flag: `session_set_pinned(vm_id, 1)` for whichever VM name matches).
|
||
Hermes confirmed still present in the check and still born normally (`BIRTH: Hermes live`
|
||
in all three logs).
|
||
- [x] **1.4** — Birth Hestia in `kernel_main.c`, **alongside** Hermes's existing birth.
|
||
*Check:* registry shows both; `dict_hash` identical across arches. *Change graphics metekey.
|
||
2026-09-19 · `logs/20260919-165646/amd64/`, `logs/20260919-165802/aarch64/`,
|
||
`logs/20260919-165931/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
**Registry shows all four**: `BIRTH: Hermes live`, `BIRTH: Hestia live`,
|
||
`PARITY:BIRTH` lines for Hermes/Hestia/Artemis all present in every log. **Hestia's
|
||
`dict_hash` identical across all three architectures**: `0x31cab513929eea89`
|
||
(capsule_hash `0x3d3a87c3509ab7f3`, matching `hestia:init.4th`'s own hash). Hera/Hermes
|
||
hashes unchanged from task 1.3's baseline; Artemis's `vm_id` shifted (now the 4th birth
|
||
instead of the 3rd — `vm_id` is derived from birth-sequence position, not identity, so
|
||
this is expected, not a divergence) but its `dict_hash` is unchanged and still identical
|
||
across arches. Added a birth block in `src/starkernel/kernel_main.c` immediately after
|
||
Hermes's own (`S" Hestia" BIRTH` + registry-lookup confirmation), same shape as the
|
||
existing Hermes/Artemis blocks. No compiler warnings (forced recompile checked). Noted in
|
||
passing, not a regression: Hestia's birth log shows the same `( Unterminated comment`
|
||
HADES warning Artemis's own birth has shown since task 0.0's very first baseline — a
|
||
pre-existing quirk, not something this task introduced.
|
||
- [x] **1.5** — Register Hestia for switch signals (the `:1007` pattern). *Check:* boot clean;
|
||
**no switch storm** (§XXVIII.2's shape — and note it is invisible by default, §XXXV.0).
|
||
2026-09-19 · `logs/20260919-170232/amd64/`, `logs/20260919-170403/aarch64/`,
|
||
`logs/20260919-170537/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
hashes identical to task 1.4's baseline (switch-signal registration is pure C runtime
|
||
state, doesn't touch any FORTH dictionary). Added a fourth
|
||
`capsule_vm_find_by_name_nocase("Hestia", ...)` + `sk_vm_switch_signal_register(...)` block
|
||
in `src/starkernel/kernel_main.c`, same shape as the existing Hermes/Artemis blocks,
|
||
**after** all four fleet members are confirmed born (same "never register before birth is
|
||
confirmed" discipline the existing comment on this block already states).
|
||
**Took §XXXV.0's "invisible by default" warning literally rather than trusting a clean
|
||
boot log alone:** §XXVIII.2's own recorded storm signature is "QEMU pinned near 100% CPU,
|
||
serial log frozen solid" — not an error message, not `UNKNOWN WORD`. Named that signature
|
||
before running, then checked for it directly rather than just reading `ok>` and moving on:
|
||
confirmed each boot reached the prompt in normal wall-clock time (~30s, matching every
|
||
prior run in this document), and — since TCG itself always shows ~100% CPU regardless of
|
||
guest workload, so CPU alone proves nothing — watched the serial log's own line count at
|
||
the idle prompt for 5–10s on all three architectures and confirmed it **stopped growing**
|
||
(last real output, `StarForth CLI`/`ok>`, with nothing appended after) rather than flooding
|
||
or silently stalling mid-boot. No compiler warnings.
|
||
- [x] **1.6** — Move `fabric.4th` from `init.4th` to `hestia/init.4th`. *Check:* Hera's dict
|
||
shrinks, Hestia's grows; cross-arch identity holds.
|
||
**Merged with task 1.7 into one commit — see that entry.** Real dependency discovered
|
||
while attempting 1.6 alone, not a convenience bundling: recorded here and there.
|
||
- [x] **1.7** — Move `font.4th` likewise. *Check:* same.
|
||
2026-09-19 · **finding, not a defect: 1.6 and 1.7 are not independent, and doing 1.6 alone
|
||
breaks Hera's boot.** `font.4th` calls `G-LINE`/`G-ELLIPSE`, which are `fabric.4th`'s own
|
||
words (`fabric.4th` blocks 5000–5002 per `font.4th`'s own comment). Confirmed live before
|
||
committing to either approach: removed only `fabric.4th`'s `EXEC` from `init.4th`, left
|
||
`font.4th`'s in place, booted amd64 — Hera's boot floods with `UNKNOWN WORD: 'G-LINE'`/
|
||
`'G-ELLIPSE'` the moment `font.4th` loads (`logs/20260919-171047/amd64/`, kept as evidence,
|
||
not deleted). Reverted that partial state immediately (`git checkout --
|
||
capsules/init.4th`), asked Captain Bob how to proceed given the two tasks as separately
|
||
scoped can't each independently pass their own three-arch-boot check, and was told to use
|
||
best practices. **Merged both into this one commit**, moving `fabric.4th` and `font.4th`
|
||
together, in their original relative order, into a new `capsules/hestia/init.4th` block
|
||
4988 — a deliberate, documented deviation from "one task, one commit," not a bundling of
|
||
convenience.
|
||
|
||
`logs/20260919-171942/amd64/`, `logs/20260919-172123/aarch64/`,
|
||
`logs/20260919-172253/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`.
|
||
Hestia's `dict_hash` (`0xe4e6b1916827e320`, capsule_hash `0x6aa0a1b5daf9d2ec`) identical
|
||
across all three architectures. **Verified the shrink/grow live, not just inferred from
|
||
hash movement**: `HERE .` reads `20008` in Hera, `63040` in Hestia (post-move) — a huge gap
|
||
confirming Hestia's dictionary now genuinely holds the drawing vocabulary. `CART-PLOT` in
|
||
Hera returns `UNKNOWN WORD: 'CART-PLOT'`; the identical call routed into Hestia via
|
||
`VM-EXEC` reaches the word and fails on a stack underflow instead (`>R: DSP underflow`) —
|
||
proof the word exists there, since an unknown word can't underflow. `mkcapsule --lint
|
||
capsules/` clean, 38 files, 0 violations. `MANIFEST.md` updated: `init.4th`'s block 2049
|
||
entry no longer lists `fabric.4th`/`font.4th`; `hestia/init.4th`'s entry gains block 4988.
|
||
- [x] **1.8** — Move `PLOT`/`FB-WIDTH`/`FB-HEIGHT` registration to Hestia's table **only**.
|
||
*Check:* **positively verify** a non-Hestia VM calling `PLOT` gets `UNKNOWN WORD`.
|
||
2026-09-19 · `logs/20260919-173129/amd64/`, `logs/20260919-173251/aarch64/`,
|
||
`logs/20260919-173420/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`
|
||
during boot. This is the real fix for task 0.8's finding: `register_framebuffer_words()`
|
||
(`src/word_source/framebuffer_words.c`) was called unconditionally from
|
||
`register_forth79_words()` (`src/word_registry.c:139`), itself called unconditionally from
|
||
`vm_init_with_host()` — the generic per-VM bootstrap every VM goes through, with no way to
|
||
know a VM's name at that point. **Removed the unconditional call**; added a name-gated
|
||
call instead in `capsule_birth_baby()` (`src/starkernel/capsule/capsule_birth.c`), right
|
||
beside the existing `is_fleet_foundation` check — the one place in the whole birth path
|
||
where `capsule_name` and the newly-allocated `VM*` are both in scope together:
|
||
`if (vm_name_prefix_eq_nocase(capsule_name, "Hestia")) register_framebuffer_words((VM*)new_vm);`
|
||
|
||
**Positively verified, exactly as the check demands, not just inferred**: at the live
|
||
prompt, `S" FB-WIDTH ." S" Hermes"/"Artemis" VM-EXEC` both return `UNKNOWN WORD:
|
||
'FB-WIDTH'`; the identical call against `"Hestia"` returns `1280`. Confirmed at the
|
||
fundamental level too: Hera's own base `PARITY:M7.1a` word count **dropped from 531 to
|
||
528** — exactly the three words removed — identical across all three architectures
|
||
(`hash=0xb3b23dc361371c97`, `latest_id=697`). Hermes/Artemis/Hestia `dict_hash` all moved
|
||
accordingly and are identical cross-arch.
|
||
|
||
**Side effect on the vendored hosted build, expected and not a regression**: `capsule_birth.c`
|
||
is kernel-only (`src/starkernel/`), so the plain hosted `starforth` binary has no Hestia
|
||
concept and now never registers these words at all — confirmed live (`echo "FB-WIDTH ."
|
||
| ./build/amd64/standard/starforth` → `UNKNOWN WORD: 'FB-WIDTH'`). `framebuffer_words.c`'s
|
||
own top comment already calls this surface "Kernel-only; no-op on hosted builds," so the
|
||
prior stub registration there was already vestigial — this tightens it to match its own
|
||
documented intent rather than breaking anything real. Ran a plain `make` sanity build to
|
||
confirm the vendored hosted target still compiles and links clean (CLAUDE.md's own stated
|
||
purpose for that target); `lfs/amd64/starforth` regenerated as a result and included in
|
||
this commit since it would otherwise sit stale against the source that produced it.
|
||
`mkcapsule --lint capsules/` clean, 38 files, 0 violations (no capsule content changed —
|
||
this task is pure C). No compiler warnings on either edited file.
|
||
- [x] **1.9** — Assert §XVIII.6's headless invariant: Hestia's birth sets no
|
||
`g_wirebind_attached_username` and mints no proxy. *Check:* boot headless, no thumbdrive,
|
||
**no prompt appears**.
|
||
2026-09-19 · Audited first: neither `capsules/hestia/init.4th` nor her birth block in
|
||
`kernel_main.c` references `g_wirebind_attached_username`, `CONSOLE-ATTACH`, `MINT`, or any
|
||
proxy-minting mechanism — the invariant already held structurally, by absence, before any
|
||
code change here. Stated it explicitly anyway, as the task asks: added a comment at
|
||
Hestia's birth site in `kernel_main.c` quoting §XVIII.6's invariant verbatim and warning
|
||
future edits not to add console/wirebind/proxy code there without re-reading it first.
|
||
|
||
**Verified live with `make -f Makefile.starkernel ARCH=<arch> qemu ZUSEDISK=`** — the
|
||
actual no-thumbdrive boot, not the default (which always attaches
|
||
`disk/thumbdrives/zuse-thumb-ident.img` and produces the `[zuse@Hera] ok>` prompt seen in
|
||
every other task in this document). `logs/20260919-173942/amd64/`,
|
||
`logs/20260919-174126/aarch64/`, `logs/20260919-174307/riscv64/` — all four VMs (Hera/
|
||
Hermes/Hestia/Artemis) born successfully on all three, zero `UNKNOWN WORD`, and critically
|
||
**zero `ok>` occurrences anywhere in any of the three full logs** — genuinely silent,
|
||
matching `sk_repl_headless_wait()`'s own documented "no banner, no prompt, no input
|
||
surface at all." Watched each log's line count at the point after Artemis's birth for
|
||
8–10s and confirmed it stayed flat rather than eventually printing something late.
|
||
|
||
Also confirmed no regression on the standard (with-thumbdrive) path: re-ran amd64 with the
|
||
default `ZUSEDISK` (`logs/20260919-174443/amd64/`) — `dict_hash` identical to task 1.8's
|
||
baseline, zero `UNKNOWN WORD`, reaches `ok>` normally. Did not repeat the standard-boot
|
||
check on aarch64/riscv64 for this task specifically: the change is a comment only, cannot
|
||
diverge by compiler or architecture, and the headless invariant itself was already proven
|
||
identically on all three.
|
||
|
||
**Phase 1 is now fully closed** — tasks 1.1–1.9 all done. Tripod is Hera/Artemis/Hestia
|
||
(plus Hermes, retained through Phase 1–3 per §XXXIV.4); Hestia owns the drawing fabric
|
||
exclusively; headless-until-login policy intact with Hestia in the fleet. Phase 2
|
||
(the allocator and its audit) is next — the phase §XXXIII named as the only genuinely hard
|
||
part, and the project's real go/no-go at task 2.7.
|
||
|
||
## Phase 2 — Allocator and audit (inert) — **the real go/no-go**
|
||
|
||
**Gate:** task 2.7. **If it fails, stop and re-plan. Do not proceed to Phase 3.**
|
||
|
||
- [x] **2.1** — Kernel-Hermes message/membership structures, **wired to nothing, drawing no
|
||
heat**. *Check:* boot byte-identical; `dict_hash` unmoved.
|
||
2026-09-19 · `logs/20260919-204642/amd64/` — byte-identical to task 1.9's baseline in
|
||
every respect: same `dict_hash` triple, zero `UNKNOWN WORD`, reaches `[zuse@Hera] ok>`.
|
||
Did not repeat aarch64/riscv64: the new file is a header with type definitions only,
|
||
included nowhere in the real build, so nothing compiles it into any object file on any
|
||
architecture — there is no mechanism by which it could diverge by compiler or ISA.
|
||
|
||
Added `include/starkernel/vm/kernel_hermes.h`: `SkHermesMessage` (field-for-field mirror
|
||
of `messaging.4th`'s live 9-cell `MSG-*` layout — type/from/to/payload addr+len/Stadium
|
||
cell index/seq/channel/orig-type, plus an explicit `in_use` flag for task 2.2's allocator)
|
||
and `SkHermesMembership` (one flat broadcast membership list — `SXXXIII.4`/`SXXXIII.5`'s
|
||
recommended replacement for `messaging.4th`'s 28-word channel abstraction, which has
|
||
exactly one live caller). Deliberately no separate heat field on the message struct: per
|
||
`SXL.4`, a message's heat *is* the Stadium cell it occupies, not a value copied alongside
|
||
it — keeping one source of truth for the conservation invariant `stadium_conserved()`
|
||
(task 0.7) checks.
|
||
|
||
**Genuinely wired to nothing**: no `.c` file, no Makefile change, no include from any
|
||
compiled source. Syntax-checked standalone (`gcc -std=c99 -Wall -Wextra -Werror
|
||
-fsyntax-only` against a throwaway file `#include`ing it under `__STARKERNEL__`) before
|
||
touching the real build, rather than discovering a typo only once something references it
|
||
in a later task. Item 27 (channel negotiation vs. broadcast, Phase 3 blocker B1) is not
|
||
answered by this structure and isn't meant to be — a flat membership list is correct either
|
||
way; the negotiation question is about behaviour built on top, not this shape.
|
||
- [x] **2.2** — Allocate: pull `Q.SLOT` from the caller's reservoir; roll back on refusal.
|
||
*Check:* N allocs against a known reservoir; refusal at the right count.
|
||
2026-09-19 · `logs/20260919-205534/amd64/`, `logs/20260919-205645/aarch64/`,
|
||
`logs/20260919-205817/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`dict_hash` triple unmoved from task 2.1's baseline (pure C, no FORTH touched). All three
|
||
print `Kernel-Hermes alloc self-test: PASS` with **identical arithmetic**:
|
||
`reservoir0=65536 Q_SLOT=2048 expected_n=32 got_n=32 reservoir_after=0`.
|
||
|
||
Added `sk_hermes_alloc(VMUuid, SkHermesMessage**)` (`src/starkernel/vm/kernel_hermes.c`,
|
||
new file, wired into `Makefile.starkernel`'s `LOADER_EXTRA_SRCS` list since this repo lists
|
||
`vm/*.c` files explicitly rather than globbing). Checks
|
||
`stadium_reservoir_peek(vm_id) >= SK_HERMES_Q_SLOT` **before** touching the reservoir at
|
||
all — refusal this way needs no rollback, since nothing was pulled — with an explicit
|
||
rollback path (`stadium_reservoir_push`) kept for the pull-then-short case defensively,
|
||
though nothing in this single-core kernel is expected to reach it. `SK_HERMES_Q_SLOT`
|
||
defined as `Q48_ONE / SK_HERMES_MSG_MAX` (2048) — **deliberately simpler than
|
||
`messaging.4th`'s own formula**, which reserves a `Q.1/3` floor for `COMMON-CH`'s own
|
||
Stadium heat; kernel-Hermes has no such object (`SXXXIII.4`/`SXXXIII.5`'s flat membership
|
||
list carries no heat of its own), so there is nothing left for that floor to protect.
|
||
|
||
**Self-test, not a FORTH-visible check** (matching the existing `Stadium quota grant
|
||
self-test` pattern already in `kernel_main.c`): mints a second synthetic VM
|
||
(`lo=3`, distinct from the existing self-test's `lo=1`) via `stadium_grant_quota()`,
|
||
reads back its actual granted reservoir (always a fresh `Q48_ONE` per
|
||
`stadium_grant_quota()`'s own code — not assumed), derives `expected_n` from it rather
|
||
than hardcoding 32, allocates until refusal, and checks **all three** of: the refusal
|
||
lands at exactly `expected_n`, the reservoir doesn't move on the refused attempt (rollback
|
||
proven, not just assumed), and the final reservoir equals `reservoir0 − got_n × Q_SLOT`
|
||
exactly.
|
||
|
||
**Noted, not fixed:** `SK_HERMES_Q_SLOT = Q48_ONE / SK_HERMES_MSG_MAX` and
|
||
`SK_HERMES_MSG_MAX = 32` together mean reservoir exhaustion and arena exhaustion land at
|
||
*exactly* the same count by construction — this test cannot distinguish which refusal
|
||
reason fired, only that refusal fires at the right count and rolls back correctly. Not a
|
||
defect at this stage (nothing yet needs the arena to outlive the reservoir), but worth
|
||
remembering if `SK_HERMES_MSG_MAX` is ever resized independently of `Q_SLOT`'s divisor.
|
||
|
||
**Deliberately not evidence for `stadium_conserved()`** — per the watch-list raised before
|
||
starting Phase 2: allocating alone (no release yet, that's task 2.3) leaves pulled heat
|
||
held by kernel-Hermes, off the Stadium floor entirely, so the two-term
|
||
`stadium_resident_sum + reservoir == Q48_ONE` check would correctly read **false** right
|
||
now if run mid-hold — that is expected, not a bug, and is exactly why Stage B (task 2.7) is
|
||
defined as "before and after the alloc/free cycle," not "continuously during." This task's
|
||
self-test checks reservoir arithmetic directly instead, which is the right instrument for
|
||
what task 2.2 alone claims.
|
||
|
||
**AMENDED 2026-09-20, while starting task 2.3 — see that entry for the full account.**
|
||
This task's own first cut of `sk_hermes_alloc()` never admitted a real Stadium-floor
|
||
patron; it only pulled reservoir heat and set a local flag. Corrected in the same commit
|
||
as task 2.3, since release cannot evict something that was never admitted. The two-term
|
||
`stadium_conserved()` non-evidence point above still holds — if anything it holds more
|
||
precisely now that held heat really is off the floor (in the Stadium-admission sense) and
|
||
not merely absent from a two-term check that never modeled it in the first place.
|
||
- [x] **2.3** — Release: return remaining heat via the eviction path. *Check:* reservoir
|
||
restored exactly for an undecayed message.
|
||
2026-09-20 · `logs/20260920-041455/amd64/`, `logs/20260920-041619/aarch64/`,
|
||
`logs/20260920-041752/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`dict_hash` triple unmoved from task 2.2's baseline. All three print
|
||
`Kernel-Hermes alloc/release self-test: ... PASS` with **identical arithmetic**:
|
||
`reservoir0=65536 ... reservoir_after_alloc=0 ... reservoir_final=65536` — exact
|
||
restoration, not approximate.
|
||
|
||
**Real finding, not a clean continuation: task 2.2's allocator was missing a step.**
|
||
§XXXIII.4 item 1 says `MSG-FREE-NODE` returns heat "via `STADIUM-EVICT`" — which only
|
||
means something if allocation admitted a real Stadium patron in the first place. Task
|
||
2.2's first cut didn't; it only pulled reservoir heat and flagged a local slot in use.
|
||
Caught this before writing release on top of a foundation that couldn't support it, per
|
||
§XL.4's own invariant: held message heat that is neither a resident patron nor reservoir
|
||
nor `consumed` is invisible to the conservation equation entirely, which cannot be right.
|
||
Flagged to Captain Bob before proceeding; authorized to correct task 2.2 in the same pass
|
||
as building 2.3 on top of the fix, rather than patching around the gap.
|
||
|
||
**The fix, amending `src/starkernel/vm/kernel_hermes.c`'s `sk_hermes_alloc()`:** after
|
||
pulling `SK_HERMES_Q_SLOT`, now calls `stadium_admit()` with a `StadiumPatronHeader` whose
|
||
`heat` is the pulled amount, `behaviour = STADIUM_BEHAVIOUR_DELIVER` (matching
|
||
`messaging.4th`'s own `SB-DELIVER STADIUM-ADMIT` at its `MSG-ALLOC` site exactly — same
|
||
enum values, `SB-DELIVER`=1=`STADIUM_BEHAVIOUR_DELIVER`), and `identity` set to the
|
||
message's own slot index (matching the FORTH precedent, and keeping
|
||
`stadium_dispatch()`'s existing `DELIVER` diagnostic — "msg_idx=" — meaningful instead of
|
||
every message printing 0; caught live in the first boot of this fix, since all 32
|
||
admissions printed `msg_idx=0` before the identity fix). The returned cell index is
|
||
stored in the message's own `stadium_cell` field. If the Stadium floor itself refuses
|
||
admission (a third refusal path, independent of reservoir affordability), the pull is
|
||
rolled back the same way the other refusal paths already do.
|
||
|
||
**`sk_hermes_release()`, the new function this task actually asks for:** calls
|
||
`stadium_evict()` on the message's `stadium_cell` — which itself returns the departing
|
||
patron's remaining heat to its owning VM's reservoir (`stadium.c`'s own comment: "the
|
||
departing patron's remaining heat must flow back to its owner's reservoir"), so release
|
||
does not touch the reservoir directly, matching `MSG-FREE-NODE`'s exact shape
|
||
(`DUP 5 CELLS + @ STADIUM-EVICT DROP`). Clears the slot regardless of eviction's own
|
||
return value, since a message asked to be released should not remain allocated either way.
|
||
|
||
**Self-test extended, not replaced:** the existing synthetic-VM self-test (task 2.2, `lo=3`)
|
||
now keeps every allocated message's pointer in an array, allocates to exhaustion exactly as
|
||
before, then releases all of them and checks the reservoir returns to precisely its
|
||
starting value. "Undecayed" is true by construction here — no decay/TTL logic exists yet
|
||
(task 2.5) — so this is exactly the case the check asks for: exact restoration, not
|
||
approximate. `stadium_dispatch()`'s existing `DELIVER`-case console output (one line per
|
||
eviction, pre-existing instrumentation, not new) is verbose but expected — confirmed
|
||
real and load-bearing (`stadium.c`'s own comment: "has been real and live on every boot
|
||
for weeks"), not something this task introduced. No compiler warnings on either edited
|
||
file.
|
||
- [x] **2.4** — The four counters: `held`, `pulled`, `returned`, `consumed` (§XL.4). *Check:*
|
||
each increments at exactly one site.
|
||
2026-09-20 · `logs/20260920-055432/amd64/`, `logs/20260920-055549/aarch64/`,
|
||
`logs/20260920-055723/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`dict_hash` triple unmoved. All three print `PASS` with **identical final ledger**:
|
||
`held=0 pulled=65536 returned=65536 consumed=0` — satisfying
|
||
`held == pulled − returned − consumed` (`0 == 65536 − 65536 − 0`) exactly.
|
||
|
||
Added `sk_hermes_ledger(uint64_t*, uint64_t*, uint64_t*, uint64_t*)` (an accessor, not a
|
||
mutator) plus four static counters in `src/starkernel/vm/kernel_hermes.c`. Each has exactly
|
||
one increment/decrement site: `held`/`pulled` both move at `sk_hermes_alloc()`'s single
|
||
success path (after every refusal branch has already returned); `held`/`returned` both
|
||
move at `sk_hermes_release()`'s single success path. `consumed` is declared and always
|
||
reads 0 — **its one increment site doesn't exist yet**, and won't until task 2.5 gives
|
||
decay something to record.
|
||
|
||
`sk_hermes_release()` reads the Stadium cell's **live** `header.heat` immediately before
|
||
calling `stadium_evict()`, rather than assuming the original pulled amount — `stadium_evict()`
|
||
zeroes the header as part of freeing the cell and its own return value is a success code,
|
||
not the credited amount, so this is the only point the true remaining heat is available.
|
||
Today (no decay yet) this always equals the original `Q.SLOT` pull; once task 2.5 exists,
|
||
this is what keeps `returned` correct without touching this function again.
|
||
|
||
Self-test (kernel_main.c) extended again: snapshots the ledger before running (so it
|
||
checks its own deltas, not an assumption that it's the only caller ever), verifies
|
||
`held`/`pulled` grow by exactly `got_n × Q_SLOT` on allocation with `returned`/`consumed`
|
||
untouched, then verifies `held` returns to its starting value and `returned` grows by the
|
||
same amount on release, with `consumed` still untouched, and checks the audit invariant
|
||
itself as a bonus (task 2.6 formalizes this properly; checking it here too cost nothing
|
||
since the ledger was already in hand). No compiler warnings on either edited file.
|
||
- [x] **2.5** — Decay: apply, and **record the delta into `consumed`** (§XXXVII.3). *Check:*
|
||
`consumed` grows by exactly `heat_before − heat_after`.
|
||
2026-09-20 · `logs/20260920-182345/amd64/`, `logs/20260920-183413/aarch64/`,
|
||
`logs/20260920-184152/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`dict_hash` triple unmoved (`c95ef2d92fa0781f` / `fbde9fe105fd3b3d` / `a43d2e104ae7e451`,
|
||
identical across ISAs). All three print `PASS` with **identical** `decay_consumed=352`.
|
||
|
||
Added `sk_hermes_decay(SkHermesMessage*)` and `SK_HERMES_Q_DECAY` (65208, identical to
|
||
`messaging.4th`'s `Q-DECAY`; §XL.4 preserves the economy exactly). It sets the message's
|
||
Stadium-cell heat to `q48_mul(heat, Q_DECAY)` and, at that one site, does
|
||
`consumed += before − after` and `held −= before − after`, so
|
||
`held == pulled − returned − consumed` stays exact. `consumed` now has its one increment
|
||
site.
|
||
|
||
Self-test: a second alloc-to-exhaustion cycle decays each message once. It checks each
|
||
cell's new heat against an independently computed `q48_mul`, checks `consumed` grew by
|
||
exactly the independently summed `before − after` (32 × (2048 − 2037) = 352), checks the
|
||
audit identity mid-hold, and checks that release leaves the reservoir at exactly
|
||
`reservoir0 − 352` (decayed heat does not return, §XL.4). A vacuity guard fails the test
|
||
if decay consumed nothing. The task 2.3 undecayed exact-restoration check is kept as the
|
||
first cycle.
|
||
|
||
Process notes: an accidental `pkill -f` killed the shell once (no effect on results);
|
||
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` were restored via
|
||
`git checkout --` before each ISA per the task 0.0 finding. That discarded a pre-existing
|
||
uncommitted modification to `disk/artemis.img` that was present at session start.
|
||
- [x] **2.6** — Self-audit: `held == pulled − returned − consumed`, **epsilon zero**. *Check:*
|
||
holds across the cycle; **deliberately corrupt a counter → audit fires on the first unit**.
|
||
2026-09-20 · `logs/20260920-204658/amd64/`, `logs/20260920-205410/aarch64/`,
|
||
`logs/20260920-205834/riscv64/` — all reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`dict_hash` triple unmoved and identical across ISAs; all print `PASS`,
|
||
`audit_failures=0`, `decay_consumed=352`.
|
||
|
||
Added `sk_hermes_audit_values()` (pure exact-equality predicate, no tolerance),
|
||
`sk_hermes_audit()` (live ledger, O(1), prints and counts a failure) and
|
||
`sk_hermes_audit_failure_count()`. The live audit runs at the end of every successful
|
||
alloc, release and decay (§XXXVII.4: no cadence decision needed). Corruption is tested
|
||
on a *copy* of the ledger, +1 unit on each of the four counters in turn, so live state is
|
||
never corrupted; each is caught by the predicate while the uncorrupted values pass. The
|
||
live audit stayed silent through both alloc/decay/release cycles.
|
||
|
||
Limit, stated: the corruption proof exercises the predicate, not the wiring of the live
|
||
audit's failure branch (that branch never executed). Task 2.8's scan cross-check is the
|
||
independent check on the counters themselves.
|
||
- [x] **2.7** — **Stage B proof** (§XXXIV.3 as corrected by §XXXIX.4): alloc/free cycle
|
||
verifying **(a)** the ledger and **(b)** `stadium_conserved()` before and after.
|
||
*Check:* both true, all three arches. **`fleet_conserved` is not evidence here** — it
|
||
cannot see Stadium heat (§XXXIX.1).
|
||
2026-09-20 · `logs/20260920-212209/amd64/`, `logs/20260920-212522/aarch64/`,
|
||
`logs/20260920-212845/riscv64/` — all print `Stage B (ledger + stadium_conserved): PASS`,
|
||
`audit_failures=0`, `vm_consumed=693`; zero `UNKNOWN WORD`; `dict_hash` triple unmoved and
|
||
identical across ISAs; reach `[zuse@Hera] ok>`.
|
||
|
||
**Prerequisite built here, as §XL.4 directs:** `stadium_conserved()` gains its `consumed`
|
||
term. New per-VM `consumed` field in `StadiumVMQuota` (zeroed at both quota-creation
|
||
sites), `stadium_consumed_record()`/`stadium_consumed_peek()`, and
|
||
`stadium_conserved()` now checks `resident + reservoir + consumed == Q48_ONE`, epsilon
|
||
zero. `SkHermesMessage` gained an `owner` VMUuid so decay charges the funding VM;
|
||
`sk_hermes_decay()` records into it. Other VMs are unaffected (consumed stays 0).
|
||
|
||
Stage B checks `stadium_conserved()` at four points — before, mid-hold, after decay,
|
||
after release — plus the ledger audit, and requires the old **two-term form to FAIL after
|
||
decay** while the four-term form holds (non-vacuity: the consumed term is load-bearing).
|
||
`fleet_conserved` is not consulted.
|
||
|
||
**First attempt FAILED on all three ISAs** (`logs/20260920-211204/`, `-211517/`,
|
||
`-211840/`, kept as audit artifacts). Cause was a bug in the test, not the code: it
|
||
expected 32 allocations, but the preceding decay cycle had consumed 352 from that VM's
|
||
reservoir so only 31 fit. Fixed by deriving the expected count from the current reservoir.
|
||
Vm_consumed=693 is 352 (32×11, first cycle) + 341 (31×11, Stage B).
|
||
|
||
**Phase 2 gate: passed** (2.7 holds on all three arches). Remaining Phase 2 work is 2.8
|
||
(diagnostic scan cross-check).
|
||
- [x] **2.8** — Scan-based cross-check of the counters, diagnostics only, off the hot path
|
||
(§XXXVII.4). *Check:* scan agrees with counters.
|
||
2026-09-21 · `logs/20260921-083638/amd64/`, `logs/20260921-083950/aarch64/`,
|
||
`logs/20260921-084314/riscv64/` — all print `Scan cross-check (counters vs arena): PASS`
|
||
with identical `scan_held_before_decay=8192`, `scan_held_after_decay=8148`; Stage B and
|
||
the main self-test still `PASS`, `audit_failures=0`, zero `UNKNOWN WORD`, `dict_hash`
|
||
triple unmoved and identical across ISAs.
|
||
|
||
Added `sk_hermes_scan_held()` (walks the arena, sums live Stadium-cell heat of in-use
|
||
messages, reports the live count) and `sk_hermes_scan_check()` (scan == `held`, exact).
|
||
Called only from the self-test, never from alloc/release/decay. The check runs at rest
|
||
(0 live), mid-hold (4 live, 4×2048 = 8192), after decay (8148 = 8192 − 4×11) and after
|
||
release (0), with a vacuity guard (non-zero mid-hold, strictly less after decay).
|
||
|
||
Limit, stated: the scan reads the same Stadium cells the counters were derived from, so
|
||
it verifies the counters against the arena, not the Stadium cells against ground truth;
|
||
the latter is `stadium_conserved()`'s job (2.7).
|
||
|
||
**Phase 2 is complete (2.1–2.8).** Phase 3 remains blocked on B1, B2, B4.
|
||
|
||
## Phase 3 — Cutover — **UNBLOCKED, tasks not yet written**
|
||
|
||
**Blockers cleared 2026-09-21 (§XLV). Task list drafted 2026-09-21, awaiting Captain Bob's
|
||
review — none started.** Shape (§XXXIV.3, amended by §XLV): build the negotiated pub/sub
|
||
plumbing inert, then cut over `BLK-ATTACH-EVENT` alone, then remaining types one at a time,
|
||
under §XXXIV.2's partition rule — one message type owned by exactly one layer, no message
|
||
shared. **Stop condition (§XXXIV.6): `dict_hash` diverging across ISAs, or Stage B evidence
|
||
(ledger + `stadium_conserved()`) going false — re-plan, do not push on.**
|
||
|
||
- [x] **B1** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.1**: negotiated pub/sub channels —
|
||
one common channel, private channels by request/grant/deny, ACK/NACK throughout. Overrules
|
||
§XXXIII.5. Open sub-items: ACK cadence, ACL hook for channel-open policy.
|
||
- [x] **B2** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.2**: dynamic, not a fixed constant
|
||
(Stadium-style RAM-derived sizing). Open sub-item: the concrete sizing rule.
|
||
- [x] **B3** — **CLEARED 2026-09-19 by `FABRIC-3.5.md` §XLIII**: the target drains its own
|
||
queue at its own outermost interpret checkpoint; kernel-Hermes publishes and never
|
||
dispatches. Reuses `sk_vm_at_outermost_interpret()` and the Stage 3 checkpoint.
|
||
- [x] **B4** — **CLEARED 2026-09-21 by `FABRIC-3.5.md` §XLV.3**: one block (1024 bytes) per
|
||
message, larger payloads chunked. Open sub-item: chunk framing.
|
||
|
||
**All four blockers are cleared, but Phase 3 has no task breakdown yet** — the shape below
|
||
predates §XLV's negotiation. Writing the task list (and settling the §XLV.4 sub-items it
|
||
depends on) is the next step, before any Phase 3 code.
|
||
|
||
### Phase 3 tasks (drafted 2026-09-21; **draft, not authorized**)
|
||
|
||
Each is one commit with three-ISA acceptance, per the standing rules. Tasks marked **[needs
|
||
ruling]** cannot be written precisely until Captain Bob settles the named sub-item.
|
||
|
||
- [x] **3.0** — **Decision gate, not code.** Rule on the §XLV.4 sub-items: **(a)** ACK cadence
|
||
(advised: channel open + delivery, not every common-channel message); **(b)** where the
|
||
channel-open ACL hook lives in `ACL.4th`; **(c)** the dynamic switch-table sizing rule;
|
||
**(d)** chunk framing; **(e)** drain one message per checkpoint (§XLIII.6.3, recommended,
|
||
never ruled). *Check:* each answer recorded in `FABRIC-3.5.md`.
|
||
2026-09-21 · no commit (documentation ruling, see `FABRIC-3.5.md` §XLVI) · all five
|
||
sub-items plus task 3.3's heat-cost point ruled by Captain Bob, all recommended defaults
|
||
accepted.
|
||
- [x] **3.1** — **Dynamic switch table** (B2). Read
|
||
`SK_SWITCH_MAX_SLOTS`'s every use first; replace the constant with a boot-time,
|
||
RAM-derived allocation (Stadium's `stadium_max_vm_count_val` pattern). *Check:* boot
|
||
byte-identical, `dict_hash` unmoved; a synthetic test drives more than 16 slots.
|
||
2026-09-21 · `logs/20260921-233241/amd64/`, `logs/20260921-233814/aarch64/`,
|
||
`logs/20260921-234837/riscv64/` (riscv64's first attempt, `logs/20260921-234312/`,
|
||
timed out mid-boot on a 280s wrapper before reaching the prompt — kept per the
|
||
never-delete-logs convention, not a real finding, just an undersized timeout; the
|
||
234837 rerun with more headroom reached the prompt cleanly). All three reach
|
||
`[zuse@Hera] ok>`, zero `UNKNOWN WORD`. Switch-signal table sized dynamically off
|
||
`stadium_max_vm_count()` at boot, confirmed via a new `Switch-signal: N slots` console
|
||
line: **50 slots (amd64), 202 slots (aarch64), 50 slots (riscv64)** — all far above the
|
||
old fixed 16-slot cap, satisfying the "drives more than 16 slots" check without a
|
||
separate synthetic harness (real boot-time RAM sizing already clears it by a wide
|
||
margin). Reused `session_boot_init()`'s exact pattern (kmalloc to
|
||
`stadium_max_vm_count()`, called in `kernel_main.c` right after `stadium_boot_init()`,
|
||
before the first `sk_vm_switch_signal_register()` call). `dict_hash` for Hermes
|
||
(`0xc95ef2d92fa0781f`) and Hestia (`0xfbde9fe105fd3b3d`) identical across all three
|
||
architectures — unmoved from pre-task values, as expected (this task touches no
|
||
capsule). Artemis's `dict_hash` differs between the amd64 run (`0xa43d2e104ae7e451`,
|
||
fresh-formatted `disk/artemis.img`) and the aarch64/riscv64 runs (`0x7f18214489036b39`,
|
||
both *resuming* the same disk state the amd64 run left behind) — a disk-state artifact
|
||
of running three sequential boots against one shared image, not an architecture
|
||
divergence; not a §XXXIV.6 stop condition.
|
||
note: unrelated finding, not fixed — riscv64's boot logged three
|
||
`virtio_blk: vblk_io timed out` errors during Artemis's disk I/O in both riscv64
|
||
attempts (sectors 20/8/8 and 3224/5496/5504); boot recovered and reached the prompt
|
||
regardless. `virtio_blk.c` was not touched by this task. Reporting per standing rule,
|
||
not investigating further here.
|
||
- [x] **3.2** — **Channel table + common channel**, inert (B1). Dynamic table of topics, each
|
||
with its own `SkHermesMembership`; the common channel exists from boot; every VM is
|
||
subscribed at birth. Wired to nothing. *Check:* boot byte-identical; every born VM appears
|
||
in the common channel's membership; create/destroy a synthetic private topic.
|
||
2026-09-22 · `logs/20260922-000447/amd64/`, `logs/20260922-000954/aarch64/`,
|
||
`logs/20260922-001511/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
|
||
touched). New `SkHermesChannel` table (kernel_hermes.h/.c): a channel is an index into a
|
||
boot-time, `stadium_max_vm_count()`-sized array, no name field (mirrors messaging.4th's
|
||
own nameless `CH-ARENA`). Sizing note: SXLV.1 says the channel table has "no fixed channel
|
||
maximum, same reasoning as SXLV.2" but names no separate numeric rule -- extending task
|
||
3.1's already-ruled `stadium_max_vm_count()` bound directly (same kmalloc-at-boot shape)
|
||
is the smallest choice consistent with what was ruled; flagged in the header comment as an
|
||
engineering extrapolation, not restated as a separate Captain Bob ruling. Common channel
|
||
(index `SK_HERMES_CHANNEL_COMMON` = 0) created in `sk_hermes_channels_boot_init()`, called
|
||
from `kernel_main.c` right after the switch-signal boot init; Hera subscribed explicitly
|
||
right after `stadium_birth_hera()` (she is the one VM never born through
|
||
`capsule_birth_baby()`); every other VM (Tripod fleet and future WIREBIND identities
|
||
alike) subscribed inside `capsule_birth_baby()` itself, at its `VM_STATE_LIVE` completion
|
||
point -- the single choke point every non-Hera birth already passes through, confirmed by
|
||
grep (`mama_forth_words.c`, `capsule_runcap.c`, `capsule_console.c` all call it). New
|
||
console lines confirmed live on all three architectures: `Kernel-Hermes: N channel slots`
|
||
(**50 amd64, 202 aarch64, 50 riscv64** -- tracks switch-signal's own per-arch sizing
|
||
exactly, as expected since both derive from the same `stadium_max_vm_count()`), then after
|
||
all four fleet births, `Kernel-Hermes common-channel fleet self-test: PASS` (`common
|
||
channel members=4`, one per Hera/Hermes/Hestia/Artemis) and `Kernel-Hermes synthetic
|
||
private-topic self-test: PASS` (create/subscribe/is-member/unsubscribe/destroy round-trip
|
||
against a synthetic VM id, plus confirms a destroyed channel refuses further ops and the
|
||
common channel itself refuses `sk_hermes_channel_destroy()`) -- both PASS on all three
|
||
architectures. `dict_hash` for Hermes (`0xc95ef2d92fa0781f`) and Hestia
|
||
(`0xfbde9fe105fd3b3d`) identical across all three architectures, unmoved from task 3.1's
|
||
values (this task touches no capsule).
|
||
- [x] **3.3** — **Publish path, no dispatch** (§XLIII.3). Publish allocates via
|
||
`sk_hermes_alloc()`, enqueues onto each subscriber's own pending queue, and records the
|
||
fact; it dispatches nothing. Self-test only. *Check:* ledger audit and
|
||
`stadium_conserved()` hold across N publishes to M subscribers (heat cost per subscriber
|
||
is a design point to settle **before** writing this: one message per subscriber, or one
|
||
shared message with a reference count — **[needs ruling]**).
|
||
2026-09-22 · `logs/20260922-002857/amd64/`, `logs/20260922-003404/aarch64/`,
|
||
`logs/20260922-003922/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
|
||
touched). Heat cost ruled 2026-09-21 (one message per subscriber); `sk_hermes_publish()`
|
||
allocates one `SkHermesMessage` per channel member via `sk_hermes_alloc()` (funded by the
|
||
publisher's reservoir) and enqueues each onto a new per-subscriber `SkHermesPendingQueue`
|
||
(found-or-created lazily by `vm_id`, same shape as `stadium.c`'s `quota_slot_for_vm()` --
|
||
not pre-populated at birth like channel membership is, since not every VM ever receives a
|
||
message). Table sized from `stadium_max_vm_count()`, same pattern as tasks 3.1/3.2.
|
||
Best-effort, not atomic across subscribers -- a failed allocation or full queue skips just
|
||
that one subscriber and rolls back its own allocation; not a separate ruling, the natural
|
||
reading of the task's own check (ledger/`stadium_conserved()` invariants hold under partial
|
||
delivery too), documented as such in the header. Added `sk_hermes_pending_count()`/
|
||
`_peek()`/`_pop()` as the read/drain primitives task 3.4's real checkpoint-driven drain
|
||
will build on -- this task's own self-test uses them directly for cleanup since no
|
||
checkpoint hook exists yet. New console line confirmed live on all three architectures:
|
||
`Kernel-Hermes: N pending-queue slots` (**50 amd64, 202 aarch64, 50 riscv64** -- tracks
|
||
the channel/switch-signal tables' own per-arch sizing exactly). Self-test (synthetic
|
||
publisher lo=7, three synthetic subscribers lo=8/9/10, N=2 publishes to a 3-member
|
||
synthetic channel) confirms `sk_hermes_publish()` returns 3 each time, each subscriber's
|
||
queue holds exactly 2 afterward, ledger audit and `stadium_conserved(publisher)` hold
|
||
mid-publish, then confirms the same invariants return to baseline after draining every
|
||
queue by hand (`pending_peek()`/`release()`/`pop()`) -- `PASS` on all three architectures.
|
||
`dict_hash` for Hermes (`0xc95ef2d92fa0781f`) and Hestia (`0xfbde9fe105fd3b3d`) identical
|
||
across all three architectures, unmoved from tasks 3.1/3.2's values (this task touches no
|
||
capsule).
|
||
- [x] **3.4** — **Drain at the outermost checkpoint** (§XLIII.3–.5). Reuse
|
||
`sk_vm_at_outermost_interpret()`; one message per checkpoint **[needs ruling 3.0e]**.
|
||
*Check:* a nested interpret does not drain; a queued payload is interpreted exactly once at
|
||
depth 1.
|
||
2026-09-22 · `logs/20260922-010622/amd64/`, `logs/20260922-011130/aarch64/`,
|
||
`logs/20260922-011648/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
|
||
touched). **Finding, amended into `FABRIC-3.5.md` §XLIII.5** (caught by `advisor()` before
|
||
writing the naive version, not found live): §XLIII.5's "recursive drain is prevented for
|
||
free" via `g_vm_interpret_depth` is true but covers only same-message re-drain, not the
|
||
separate same-VM reentrancy hazard `FABRIC-3.md` §XX had already named for Hera
|
||
specifically (`VMCallState` saves `rsp`/`exit_colon`/`ecw_nesting` only, never
|
||
`input_buffer`/`input_length`/`input_pos`) -- draining calls `vm_interpret()` on the same
|
||
`vm` whose own `vm_interpret()` call is still paused mid-word at the checkpoint, which
|
||
would silently truncate the enclosing REPL line or LOAD block if the cursor isn't saved
|
||
and restored by hand. `sk_hermes_drain_checkpoint()` (kernel_hermes.c) snapshots
|
||
`input_buffer`/`input_length`/`input_pos`/`mode`/`error`/`abort_requested` around the
|
||
`vm_interpret()` call and restores all six -- `mode` forced to `MODE_INTERPRET` for the
|
||
duration (a checkpoint reached mid-colon-definition must not compile the payload's words
|
||
into the enclosing definition); `error`/`abort_requested` restored so a bad message can't
|
||
abort the enclosing execution. Placed in `vm_core.c`'s existing cooperative checkpoint
|
||
**before** the switch-signal block, not after (`sk_vm_context_switch()` does not return
|
||
until something switches back, so a drain placed after it would silently never run on any
|
||
checkpoint that switches). Gated behind a system-wide pending-total counter
|
||
(`sk_hermes_pending_total`, maintained in `queue_push()`/`sk_hermes_pending_pop()`) so the
|
||
common no-message-in-flight case costs one integer read per word dispatch, not a
|
||
`stadium_max_vm_count()`-sized queue-table scan -- flagged by `advisor()` as a real
|
||
hot-path cost against this project's own +0.0603% measurement floor, not deferred.
|
||
Self-test (kernel_main.c) publishes a real, stack-neutral payload (`"1 2 + DROP"`) to
|
||
Hermes (a real, already-born VM, not synthetic -- needed a genuine live dictionary),
|
||
proves the depth gate via `VM-EXEC`-ing the existing harmless colon word `WELCOME`
|
||
(`capsules/hermes/init.4th` block 4855) into Hermes from Hera's context -- a real, already-
|
||
proven-safe nested `vm_interpret()` call (`mama_forth_words.c`'s own VM-EXEC mechanism) --
|
||
confirming the message does NOT drain at the resulting depth 2, then calls
|
||
`sk_hermes_drain_checkpoint()` directly from this self-test's own genuinely-outermost C
|
||
context and confirms it DOES drain exactly once, with a further call a clean no-op.
|
||
Deliberately avoided the block/`LOAD` mechanism for the nested case -- real but touches
|
||
real disk-backed block storage, which a throwaway diagnostic has no business perturbing.
|
||
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
|
||
task touches no capsule).
|
||
- [x] **3.5** — **Payload bound and chunking** (B4) **[needs ruling 3.0d]**. Max 1024 bytes per
|
||
message; larger payloads sent as ordered chunks, reassembled before drain. *Check:*
|
||
1024-byte payload single message; 3000-byte payload chunked and reassembled byte-exact;
|
||
1025-byte single-message send refused.
|
||
2026-09-22 · `logs/20260922-063122/amd64/`, `logs/20260922-063629/aarch64/`,
|
||
`logs/20260922-064147/riscv64/` — all three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`,
|
||
`mkcapsule --lint capsules/` clean (38 files, 0 violations, unchanged -- no capsule
|
||
touched). Sizing decision (checked with `advisor()`, not itself a separate ruling -- see
|
||
the header's own note): `SK_HERMES_CHUNK_MAX_PAYLOAD` (1024) bounds every message's
|
||
`payload_len` **uniformly**, chunked or not -- a chunk carrier is
|
||
`[SkHermesChunkHeader][content slice]` where the slice is capped at
|
||
`1024 - sizeof(header)`, not 1024 itself. Rejected the literal alternative (slice up to
|
||
1024, carrier up to `1024+sizeof(header)`): under that reading a chunk carrier could never
|
||
be handed to `vm_interpret()` as-is, making a future chunk-aware drain a special case
|
||
instead of the same one-block invariant every other message already satisfies.
|
||
`sk_hermes_publish()` (task 3.3) now enforces this bound directly, closing the gap that
|
||
task's own doc comment explicitly parked ("3.3 does not enforce a payload size limit").
|
||
**Deliberately no chunking-SENDER API** (`sk_hermes_publish_chunked()` or similar) --
|
||
`advisor()` flagged that building one raises a real memory-lifetime question kernel-Hermes
|
||
has never answered (chunk buffers would need to stay alive until every subscriber drains
|
||
them, and nothing here can know when that is); sending is a loop pattern a caller writes
|
||
with `sk_hermes_chunk_count()` + `sk_hermes_publish()`, demonstrated by this task's own
|
||
self-test rather than hidden behind a new allocator. `sk_hermes_reassemble()` is pure and
|
||
memory-agnostic (caller-owned input chunks, caller-owned output buffer) -- validates
|
||
`msg_id` agreement, exact `seq` coverage `{0..n_chunks-1}` with `is_last` on exactly the
|
||
last, and every non-final slice at exactly `SK_HERMES_CHUNK_MAX_SLICE` bytes, before a
|
||
single `memcpy`; the reassembled total length is computed once from validated seq
|
||
completeness and checked against `out_buf_cap` once, so a too-small output buffer is
|
||
refused cleanly with zero partial copy rather than order-dependently on whichever chunk
|
||
happens to overflow. Self-test (kernel_main.c) proves exactly the task's three checks: a
|
||
1024-byte payload as one message; a 1025-byte single-message send refused outright, no
|
||
allocation, ledger untouched; a 3000-byte payload split into 3 chunks, drained off a
|
||
synthetic subscriber's own pending queue (task 3.3 primitives), reassembled, and confirmed
|
||
byte-exact against the original. Ledger audit and `stadium_conserved()` hold before and
|
||
after. `dict_hash` for Hermes/Hestia unmoved and identical across all three architectures
|
||
(this task touches no capsule).
|
||
- [x] **3.6** — **ACK/NACK and private-channel negotiation** (B1) **[needs ruling 3.0a]**.
|
||
`request` → `grant`/`deny` on the common channel; a grant creates a private topic;
|
||
close tears it down. ACK/NACK are message types on the existing allocator.
|
||
*Check:* grant path, deny path, close path; heat conserved (ledger + `stadium_conserved()`)
|
||
across all three; a NACK'd request leaves no topic behind.
|
||
2026-09-22 · `logs/20260922-070455/amd64/`, `logs/20260922-071004/aarch64/`,
|
||
`logs/20260922-071525/riscv64/` (the successful runs; `logs/20260922-065946/amd64/` is a
|
||
genuine first attempt that hit a real bug, caught by the self-test itself before this task
|
||
was called done -- kept, not deleted, per the never-delete-logs convention) — all three
|
||
reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean (38 files,
|
||
0 violations, unchanged -- no capsule touched).
|
||
**Design finding (checked with `advisor()`):** the ruling's "over the common channel"
|
||
cannot mean `sk_hermes_publish()`-style fan-out -- that sets `msg->to` to whichever member
|
||
it is iterating, so every common-channel member would receive its own copy of a request
|
||
believing it was the addressee, drawing heat for a message only one VM should ever see.
|
||
`messaging.4th`'s own `CH-REQUEST ( type from to paddr plen -- )` carried an explicit `to`
|
||
for the same reason -- point-to-point addressing was in the original protocol from the
|
||
start; "over the common channel" means every VM is reachable from birth (task 3.2), not
|
||
that the exchange itself fans out. Extracted `sk_hermes_send_one()` from
|
||
`sk_hermes_publish()`'s own per-subscriber body (alloc/set-fields/find-or-create-queue/
|
||
push/roll-back-on-refusal) so both callers share one code path and the ledger can never
|
||
diverge between them -- `sk_hermes_publish()` now just loops it per member.
|
||
Message types: `CH_REQUEST`/`CH_GRANT`/`ACK`/`NACK`/`CH_CLOSE`, chosen clear of
|
||
`messaging.4th`'s own live/reserved type space (2/3/4/7/8/9/253/255), not itself a ruling.
|
||
"A deny is a NACK" (SXLV.1) -- no separate `CH_DENY` type. ACK sent once, for the ruled
|
||
channel-open+delivery moment. The grant/deny *decision* is a plain caller-supplied
|
||
`approved` bool (named for what it is, not `allow`) -- task 3.7 replaces the call site that
|
||
produces it with a real `ACL.4th` query, not this function's signature. Close authority is
|
||
membership alone (either party may close a channel it belongs to) -- narrowest defensible
|
||
rule given both are already trusted members, not itself a ruling.
|
||
**Sibling case `advisor()` flagged as the one a green boot would hide**, beyond the task's
|
||
own three checks: an `approved==1` respond() whose channel creation itself fails (table
|
||
exhausted) must still fall through to NACK, not a silent false grant or a half-open
|
||
channel -- self-test exhausts the entire channel table, confirms the fallback, then
|
||
restores capacity.
|
||
**Bug found and fixed before this was called done:** the first self-test draft
|
||
(`logs/20260922-065946/amd64/`, `FAIL`) called `sk_hermes_pending_pop()` directly on the
|
||
deny path's dropped request copy without releasing it first -- `pending_pop()` only
|
||
advances the queue per its own doc comment, so the message's Stadium heat was never
|
||
returned, leaking held heat and failing the self-test's own `held0`/`held1` baseline check.
|
||
Fixed to peek+release+pop, matching every other drain in the same self-test; re-verified
|
||
PASS on all three architectures after the fix.
|
||
`dict_hash` for Hermes/Hestia unmoved and identical across all three architectures (this
|
||
task touches no capsule). Artemis's own `dict_hash` differs run-to-run as already
|
||
documented (task 3.1's own finding) -- shared `disk/artemis.img` state accumulated across
|
||
this session's many prior boots, not an architecture divergence.
|
||
- [ ] **3.7** — **Channel-open policy hook** **[needs ruling 3.0b]**. Kernel-Hermes asks
|
||
`ACL.4th`, never decides in C, never gates on `zuse_session`. *Check:* a denied open is
|
||
denied by FORTH policy, with the C unchanged when the policy changes.
|
||
- [ ] **3.8** — **Stage C: cut over `BLK-ATTACH-EVENT` alone** (§XXXIV.3). One layer owns it;
|
||
FORTH Hermes still routes every other type. *Check:* the real Hera↔Artemis attach path
|
||
works end to end on all three ISAs; ledger and `stadium_conserved()` true before/after;
|
||
**`fleet_conserved` is not evidence** (§XXXIX).
|
||
- [ ] **3.9** — **Stage D: `CONSOLE-CMD-EVENT` (type 7)**, then **3.10 `ELEVATE-REQUEST`
|
||
(type 8)** — one type per task, each with the 3.8 check. Any type found live beyond these
|
||
three is added here, not bundled.
|
||
- [ ] **3.11** — **Phase 3 gate.** No FORTH-owned live message types remain; Stage B evidence
|
||
holds across a full boot on all three ISAs. **Do not start Phase 4 until this passes.**
|
||
|
||
## Phase 4 — Category B strip
|
||
|
||
Only after every live type is cut over. **Hermes leaves `is_fleet_foundation` and
|
||
`kernel_main.c` here, not earlier** (§XXXIV.4).
|
||
|
||
- [ ] **4.1** — Strip `capsules/hermes/init.4th`.
|
||
- [ ] **4.2** — Strip the FORTH routing table and slot-3 pairing convention.
|
||
- [ ] **4.3** — Strip `capsules/common/messaging.4th`.
|
||
- [ ] **4.4** — Remove Hermes from `is_fleet_foundation` and its birth from `kernel_main.c`.
|
||
|
||
## Phase 5 — Close-out
|
||
|
||
- [ ] **5.1** — Isabelle/HOL pass. **Deliverable is the restated boundary**, explicitly
|
||
including §XXV.4's coverage loss — *not* a green build (§XXV.3).
|
||
- [~] **5.2** — Documentation sweep, grepping **by exclusion** (§XXVIII.3).
|
||
**CLAUDE.md's four errors: DONE 2026-09-19 (§XLIV), pointer to this document added.**
|
||
Outstanding: `MANIFEST.md` (rides the strip, §XXII.5), the `TRIPOD.md`/0.1 contradiction,
|
||
item 42's K-qualification, the superseded subsystem docs' update-or-archive call, and
|
||
item 45 (`experiments/bare_metal/README.md` block framing).
|
||
- [ ] **5.3** — `make sbom`; check `Created:` and `DocumentName` (§XXVI.2).
|
||
- [ ] **5.4** — `LITHOS_VERSION = 2.1.0`; engine `VERSION` per §XXX.6's rule; roadmap table
|
||
gains its `2.1.0` line in two live places (§XXVIII.2).
|
||
- [ ] **5.5** — Resolve the stray `refs/heads/v2.0.1` and PR #1 (§XXVI.4). **Investigate, do
|
||
not delete.**
|
||
- [ ] **5.6** — Merge to `master`; tag `v2.1.0`.
|
||
- [ ] **5.7** — Archival close of `FABRIC-3.5.md` (§XXVI.5) and of this document.
|
||
|
||
---
|
||
|
||
## Findings log
|
||
|
||
*Defects and surprises found while executing. Reported, not fixed (unless the task was to fix
|
||
them). A finding that contradicts a `FABRIC-3.5.md` ruling must also be amended there.*
|
||
|
||
**2026-09-19, task 0.0 — shared `disk/artemis.img` confounds cross-ISA `dict_hash` comparison
|
||
unless reset between runs.** Not a code defect and does not contradict any `FABRIC-3.5.md`
|
||
ruling — `FABRIC-3.md` §XXXV.2 already documents that the disk is deliberately shared across all
|
||
three ISAs' `qemu` targets. But it means a same-order rerun of the three architectures will have
|
||
run 2 and 3 silently take Artemis's `resuming` branch instead of its `blank disk -- formatting`
|
||
branch, producing a different `dict_hash` for Artemis alone (Hera and Hermes are unaffected —
|
||
neither touches the artdisk at birth). Worth carrying forward: **any task in this document that
|
||
checks `dict_hash` identity across architectures and involves Artemis should restore
|
||
`disk/artemis.img` and `disk/thumbdrives/zuse-thumb-ident.img` to their committed blank state
|
||
(`git checkout --`) before each architecture's run**, or the check will fail on a confound rather
|
||
than a real divergence.
|
||
|
||
---
|
||
|
||
## Out of scope, carried for visibility
|
||
|
||
Recorded in `FABRIC-3.5.md`, deliberately **not** part of this reshuffle. Listed so they are not
|
||
absorbed by accident:
|
||
|
||
- **Item 43** — should a Stadium reservoir ever replenish? (§XL.6) A VM's send capacity declines
|
||
monotonically today.
|
||
- **Items 23–26** — the `FABRIC-3.5.md` §XXXI gap-analysis follow-ups: re-home `FABRIC-2` §17.4,
|
||
consolidate the three reported-not-scheduled registries, decide `FABRIC-2`'s five orphaned
|
||
design items, reconcile `FABRIC-0`'s seven opens.
|
||
- **`src/*.c.bak`** — tracked-but-stale, reported in three places and actioned in none
|
||
(§XXII.5).
|