# M5-D6 — presence resolution (JIT plan)

**Status:** authored 2026-06-05 · D6a shipped `1ded0b7` FINAL (`Instance.last_active_ms`
+ `advertise_local` carry) · D6b shipped (`spt-daemon::presence` MRA API; `first_fire`
= the one swap point — update-consent + grants escalation route through it;
`FirstFireOutcome::RemoteTarget` + `dispatch::surface_fresh_rows` receiving half;
`resolve_wake` WAKE_FORWARDED via D5b rest op; loopback int E2E) · D6c shipped
(PresenceChannel seam note) · REQ-PRES-1 `[impl, unit, int]` · **D6 CLOSED** (`10e2dcc` CI-green-FINAL; next:
`M5-D7-PLAN.md`). Upstream: D5 closed (`aa88033`,
CI-green-FINAL — see `M5-D5-PLAN.md`). D6 ships presence *resolution only* (M5 scope
decision 1): the presence datum gossiped subnet-wide + ONE first-class
most-recently-active (MRA) API swapped into the four per-feature precursors. The
PresenceChannel endpoint stays deferred (D6c documents the seam). Authoritative:
**CONTEXT §presence**, M5-PLAN §D6, `REQ-PRES-1` (registered, `required_stages = []`).

## Goal

Anywhere the system needs "the user's most-recently-active session" — notif
first-fire, update-consent delivery, consent escalation, shell wake resolution — one
API answers it from data that is **subnet-wide**, not node-local: activity shifting
nodes redirects notif/consent delivery cross-node.

## Researched code map (explorer-verified 2026-06-05)

- **Heartbeat (the signal):** `lifecycle.rs:180-183` `touch_active` →
  `info::set_last_active(perch, now_ms)` once per pulse tick, **only when
  Active/recordless** (lifecycle.rs:192-207 — Dormant checks auto-suspend instead,
  Suspended only ingests; a resting seat never wins recency). Sole writer.
- **Registry emit (the ride):** `registryhost.rs:198-282` `advertise_local` — scans
  Self perches, derives `Status` from `resting::read_rest` (229-237), mints a fresh
  epoch per tick (247, EpochSource), emits `RegistryUpdate` per **visible** endpoint
  per subnet; peer pump drives it on `cadence.registry` (peerloop.rs:256). Snapshots
  → `identity/registry/<subnet>.json` atomic (304-315) — the CLI read contract.
- **Inbound gate:** `registryhost.rs:57-75` `RegistryGatePolicy::admits` = member
  subnet ∧ trusted origin, fail-closed, reloaded per feed; flips detected only on
  observed Dormant→Active with epoch bump (175-182).
- **The four MRA precursors (all LOCAL-perch-only today):**
  1. notif first-fire — `notif.rs:99-134` `most_recently_active_where` (the shared
     resolver core; scans `list_self_perch_ids` + `is_perch_alive` + max
     `last_active_ms`) / `most_recently_active_visible` (subnet-visibility-scoped);
     `first_fire` consumes at notif.rs:167.
  2. update-consent delivery — `peerloop.rs:349-351` via
     `consent::most_recently_active` (delegates to the notif core, unscoped).
  3. consent escalation — `grants.rs:99-108` (same delegate; escalation notif routed
     to the target perch).
  4. shell wake resolution — `shellwake.rs:267-282` `resolve_wake`: local rest record
     only; **no local owner perch ⇒ refusal naming the instantiate-anywhere deferral**
     (the "active elsewhere" arm explicitly parked for D6/D8c, line 281).
- **Wire precedent:** `Instance.resources` (`spt-net registry.rs:88-89`) — an
  additive serde-default field riding the existing epoch lease + `RegistryUpdate`
  replication; "not a separate registry." Presence does exactly this.
- **Remote rest op (D5b, available):** `resthost::request_rest` — a wake can be
  fired at another node through the remote-drive trust class.

## Decisions (locked)

- **The datum is a field, not a feed:** add `last_active_ms: Option<u64>` to
  `spt_net::registry::Instance` (serde default + skip-if-none — a pre-D6 row parses
  clean, the `resources` precedent verbatim). `advertise_local` copies the perch's
  `last_active_ms` into the row at scan time. No new gossip channel, no new cadence:
  presence rides the registry tick, visibility-gated by construction
  (`advertise_if_visible` path). Granularity = the registry cadence — fine for a
  routing heuristic.
