# REDISPATCH-TRUTH — milestone triage (JIT)

> Doyle triage 2026-07-16, dispatched at the v0.33.0 boundary (ranked-first
> from the hertz same-day RCA; displaces MESH-RECOVERY). Canonical RCA:
> `.claude/reports/2026-07-16-redispatch-truth/hertz-refresh-rca.txt`
> (hertz source-level, doyle seam-verified same day). Decision record:
> ADR-0038. Lane: doyle triages → todlando builds → doyle gates → deployah
> releases → hertz field-verify (they hold the incident logs).

## Why (field: 4/5 endpoints frozen on one refresh, twice in one day)

`spt daemon refresh` / `spt update` (same brain-cycle path, `applyhost.rs`)
freezes active remote `spt rc` sessions. The PTYs and broker-held QUIC
streams survive by construction — the freeze is a **production redispatch
bug** in the fresh brain's stream reconstruction:

1. **Stale replay steals live controllers.** The fresh
   `dispatch::run_dispatch_loop` starts with an EMPTY per-process `claimed`
   set (dispatch.rs:206) and its claim condition
   (`initiated_locally || next_seq == 0 || !claimed.insert`, dispatch.rs:214)
   has NO finished filter — while `NetShared.streams` has NO removal path
   (single insert nethost.rs ~649; `StreamLog::finish` (:245) only marks).
   So every dispatcher generation replays EVERY historical peer stream,
   including finished attach streams. A replayed historical Attach calls
   `attach_as(Control, same origin)` → same-identity silent
   `become_controller` (no `Displaced`) steals the CURRENT controller;
   the replayed EOF then `detach_session` CLEARS it. The live serve loop
   still believes it is Controller but its sink is deselected → operator
   sees a FROZEN PTY. Detach + fresh `spt rc` re-takes → explains the
   no-PTY-restart recovery.
2. **Opener eviction permanently abandons an active stream.** Reconstruction
   classifies by `peek_first_line` (dispatch.rs:256) from ring seq 0, but
   `StreamLog` is a bounded 4096-transport-chunk ring
   (nethost.rs:111) — a high-traffic ACTIVE stream's Request record evicts
   → replay classifies Unknown/Failed; `claimed.insert` happens PRE-spawn
   and is never cleared/retried → that active stream is PERMANENTLY
   abandoned.

Field evidence (HFENDULEAM, hertz preserves
`AppData/Local/spt-core/logs/daemon.stderr.log{,.1}`, brain generations
2/3): 4/5 endpoints frozen across both 2026-07-16 doyle refreshes
(deployah-recovery + the v0.33.0 apply) + lia on the update;
`DISPATCH:N:Failed` lines with low/historical stream ids in fresh
generations = the indiscriminate-replay supporting evidence.

Why CI never caught it: `attach_survives_target_brain_restart_exactly_once`
(crates/spt-daemon/tests/attach.rs:779) manually re-serves a known short
stream — it BYPASSES production rediscovery/classification and never rolls
the opener.

**Distinct from the CLOSED 0.30.x resume-steal saga** (brain session-cursor
resume, fixed cursor-only in 0.30.5/6). This is the NET DISPATCHER leg —
do not re-anchor the old theories.

## Shape (hertz A–D, doyle-endorsed; ADR-0038)

Broker stream-table rows gain explicit **lifecycle truth**, and the
dispatcher redispatches only what is genuinely live:

- **A — finished rows retire from redispatch eligibility.** A fresh
  dispatcher must never re-serve a terminal Attach request. Retain only the
  lifecycle state genuinely needed after EOF (presence/log reads); the
  redispatch enumeration must not see terminal rows.
- **B — restart-durable opener/classification metadata.** Pin the
  per-stream immutable opener fact (or the complete first NDJSON record)
  OUTSIDE the evictable data ring, held until stream close. Recovery
  classifies from that metadata, never from ring seq 0.
- **C — retryable claims.** Distinguish active/retryable vs served vs
  finished; a transient worker-setup failure requeues the claim; terminal
  outcomes do NOT hot-loop.
- **D — three production-path regressions** (all through the REAL
  `run_dispatch_loop`, no manual re-serve):
  1. finished historical Attach + current active Attach, same
     endpoint/origin; restart target brain only → historical stream neither
     takes nor clears the current controller; current input/output
     exactly-once without detach;
  2. >4096 transport chunks on an active Attach (opener evicted); restart
     dispatcher → durable classification resumes the same operator stream;
  3. one injected transient worker-start failure → claim retry recovers, no
     duplicate controller/output.

## Waves

### W1 — the fix cluster (single build wave; todlando)
- **REQ-HAZARD-REDISPATCH-CONTROL-STEAL** — the invariant (KNOWN-HAZARDS
  7.41) + regression D1.
- **REQ-REDISPATCH-FINISHED-RETIRE** — mechanism A.
- **REQ-STREAM-OPENER-DURABLE** — mechanism B + regression D2.
- **REQ-DISPATCH-CLAIM-RETRY** — mechanism C + regression D3.

Then: doyle gates → deployah releases → hertz field-verify on HFENDULEAM
(daemon refresh with active remote `spt rc` sessions: no frozen PTYs, no
historical `DISPATCH:N:Failed` replay storm in the fresh generation).

## Gate rulings (pre-made, binding at gate)

1. **Production-path only.** Every D regression exercises the real
   rediscovery path (`run_dispatch_loop` → peek/classify → serve). A test
   that manually re-serves a known stream does not count as evidence (the
   existing-e2e bypass is the lesson). The existing
   `attach_survives_target_brain_restart_exactly_once` stays as-is but is
   no longer the coverage for this class.
2. **Successor re-take parity is binding.** The legitimate same-`by`
   successor re-subscribe after a brain restart (CONTEXT.md controller
   identity — silent re-take, `Displaced` only on genuine cross-operator
   Take) MUST keep working. The fix removes STALE-replay takeover, not the
   live reconstruction path. The discriminator is lifecycle (finished vs
   active), never origin identity.
3. **B must not grow the evictable ring.** Opener metadata is a bounded
   per-stream side fact (one record), pinned until close; ring semantics
   and `DEFAULT_STREAM_RING_CHUNKS` untouched.
4. **C must not hot-loop.** Terminal classification outcomes are terminal;
   retry is bounded/backoff-shaped for transient setup failures only —
   show the distinction in the test, not just the code.
5. **Every new e2e that spawns a daemon tree enters the HEAVY nextest group
   AT BIRTH** (the HEAVY-AT-BIRTH ruling, doyle 2026-07-15; three identical
   strings: ci.yml Phase A +
   Phase B + nextest.toml).
6. Standard gate hygiene: isolated worktree-of-sha + fresh
   `CARGO_TARGET_DIR`, nextest, workspace-wide clippy preflight,
   `traceable-reqs check` green.

## Possible kin (NOT folded — verify, don't assume)

- deployah zombie controlled-by latch (REQ-ENDPOINT-CYCLE-HONEST seed,
  daemon-lifecycle cluster): plausibly the same stale-stream fingerprint,
  but unproven. The seed stands in its own cluster unless W1 evidence
  shows the same root; do not widen this milestone on speculation.
- stall-evict 15→17 baseline shift: likely same fingerprint; re-measure at
  field-verify, no dedicated work.

## Explicitly out of scope

- MESH-RECOVERY (sibling milestone candidate, separate contract; next in
  line).
- Daemon-lifecycle cluster (endpoint-cycle-honest + comeback + massacre +
  orphan-listener).
- Any change to attach-intent semantics, controller/viewer model, or the
  brain session-cursor resume path (0.30.5/6 fix stands).
