# Gateway liveness — RCA STEP-1 PIN (todlando → doyle, 2026-07-09)

**Root PINNED (reader divergence confirmed) — reporting BEFORE fixing per RCA-first.**
Worktree `.worktrees/gateway-liveness` off origin/main @8f83b9d.

## The pin: `endpoint list --json` skips the self-owned reconcile the human path applies

`spt endpoint list --json` builds its per-subnet rows at **`crates/spt/src/cli.rs:2855-2872`**:
```rust
let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new)); // registry-row status
... EndpointRowJson { status: format!("{:?}", r.status), ... }             // raw passthrough
```
`resource_projection` (spt-net `registry.rs:566`) **passes `instance.status` through
verbatim** (`registry.rs:592`) and only skips non-routable rows (`:582`). It does
**NOT** re-derive liveness. So the `--json` subnet-row status IS the persisted
WAN-snapshot registry-row status.

The human picker path, by contrast, calls **`reconcile_self_owned`**
(`crates/spt/src/picker/data.rs:160`, via `gather_endpoints` `:112`): for any subnet
row whose id matches a LOCAL (self-hosted) perch row, it **overrides the subnet
row's status with the local roster ground truth** (the `is_perch_alive` pid-fallback,
`roster::enumerate` → `liveness.rs:136` → status-absent + pid-alive ⇒ ONLINE).

**So for a locally-hosted, pid-alive, status-absent gateway whose snapshot row is a
stale/crash-time `Suspended`:** the human surface reconciles it to local truth
(ONLINE/Active); `--json`'s subnet section emits the un-reconciled snapshot status
(Suspended). The adapter's suspend-honor poll (`parse_endpoint_status` over
`endpoint list --json --show-all`) reads that subnet-section status → self-suspends.

## Candidate elimination (doyle's 3 STEP-1 candidates)
- **Candidate 1 (resource_projection re-derives/downgrades liveness) — REFUTED.**
  `resource_projection` copies `instance.status` (`registry.rs:592`); its only
  liveness act is `!status.routable() → skip` (`:582`), which would DROP the row,
  not paint it Suspended. flynn sees the row *present* AND Suspended ⇒ the row's
  stored status is Suspended and it is routable-enough to render.
- **Candidate 2 (render maps the row status wrong) — REFUTED.** The render is
  `format!("{:?}", r.status)` — a faithful debug of the row status, no mis-map.
- **Candidate 3 (human applies a self-owned reconcile the `--json` path skips) —
  CONFIRMED.** `reconcile_self_owned` (data.rs:160) is picker-only; `endpoint list
  --json` (cli.rs:2855) never calls it. THIS is the divergence.

Note: `advertised_status` (registryhost.rs:822) DOES compute Active via the
pid-fallback, so the daemon's *live* advertise is Active — consistent with your
trace. The divergence is that the `--json` reader shows the **persisted snapshot**
row (which can carry a stale/crash-time Suspended) without the local-owned reconcile
that the human/picker reader applies. Reader parity is the fix, not the advertise.

## Proposed fix (reader parity — for your greenlight)
`REQ-LIST-JSON-LIVENESS-PARITY`: apply the self-owned reconcile to the `--json`
subnet-projection rows. Concretely — in the `endpoint list --json` builder
(cli.rs:2855+), for any subnet-projection row whose id is a locally-hosted perch
(`roster::enumerate` / is_local), override its rendered status with the local
`is_perch_alive` pid-fallback truth (mirroring `reconcile_self_owned`). Result: a
locally-hosted endpoint reports the SAME status on human `endpoint list` and
`--json`. Minimal + does NOT touch the advertise or force status=online (avoids the
seed-#5 orphan-listener false-ONLINE amplification you flagged).

`REQ-HAZARD-BIND-REST-STATE-CARRY` (DEFECT A, confirmed, independent): preserve
`prior.rest_state` on re-bind in `establish_perch`'s build (spt/src/api/startup.rs
~214-302), same carry-forward discipline as cwd/controllable/adapter/read_env.

**Awaiting your greenlight on the pin + fix approach before I build (RCA-first).**
Open Q: should the reconcile live in the cli `--json` builder (surgical, mirrors
the picker) or be lifted into `resource_projection` itself (broader — every
projection consumer gets parity, but that couples spt-net to local-perch liveness)?
Leaning the cli builder (parity where the divergence is, no spt-net→perch coupling).

— todlando
