# REGISTRY-LIFECYCLE — milestone triage (doyle, 2026-07-17)

Operator-fired 2026-07-17: REGISTRY-STALL and the DAEMON-LIFECYCLE cluster roll into ONE
milestone. Target release: **v0.37.0 (counter 64)** after all waves gate.

Sources (all doyle code-verified before this triage):

- REGISTRY-STALL: hertz post-close v0.36 field RCA 2026-07-17 —
  `.claude/reports/2026-07-17-registry-stall-rca/README.md` (canonical, incl. the
  family-agnostic seat-retention correction and the dropped-W4-rider provenance of the
  self-hash defect). Evidence baseline: hertz-preserved live log + byte-for-byte snapshot
  (15 poisons and growing at pump cadence).
- DAEMON-LIFECYCLE: three hertz reports (2026-07-16, verbatim in the findings ledger) —
  emphasys triple (C1 hybrid-online / C2 viewport stream leak / C3 poll-vs-reap race),
  rest-normalize (zombie WAKE_RESUME), spawn-fresh (`--create` silently reattaches) — plus
  the operator-field deployah cycle wedge (ALREADY_LIVE vs OFFLINE contradiction).

Decisions: **ADR-0040** (bounded stream/seat lifecycle + one-way terminal semantics + lease
classes — completes ADR-0038's lifecycle story), **ADR-0041** (endpoint lifecycle truth).
Hazards: **KNOWN-HAZARDS 7.44, 7.45**. Requirements registered in `traceable-reqs.toml`;
stages activate per wave (traceable-per-wave-activation).

## Wave plan

### W1 — REGISTRY-STALL (todlando; REQs ACTIVATED)

Fixes, in dependency order:

1. **R1 `REQ-BRAIN-HASH-ONCE`** (dropped THE-FORKENING W4 rider, escalated): capture
   `current_exe_hash()` ONCE per brain process (`OnceLock` in `run_brain`, before the
   heartbeat loop); every `write_ready` reuses the cached value. Failed capture stays `None`
   — no per-tick retry. Fix the `current_exe_hash` doc-comment in the same commit (today it
   claims once-per-start while the call site runs 2×/s — and the per-tick re-hash reads the
   exe PATH, so a post-swap resident-old-bytes brain publishes the NEW file's hash: the
   breadcrumb lies in the exact enlyzeam class it exists to catch; once-at-start capture IS
   the truthfulness fix). 500ms ready-write cadence unchanged.
2. **R2 `REQ-ONEWAY-STREAM-TERMINAL`** (B1): the registry feed is a fire-and-forget one-way
   family — the pump retires its OWN feed row after successful write+FIN via the existing
   `net-stream-retire` verb (N-1 best-effort per ADR-0038 A). Sender-side history stops
   accumulating on the long-lived pump conn.
3. **R3 `REQ-STREAM-INFOS-SERVER-FILTER`** (B2): broker `stream_infos` excludes
   `initiated_locally` rows server-side (retired already excluded). Dispatcher keeps its
   client-side guard (double-filter harmless; N-1 safe both directions — an older dispatcher
   skips those rows anyway, an older broker just keeps paying the old cost).
4. **R4 `REQ-REGISTRY-APPLY-TRANSACTIONAL`** (B3): `serve_registry_feed` accumulates decoded
   labels + instance updates in memory across chunks and applies ONCE at EOF (or one bounded
   batch commit for oversized feeds) — `write_snapshots` runs O(feeds), never
   O(chunks × record-kinds). Attention-shift side effects fire once, post-merge.
5. **R5 `REQ-REGISTRY-REPLAY-BOUNDED`** (B4): a deadline-poisoned Registry replay gets a
   per-stream strike budget; at budget the row is retired TERMINAL with loud telemetry.
   Safe by family semantics: a feed is an idempotent snapshot advertisement — the next pump
   round (~30s) re-advertises everything; dropping a poisoned one loses nothing durable.
   Defense-in-depth behind R2 (which removes the historical-replay population at source).
6. **R6 `REQ-SEAT-LIFETIME-BOUNDED`** (B5, CROSS-FAMILY per hertz correction): new
   `KIND_NET_STREAM_UNSUBSCRIBE` broker verb — removes the SubscriberSeat, stops/joins its
   writer thread, drops the cursor. Called by every dispatch worker on serve completion
   (ALL families, success or failure) and by the retire sweep. N-1: unknown-kind on an older
   broker is the tolerated `send_error` path; caller is best-effort. Rider: NAME spawned
   daemon threads (SubscriberSeat writer `sub-writer` minimum; accept handlers) so the next
   field census isn't 93% unnamed.
7. **R7 `REQ-HAZARD-REGISTRY-STALL`** (KH 7.44 umbrella): hertz's regression seam verbatim —
   real long-lived pump + dispatcher int test, N registry rounds over ONE persistent
   connection, asserting: dispatcher-eligible Registry rows plateau O(active) not O(N);
   physical stream/subscriber/seat counts plateau after completion (CROSS-FAMILY — assert on
   sync/update seats too, not just Registry); snapshot writes O(feeds) not O(chunks×kinds);
   brain refresh produces zero historical Registry replay subscriptions; zero
   CONN_WRITE_POISONED / replay-write-failed; broker thread count returns to bounded
   baseline. HEAVY nextest group at birth.

Binding gate additions: redispatch D1/D1b + REDISPATCH-STALL T1–T7 + mesh-recovery legs stay
green every leg (retire machinery + registry gate = substrate). Field discriminator
post-release: hertz's snapshot poison count freezes; brain CPU drops to noise; broker thread
census bounded.

### W2 — ENDPOINT-LIFECYCLE (todlando; REQs registered, activate at W2 start)

1. **L1 `REQ-ENDPOINT-ONLINE-TRUTH`** (emphasys C1, P0): creator gate — `cmd_listen` stamps
   ONLINE only from actual persisted state/hosting authority, never from manifest
   `psyche_init` alone; livehost reconcile SPLITS control cleanup (clear
   `controlled`/`driven_by`/`viewer_count` for every endpoint absent from session truth,
   regardless of state/controllable) from offline classification (live_agent+controllable
   only); legacy hybrid rows self-heal after a SUCCESSFUL broker query (broker failure ≠
   empty set); terminal signoff/owner-loss = atomic CAS-guarded offline + ready/address
   removal without overloading soft api session-end (/clear preserves the live listener).
2. **L2 `REQ-STREAM-LEASE-CLASSES`** (emphasys C2, P0): explicit stream lifetime classes —
   RC attach/view streams are **ConnectionBound** (opener conn EOF ⇒ FIN/detach at the
   target: serve_attach sees EOF, detach_session runs, controller slot + CONTROLLED stamps
   clear); inter-brain durable streams stay durable. NEVER globally retire on Brain
   disconnect (breaks brain-swap). detach identity preserved (late close from opener A can't
   evict newer controller B — rides W1 R6's ownership machinery + REDISPATCH-STALL fix 6
   generation tokens). Builds directly on W1's unsubscribe/teardown verb — same
   neighborhood, built once (the standing C2×stall-fix-4 coordination ruling).
3. **L3 `REQ-REST-TERMINAL-NORMALIZE`** (rest-normalize, P0): ONE store-level atomic
   mutation for definitive hosted-session loss — status=offline + rest_state=suspended +
   clear dormant_since_ms in the SAME info.json write; used at authoritative
   broker-session-loss/liveness-reap + `cmd_stop`. NOT `daemon_rest_event(Suspend)` (no
   edge when effective already Suspended); NOT reader-side blanket offline⇒suspended
   (destroys explicit-Wake semantics). Kills the zombie WAKE_RESUME loop.
4. **L4 `REQ-CONTROL-STAMP-CONVERGENCE`** (emphasys C3, P1): session/generation-validated
   stamp convergence (or serialized per-endpoint stamp transitions) so a pre-reap
   KIND_SESSIONS poll can't relatch `controlled=true` after the exit-waiter reap; no fs I/O
   under global locks (KH 7.12); every no-session path clears all three control fields.
5. **`REQ-HAZARD-ENDPOINT-LIFECYCLE`** (KH 7.45 umbrella): regression matrix from the three
   reports — dead-PID hybrid row does NOT survive reconcile; raw viewport close frees the
   controller (full chain, broker restart included); definitive death ⇒ offline+suspended
   atomically, next reconcile NO WAKE_RESUME; explicit Wake still launches exactly once;
   poll-vs-reap interleave converges to cleared stamps.

### W3 — SPAWN-TRUTH (todlando; REQs registered, activate at W3 start)

1. **S1 `REQ-SPAWN-FRESH-TRUTHFUL`**: broker = atomic authority; split spawn policies
   Fresh/RejectExisting vs ResumeOrReuse; new `KIND_SPAWN_FRESH` wire verb with typed
   conflict (distinct kind fails LOUD on an N-1 broker vs a silently-ignored serde field),
   sharing the single-flight/PTY spawn body. Fresh at AlreadyLive/lost-race returns the
   typed conflict, NEVER `Spawned(existing)`. CLI contract: exit 1 + stable
   `ENDPOINT_CREATE_CONFLICT:<id>:…no session was created`, no attach, no state mutation,
   never kills the live session. Product ruling (standing): omitted `--resume` keeps the
   documented create-default = fresh-and-conflict same as explicit `--create`.
2. **S2 `REQ-ENDPOINT-CYCLE-HONEST`**: the ALREADY_LIVE dup-guard liveness-probes the
   claimed session's client tree before refusing (dead client tree ⇒ reap + respawn
   honestly); reconcile shutdown's NO_EDGE state machine with the dup-guard so the two
   "is it live" sources can't contradict (the deployah stop/run wedge shape).

W3 rides the same release; if W3 slips, operator call whether v0.37.0 ships W1+W2 only
(waves are independently releasable — W1 alone fixes the active field regression).

## Lane + release

- todlando builds per wave on `build/registry-lifecycle-w<N>` branches; PR per wave; doyle
  gates each in an isolated worktree + throwaway target (binding memories: nextest, clippy
  --workspace preflight, grep-old-behavior-tests, seam-test sweep).
- **hertz root-causes every NEW roadblock that arises during the milestone** (operator
  ruling 2026-07-17): unexplained build/gate reds, field anomalies, wedges — route to hertz
  for RCA first; doyle rules on the RCA; todlando fixes. Nobody fixes an ungrounded red.
- `traceable-reqs check` green before any wave declares done.
- Release: deployah drives v0.37.0 counter 64, bump rides IN the release PR, CHANGELOG by
  observable user surface (field census: 15%/11% core CPU gone, attached-endpoint stalls
  from Registry poisons gone, zombie wake loops gone, --create truthful), notes in end-user
  voice per RELEASE-RUNBOOK step-2 standard.
- hertz field-verifies against their preserved baseline (poison count freeze + CPU + thread
  census + WAKE_RESUME absence are the discriminators).
