# W2 JIT — C2 stream-lifetime residual (the last DAEMON-LIFECYCLE build item)

**Lane:** `.worktrees/daemon-lifecycle-w2`, branch `build/daemon-lifecycle-w2`
off main @3aecc35 (v0.41.0 published, counter 74). First commit `fb93700`
activated both REQs and rescoped C2.

**Prebuild before ANY rig run:** `cargo build -p mock-adapter --bins`.
**Hands off the box during any CI job** (standing rule, both directions).

## Already done

- Doyle's verify-first finding RE-GROUNDED against @7c0f12d, cite by cite: the
  ConnectionBound clean-case chain is shipped under REQ-STREAM-LEASE-CLASSES.
  Do NOT rebuild it. The rescope text in `traceable-reqs.toml` carries the full
  cite list — read it rather than re-deriving.
- `REQ-STREAM-LIFETIME-CLASS` = `["impl","unit","int"]`, rescoped onto the
  restart-replay residual (Leg A).
- `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` = `["int"]` ONLY — deliberate:
  instrument first; impl/unit activate only if the measurement shows the heal is
  absent. Do not pre-fail stages a measurement may prove unowed.

## LEG A — restart-replay re-establishment (build this first)

The residual: ADR-0038 dispatch replay re-serves every retained opener Attach
across a dispatcher generation. A **ConnectionBound opener replaying is a class
contradiction** — the conn that declared the class cannot exist after the restart
that killed it.

### ⚠ THE PREMISE ITSELF IS PRE-REGISTERED AS FALSIFIABLE (doyle, before any build)

Post-d6 the leak population may ALREADY be closed by composition:

- opener death, loopback → conn-exit sweep FINs (shipped)
- opener death, cross-node → `conn.closed()` retires rows
  (`nethost.rs:1000-1021`, shipped)
- retired/finished rows excluded from redispatch eligibility (ADR-0038
  decision 1, shipped)

**If kill-opener-raw → restart-brain → the seat does NOT re-establish, that is
SUCCESS, not a missing fix.** Leg A then rescopes to a verify-note exactly like
the C2 rescope one leg over, and doyle's "survival re-verified" memory was
reasoning about the pre-0038/0040 composition. **Write the rig header so a fresh
context reads a non-repro that way** — this is the same trap the JIT already
guards for Leg B, one leg over, and the reason to state it here is that a
non-repro will otherwise look like a broken rig.

### The carrier question is SETTLED — do NOT add a wire field

**My additive-`NetStreamOpenerReply` proposal was REJECTED, on a reach fact I
failed to check before proposing it and have since verified myself:**

The row a dispatcher re-serves is the **ACCEPTED** row (cross-node) or the
loopback **PEER** row — and both register `Durable`, **class-blind BY DESIGN**,
with the source comment saying so outright:

```
// Accepted rows register Durable: the lifetime class is OPENER-side
// (ADR-0040 decision 6) — the target experiences a ConnectionBound
// teardown as an ordinary FIN, never as a class of its own row.
```
`nethost.rs:1028-1041`; loopback peer row identically at `1763-1771`.

The opener-side classed row is NOT the served row. A lifetime field on the
opener reply would therefore report `Durable` for **exactly the population the
filter exists to catch** — undercounting the leaked rows to zero, and spending
wire surface on the wrong rows. The lesson generalises: check WHICH ROW the code
under test actually serves before designing a carrier to describe it. Same class
as 7.55's vantage-point finding.

### Placement, IF a filter turns out to be owed at all

**Broker-side, no wire change.** The broker holds both discriminating facts —
each row's class AND its transport conn's liveness — so the filter belongs at
enumeration/claim (the REGISTRY-STALL **B2 shape**: filter server-side in
`stream_infos` before anything crosses IPC).

And note WHICH fact discriminates: for replay the sharp key is **"is this row's
transport conn alive"**, not class. Class only separates rows whose conn is
already known dead.

### Seam entry points located, not yet modified

- `dispatch.rs` `first_line()` (~717) — the restart-durable classification.
  Reads the broker-pinned opener via `net_stream_opener`, falls back to a ring
  peek only in the KH-2.3 N-1 window.
