HERTZ -> DOYLE TRIAGE: two perri lifecycle defects (2026-07-16) Context - `perri` currently has info.json `status:"offline"`, `rest_state:"active"`, `controllable:true`, no ready marker, dead/stale resume custody, and newest ledger row `anchor-int-proof` (the separately-proven synthetic SessionStart contamination). - This report is read-only diagnosis and suggested fixes; no endpoint/core state was mutated. ISSUE A — definitive endpoint/session death leaves durable rest intent Active Root cause 1. `rest_state` is intent/state for active/dormant/suspended, distinct from liveness. Cold liveness normally masks any stored Active to effective Suspended (`crates/spt-daemon/src/resting.rs:225-236`). 2. Definitive session loss only stamps `status=offline`: `BrainLifecycle::mark_offline` (`lifecycle.rs:567-568`) and livehost liveness reconciliation (`livehost.rs:665-744`). They do not update `rest_state` or clear `dormant_since_ms`. 3. `spt endpoint stop` has the same gap: `crates/spt/src/cli.rs:6935-6948` removes ready/unregisters and stamps offline only. 4. Wake-resume does NOT use effective state. `livehost.rs:322-405` reads raw stored rest state and treats raw Active as an outstanding wake intent. Reconcile ordering (`livehost.rs:997-1048`) therefore offlines a dead session, then a later pass resumes it from the ledger. 5. Exact perri sequence: `daemon.stderr.log.1:928` DAEMON_RESTART_RESUME_REFUSED due live custody; `:932-934` clears control and records LIVENESS_RECONCILE_OFFLINE while preserving Active; later `:1026` and `:1305`, then current log `:82,:1335,:2410,:2733,:2933`, repeatedly logs `WAKE_RESUME:perri ... anchor-int-proof`. Classification - Missing terminal session-loss transition, not an honest `offline + active` steady state. - Caveat: cold+Active can be legitimate briefly immediately after an explicit Wake; wake writes intent first and reconcile consumes it. A blanket rule "offline implies persist suspended" in every reader would destroy wake semantics. Suggested fix A - Add one store-level atomic mutation for definitive hosted-session loss: set `status=offline`, set `rest_state=suspended`, clear `dormant_since_ms` in the same info.json write. - Use it in the authoritative broker-session-loss/liveness-reap path and `cmd_stop`. - Do not implement as `daemon_rest_event(Suspend)` after stamping offline: effective state is already Suspended, so the transition table returns no edge and leaves raw Active unchanged. - Do not fire transition echo after the harness is already dead. This is terminal-state normalization, not a graceful active->suspended edge. Graceful `endpoint shutdown` retains its existing echo-before-teardown path. - Preserve explicit Wake by normalizing only at a definitive online/unbound -> offline/session-reaped observation or explicit Stop, never on every scan of an already-cold Active row. Focused tests A - Store unit: atomic offline+suspended mutation clears dormant anchor and cannot expose a mixed pair. - Livehost regression: broker session disappears from online Active endpoint -> offline+suspended; next reconcile emits no WAKE_RESUME. - `endpoint_stop_marks_offline` extension: also asserts rest_state=suspended and anchor cleared, including already-offline/raw-Active input. - Explicit wake guard: suspended/offline -> Wake writes Active; next reconcile launches once rather than being normalized away. - Restart custody race: RefuseLivePid followed by a valid bind must not strand the successfully revived seat suspended; normalize only on definitive loss. ISSUE B — `endpoint run --create` silently reattaches/no-op-acks instead of creating Root cause 1. CLI help promises "Mint a fresh session" (`crates/spt/src/cli.rs:359-360`). 2. Dispatch deliberately discards `create` (`cli.rs:1385-1393`); later code only sees `resume == None`. 3. Live preflight (`cli.rs:1988-2018`) maps broker-live + default attach/view to Reattach, and broker-live + `--start` to `ENDPOINT_ALREADY_LIVE` exit 0. It cannot distinguish explicit fresh intent. 4. Even if the preflight misses/races, the broker universally deduplicates every labeled SpawnReq (`crates/spt-daemon/src/broker.rs:3235-3302`) and returns ordinary `Spawned(existing_session)`. SpawnReq/Spawned have no policy/disposition (`msg.rs:260-333`), so CLI can print apparent success for a spawn that never occurred. 5. Existing tests codify the safety behavior, not the freshness contract: `cli.rs:2376-2390`, `crates/spt/tests/run_no_dup_session_e2e.rs:275-319`, and `wake_single_flight.rs`. 6. For perri, logs prove repeated automatic wake-resume sessions around the incident. [INFERENCE] The exact create invocation was either caught by CLI preflight (likelier) or broker dedup race; no surviving exact argv/output distinguishes them. Both paths produce no fresh session. Suggested fix B - Preserve the no-duplicate invariant, but separate broker operations/policies: * Fresh/RejectExisting for explicit/implicit create. * ResumeOrReuse for daemon wake/restart resume. - Recommended wire shape: new `KIND_SPAWN_FRESH` with typed conflict, sharing the same broker single-flight/PTY spawn body. A distinct kind is safer against an N-1 broker: old broker fails unknown-kind loudly rather than silently ignoring a new serde-defaulted policy field. - Broker is the atomic authority. At AlreadyLive or when a concurrent claimant wins: * Fresh returns typed conflict; never returns ordinary Spawned(existing). * ResumeOrReuse returns existing session as today. - CLI preflight may remain only as a fast UX check; it cannot close the TOCTOU race. - Fresh conflict contract: exit 1; stable stderr e.g. `ENDPOINT_CREATE_CONFLICT:: a live broker session already exists; no session was created`; no automatic attach/view, no `ENDPOINT_RUN*`, no state mutation. Do not kill/replace the live session. - Because help says create is the default, omitted `--resume` should currently get the same fresh-and-conflict semantics as explicit `--create`. If product wants omission to mean ensure/attach, define and document a third mode; do not silently overload explicit `--create`. - Scope guarantee precisely: create selects `[session.self]`, generates fresh provisional session material, and requires a new broker PTY; it reuses endpoint identity/history. Actual harness-conversation freshness remains adapter-owned. Focused tests B - Unit decision matrix includes request intent: fresh+live => CreateConflict for start/attach/view; no live => Spawn. - CLI/broker E2E: `--create` over live returns 1, exact conflict token, no attach, same single broker PID/session. - Broker race: two concurrent fresh requests => exactly one Created and one Conflict; never two children and never Reused-as-success for loser. - Mixed fresh vs resume race => one launch tree; fresh caller gets Created or Conflict, resume may reuse. - Offline/raw-Active plus stale ledger but no broker session => create selects self, ignores/preserves old ledger, and adds newly bound harness session. - IPC compatibility: new client against old broker fails loud, never reports Created. Suggested triage split - A: lifecycle/store correction at terminal hosted-session loss + stop. - B: broker protocol/API semantic split; larger than a CLI patch because CLI-only refusal leaves the race. - Keep the two fixes separate: A removes stale auto-resume contenders; B makes create truthful even with legitimate concurrent/live sessions.