# Gateway liveness fix — DISPATCH (doyle → todlando, 2026-07-09) — RCA-FIRST

Field bug (flynn, operator's live phone-comms link mobile-gw DOWN). Root LOCALIZED via flynn's 3-probe field data + doyle source trace. **The operator held an earlier dispatch on an incomplete root — this is the reconciled clean root. RCA-first: pin the exact site, report, then fix.**

## Confirmed root — a READER DIVERGENCE (not the earlier psyche-gate theory)
At the SAME instant, for the SAME pid-alive locally-hosted gateway (mobile-gw, status field ABSENT in info.json, pid alive):
- **human `spt endpoint list`** → **ONLINE** (renders local perches via `roster::enumerate`, roster.rs:72 = `is_perch_alive` WITH the pid-fallback: status-absent → `is_process_alive(pid)` → alive).
- **`spt endpoint list --json`** → **Suspended** (renders the endpoint via `resource_projection` over the registry snapshot, cli.rs:2855 → `EndpointRowJson.status` = the registry row status).
- flynn confirmed mobile-gw IS advertised in all 3 subnet registry files (NOT a re-advertise gap — his earlier "empty by_id" was a walker error, retracted).

The adapter's suspend-honor poll reads `--json` (`parse_endpoint_status` over `endpoint list --json --show-all`) → sees Suspended → self-suspends the gateway, despite pid-alive + human-ONLINE.

## Key source facts (already traced)
- `advertised_status` (registryhost.rs:822-833) DOES feed `effective_rest_state` the pid-fallback `is_perch_alive` (line 825). So the DAEMON's advertised value should be Active for a pid-alive gateway — the divergence is NOT the advertise computation.
- So the `--json` Suspended comes from the projection/rendering path dropping the pid-fallback the human local-roster applies. **Candidate sites to pin (STEP 1):**
  1. `resource_projection` (spt-net registry.rs:566) — does it re-derive/filter liveness (e.g. `is_registry_entry_alive`, status-based: liveness.rs:152, which for a status-ABSENT row = not ONLINE||UNBOUND → stale/offline) and DOWNGRADE the row to Suspended/offline, ignoring the advertised Active? (its test `resource_projection_filters_hidden_and_offline` @registry.rs:1821 says it DOES filter on liveness.)
  2. OR the registry ROW's advertised status is genuinely Active but the `--json` render maps it wrong.
  3. OR the human path applies a `reconcile_self_owned` local-truth override for locally-owned endpoints that the `--json` path skips (roster.rs:52 mentions reconcile_self_owned overriding stale subnet gossip).
- **DEFECT A (separate, CONFIRMED, stands):** `establish_perch`'s build (spt/src/api/startup.rs:214-302) carries `cwd/controllable/adapter/read_env` forward from the prior record but NOT `rest_state` → a re-bind writes a fresh InfoJson that WIPES the wake intent (flynn's tick11 rest_state:active vanish).
- **DEFECT B (RECLASSIFIED to LATENT — do NOT make it the primary fix):** the `status=online` write in `cmd_listen` (startup.rs:486-491) is gated on `live_capable` (manifest `[session.psyche_init]`); a gateway has no psyche_init so it never writes status=online. BUT the pid-fallback is DESIGNED to cover an absent status and DOES for the roster + advertised_status. The operator's ready_agent counterexample proved this: ready_agents work because claude-spt declares psyche_init (live_capable-by-manifest), NOT because of a gateway-specific path. So the bug is the READER parity, not the missing status write. (v0.8.0 gate, not a regressor; the trigger was the crash.)

## The fix (after STEP-1 pin)
**Reader parity:** `endpoint list --json` must report the SAME status for a locally-hosted endpoint as the human `endpoint list` does — i.e., honor the pid-fallback / local-owned truth (`roster::enumerate` / `reconcile_self_owned`) so a machine consumer never sees a different liveness than a human. A pid-alive locally-hosted gateway must read the same (ONLINE/Active) on both surfaces.
- Fix at the pinned site (most likely: apply the local-roster / self-owned reconcile to the `--json` local-endpoint rows, OR make `resource_projection`'s liveness for a locally-owned row honor the pid-fallback instead of a status-only check).
- Do NOT reach for DEFECT B (forcing status=online for gateways) as the fix — it risks the seed-#5 orphan-listener falsely-ONLINE amplification; the reader-parity is the correct, minimal fix. (If STEP-1 shows the cleanest fix genuinely is establishing status for a relay-holding listener, flag it back to me before taking it — that's a design call, not a silent choice.)
- **DEFECT A:** preserve `prior.rest_state` on re-bind in `establish_perch`'s build (same carry-forward discipline as cwd/controllable).

## REQs (mint at build-start, activate-don't-pre-fail)
- `REQ-LIST-JSON-LIVENESS-PARITY` — `endpoint list` human and `--json` report identical status for a locally-hosted endpoint (esp. a pid-alive status-absent gateway).
- `REQ-HAZARD-BIND-REST-STATE-CARRY` — re-bind preserves the resting intent.

## Gate (what I'll check)
- **Regression int/unit:** a pid-alive locally-hosted gateway (status-absent, no psyche_init) reads the SAME status on human `endpoint list` AND `endpoint list --json` — both non-Suspended. This is the load-bearing assertion (the exact flynn divergence).
- re-bind preserves rest_state (unit).
- clippy `--workspace --all-targets -D warnings` 0; `traceable-reqs check --json` 0.
- Full-workspace nextest green (no sibling breakage — this touches the list/projection + establish_perch, shared surfaces).
- Build in a fresh `.worktrees/` worktree off current origin/main (0825053, W2 just merged), NOT the shared checkout.

Field-verify partner: flynn (mobile-gw). RCA-first — pin STEP-1 site + report before fixing. Qs → doyle.

— doyle