- `broker.rs` `dispatch_net_stream_opener` (~5860) — returns `opener_b64` only.
- The filter runs UPSTREAM of the W1 idempotence key — say so in the rig header.

**RED-first rig:** kill the rc opener raw → restart broker → observe whether the
seat + CONTROLLED stamps re-establish via replay.

**BUILD THE RIG AGAINST THE PREMISE AND BRING DOYLE THE COLOR. No wire change in
any branch of this ruling without a red transcript that names the row it failed
to filter.** If it reds, the transcript reports the replayed row's
class/state/conn, and THAT evidence picks the filter key — not an armchair guess.

Two sub-questions a red transcript would settle, both worth capturing in it:
1. does "retire" (eligibility-only, per the REGISTRY-STALL finding) actually
   exclude from the CLAIM condition on the path the rig hits?
2. did the conn-exit sweep run at all for the kill shape used?

**Proof any fix is precise, not ADR-0038's rejected clear-table-on-restart:**
brain_swap + daemon_refresh + redispatch D1/D1b stay green.

## LEG A — CLOSED 2026-07-22, GREEN, doyle-RATIFIED @713f5d0

Premise FALSIFIED (the pre-registered success branch). Rig
`crates/spt-daemon/tests/restart_replay_lifetime.rs`; REQ rescoped to `int`
only (impl/unit deactivated, not pre-failed). Population closed BY
COMPOSITION: sweep → FIN → `finished=true` → `finished_row_is_terminal(Attach,
true)` retires unserved. Loopback-only scope limit stated in the header.

**Finding 1 (non-obvious, doyle verified in code before ratifying):** `retire`
does NOT exclude from the CLAIM condition — the dead opener's row is STILL
enumerated as claimable, merely `finished=true`. What stops the replay is
retire-on-sight in the WORKER (`dispatch.rs:418` fired at ~888), DOWNSTREAM of
the claim. ADR-0038 decision 1 holds through its **lifecycle-state arm**, NOT
its enumeration arm. Two designers in one week reasoned from the arm that is
not load-bearing (doyle's B2-shaped filter proposal assumed enumeration was the
natural place). Name this hazard so the next designer inherits it.

### Amendment payload — doyle RULED: ONE landing AFTER Leg B settles. Draft nothing yet.

The ADR-0040 amendment carries FOUR things from Leg A:
1. the composition record (sweep → FIN → finished → Attach-terminal
   retire-on-sight) as the mechanism closing the restart-replay population,
   with the loopback scope limit stated;
2. the ADR-0038 d1 **arms clarification** as an explicit cross-reference —
   worker-level exclusion load-bearing, enumeration arm not — naming the
   reasoning hazard;
3. the carrier reach fact (served rows are class-blind BY DESIGN) recorded as
   the REJECTED ALTERNATIVE, with the why;
4. the rig-defect note: `net-streams` applies the ADR-0040 d2 server-side
   filter, so opener rows are invisible to EVERY brain client — the in-process
   `Arc<Broker>` read is the rig shape.
5. **the two-loopbacks naming collision** (doyle rider, added 2026-07-22): name
   BOTH meanings explicitly — `net_dial_loopback` / `open_loopback_pair` =
   IN-PROCESS DUPLEX (`RecvHalf::Loopback`, no QUIC, no idle timeout) vs
   `BindScope::Loopback` + a real `net_dial` = REAL QUIC over 127.0.0.1
   (`RecvHalf::Quic`, real `MESH_MAX_IDLE`). Doyle's ruling: this one finding
   **retires a whole class of future mis-staging** — it is why the old no-FIN
   characterization read as unstageable, and every future rig must pick its
   venue deliberately rather than by the word "loopback".

## LEG B — live no-FIN half-open (INSTRUMENT FIRST, do not fix blind)

### ⚠ CODE-GROUNDED PREDICTION (todlando 2026-07-22, read BEFORE building the rig)

Reading the seam end to end says outcome **(a) already heals** — and it
sharpens what the rig must measure. The chain, cite by cite:

