# The 65 s helper stall — experiment design, written BEFORE the run

hertz, 2026-09-08. Zero-cost prep during doyle's W3 gate; no box touched. Predictions are stated
here first, on purpose: a discriminator chosen after seeing the numbers is not a discriminator.

## What is established (measured, not assumed)

- `two_host_web_helper_role_a` takes **66.5 s** (Windows, f19-3) / 66.2 s (doyle's battery) /
  65.0 s (Linux). Its siblings in the same binary and the same run take 0.135–0.753 s.
- A's brain shows **no activity between mono 0 and 65005 ms**; the first `CONN_LIFECYCLE`
  write-start is at 65.0 s. B's raw shows SIX consecutive `A not ready for the message yet
  (broker QUIC op exceeded the 10s bound (peer unresponsive))` before the user-msg is admitted at
  B-mono 75.2 s.
- The only lines inside A's gap are `PAIR_MEET_UP:twohost` at steps `…243/244/245` — a CLOCK, not
  activity: `pairhost.rs:219` rebinds one rendezvous listener per TOTP window per subnet, and
  `PERIOD_SECS = 30`. Boot fell in step `…243`, recovery in `…245`.
- **Correction to my own earlier read:** B's retry sleep is `Duration::from_secs(2)`
  (`twohost_web.rs:524`), not 10 s. The ~12 s per attempt is the QUIC dial's own 10 s bound plus
  that 2 s. So "B dials every 10 s" was wrong in mechanism even though it was close in magnitude,
  and the bound it puts on A's true readiness is **(63.2, 75.2] on B's clock**, ≈ (53, 65] on A's.

## Three rival hypotheses — not two

Naming only two was my error in the first pass; a duration that fits one period usually fits
several, and a flat result would not have separated H2 from H3.

- **H1 — rendezvous window.** A becomes dialable only when a new pairing window binds, so the stall
  is a wait for a 30 s boundary. Fits: 65.005 s = 2×30 + 5, and the gap spans exactly two boundaries.
- **H2 — detection quantization.** A is dialable at some earlier instant T; B simply cannot notice
  until its next attempt, and each attempt costs ~12 s. Fits: any T in (53, 65] on A's clock.
- **H3 — a fixed bring-up cost inside A.** A's endpoint is bound (the rig logs `broker udp 7483` at
  t≈0) but not yet ACCEPTING, because iroh bring-up — relay handshake, discovery publish — has not
  completed, and that costs ~60 s. Fits the same numbers as H2 and is a different defect entirely.

**H2 and H3 are indistinguishable by any rerun**, which is why the first design was inadequate.

## RESHAPED 2026-09-08 12:55Z — H1 RETIRED, and the reason is the finding

doyle's GATE-W3 readout added the datum that changes this: **every ladder rig — win-onebox,
linux-onebox, cross-box — logs exactly ONE `PUMP_PEER_FAIL:<node>:peer reply-read: no progress
within budget — dropping peer (brain IPC read deadline elapsed)` on B before completing. 3/3, one
each, never on A.** Helper cell in that run: 61.956 s (Windows) / 69.096 s (Linux).

**That line is not a symptom, it is a named product deadline firing.** Constants read at
`ff034bf5`, not recalled:

    crates/spt-daemon/src/pump/mod.rs:118   PUMP_PEER_IO_TIMEOUT   = 30s
    crates/spt-daemon/src/pump/mod.rs:121   SUPERVISE_BACKOFF_BASE = 5s   (first restart after a pump death)
    crates/spt-daemon/src/pump/mod.rs:111   HEARTBEAT_PERIOD       = 5s
    crates/spt-daemon/src/pump/mod.rs:135   PEER_BACKOFF_BASE      = 5s

A TimedOut brain-IPC read is ruled a poisoned client escalated to a SUPERVISED RESTART, never a
per-peer retry (`:117`, REQ-HAZARD-PUMP-IPC-DEADLINE).

**H1 (the 30 s TOTP rendezvous window) is retired**, and how it died is worth more than the
hypothesis was. I fitted 65.005 s to the pairing window because `PAIR_MEET_UP` was the only
periodic thing PRINTING in the gap. But a log shows the periods that PRINT, and a deadline that
fires silently prints nothing until it fails — so the rule "enumerate every periodic thing whose
period could produce this" has to be executed against the CONSTANTS, not against the log. Had the
phase-offset test run, it would have varied the pairing phase while the pump deadline sat
untouched, and a flat series would have read as "not the window" while the 30 s that mattered was
never a variable. A correct experiment aimed at the wrong period.

**H4 (new, leading): B's first peer dial black-holes, the pump's 30 s brain-IPC deadline elapses,
the pump bubbles to its supervisor, and the restart's re-primed dial succeeds.**

**THE ARITHMETIC IS OPEN AND STAYS OPEN.** One deadline plus one supervise floor is 30 + 5 = 35 s.
The observed stalls are 61.956–69.096 s across five runs. ~27–34 s is unaccounted — itself close to
another 30, which is exactly the coincidence this document already warns about — and the count of
**exactly one** `PUMP_PEER_FAIL` forbids the easy reading that two deadlines elapsed. **Do not close
this with a story** (doyle, ruled 12:55Z). The rider's first question is now: what consumes the
other half, given only one deadline is reported? Separate rather than assume: a first ~30 s burned
before the deadline's clock starts; a second budget stacking on the first; or the restart's
re-prime costing materially more than its 5 s floor.

**Cheap discriminator, already instrumented: the FAIL COUNT.** `PUMP_PEER_IO_TIMEOUT` is a hard
const with no env knob, so varying it costs a rebuild — deferred until the count and the timestamps
say which half is missing. The probe leg therefore logs the deadline's own timestamps against the
fail line, not merely first-dial success.

## The discriminators, with predictions committed

1. **Phase offset (separates H1 from H2/H3).** Align cell 3's start to a chosen offset from the
   30 s boundary before creating the requester (`while now_secs() % 30 != OFFSET { sleep }`), run
   OFFSET ∈ {0, 10, 20, 25}.
   - H1 predicts the stall TRACKS the offset — shrinking toward the next boundary, ≈ (30 − offset)
     mod 30 plus a constant.
   - H2 and H3 both predict ≈65 s REGARDLESS of offset.
2. **Direct readiness probe (separates H2 from H3, and is the strongest leg).** From B, poll a
   cheap connect against A's UDP/QUIC port every 500 ms from B's start and log the first success.
   This converts a bounded interval into a MEASURED INSTANT and removes the quantization that makes
   H2 and H3 look alike.
   - H2 predicts first-success EARLY (T ≈ a few seconds) with B's admitted-at lagging it by up to
     one ~12 s attempt.
   - H3 predicts first-success LATE, ≈65 s, i.e. the stall is real and inside A.
3. **Cadence knob (confirmatory only).** Drop B's retry sleep to 250 ms.
   - H2 predicts the stall collapses toward T.
   - H3 predicts it stays ≈65 s.

## Rules for the run

- Rig-side only. No product edit — and specifically NOT an instrument on `run_brain`
  (`brainproc.rs:252/:293/:315`), which the rig never calls: it builds `Brain::cold_start` against
  `rig_broker`. An instrument there would print nothing and read as "no cost found".
- One box, one run at a time, output dir locked, `procs-before: []` asserted, `grep -c Summary`
  refusing with the count. Both runs of an overlapping pair are void, not just the first.
- Timestamp every tree/state read AGAINST the event it describes; a reading taken after the window
  closed is not evidence about the window.
- Report the offset series as a TABLE with every point, not a summary — a flat series and a
  tracking series are the whole finding, and a mean hides both.

---

## QUEUED BEHIND THIS ONE — the docs_dir refusal-arm rider (doyle-ruled 2026-09-08)

Written here rather than left in a chat turn, because a ruling that lives only in a message does
not survive a clear.

**Origin.** GATE W3 RED 3b: `validate_docs_dir` (`crates/spt-store/src/serving.rs`) has THREE
refusal arms and only two name the key. Empty says `adapter.docs_dir must name a directory`,
absolute says `adapter.docs_dir is adapter-relative and must not be absolute`, and the ParentDir
arm said only `a parent-directory component leaves the adapter directory`. todlando fixes that arm
and the stale ":549 two rules -> three" wording in the same commit (doyle-ruled); the CELL is mine
and is not part of that fix.

**The rider.** One cell that walks EVERY refusal arm of `validate_docs_dir` and asserts each one
names `adapter.docs_dir`, keeping the `MANIFEST_DOCS_DIR_OUTSIDE:<adapter>` prefix assertion the
spt-store cells already make.

**Why it is worth a cell rather than a habit.** A convention enforced by no mechanism degrades one
arm at a time, and every test aimed at a still-complying arm reports the convention as held. Two of
three arms named the key here, so the defect was invisible to any cell but the one that happened to
exercise the third — my four cells caught it by luck, not by coverage. The cell converts "we name
our refusals" from a sentence in a doc-comment into something the suite refuses to let lapse, and
it fails on the NEXT arm someone adds rather than on the next person to read the file.

**Falsifiable, and the negative control matters:** the cell must go RED if any arm's message drops
the key. Prove that by mutating one compliant arm's text to omit `adapter.docs_dir` and watching the
cell fail — a cell that has never been made to fail on purpose is untested.

---

## RIDER 3 — make a leg's pool-guard verdict READABLE (2026-09-08, from the W3 hatch thread)

**The gap.** A clean pool-guard pass is SILENT, so a raw cannot distinguish "the guard ran and
passed" from "the guard never ran for this unit". That ambiguity cost this lane a claim, a
confirmation of mine that was wrong, and a retraction from todlando.

**What already exists on disk.** Cargo keeps each build script's last invocation:
`target/debug/build/<pkg>-<hash>/invoked.timestamp` is stamped WHEN THE SCRIPT RAN, and the sibling
`output` file holds WHAT IT PRINTED. A fresh timestamp with a zero-byte output is exactly
"ran, and passed silently".

**The limit todlando found, and it is what makes this a rider rather than a habit:**
`invoked.timestamp` is overwritten IN PLACE per unit. It is a record of the LAST invocation, never
a log — so every earlier run through the same unit is already destroyed, and no post-hoc read can
reconstruct a battery once another battery has run over the pool. It can prove the guard ran at an
instant, and it can time-EXCLUDE an agent (todlando did exactly that to clear himself of the
13:11:11Z hatched build). It cannot attribute, and it cannot reach backwards.

**The rider.** The driver snapshots `invoked.timestamp` + `output` for the guarded package's units
BETWEEN legs — after each leg, before the next — and prints one line per leg:

    POOL_GUARD: leg=<name> ran=<ts|UNCHANGED> banner=<none|text>

**Why the between-legs copy is the whole design:** the next leg through the same unit destroys the
evidence, so a single read at the end answers only about the last leg. Copying between legs is what
turns a last-value into a per-leg record.

**And it is three-valued by construction, which is the point.** Comparing consecutive snapshots
gives, per leg: timestamp ADVANCED + output empty = ran and passed; ADVANCED + output non-empty =
ran and said something (the hatch banner is this case); UNCHANGED = the guard DID NOT RUN for that
leg, which is a real and currently invisible state — a tree rebuilding itself does not re-fire the
script (`crates/spt-store/build.rs:10-32`, measured: `A B A A B` fired four times, the consecutive
`A A` fired once). A two-valued check would report that third state as a pass.

**Cost:** driver-side only. No product change, no guard change, no rebuild.
