FABRIC.md §26: rule the physical heartbeat must be adaptive, not fixed-rate

Loop #7 (vm_runtime.c:703-752) already computes an execution-derived adaptive
period and it is orphaned -- nothing in the kernel build ever reads
tick_target_ns. Traces the pthread-based consumer it was written for
(deliberately disabled for kernel builds, no pthread on bare metal) and
confirms via the §18.5 argument that consuming it via the physical re-arm
period does not reopen GAP-A1, since the decision to change the period stays
execution-derived and the virtual tick still owns all patron state.

Rules the scale mismatch (10us hosted base vs 10ms/100Hz kernel base) in
favor of reusing Loop #7's decision logic against the kernel-appropriate
base, and the mechanism (single-writer/single-reader shared state, no
thread/scheduler) against §21.1's existing free-locking finding.

Amends item 0.8 to include this -- no new punch-list item, this is inside
what 0.8 already builds.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Robert Allan James
2026-08-03 23:04:09 -04:00
co-authored by Claude Sonnet 5
parent 43aa0a2a36
commit da4cb14702
+135 -3
View File
@@ -2117,15 +2117,28 @@ on until there is a tick on all three architectures (§16.1, §16.5).*
each tick.
*Done when:* `heartbeat_ticks()` advances on aarch64 at the configured rate.
- [ ] **0.8 — Converge the three architectures on one tick path.**
- [ ] **0.8 — Converge the three architectures on one tick path, and make the physical
heartbeat adaptive.**
One `heartbeat_tick()` call site per architecture; the ISR does counter, timestamp and
flag only. **Per the GAP-A1 ruling, the hardware tick drives instrumentation only:** the
bottom half services TIME-TRUST bookkeeping, and the engine (`vm_tick()`, decay,
inference) stays on the virtual tick — execution-paced, exactly as today. Nothing that
feeds patron state reads the hardware counter.
**Per §26 (ruled):** the physical re-arm period is no longer a fixed 100 Hz constant.
`heartbeat.c` owns the current adaptive period (`heartbeat_set_adaptive_period_ns()` /
`heartbeat_next_period_ns()`); Loop #7's existing site in `vm_runtime.c` calls the setter
with its stable/volatile-derived value, rescaled to the 10 ms kernel base per §26.3 (not
the 10 µs hosted `HEARTBEAT_TICK_NS`); each architecture's re-arm function reads the
getter and converts to its own raw counter units instead of using a hardcoded period. No
new concurrency primitive — single writer (mainline), single reader (ISR), same shape
§21.1 already found free on one hart.
*Done when:* all three architectures drive the same TIME-TRUST bottom half; no loop math
runs in interrupt context; and `vm_tick()`'s call sites are unchanged.
*Refs:* §16.4 (as ruled), §18.4, §21.2.
runs in interrupt context; `vm_tick()`'s call sites are unchanged; **and** the re-arm
period measurably varies with Loop #7's stable/volatile signal in a live QEMU run (not
merely computed and discarded) — verified the same way item 0.6's IRQ-storm question was
verified: direct measurement, not assumption.
*Refs:* §16.4 (as ruled), §18.4, §21.2, §26.
- [ ] **0.9 — Write the concurrency constraint at the mutex stub.**
Add a comment at `src/starkernel/vm/host/shim.c:415` stating that the no-op is correct
@@ -2451,3 +2464,122 @@ pin-and-panic, and §25.0's rules themselves. None of them moved.
means. B1/B2 are ten-minute reads. C and D are mechanical once A1 is ruled. Nothing should
go to a coding model before C1, C2 and C3 are fixed at minimum — those are the ones it
will hit in its first hour.
---
## 26. The hardware heartbeat must itself be adaptive — RULED 2026-08-03
Raised mid-0.8: the punch list, as written, makes the hardware tick a fixed-rate
instrument (100 Hz, unconditionally, on all three architectures — the `apic_timer_init(...,
100)` calls built across items 0.10.7). That is correct for what §18.4/§18.5 require of
the *engine* — the virtual tick stays execution-paced regardless. But it leaves the
*physical* heartbeat monotonic, and the physical heartbeat was never supposed to be
monotonic. Captain Bob: *"the heartbeat is adaptive... it spreads the heartbeat out when
your heart goes faster when you're running... it's gotta be an adaptive heartbeat, that's
the whole thing to cage variance."*
### 26.1 The finding — an adaptive-rate engine already exists, and it is orphaned
`vm_runtime.c:703-752` ("Loop #7 — Adaptive Heartrate") computes a bounded adaptive period,
`vm->heartbeat.tick_target_ns`, from the same ANOVA-driven stability signal Loop #5 already
uses: early-exit (stable) slows it down, full-inference (volatile) speeds it up, ±25% per
step, clamped to `[¼×, 4×]` of a configured base. This is real, executing code, faithful to
the design in the sibling StarForth repo's own
`docs/working/architecture/03-architecture/heartbeat-system/architecture.md` (Option A vs.
Option B). It is not a proposal — it already runs, on every `vm_tick()`.
It is orphaned. `tick_target_ns` is written to `vm->heartbeat.worker->tick_ns`
(`include/vm.h:286,289`), and `worker` is always `NULL` in kernel builds — nothing in
`src/starkernel/` reads `tick_target_ns` at all. The mechanism that was meant to *consume*
it, a `pthread_create()`-based background worker (`heartbeat_thread_main()`, vendored into
`vm_bootstrap.c:310`), is deliberately and redundantly disabled for kernel builds
(`Makefile.starkernel:293,338`, `HEARTBEAT_THREAD_ENABLED` forced to `0` twice) — correctly:
this is a bare-metal single-hart kernel, there is no pthread implementation, and the
mechanism's own history (`segfault-analysis.md` in the StarForth repo) is a real
concurrent-access bug against `RollingWindowOfTruth`, fixed by a mutex a single hart doesn't
need and can't cheaply provide (§21.2, item 0.9).
So: the *decision logic* is real, tested by inheritance, and currently produces a number
nothing downstream ever reads. Phase 0 as written would ship a heartbeat that looks adaptive
in the source tree and is not adaptive on the wire.
### 26.2 This does not reopen GAP-A1
§18.5 already proved the general shape of this argument for Loop #5 and is directly
reusable for Loop #7: **adaptation is safe exactly when its inputs are execution-derived**,
because then the *decision* to change the period is itself a deterministic function of the
execution stream, not of wall-clock jitter. §18.5 point 3 already establishes that every
`InferenceInputs` field feeding this ANOVA machinery — rolling window, trajectory length,
prefetch hit rate, hot/stale word counts, total heat, word count — is execution-derived with
zero timing input. Loop #7's stable/volatile classification is downstream of that same
machinery. Nothing new needs proving there.
What must not change, and does not under this design:
- The **virtual tick stays the engine's clock** (§18.4, unchanged). Adjusting the physical
re-arm period changes *when TIME-TRUST samples land and how often the idle path wakes*
it does not move decay, reap, or inference off the virtual tick onto the hardware one.
- The hardware tick's **output** still feeds nothing that reaches the parity hash (§18.5
points 1, 5, unchanged) — only its *input* (the period it's told to re-arm at) becomes
execution-derived instead of fixed.
- What was already true and already inert under §18.5 — that wall-clock interrupt *arrival*
timing is not reproducible run to run — stays true and stays inert. Nothing patron-facing
ever depended on it; this design doesn't change that.
### 26.3 The scale mismatch, and the ruling
`tick_target_ns`'s configured base, `HEARTBEAT_TICK_NS` (`include/starforth_config.h:71`),
is `10000ULL` — 10 microseconds. Its live range under Loop #7's ±25%/`[¼×,4×]` bounds is
therefore 2.5µs40µs. The hardware timer configured throughout items 0.10.7 runs at 100 Hz
— 10 milliseconds. That is a three-orders-of-magnitude mismatch: reprogramming the physical
re-arm to the literal `tick_target_ns` value would fire the timer 25,000400,000 times a
second, which on a bare-metal single-hart kernel means the core spends effectively all its
time in trap entry/exit and the REPL is never reached. `HEARTBEAT_TICK_NS` was tuned for a
hosted OS thread's sleep granularity, not a bare-metal ISR period.
**RULED (Captain Bob, 2026-08-03):** same relationship, kernel-appropriate scale. The
mechanism must be real and load-bearing — a genuine, measurable effect on the physical
re-arm period, the same kind of effect the original hosted pthread experiments showed — but
computed against the 10 ms / 100 Hz base already established for this kernel, not the 10 µs
hosted base. Loop #7's *decision logic* (stable → slower, volatile → faster, ±25% per step,
clamped `[¼×, 4×]`) is reused unmodified; only the base it is applied to changes.
### 26.4 The mechanism — no thread needed
Captain Bob authorized building a kernel-native thread/task if one were required
("if we gotta run a thread or whatever, it doesn't matter"). One is not required, and adding
one would need a preemptive scheduler this single-hart kernel does not have (§21.2 already
rules out real locking for exactly this reason). The existing shape gets there without new
infrastructure:
- `vm_tick()` already calls Loop #7 on the **mainline path** (execution-paced, never in
interrupt context) and already produces a fresh `tick_target_ns` there.
- Item 0.8 already introduces a shared `heartbeat.c` owning the top/bottom-half split. That
file is the natural owner of one new piece of state: the *current adaptive period*, set by
a new `heartbeat_set_adaptive_period_ns(uint64_t ns)` (called from `vm_runtime.c`'s Loop #7
site, scaled to the kernel base per §26.3) and read by a new `heartbeat_next_period_ns(void)`.
- Each architecture's existing re-arm function (`apic_timer_rearm()`, `riscv64_timer_rearm()`,
the aarch64 equivalent) already runs in interrupt context at the top of every tick (§18.4's
"one tick" call sites, unchanged). It converts `heartbeat_next_period_ns()` to that
architecture's raw counter units — a conversion each already does today for its fixed
period — instead of using a hardcoded constant.
No new concurrency: the write happens on the mainline execution path, the read happens in
interrupt context, and the value read is whatever was last written — the same single-writer/
single-reader shape every other piece of ISR-read, mainline-written state in this kernel
already has (§21.1's finding that locking here is already free, because nothing here is
actually concurrent on one hart). No pthread, no kernel task, no scheduler.
### 26.5 Open, deferred
- Multi-VM: today Hera is the only VM, so "whose `tick_target_ns` drives the one physical
timer" has one answer. Not resolved for when Hermes/Artemis return — deferred, not
applicable yet (consistent with §20.5's other Tripod-return deferrals).
- `HEARTBEAT_TICK_NS`'s name and its hosted-scale value are unchanged by this ruling — the
kernel-side base (10 ms) is a **separate** constant, not a redefinition of the hosted one.
Naming the kernel constant is an implementation detail of the item that builds this, not a
document-level open question.
**RULED.** Item 0.8 is amended below to include this; no new punch-list item is needed —
this is squarely inside what 0.8 already builds (`heartbeat.c`, the three re-arm call
sites).