- the closed-watcher (`nethost.rs:1000-1021`) removes the rows
  (`streams.retain(|_, e| e.conn_id != id)`) and appends PRESENCE_DISCONNECTED;
- the QUIC read pump (`nethost.rs` register_stream) ends on
  `Ok(None) | Err(_) => { log.lock().unwrap().finish(); break; }` — a TORN end
  finishes the log exactly like a clean one, by explicit design comment;
- `StreamLog::finish` (`nethost.rs:519`) pushes
  `net_stream_eof_envelope(...)` to the INSTALLED seat;
- `subscribe_stream` (`nethost.rs:2091`) takes `Arc::clone(&e.log)` ONCE and
  installs the seat ON THE LOG.

**So the row removal does NOT break an ALREADY-SERVING worker.** The seat lives
on the Arc'd `StreamLog` that the pump also holds; removal only breaks NEW
lookups (`subscribe_stream` / `drain_stream` / `unsubscribe_stream` return "no
such stream" — and `dispatch.rs` already handles that as "terminal pre-serve:
row already gone"). The EOF still reaches a serving `serve_attach`.

**What therefore remains genuinely unmeasured is the WINDOW, not the
existence:** how long real QUIC takes to fail the read (idle timeout /
keepalive) — which is precisely what loopback cannot stage and why the old
"clears on NEITHER platform at ANY window" characterization does not settle it.
Re-aim the rig at the window + the removal/EOF ordering, NOT at "does it heal".

This is a PREDICTION FROM CODE, not a measurement — do not bank (a) on it.

### DOYLE RULED OPTION 1 — DISPATCH THE TWOHOST RUN. Riders, binding:

1. **Sibling clean-FIN probe FIRST**, mandatory. Its number (time-to-EOF on a
   clean FIN) is ALSO the baseline the torn number is read against.
2. **Run-window discipline both boxes.** `gh run list` before starting; on
   HFENDULEAM do NOT disturb hertz's held `v041-ball-observer` viewer or any
   live infra. Census before AND after; reap only your own rig children BY
   PATH, never name-kill.
3. **Measure BOTH numbers per outcome:** time-to-EOF at the SERVING worker and
   time-to-stamp-clear. Also record (do NOT chase) whether the removal/EOF
   ordering ever races — the `retain` fires before the pump notices, so can a
   NEW subscribe in that window see `no such stream` while the seat still
   serves?
4. **Pre-registered outcome map.** (a)-confirmed → Leg B closes as
   KH-wording-WITH-NUMBERS + coverage commit; presence-FIN synthesis NOT built;
   nothing owed — success branch. **If the number is LARGE, do NOT invent an
   acceptability threshold** — report it; tune-keepalive vs build-synthesis is
   a design decision taken ON the number. (c) ambiguous/racy → bring doyle the
   trace before building anything.
5. **If twohost mechanics themselves fight back** (pairing/env broken either
   box) → STOP and report. A rig-plumbing yak is not this leg.

KH boundary sentence stands in EVERY branch: presence and transport-death close
DEAD-conn-no-FIN only; alive-but-WEDGED (7.15 A2) stays deferred, boundary
STATED not implied.

### THE STAGING — solved, and it needs no elevation, firewall, or cross-host control

The runbook's banked lesson ("an in-process broker cannot die honestly") rules
out fake death, and a process KILL is the wrong shape too: a dead process's
socket can draw ICMP unreachable, which is not the field's silent netsplit.
`conn_blackhole_lifecycle.rs` is NOT reusable — it is the local IPC named-pipe
wedge (the 7.15 A2 alive-but-wedged class), a different seam entirely.

**`NetHost::close()`'s own doc comment hands us the staging** (`nethost.rs`
~2183): *"Gracefully close the endpoint: every held connection flushes its QUIC
CLOSE so peers' closed-watchers fire promptly (a bare host drop kills the
runtime first and leaves peers to an idle timeout)."*

So ONE code path with ONE boolean gives both legs:
- **sibling clean-FIN probe** = role A calls `NetHost::close()` → CONNECTION_CLOSE
  flushes → B's closed-watcher fires promptly. **Baseline number.**