- **MRA compares wall-clock ms, and that is OK here:** epochs stay the only
  *merge-precedence* key (per-node monotonic, never cross-compared — the lease is
  untouched); `last_active_ms` is cross-compared **only** as a routing heuristic
  (presence is best-effort by definition; clock skew degrades to a suboptimal
  surface target, never to wrong data winning a merge). Document this split loudly
  in the presence module docs.
- **One API, new module `spt-daemon::presence`:**
  `most_recently_active(owlery, regs, local_node, scope) -> Option<PresenceTarget>`
  where `PresenceTarget { id, node, last_active_ms, local: Option<perch_path> }` —
  the max over (local live perches) ∪ (visible registry rows with the datum,
  routable status only). `scope` carries the subnet-visibility closure the notif
  variant needs; consent/grants use the unscoped form. The notif.rs core
  (`most_recently_active_where`) becomes the local leg of this resolver; keep its
  signature working (consent.rs delegate unchanged) — swap internals, not callers'
  shape, except where the caller must now handle a **remote** winner.
- **Cross-node redirect = convergent local decisions, no new wire op:** every node
  holds the same gossiped presence data, so every node computes the same MRA winner.
  A producer whose MRA winner is **local** surfaces exactly as today; a producer
  whose winner is **remote** skips the local surface (target-less, the existing
  boundary-resurface fallback posture) and lets the notif row ride the EXISTING
  notif replication; the winner's own node surfaces it on its feed-apply/pump tick
  (a new "surface what's addressed to my MRA endpoint" check beside
  `apply_notif_feed` — reuses `first_fire`'s mark discipline). Skew race worst case
  = duplicate surface on two nodes; the existing suppression window
  (`SUPPRESSION_WINDOW_MS`) is the dedup. KH 4.3 stale-tolerance carries.
- **Wake resolution swap is the *refusal upgrade*, not the attach arm:**
  `resolve_wake`'s no-local-owner branch consults the MRA API; owner present on
  another node ⇒ (a) fire the D5b remote wake (`request_rest(Wake)` — machinery
  exists, remote-drive trust class) and report `WAKE_FORWARDED:<node>`; the shell
  stays offline locally (cross-node shell *link* is D8c — say so in the report).
  Owner nowhere ⇒ today's `WAKE_NO_REACHABLE_INSTANCE` refusal unchanged.
- **REQ-PRES-1 activation:** `[impl, unit]` at D6a, add `[int]` at D6b's loopback
  leg. D6c is a doc note (PresenceChannel seam — CONTEXT §presence + DEFERRED.md row
  already notes "M5 ships resolution"; the seam note names the broker PresenceLog +
  REQ-EP-4 as the future consumer's substrate).

## Pieces (build order — one CI-green slice each)

1. **D6a — the gossiped datum** (`REQ-PRES-1` → activate `[impl, unit]`).
   `Instance.last_active_ms` additive field; `advertise_local` carries it from
   `info.json`; lease/serde tests (newer epoch supersedes the datum, lagging update
   drops, pre-D6 row parses clean — the `resource_blurb_rides_the_lease_and_serde`
   pattern); visibility gating unchanged by construction.
2. **D6b — the MRA API + the four swaps + cross-node redirect.**
   `spt-daemon::presence` module; swap notif first-fire, update-consent, grants
   escalation; `resolve_wake` refusal upgrade (remote wake forward via D5b);
   feed-apply surface check for remote-won rows. Loopback int test (two in-proc
   brokers, production dispatcher + pump pieces): endpoint more-recently-active on
   "node B" per gossiped datum ⇒ a consent notif produced at A surfaces at B, not A;
   shift recency to A ⇒ surfaces at A. Tag `[int->REQ-PRES-1]`; rig `[twohost]` leg
   waits for D9a.
3. **D6c — PresenceChannel seam note** (docs-only, rides D6b's commit): the deferred
   consumer (dispatch/bind/thread styles) builds on this datum + the broker
   PresenceLog seam (REQ-EP-4).

## NOT in D6
- PresenceChannel endpoint — deferred (M5 scope decision 1).
- Cross-node shell link/attach on wake — D8c (the wake *forward* ships; the link
  does not).
- OS-level input-activity signal — deferred (agent-interaction heartbeat is the v1
  signal).
- Rig two-host legs — D9a.

## Conventions (carried)
- NO `cargo fmt`; tag evidence in the same commit; `traceable-reqs check` before done.
- Linux clippy `-D warnings` + `--no-default-features` stay green.
- Push per slice → sha-pinned FINAL; never push over an in-flight run.
- Unix liveness probes: zombie rule (`spt_store::proc` zombie-aware since `78e9e18`).
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
