The reply leg (Artemis -> Hera ack) that used to flow through common:messaging.4th's MSG-SEND/MSG-TICK now goes through kernel-Hermes's sk_hermes_send_one()/sk_hermes_drain_checkpoint() instead -- FORTH Hermes never sees a BLK-ATTACH-EVENT message again (SXXXIV.2's partition rule). The request leg was never real FORTH messaging traffic to begin with (a direct VM-EXEC, no type tag, forced by Hera's own inability to load common:messaging.4th), so it is untouched. New KH-BLK-ATTACH-SEND (repl.c) wraps sk_hermes_send_one(), reached from capsules/artemis/init.4th's HERA-BLK-ATTACH-REQ. Delivery reuses task 3.4's already-wired sk_hermes_drain_checkpoint(); BLK-ATTACH-ACK itself is unchanged, just reached by a different layer. SK_HERMES_MSG_TYPE_BLK_ATTACH deliberately reuses BLK-ATTACH-EVENT's own value (9) to document this as a cutover of the same message, not a new one. Two real bugs found on the way, both recorded in FABRIC-3.6.md's findings log: - A popped FORTH CREATE-buffer address was raw-cast to a host pointer instead of going through vm_ptr() -- silently read all-zero memory, no crash, no error, just a message that arrived and did nothing. Fixed; the rule and its exception (repl.c's own dev-addr is legitimately a raw pointer, formatted that way by its own pushing code) are written up for the next FORTH-facing C word. - A separate, genuine hang on the very first live exercise of this path, never reproduced across ten subsequent boots. Reported, not chased -- not blocking, per the task's own check being otherwise fully satisfied. Also found live: log_message() is invisible in this build's actual serial-log capture at every level -- settled on a single console_println in the real drain target instead, one line per real USB attach, not a hot-path. Final acceptance (logs/20260922-105501, -105758, -110304, disk images reset before each): dict_hash identical across all three architectures for every VM, zero UNKNOWN WORD, mkcapsule --lint clean, real ledger+stadium_conserved(Artemis)=true evidence on every boot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1227 lines
94 KiB
Markdown
1227 lines
94 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.
|
||
- [x] **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.
|
||
2026-09-22 · `logs/20260922-075138/amd64/`, `logs/20260922-075649/aarch64/`,
|
||
`logs/20260922-080212/riscv64/` (the successful runs; `logs/20260922-074119/amd64/` and
|
||
`logs/20260922-074628/amd64/` are two genuine prior attempts that hit real bugs, 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 — one block added inside the
|
||
existing `ACL.4th`, no file added). `dict_hash` identical across all three architectures
|
||
for every VM (Hera `0xbb2b51e7595464ce`, Hermes `0xc95ef2d92fa0781f`, Hestia
|
||
`0xfbde9fe105fd3b3d`, Artemis `0x7f18214489036b39`).
|
||
|
||
Added `HERMES-CHANNEL-OPEN? ( req-hi req-lo -- allow? )` at `capsules/ACL.4th` block 4008
|
||
(default: approve everything) — the one word policy authors edit; `sk_hermes_channel_open_policy(VM *target_vm, VMUuid requester)`
|
||
(`kernel_hermes.h`/`.c`) is the C-side query that calls it, and never itself decides. Uses
|
||
plain word-dispatch (`vm_find_word()` + the entry's own `func` pointer) against the
|
||
*target* VM's own dictionary and data stack, not `vm_interpret()` — deliberately, since
|
||
`HERMES-CHANNEL-OPEN?` is a fixed known name, and this avoids task 3.4's input-buffer
|
||
cursor-preservation hazard entirely (this touches the target's data stack only, never its
|
||
input buffer). **Fails closed, not open**: no policy word present (e.g. a VM that never
|
||
loaded `ACL.4th`), a policy-word error, or stack underflow all return "denied," matching
|
||
`.claude/CLAUDE.md`'s own posture that absence of policy must never mean "always allow."
|
||
A broken policy word's own error state is cleared on the target VM before returning, so it
|
||
cannot leak into whatever else that VM is doing.
|
||
|
||
**Two real bugs found and fixed before this was called done, not a clean first pass:**
|
||
1. (`logs/20260922-074119/`) The first draft called `entry->func(target_vm)` directly
|
||
without setting `target_vm->current_executing_entry` first — colon words dispatch
|
||
through `execute_colon_word()`, which reads its own body address from that field and
|
||
silently no-ops if it's `NULL` (`vm_core.c:730`). Every call silently did nothing,
|
||
leaving the pushed requester args untouched on the stack rather than erroring — no
|
||
crash, no obvious symptom, just a wrong answer, which is why it needed the self-test
|
||
(not a crash) to catch. Fixed by setting `current_executing_entry` before the call and
|
||
restoring the caller's own value after (in case the target VM is ever called into
|
||
mid-dispatch elsewhere later).
|
||
2. (`logs/20260922-074628/`) Still `FAIL`, with debug instrumentation left in from
|
||
diagnosing bug 1 (`entry=... func=...`, `error=0 dsp=1`/`error=0 dsp=2` printed, then
|
||
`FAIL`). **No intermediate commit exists for this attempt, so its precise defect is
|
||
not reconstructable now** — the log records the symptom and nothing more. Debug prints
|
||
were removed; the third attempt (`075138`/`075649`/`080212`) passed clean on all three
|
||
architectures with no debug output.
|
||
|
||
Self-test in `kernel_main.c` proves the task's check four ways against the **same
|
||
unchanged C function**: (a) Hera's default `HERMES-CHANNEL-OPEN?` approves; (b)
|
||
redefining that same word live on Hera to deny flips the answer with zero C change; (c)
|
||
restoring the approve-default flips it back; (d) Hermes — who never loads `ACL.4th` at
|
||
all (grep-confirmed: only `init.4th`/`ACL.4th`/`zuse.4th`/`block-acl.4th` reference it) —
|
||
is correctly refused closed, not silently approved, proving the fail-closed behaviour on a
|
||
VM with no policy at all. A fifth check wires the policy result straight into task 3.6's
|
||
`sk_hermes_channel_respond()` end to end: a denied policy really produces a NACK and no
|
||
channel, with the ledger audit and `stadium_conserved()` holding before and after —
|
||
exactly task 3.6's own deny-path shape, reused rather than re-derived.
|
||
|
||
**Not yet done, by scope**: the actual call site inside `sk_hermes_channel_respond()`'s
|
||
caller still takes a plain `approved` bool (task 3.6's own signature, unchanged, as
|
||
promised) — this task builds and proves the query function itself; wiring it as the
|
||
*only* source of that bool at a real call site is deferred to whichever later task first
|
||
needs a live (not self-test-only) channel-open decision, consistent with every other
|
||
Phase 3 task before Stage C being self-test-only. `dict_hash` for Hermes/Hestia unmoved
|
||
from task 3.6's values, as expected (neither VM loads `ACL.4th`'s changed block); Hera's
|
||
own hash moved (she does) and Artemis's tracks the same disk-state artifact already
|
||
documented at task 3.1.
|
||
- [x] **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).
|
||
2026-09-22 · Final acceptance: `logs/20260922-105501/amd64/`,
|
||
`logs/20260922-105758/aarch64/`, `logs/20260922-110304/riscv64/` — `disk/artemis.img` and
|
||
`disk/thumbdrives/zuse-thumb-ident.img` reset (`git checkout --`) before each of the three,
|
||
per task 0.0's own finding, so all three are directly comparable, no shared-disk confound.
|
||
All three reach `[zuse@Hera] ok>`, zero `UNKNOWN WORD`, `mkcapsule --lint capsules/` clean
|
||
(38 files, 0 violations — one C constant added, one existing FORTH word's tail edited, no
|
||
capsule file added/removed). **`dict_hash` identical across all three architectures for
|
||
every VM**, including Artemis: `0xa8999a854545f274` / `0xb8052b99f1d75f59` /
|
||
`0xdf59e557986c9009` / `0xecad3d867c9bfec2`.
|
||
|
||
**What actually cut over.** Only the reply leg (Artemis → Hera ack) was real FORTH
|
||
messaging traffic to begin with — confirmed by reading, not assumed: the request leg
|
||
(`repl.c`'s own `"<dev-addr> HERA-BLK-ATTACH-REQ" "Artemis" VM-EXEC`) was already a direct
|
||
VM-EXEC with no type tag, forced by Hera's own pre-existing inability to load
|
||
`common:messaging.4th` (its own header comment says why). So this task's real scope was:
|
||
`capsules/artemis/init.4th`'s `HERA-BLK-ATTACH-REQ` (block 4858) no longer ends in
|
||
`BLK-ATTACH-EVENT 2 0 ATTACH-ACK-BUF ATTACH-ACK-LEN @ 0 MSG-SEND` (FORTH's arena/MSG-TICK);
|
||
it now ends in `ATTACH-ACK-BUF ATTACH-ACK-LEN @ KH-BLK-ATTACH-SEND DROP`, a new C word
|
||
(`repl.c`) wrapping `sk_hermes_send_one()` (task 3.6). Delivery is task 3.4's already-wired
|
||
`sk_hermes_drain_checkpoint()` calling `vm_interpret()` on Hera with the identical payload
|
||
text — `BLK-ATTACH-ACK` (`repl.c`, unchanged) fires exactly as before, just reached by a
|
||
different layer. `SK_HERMES_MSG_TYPE_BLK_ATTACH` (`kernel_hermes.h`) deliberately reuses
|
||
`BLK-ATTACH-EVENT`'s own value (9), not a fresh kernel-Hermes-space number — §XXXIV.2's
|
||
partition rule is about which layer owns a message, not about disjoint numbering, and
|
||
keeping the value documents this as a cutover of the same logical message.
|
||
|
||
**Evidence for the check, and its honest limit.** `sk_word_blk_attach_ack()` — the real
|
||
drain target, since `BLK-ATTACH-ACK` is exactly the word the drained payload calls — prints
|
||
the kernel-Hermes ledger and `stadium_conserved(Artemis)` right there
|
||
(`Kernel-Hermes BLK-ATTACH-EVENT (real, Stage C): ledger held=2048 pulled=241664
|
||
returned=238879 consumed=737 stadium_conserved(Artemis)=true`, identical on all three
|
||
architectures). This is evidence for the **mid-hold instant** — before
|
||
`sk_hermes_drain_checkpoint()`'s own `release()`/`pop()` run, which happen after this
|
||
handler returns, in the caller — not literally "after the full cycle." That's a real
|
||
instant to check, not a weaker substitute: task 2.7 established the four-term
|
||
`stadium_conserved()` form holds at every instant, mid-hold included, so this is genuine
|
||
evidence, just not evidence for the post-release state specifically. Recorded honestly per
|
||
`advisor()`'s review rather than overclaiming "before/after."
|
||
|
||
**Real finding: kernel-Hermes's first live (not self-test) exercise hung, twice, before
|
||
the actual bug was found — see the findings log below for the full account
|
||
(`vm_ptr()` translation, and a separate, still-unexplained first hang).** Both are recorded
|
||
there, not restated here.
|
||
|
||
**Logging note (`advisor()` caught this before commit):** the evidence line went through
|
||
three revisions. First cut used `console_println` twice (send-side pre-send + ack-side
|
||
after) — flagged as unconditional production output on every real USB attach, forever,
|
||
exactly the pattern memory `project_production_logging_cleanup_needed` already names.
|
||
Tried `log_message(LOG_DEBUG, ...)`, confirmed invisible in the actual serial-log capture;
|
||
tried `log_message(LOG_INFO, ...)`, **also invisible** — confirmed live that `repl.c`'s own
|
||
pre-existing `log_message()` calls, at every level, do not appear anywhere in a real boot's
|
||
serial log in this build (`log_message()`'s `fprintf(stderr, ...)` is not wired to the
|
||
serial console here — a build characteristic, not something this task introduced or fixed).
|
||
Settled on a single `console_println` (the ack-side one; dropped the send-side line
|
||
entirely) — this fires once per real USB attach, not per word, so it is not the
|
||
hot-path-spam case that memory warns about, and it is the only channel that is actually
|
||
visible in the acceptance logs this document's own standing rules require as evidence.
|
||
|
||
**Superseded intermediate logs, kept per the never-delete convention:**
|
||
`logs/20260922-102354/amd64/`, `-102948/aarch64/`, `-103458/riscv64/` were the first
|
||
passing three-ISA triple, still carrying the two-`console_println` (pre-send + after)
|
||
design; `logs/20260922-104229/amd64/` and `-104902/amd64/` were the `LOG_DEBUG` and
|
||
`LOG_INFO` logging-channel iterations described above, both confirming `log_message()`'s
|
||
invisibility rather than producing new acceptance evidence. All superseded by
|
||
`105501`/`105758`/`110304`, cited above, once the disk-reset-before-each-arch procedure and
|
||
the final single-line evidence design were both settled.
|
||
- [ ] **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.
|
||
|
||
**2026-09-22, task 3.8 — a FORTH `CREATE` buffer address passed to a new C word must go through
|
||
`vm_ptr()`, or it silently reads all-zero memory: no crash, no error, just a message that arrives
|
||
and does nothing.** `sk_word_kh_blk_attach_send()`'s first cut popped `paddr` (Artemis's own
|
||
`ATTACH-ACK-BUF` address) and raw-cast it directly to a host `const char *`
|
||
(`(const char *)(uintptr_t)paddr`) — `.claude/CLAUDE.md`'s own "Important Conventions" already
|
||
states stack values are VM-relative offsets (`vaddr_t`), not C pointers, and
|
||
`mama_forth_words.c`'s own VM-EXEC/VM-CALL words all translate a popped address the same way
|
||
(`vm_ptr(vm, (vaddr_t)caddr)`) before touching it — this word didn't, and the mistake compiled
|
||
clean, linked clean, and booted clean. Diagnosed with a temporary probe (reverted after capture,
|
||
per memory `feedback_revert_probes_after_capture`): `logs/20260922-101153/amd64/` and
|
||
`logs/20260922-101937/amd64/` (a second, redundant run of the same debug build, kept per the
|
||
never-delete convention rather than for any independent evidence) both show `DBG send: plen=25
|
||
n=25 buf=[]` — correct length, empty content — followed by `DBG drain-enter #2 payload=` (empty)
|
||
and a clean `DBG drain-return`.
|
||
`vm_interpret()` on an empty string is a no-op: no error, no `UNKNOWN WORD`, `BLK-ATTACH-ACK`
|
||
simply never ran, and the pending USB attach was silently never confirmed. Fixed by reading
|
||
`src` via `vm_ptr(vm, (vaddr_t)paddr)` instead of a raw cast (`repl.c`'s own comment on the fix
|
||
cites the precedent directly). This is exactly `FABRIC-3.5.md` §XXXV.0's named failure signature
|
||
("things here fail without saying anything") — carrying the rule forward explicitly for whoever
|
||
next writes a FORTH-facing C word: **any popped stack value that names a FORTH-visible address
|
||
goes through `vm_ptr()`/`VM_ADDR()`, never a raw pointer cast, unless the value was itself
|
||
explicitly formatted as a raw host pointer by the pushing code** (as `repl.c`'s own existing
|
||
`dev-addr` already legitimately is, built via `%llu` from a real `uintptr_t` at the VM-EXEC call
|
||
site — the exception that makes the rule easy to misapply by analogy).
|
||
|
||
**2026-09-22, task 3.8 — a separate, real hang, root cause not found; recorded as a genuine open
|
||
item, not folded into the `vm_ptr()` finding above.** The very first attempt to exercise the real
|
||
send/drain path (`logs/20260922-092154/amd64/`, before the `vm_ptr()` bug above was even
|
||
suspected) froze solid — serial log stopped growing entirely, QEMU pinned near 100% CPU, no
|
||
further output ever, for over 40 minutes of wall time before being killed by hand. This happened
|
||
*before* the empty-payload bug was diagnosed or fixed, so it is tempting to assume the same root
|
||
cause — but the empty-payload runs (`101153`/`101937`) did **not** hang; they drained cleanly
|
||
(if uselessly) and reached `ok>` on schedule. Something else froze that first boot, at
|
||
approximately the same point in the boot sequence, and it was never reproduced again across
|
||
every subsequent run this session (with or without the `vm_ptr()` fix). Per §XXXV.0's own
|
||
standing warning, an unexplained freeze in a brand-new, live-for-the-first-time nested-drain code
|
||
path (task 3.4's `sk_hermes_drain_checkpoint()` calling `vm_interpret()` on a VM that is itself
|
||
mid-dispatch, exercised live for the first time by this exact task) is not something to write off
|
||
as a fluke. Reported, not chased further here — `logs/20260922-092154/` is kept as the sole
|
||
evidence.
|
||
|
||
---
|
||
|
||
## 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).
|