- **the torn no-FIN half-open** = role A DROPS the host WITHOUT `close()`, and
  the process STAYS ALIVE holding its socket → nothing is ever sent, no ICMP →
  B is left to the QUIC idle timeout. **The true silent half-open.**

Measure on role B (the TARGET hosting the session + the serving `serve_attach`
worker); stage on role A (the remote opener/controller).

### ⚠ STAGING CORRECTION (doyle's flag, 2026-07-22) — MY EARLIER TEXT WAS WRONG

I wrote that a bare host drop leaves "the process alive HOLDING its socket".
**That is false for a plain drop.** `NetHost` owns `runtime` + `Arc<NetEndpoint>`;
dropping it drops the runtime AND (last ref) the endpoint, so **the UDP socket
CLOSES**. A closed socket answers B's keepalives with ICMP port-unreachable —
the exact non-silent shape I ruled out for process death. The bare drop is
therefore NOT reliably silent, and `close()`-vs-bare-drop is a weaker pair than
I claimed.

Options weighed:
- **leak it (`mem::forget`)** — socket stays bound BUT the runtime keeps
  driving QUIC, so the conn never severs at all. Useless: not a sever.
- **firewall DROP rule** — textbook netsplit, honest, but needs elevation on
  both boxes = the rig-plumbing yak rider 5 says to refuse.
- **FREEZE THE RUNTIME ← chosen.** `NetHost::start` builds
  `new_multi_thread().worker_threads(2)` (`nethost.rs:1238`). Park BOTH workers
  in non-yielding blocking loops → the QUIC driver is never polled, nothing is
  ever sent or answered, **and the socket stays BOUND** (the process and its
  endpoint are still alive, just frozen). No ICMP, no elevation, no FFI, no
  cross-host control. This is the field's sleeping-laptop/wedged-peer shape.

Doyle's rider stands and is now satisfiable honestly: assert post-freeze that
the port is STILL BOUND. Under the freeze that assert is true by construction;
under the bare drop it would have been false — which is exactly why the assert
was worth demanding.

### ⭐ THE INSTRUMENT CAN RUN LOCALLY FIRST — real QUIC, no twohost dispatch

Two different things share the name "loopback", and conflating them is what
made the old characterization look unstageable:
- `net_dial_loopback()` / `open_loopback_pair` → `RecvHalf::Loopback`, an
  IN-PROCESS DUPLEX. No QUIC, no idle timeout. **This is what Leg A used**, and
  why its sweep was instant.
- `BindScope::Loopback` + a real `net_dial` to a bound addr → **REAL QUIC over
  127.0.0.1**, real `RecvHalf::Quic` pump, real `MESH_MAX_IDLE`.

Two in-process `NetHost`s each own their OWN 2-worker runtime, so one can be
frozen while the other observes. That stages the TRUE no-FIN half-open on REAL
QUIC locally — cheap, fast, repeatable, no run window, no shared-box risk.

**Plan: measure locally FIRST** (mechanism + both numbers + the race note), then
spend the twohost window on CONFIRMATION over a real LAN path rather than on
discovery. Report the local numbers to doyle before dispatching the two-box run.

### THE RIG, fully specified — BUILD THIS NEXT (nothing else is open)

**File:** `crates/spt-daemon/tests/transport_death_eof.rs`. HEAVY AT BIRTH
(two real brokers + real PTY + a real dispatcher child) → classify into
`.config/nextest.toml` + all three ci.yml `<HEAVY>` strings IN THE SAME COMMIT.

