From 29447abb7d4bc6f632f9c54fee9b796eff563bd6 Mon Sep 17 00:00:00 2001 From: Robert Allan James Date: Wed, 23 Sep 2026 16:06:59 -0400 Subject: [PATCH] =?UTF-8?q?FABRIC-3.8=20=C2=A73.13:=20Stage=20C=20design?= =?UTF-8?q?=20=E2=80=94=20G9=20(self-driven=20drains=20move=20no=20heat),?= =?UTF-8?q?=20per-beat=20charge=20with=20carry,=20decision,=20idle=20point?= =?UTF-8?q?;=20C=20split=20into=20C-a/C-b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- FABRIC-3.8.md | 314 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 2 deletions(-) diff --git a/FABRIC-3.8.md b/FABRIC-3.8.md index 6da060c0..98713a74 100644 --- a/FABRIC-3.8.md +++ b/FABRIC-3.8.md @@ -382,6 +382,298 @@ Hera resuming while Hestia is still parked mid-payload. **Baseline first:** the the pre-frame code must *fail* on that third entry (reading Hestia), proving the test detects the defect before the fix is claimed. +### 3.13 Stage C design: heat under self-driven execution, and the switch decision (FOR REVIEW, 2026-09-23) + +§3.11 ruled *coolest VM with pending work goes next, no scheduler*. Before designing the +comparison, I traced (read-only) what that heat actually does once a VM runs its own mail on its +own stack. **The finding blocks Stage C as written.** Everything below is traced unless marked as +a proposal. + +#### 3.13.1 What the trace found + +**G9: when a VM runs its own mail, VM-physics heat doesn't move.** + +- `vm_physics_touch()` (`capsule_vm_physics.c:284`) is the only thing that moves + `execution_heat_q48` between live VMs (apart from `retire`, which moves heat to the root on + kill). Its only callers are Hera's three orchestration verbs: `VM-STEP`, `VM-EXEC` and + `VM-CALL` (`mama_forth_words.c:723/811/946`). Each one touches the *target* at dispatch. +- `sk_hermes_drain_checkpoint()` and the trampoline (`switch.c`) call nothing in physics. A + VM that runs a kernel-Hermes payload on its own stack, which is the whole point of Stages + C–H, leaves every heat value exactly where it was. +- Starting state (`vm_physics_init:188`): Hera holds all of `Q48_ONE` (0x10000 raw) and every + other VM starts at 0. +- **Consequence:** once the DoE is rebuilt on `SEND` (§3.9), nothing ever calls `VM-EXEC`. Every + worker stays at 0 and Hera stays at `Q48_ONE` forever. "Coolest with work" becomes a permanent + tie among all workers, and whatever breaks that tie is the scheduler in all but name. That + violates *"no scheduler allowed."* + +**The transfer law barely moves heat at switch granularity** (`vm_physics_touch`, lines +301–343): + +- The formula is `amount = elapsed_beats × slope >> 16`, with slope seeded at `65536/3` = 21845 + raw. +- `elapsed_beats` counts fleet beats since *this VM's own last touch*. Every touch resets it + (`last_active_tick = now`), and a VM's first-ever touch moves nothing (`touched == 0`). +- Worked out: elapsed 1, 2 or 3 moves **0**. 3 × 21845 = 65535, and 65535 >> 16 = 0. Elapsed 4 + moves 1 raw unit (1/65536 of the fleet). Elapsed 100 moves 33 raw units, about 0.05%. +- The truncated remainder is thrown away, not carried. So touching often moves *nothing*, which + rules out the obvious "touch the running VM at each of its beats": elapsed would always be 1. +- The seed's own comment (lines 138–144) already says the slope is **"UNVALIDATED after the + ns-to-tick restatement… a real re-tune is DoE work (item 5.1)."** + +**What a beat is, and that every VM produces them** (checked because Bob ruled out the timer): + +- The fleet beat (`fleet_heartbeat_tick_count`) advances once per `vm_heartbeat_run_cycle()` → + `vm_tick()` → `vm_physics_heartbeat_tick()`. +- That runs every `HEARTBEAT_CHECK_FREQUENCY` (256, Kconfig) words of **a VM's own execution**. + The per-VM `check_counter` is bumped at both `vm_core.c:909` (each colon word) and `:1286` + (each word parsed by `vm_interpret`). +- `heartbeat_enabled = 1` and `worker = NULL` are set for every VM in `vm_init` + (`vm_bootstrap.c:305/308`). The pthread worker path is hosted-only. +- **So the beat is execution-paced, produced by whichever VM is running, and not the timer.** + The *timer* ISR is a different function, `heartbeat_tick()` in `heartbeat.c`. Today it calls + `sk_vm_switch_signal_tick()` (`heartbeat.c:179`), which is the readiness path Bob ruled out. + In this doc, "beat" always means the execution-paced one. +- Each VM also keeps its own beat count, `vm->heartbeat.tick_count` (`vm_runtime.c:118`), so + the beats a VM produced during one turn can be read directly. + +**Stadium heat doesn't answer "whose turn" either.** + +- A message's Stadium heat is pulled from the **sender's** reservoir at `sk_hermes_alloc()`, and + the cell's owner is the sender. On release it goes back to the **sender** (`stadium_evict()`). +- The recipient's Stadium state never changes when it receives or drains. +- `sk_hermes_decay()` is only called from the boot self-tests (`kernel_main.c:969/1006/1033`). +- So Stadium heat measures a VM's *outstanding sent mail*. It says nothing about how much CPU a + recipient has had. It's listed for completeness, not proposed. + +**Stage C reverses a recorded decision.** `capsule_vm_switch_signal.h:28–34` deliberately did +*not* use VM-physics heat for switching, on timescale grounds: word-level dispatch over millions +of executions versus instant-by-instant readiness. Bob's §3.11 ruling overrides it. The +truncation above is that timescale gap in concrete form, and §3.13.2 is how it would be closed. + +**The existing law measures age, not work** (derived from the code, worth stating because it +changes what "coolest" means): + +- A touch pulls in proportion to beats *since the last touch*. +- Over a VM's life, the sum of its pulls is its age in beats × slope, however often or rarely it + actually ran. How often it ran only changes *when* it gets charged. +- It loses heat when others are touched, in proportion to its share. +- So under the current law, "coolest" ranks VMs by charge timing and share, not by CPU used. + +#### 3.13.2 Proposal: charge a VM for the beats it produced (ruling C-1) + +**Proposed: charge the running VM once at each of its own beats, keeping a carry.** + +- At every beat a VM produces while it is the switch-current VM, it is charged one beat's worth: + `slope` raw units, plus the carry left over from its previous beats. +- The whole units are pulled from the rest of the live fleet in proportion to each VM's heat, + through the existing conservative `vm_physics_transfer()`. Nothing is created, and fleet K is + still conserved. +- The fraction below one unit stays in a per-VM carry. That carry is what makes a per-beat charge + work where a per-beat *touch* can't (see row (d)). +- It would be one new entry point in `capsule_vm_physics.c`, e.g. `vm_physics_charge(vm_id)`, + called from the beat path (`vm_physics_heartbeat_tick()`'s caller) for the running VM. +- It is kernel-side and invisible to anything a VM executes. +- Result: **the VM that has done the most work is the hottest,** and its heat rises *while it + runs*. That matches the plain meaning of "coolest VM with work goes next." + +**Why per beat and not at switch-out (corrected before review).** An earlier draft of this +section charged only at switch-out. That design is **purely cooperative**, which contradicts +§3.11: + +- During a turn nothing would change. The running VM's stored heat stays where it was at + switch-in, and the waiting VMs are untouched. +- So every decision taken during the turn repeats the answer from switch-in. The running VM is + never displaced, and a long payload keeps the CPU until it finishes. +- Charging per beat is what makes the running VM's own execution displace it, which is the + preemptive effect. + +**Worked example.** Two workers start at 0 with equal work. + +- The running worker gains 21845/65536 of a unit per beat, so a whole unit moves every 3 beats. +- At that point the other worker is strictly cooler and takes over at the next beat decision. +- They alternate every ~3–6 beats. The turn length comes out of the physics; nobody chose it. + +| Option | Moves heat under self-driven execution? | Preemptive in effect? | Side effects | +|---|---|---|---| +| **(a) Charge at each of the running VM's own beats, with carry** (proposed) | Yes, in proportion to work | **Yes**, because heat rises during the turn | New entry point. Proposed: records **no** `fleet_window` samples (see "Frozen slope" below). | +| (a′) Charge at switch-out by `tick_count` delta | Yes, in proportion to work | **No**, unless decisions compare *projected* heat: stored heat plus the charge for the beats produced so far this turn. With projection it is equivalent to (a), but without any mid-turn transfer. | Same as (a). | +| (b) `vm_physics_touch()` on switch-in (the `VM-EXEC` analogue) | Yes, but it charges *time away*, not work (see "age, not work"), and needs ≥4 beats away before anything moves | No | Writes `fleet_window` samples, which changes slope adaptation and comparability with existing DoE heat columns. | +| (d) `vm_physics_touch()` at each beat | **No.** Elapsed is always 1 and the remainder is discarded, so 0 moves, forever | — | Rejected on the arithmetic. Row (a) differs only by keeping the carry and not resetting the elapsed count. | + +**Truncation (ruling C-2).** With the carry, nothing is lost to rounding under (a). The other +choices: + +- (i) no carry: then (a) moves nothing, the same as (d); +- (iii) refit the slope at beat scale. That is DoE item 5.1 and **out of scope** here. + +The carry applies to the new charge only. `vm_physics_touch()` is untouched, so `VM-EXEC` +transfers stay byte-for-byte as today. + +**Frozen slope (part of C-1).** + +- With no samples recorded by the charge, and no `VM-EXEC` in the rebuilt DoE, the adaptive + `fleet_transfer_slope_q48` **never updates under self-driven load**. It stays at the + unvalidated seed. +- Recording samples would let it adapt. It would also change what the existing heat columns mean + compared with past campaigns. +- Bob should choose this knowingly. + +**Two laws on one ledger.** Artemis, and any participant that Hera also `VM-EXEC`s, is both +*touched* (the dispatch law) and *charged* (the work law). Both go through `vm_physics_transfer()` +on the same heat, so conservation holds, but the VM's heat then mixes two meanings. Proposed: +accept this and document it. `VM-EXEC` into a participant becomes rare once §3.8's `SEND` exists. + +**Where the charge starts.** Each VM's charge baseline is its beats at the moment it becomes +switch-current. Hera's is seeded at `sk_vm_switch_set_current()`, so her first charge doesn't +cover every beat since boot. The clamp to `others_total` would make that harmless anyway, but it +should be defined rather than left to the clamp. + +**What a beat counts.** A beat is 256 of *either* colon-word entries or words parsed at top level +by `vm_interpret` (the two `check_counter` sites). Primitives executed inside colon bodies are +**not** counted. So the "words" figures below are beat counts × 256 in that sense, not total +primitive executions. + +**Hera's reservoir mass (ruling C-4, open, arithmetic shown).** + +- Hera starts with all of `Q48_ONE` as the fleet's structural root (`vm_physics_init`'s "cold mass + added to a closed system"). +- At slope 1/3 raw per beat, the workers together have to produce about `65536 × 3 × (N/(N+1))` + beats before Hera cools to a worker's level. For N = 8 that is ≈175k beats, ≈45M + colon-entries/top-level words (see "What a beat counts" above). +- Until then, Hera is never the coolest while any worker has work, **so the console gets no turn + under load.** This doesn't block Stage C's relay evidence, because the relay runs while the + workers are idle. It does block Stage F/H usability. +- Options for Bob: + - (α) accept it; + - (β) compare the heat each VM has been *charged* (its turn heat) rather than its absolute + heat, so the root's structural mass doesn't count against it; + - (γ) something else Bob rules. +- I have no recommendation yet. (β) is a different physical quantity, and Bob should choose it + knowingly. + +#### 3.13.3 Proposal: the decision (ruling C-3 on ties) + +**Eligible** means the VM is a registered participant, is not the one running, is not dead or +stillborn, and **either** of the following holds: + +- its kernel-Hermes pending count is > 0, read directly from the queue + (`sk_hermes_pending_count`), not from a `has_work` flag. The queue *is* the eligibility, so + `SWITCH-MARK-WORK` and G1's missing mark become unnecessary; +- it is switched out, its **frame stack is non-empty, and its top frame is not + `VM_FRAME_IDLE`**. That means it was displaced mid-task, or it is Hera with her REPL chain live. + - Without this clause, a VM preempted in the middle of a payload never gets picked again. This + is what the frame stacks were for. + - A never-entered VM has an empty stack, so it is not eligible unless it has mail. + - Hera parked at an idle point pushes an IDLE frame (§3.12, and §3.13.5 below), so she is not + eligible there unless she has mail or input. + +**Switch when** some eligible VM is **strictly** cooler than the one running. If it is equal, the +running VM stays: a tie with the incumbent is "no reason to move," not a coin toss. + +**Ties among several equally cool candidates (ruling C-3).** + +- **Proposed:** the candidate that has been *waiting longest*, measured by one single-writer + event count. + - Each message is stamped with it when it is admitted (a new `SkHermesMessage` field). + - Each VM is stamped with it when it is displaced mid-task. + - A candidate's wait stamp is the minimum of its oldest pending mail and its displacement. +- Every stamp is unique, so the result is unique. +- This is the arrival order of the mail itself, the kernel's FIFO. It is a fact about the + messages, not a ranking of VMs. +- **Bob to rule** whether arrival order counts as "an ordering" under *"no scheduler allowed."* +- Rejected outright: slot index, UUID and registry order. Each of those is a fixed order of VMs. + +**Testable "not a scheduler" criterion.** + +- The decision is a pure function of physical state: heat, pending mail, frames and wait stamps. + It keeps no queue of its own and remembers nothing between decisions. +- It must return the same VM whichever order the participant table is walked. +- This is Stage C-b's evidence (below). + +**When the decision is made** (only at moments the physics exposes, never on a clock): + +1. **The running VM's own beat.** The beat sets a per-VM "decide" flag. The next *owned* + checkpoint consumes it (`sk_vm_owns_outermost_frame`). There are two checkpoint sites: + - the existing one in `execute_colon_word`; + - the between-words point in `vm_interpret_kind`'s loop, which needs a new gated checkpoint + so that a payload made only of primitives can be displaced. +2. **Mail arrival** to a VM that would then win. +3. **The trampoline's between-message point**, as already noted in `sk_vm_switch_entry()`. +4. **Idle points** (Stage F). + +**Why not every word.** Under (a), heat changes only at beats, and eligibility changes only when +mail arrives or a queue empties. Between those events every decision would repeat the last one, +so deciding every word is pure overhead. Deciding at the beat means a turn lasts at least one beat +unless the VM runs out of work first. + +**Flagged honestly:** that minimum is the existing Loop #7 cadence constant +(`HEARTBEAT_CHECK_FREQUENCY`). It is not new, but it does act as a floor on turn length. + +**The switch target on an empty queue.** + +- Today the trampoline yields to whoever switched in (`native_switch_back_to`). +- **Proposed:** it asks the decision instead, and falls back to `native_switch_back_to` only when + no VM is eligible. + +#### 3.13.4 What removing the timer path changes + +- `heartbeat.c:179`'s `sk_vm_switch_signal_tick()` call is removed. The ISR returns to its + pre-§XXVIII minimal shape, so the timer no longer decides anything. +- These are removed: + - the readiness accrual; + - `SK_SWITCH_READINESS_THRESHOLD` (50); + - `has_work`; + - `take_pending`. +- `note_switch_performed` is kept only as the counter. +- **Effect on the DoE CSV** (`doe_log.c:205–230`): + - `switch_count` and `switch_current_slot` survive. + - `switch_ticks_since` loses its meaning. Proposed: it becomes beats since the last switch. + - The three `*_readiness` columns die. + - Found while tracing, *not fixed*: those columns and the heat columns still look up + `"Hermes"`, which no longer exists as a VM since the reshuffle, so those values are always 0 + today. + +#### 3.13.5 The relay needs the idle point in the same stage (traced) + +Stage C-b's own evidence, the WIREBIND relay delivered by a real switch, **can't be reached +without §3.7's idle point** (Stage F): + +- The console line is forwarded from C. `sk_hermes_send_one(vm->stadium_vm_id, paired.vm_id, + CONSOLE_CMD, …)` at `repl.c:1510`, then `goto out`. Hera interprets nothing and goes straight + back to polling. +- Today the `~user` VM's mail is delivered by the idle pump (`repl.c:~909–928`), which drains + every live VM *on Hera's stack*. That is G3. +- Once the pump skips participants (C-b), the only places a switch can happen are owned word + checkpoints and beats. Hera at her poll loop reaches neither: she is running C, not words. +- So nothing switches, the `~user` VM's `CONSOLE-CMD-EVENT` sits in its queue, and the relay + breaks. + +**Proposed:** fold §3.7's idle point into C-b. + +- The pump loop's own location (the idle point, Hera between lines) becomes a decision point. + Hera pushes an IDLE frame, consults the decision, and switches to the coolest eligible VM + instead of draining participants on her stack. The pump then shrinks to Hera's own queue. +- Stage F then only has to show that work proceeds with the console idle. +- The alternative is to keep the pump draining participants until F. That keeps G3's + borrowed-stack delivery alive through C-b, so C-b's relay evidence would prove nothing about + switching. + +**Hera's eligibility at the idle point.** Keyboard or serial input pending is Hera's mail. At +the idle point she is eligible when input is pending (the REPL's own input poll), exactly as a +worker is eligible when its kernel-Hermes queue is non-empty. **Ruling C-5**, below. + +#### 3.13.6 Stage C, split + +This splits the single C row in §4 in two: + +- **C-a** is physics only: heat starts moving and nothing switches yet. +- **C-b** is the decision: pump skip, eligibility read from the queue, the ISR path removed, and + the idle point (§3.13.5). + +C-a is observable on its own, and doing it first proves the heat signal is alive before anything +depends on it. The §4 table below is updated to show this split, **pending rulings C-1 to C-4.** + --- ## 4. Staged plan @@ -395,10 +687,12 @@ healthy idle produce the same serial log (`FABRIC-3.md` §XXVIII.1). | **A** | §3.1 per-VM ownership gate (+ `MODE_COMPILE` gate). Nothing else. | Byte-identical behavior: `dict_hash` triple unchanged, identical boot, WIREBIND mint→attach→`USE`→relay (the `FABRIC-3.6.md` Phase 4 acceptance sequence) still `1 2 + .` → `3`. | | **B** | §3.2 trampoline idle loop + §3.3 `hermes_draining` + §3.5 parked-idle flag. Still inert (nothing marks work yet). | Same as A: nothing can reach the new trampoline branch yet, so behavior is identical. A synthetic self-test (kernel_main.c, the §XXVIII Stage 2 `SWITCH-TEST` pattern: write, capture, revert) switches into **Hestia** (not Artemis, whom the Zuse attach flow `VM-EXEC`s; a parked VM refuses `VM-EXEC`) with **two** messages queued, the first calling a colon word so the in-word drain checkpoint is actually reached. The ordering witness `SB-SEQ` must equal 12 (one-at-a-time drain), not 21 (nested). She must end parked-idle at depth 0 with her context saved on her own stack, current VM back to Hera, and the ledger and `stadium_conserved()` restored. | | **FR** | §3.12 VM frames: `VMFrame` header; Stage A/B fields move into it; stack-scoped log attribution. | Probe `SB-ATTRIB`/`SB-GO`/`SB-BACK` (reverted): attribution sequence Hera, Hestia, **Hera**, Hestia on all three ISAs, and the **same probe fails on the pre-frame baseline** (third entry reads Hestia). Then formal acceptance: `dict_hash` unchanged, relay works. | -| **C** | §3.4 eligibility marked + pump skips participants (one commit). | The WIREBIND relay still works, now delivered by a real switch: `switch_count_cumulative` increments across the relay and the `~user` VM's own drain line appears. The ledger and `stadium_conserved()` hold (`fleet_conserved` is not evidence). No `native_stack_saved_sp` ever equals an address inside Hera's stack range (temporary probe). | +| ~~**C**~~ | ~~§3.4 eligibility marked + pump skips participants (one commit).~~ Split into C-a/C-b by §3.13 (pending rulings C-1–C-4). | (see C-a, C-b) | +| **C-a** | §3.13.2 the running VM is charged at each of its own beats, with carry. Physics only; switching unchanged. | Temporary probe (reverted), which switches Hestia in with the Stage FR probe's `SB-GO`/`SB-BACK` words, because C-a itself switches nothing. Hestia runs a colon-word payload that produces ≥ 8 beats. Her `VM-HEAT` is sampled **at a beat during the turn** and again after it, and must rise from 0 by `beats × slope` (whole units, remainder kept in carry). The rest of the live fleet must fall by the same total. `vm_physics_conserved()` holds, and the `fleet_window` sample count is unchanged. **Baseline first:** the same probe on current code shows heat unchanged (proves G9). Then acceptance: `dict_hash` unchanged and the relay works. | +| **C-b** | §3.13.3 decision at owned checkpoints, beats, the trampoline and **the idle point** (§3.13.5, which absorbs Stage F's §3.7). Eligibility comes from the queue or a non-IDLE top frame. The pump skips participants. The ISR switch path and readiness are removed (§3.13.4). | The WIREBIND relay still works, now delivered by a real switch: `switch_count` increments across the relay and the `~user` VM's own drain line appears. Ledger and `stadium_conserved()` hold. `heartbeat_tick()` contains no switch call (source). **Walk-order probe** (temporary): every decision is also computed with the participant table walked in reverse; disagreements = 0. No `native_stack_saved_sp` ever inside Hera's stack range (temporary probe). No switch storm (log growth at idle `ok>`, QMP `send-key` computes a value). | | **D** | §3.5 kill of parked-idle + unregister in `capsule_vm_kill()`. | Birth, message, idle-park, `KILL` succeeds; the switch-signal slot count returns to its prior value. | | **E** | §3.6 generic registration in `capsule_birth_baby()`, duplicate call sites removed. | The Tripod and WIREBIND VMs still register exactly once (slot count), and a `BIRTH`-ed VM appears as a participant. | -| **F** | §3.7 idle point is a switch point. | With Hera idle at `ok>`, a message sent to a worker **gets processed without typing anything**: the worker's output appears while the console is idle. | +| **F** | §3.7 idle point is a switch point. *(If C-5 is ruled yes, the mechanism lands in C-b and F keeps only this evidence.)* | With Hera idle at `ok>`, a message sent to a worker **gets processed without typing anything**: the worker's output appears while the console is idle. | | **G** | §3.8 `SEND`. | Two workers, each sent a payload that loops printing a tagged marker (`A` / `B`) N times: **the markers interleave** in the serial log. This is the one-line proof of concurrency, and it's the test this whole document exists to pass. | | **H** | §3.9 DoE driver rebuilt (capsule change only). | A single trial at cfg 4 (8 workers): `switch_current_slot` in the per-beat DoE rows moves across **multiple worker slots within one trial**, and `MU-TRIAL … fail=0`. Then Bob's call on relaunching the campaign. | | **I** | Strip dead code: the pump loop once every VM is a participant, `SWITCH-MARK-WORK` if nothing calls it, the global depth counter once attribution no longer needs it. | Per `FABRIC-3.5.md` §XXII's strip discipline: reachability shown three ways, not by grep alone. | @@ -427,6 +721,22 @@ QMP `send-key` computing a value, per `FABRIC-3.6.md` task 1.5). 5. **§3.10's shared PRNG:** accept it for this pass (run order is still deterministic), or put per-VM `RANDOM` state in scope. +6. **C-1 (§3.13.2):** how heat is charged under self-driven execution. + - Proposed (a): the running VM is charged at each of its own beats. Switch-out-only charging + is purely cooperative, unless decisions compare projected heat (a′). + - Also: charge records no `fleet_window` samples, which means the slope stays frozen at its + unvalidated seed under self-driven load. Or record them and let it adapt. +7. **C-2 (§3.13.2):** a per-VM carry on the new charge only. Without it, (a) moves nothing. +8. **C-3 (§3.13.3):** ties. The incumbent stays on a tie with itself. Among several candidates: + whether "waiting longest," by a single-writer arrival stamp, is acceptable under *"no scheduler + allowed,"* or whether Bob wants something else. Slot, UUID and registry order are rejected. +9. **C-4 (§3.13.2):** Hera's reservoir mass means the console gets no turn under worker load. + Options are (α) accept it, (β) compare charged heat instead of absolute heat, or (γ) something + else. No recommendation. +10. **C-5 (§3.13.5):** fold the idle point (Stage F's §3.7) into C-b, since the relay can't be + delivered by a switch without it. Also: Hera's pending console input counts as her mail for + eligibility. + **Nothing in this document authorizes an edit.** Per Captain Bob's Law, no stage starts without an explicit instruction.