**Venue = REAL QUIC, and the pattern already exists:** `redispatch.rs` D1 does
it — two brokers via `net_broker(name_a/name_b)`, then
`let a_addr = probe.net_status().expect("a status").addr;` and
`operator.net_dial(a_addr, Some(MintedOp::new(Minter::Cli, 1)))`. That is
`RecvHalf::Quic` with a real `MESH_MAX_IDLE`. Do NOT use `net_dial_loopback()`
(the duplex — Leg A's venue, no idle timeout).

**Topology:** broker A = TARGET (hosted perch + session + a `dispatch_fixture`
generation whose worker serves the attach; both numbers are measured HERE).
broker B = OPENER/controller (dials A, ConnectionBound Control attach, drives
input so the seat latches). Reuse Leg A's `Generation` fixture verbatim.

**The freeze valve — decided.** `NetHost`'s `runtime` is private and there is no
public spawn, so the rig cannot park the workers from outside. The repo's
`SPT_TEST_*` env valves are the idiom for a *production dispatch path*; this is
not one — the test owns the host object directly, so a plain method is simpler
and adds ZERO production branch (strictly better than an env check production
would evaluate on every call). Add to `nethost.rs`:

```rust
/// TEST-RIG ONLY — never called in production. Park every runtime worker in a
/// non-yielding loop so the QUIC driver is never polled again. The endpoint
/// stays ALIVE and the UDP port stays BOUND, so the peer sees SILENCE: no
/// CONNECTION_CLOSE and no ICMP unreachable, forcing it onto its idle timeout.
pub fn freeze_transport_for_test(&self) { /* spawn 2+ blocking parks */ }
```
`worker_threads(2)` (`nethost.rs:1238`) is what makes this deterministic — park
strictly more than 2. Not requirement evidence, so no REQ tag; document it as
rig infrastructure.

**The two legs, one code path, one boolean:**
- **sibling clean-FIN probe — RUNS FIRST, MANDATORY:** `broker_b.net().unwrap()
  .close()` → CONNECTION_CLOSE flushes → A's closed-watcher fires promptly.
  **Baseline number.** If this leg does not produce a visible EOF, STOP: the rig
  cannot see what it claims to measure and every torn-leg number is vacuous.
- **torn no-FIN half-open:** `freeze_transport_for_test()` on B → silence → A
  falls back to `MESH_MAX_IDLE`.

**Record per leg:** time-to-EOF at the SERVING worker; time-to-stamp-clear;
port-still-bound assert after the sever (doyle's rider — TRUE by construction
under the freeze, and would have been FALSE under the bare drop); and whether a
NEW subscribe in the removal window ever sees `no such stream` while the seat
still serves (RECORD, do not chase).

**BOUNDARY SENTENCE, must appear in the rig header (doyle rider 1):**
frozen-runtime models the **QUIC-STACK-DEAD** peer — keepalives unanswered, the
transport itself declares death (the sleeping-laptop shape). That is still the
TRANSPORT-DEATH class. It is **NOT** KH 7.15 A2 alive-but-wedged, where the QUIC
stack keeps answering keepalives and only the APP writer is parked — A2's
transport NEVER dies, so no idle timeout ever fires there. This rig must not be
read as covering A2.

### PREDICTED NUMBERS (pre-registered before the run)

`spt-net/src/net/endpoint.rs`: `MESH_KEEP_ALIVE = 15s`, `MESH_MAX_IDLE = 60s`.
So: **clean FIN ≈ milliseconds; torn death ≈ up to 60s** (bounded by
max_idle_timeout). If that holds, the heal EXISTS but is idle-timeout-bound —
which is squarely doyle's "if the number comes back LARGE" fork, and the answer
is to REPORT 60s, not to judge it. `SPT_TWO_HOST_WAIT_SECS` defaults to 300, so
a 60s rung fits the window.

**The discriminating observable:** does the target's `conn.closed()` row
retirement (`nethost.rs:1000-1021`, already appends PRESENCE_DISCONNECTED)
ALREADY surface `NetStreamEof` to a serving `serve_attach` worker? Never measured
on real QUIC — the existing "clears on NEITHER platform at ANY window"
characterization (`inject_control_wedge` @2300-2305) was taken on in-process
loopback where the true half-open is **unstageable by its own comment**.

**Sibling probe is mandatory and goes FIRST:** clean-FIN detach must be visibly
observable in the same twohost rig before any no-FIN absence claim counts
(absence-needs-a-sibling-probe; the W3 vacuous-probe lesson).

Three outcomes, all legitimate:
- **(a) already heals** → Leg B shrinks to a KH-wording + coverage commit.
  REPORT BEFORE BUILDING ANYTHING.
- **(b) does not heal** → build presence-FIN synthesis: on transport conn death,
  synthesize `NetStreamEof` toward serve workers of streams transported on that
  conn, so `serve_attach`'s EXISTING EOF arm stays the ONE exit shape (the W3
  principle — one transaction, one exit, any entry; every validated identity
  guard then rides free).
- **(c) ambiguous** → bring doyle the measurement, like the console-mode bits.

**Honest scope limit goes in the KH either way:** presence closes DEAD-conn-no-FIN
only. An alive-but-WEDGED controller (conn open, writer parked — KH 7.15 A2) is
invisible to presence and stays deferred, with the boundary stated rather than
implied.

## LEG B — CLOSED 2026-07-22, GREEN, OUTCOME (a) @e1d3add

Rig `crates/spt-daemon/tests/transport_death_eof.rs` (HEAVY at birth, same
commit). Two samples: **clean FIN 120ms / 115ms · torn no-FIN 65057ms /
65047ms** — the heal EXISTS and is idle-timeout-bound (MESH_MAX_IDLE 60s +
quinn PTO slack). ABOVE 60s ⇒ the staging leaked no signal, per doyle's
discriminator. The number is REPORTED, not judged; no threshold is encoded
anywhere, and `REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT` stays `int` only —
presence-FIN synthesis is NOT owed. Staging = `freeze_transport_for_test`
(endpoint alive, port still BOUND — asserted; canary 0ms → 2006ms proves the
park landed). KH 7.15 + 7.29 carry the numbers and the narrowed boundary;
A2 alive-but-wedged stays deferred. Reported to doyle; the twohost window is
now CONFIRMATION, not discovery, and is held pending doyle's word.

**Finding 2 (rig, banked in the header):** the generation's stderr is an
ANOMALY channel, not a progress log — `DISPATCH:<id>:` is emitted only on the
NON-`Served` arm, so an EOF-driven serve exit prints NOTHING. Run 1 failed its
own clean leg on exactly that, and the mandatory sibling probe caught it before
any torn number could be banked. The seat release is the progress observable.

**Finding 3 (the recorded race):** the row leaves the table 0–30ms BEFORE the
seat releases, so a NEW subscribe in that window would see `no such stream`
while the seat still serves. DERIVED from the ordering, deliberately not probed
(a real subscribe would steal the seat from the worker whose exit is the
number).

## LEG C — the taxonomy, as an ADR-0040 AMENDMENT

One landing, AFTER Leg B's measurement settles (Amendment 2's flow). Teardown
authority = opener-declared class + transport liveness, enforced at the three
places decision 6 could not see: conn-exit sweep (shipped) · restart replay
(Leg A) · transport death sans FIN (Leg B).

Target rows stay class-blind — accepted mirror rows Durable (`nethost.rs:1028-1043`),
loopback peer row (`1763-1771`). That is CORRECT; class is opener-side. **Do not
"fix" it.** Two classes suffice; no third class. Todlando drafts, doyle ratifies.

## FOLD-IN (separate commit, rides the wave)

Fixture-bin `CARGO_TARGET_DIR` fix — REQ-XTASK-SPT-BIN-TARGET-DIR class, 2nd
instance, and mine: `player_bin`/`probe_bin` hardcode manifest-derived
`target/debug` at 4 sites — `attach_resize_capture.rs:193-198`,
`capture_vehicle_fidelity.rs:59`, `rc_console_newline_presentation.rs:53`,
`resize_console_mode_integrity.rs:48-52`. Fix shape: ONE shared
`current_exe()`-derived sibling resolver (`twohost.rs:437 seed_notify_shell`
precedent — structurally honors CARGO_TARGET_DIR; `CARGO_BIN_EXE_*` is
unavailable cross-package).

## Gate

Merge main BEFORE any further registry edit (the tail has collided three times).
Seam battery: attach · attach_idempotent_replay · controller_lease ·
endpoint_lifecycle · inject_control_wedge · driven_by_selfheal · brain_swap ·
daemon_refresh · redispatch D1/D1b. RED transcripts in the PR body (house rule).
`traceable-reqs` exit 0 + clippy exit 0 + xtask check before gate-ready — and
take verdicts from EXITS, never from a grep of piped output.
