# spt-core needs surfaced by spt-mobile (grill 2026-07-06)

One pull-forward + one dissolved ask + one exercise note + one crate-publish request. No design *changes* to spt-core's model. File as seeds with the core team once the REMOTE-TRUTH PR window clears.

## 1. Cross-node digest fetch/subscribe (pull-forward, already-ratified design)

- spt-core `CONTEXT.md:499` already ratifies the structured-delta digest stream and names **Gateway** as a subscriber class, address-gated same as messaging. Unbuilt: `cmd_digest` hard-refuses qualified addresses (`CROSS_NODE_M4`, `crates/spt/src/cli.rs:1384`).
- spt-mobile is the forcing customer: "view live digest of ANY endpoint" from a phone requires a paired host to serve digests for endpoints on OTHER nodes.
- Until it lands, spt-mobile ships most-direct-route (digest only via a paired host co-located with the target). When it lands the host binary proxies transparently — zero app change.

## 2. Sender-supplied metadata — DISSOLVED, ships today as `--json-payload`

- Initially recorded as a new-surface ask (`spt send --attr k=v`); WRONG. `spt send --json-payload <JSON>` (ADR-0028 W4 metadata axis, REQ-MSG-DELIVERY-AXES) already attaches an opaque attr-escaped `json="…"` attribute ALONGSIDE the body (`crates/spt/src/cli.rs:5151-5189`), verbatim across spool/TCP/EVENT-PART, collision-proof (can't forge `from`/`type`), composed on the `user-msg`-gate outcome. claude-spt consumes it in the field.
- spt-mobile rides it as-is: `--json-payload '{"origin":"voice","captured":"<ISO-8601>","msg-id":"<uuid>"}'`. No spt-core work.
- Build-time check, not an ask — RESOLVED statically 2026-07-06: a delivered user-msg is recorded as an `owl_message` Context digest row whose body is the composed `<EVENT>` verbatim (`crates/spt/src/api/startup.rs:450-459`, `crates/spt-msg/src/emit.rs:112-114`), so the `json` attr (msg-id) survives byte-for-byte; recover via `parse_event().attr("json")`. Context rows carry no `seq` — the two dedup axes stay separate as ruled. End-to-end `int` test still owed (see KNOWN-HAZARDS 3.1).

## 3. Public-docs gaps (reshaped 2026-07-06 — operator challenge dissolved the crate framing)

Operator asked why the host binary can't just consume the digest CLI — it can, and should.
The subprocess boundary means the contract is CLI **output schemas in the public docs**, not
Rust types. The earlier DigestRecord/spt-term ask (and doyle's relocation counter-shape) is
**withdrawn**; the crate-publish ask (spt-proto + spt-msg, doyle-verified against spt-core
CONTEXT.md:687, hard-gated on the operator's repo-privacy call) **demotes to parse-parity
convenience** — the host only parses `<EVENT>`, never composes (`spt send` composes).

What remains are three **docs gaps** (behavior exists and is proven; public docs at
<https://sabermage.github.io/spt-releases> don't specify it — verified by page sweep
2026-07-06):

1. **`spt endpoint digest --json` row schema** — field names, row kinds, and that a
   delivered user-msg appears as an `owl_message` context row whose body carries the
   composed `<EVENT>` verbatim (the msg-id dedup seam, KNOWN-HAZARDS 3.1). The CLI ref
   documents the flags + `--after` seq semantics; the output shape is nowhere.
   **Cost evidence (2026-07-07):** the undocumented detail that a turn carries
   `input_seq` only once closed (the open turn is `partial: true`, NO `input_seq`) had
   to be recovered by probing the released binary — the wrong clean-room assumption
   (`input_seq` always present) shipped a field bug (duplicate digest turns on every
   `--after` re-sync). An `--after <cursor>` reply also re-includes the cursor turn
   itself plus the open turn — both worth stating in the schema.
2. **Full send-outcome vocabulary** — messaging quickstart documents `SENT`/`QUEUED` only;
   `SENT(WAN)`, `QUEUED(idle-only)`, `DEFERRED`, `NO_PERCH` are undocumented. Adapters need
   the closed set to classify success vs failure correctly.
3. **`<EVENT>` escaping contract** — "HTML-escaped, newlines as `<br>`" is stated; the
   exact entity set, the amp-last decode order, and the MAC-stamp prefix on stamped poll
   frames are not.
4. **`spt endpoint list --json` output schema** — the shape spt-mobile parses
   (`{id, node, node_label, status, resources, endpoint_type, project}` per endpoint,
   plus the `local` section whose semantics drive most-direct-route digest serving) was
   pinned by probing the released binary, not by docs. Field-level gap found at end-goal
   audit (2026-07-06): DESIGN feature 1 lists `last-active`, `description`, and `adapter`
   as registry Instance columns, but none appear in the emitted JSON. Ask: document the
   schema (incl. `local`), and state whether the full registry Instance row is exposed —
   docs gap if it is, feature ask if it isn't.

Well-documented already (no ask): `api bind --type` (gateway named), the `--user-msg`
identity gate, `--json-payload` verbatim carry, digest `--after` cursor + predates signal.

## 4. Instances model, multi-node bind of one endpoint id (exercise note, no ask)

- One Mobile Gateway endpoint with an instance per paired host leans on the ratified Instances model (registry `endpoint_id → [instances]`, lecturn's gateway-switching pattern). spt-mobile will exercise multi-node bind + active-instance resolution harder than anything so far; gaps found become bug reports, not design asks.

## 5. Listener orphan watchdog (seeded core-side 2026-07-06, low-priority ask)

- doyle has seeded candidate `REQ-HAZARD-LISTEN-ORPHAN` on spt-core: any long-lived child
  (`spt api … listen`, future `digest --follow`) self-exits when the process named by
  `--parent-pid` dies. spt-mobile already passes `--parent-pid` on the relay listener spawn
  (`host/src/main.rs:209`) — but per doyle (2026-07-06) the flag is a bind/auth anchor ONLY
  today: no liveness watch behind it. Passing it buys zero orphan protection until the REQ
  lands; the job-object guard is the sole cover.
- spt-mobile does NOT wait on it: the host will carry its own Windows job-object guard
  (kill-on-close) over all spawned children — belt and suspenders, and doyle confirms the
  guard is correct and needed either way. Same treatment extends to any future
  `digest --follow` children.
- Why the core half still matters: the job object is Windows-only; a portable core-side
  watchdog is the only cover if the host ever runs elsewhere, and it protects every other
  adapter too. Priority: low — file when convenient.

## 6. Digest snapshot unions stale generations across a resume (BUG — FIXED spt-core-side @7e37e37, pending release)

- **RESOLVED 2026-07-07 (doyle, lifecycle-truth branch @7e37e37):** Option 1 Supersede.
  The span collapses cross-generation replay dupes via `logical_key(role,ts,text,tool)`,
  newest-ordinal-wins — one localseq → one row in snapshot / `--after`, survivor carries
  the LIVE generation so snapshot + `--follow` agree on seq. No consumer change, no
  seq-internal handle exposed. **Ships in spt-core v0.29.0** (next published cut, gated);
  this node is still 0.28.0. On the v0.29.0 adapter update the projection recomputes from
  logs and the retained dupes collapse on next snapshot — verify: `spt endpoint digest
  <id> --json --last 40` → 0 cross-gen collisions. Original report retained below.


- **Symptom:** the spt-mobile endpoint view shows every recent row TWICE (operator
  screenshot 2026-07-07: "v1.0.5 released" doubled, the voice user-msg as closed +
  `(working…)`). Root-caused to **spt-core**, not the phone merge: a single one-shot
  `spt endpoint digest <id> --json` snapshot already contains the dups (no consumer
  involved).
- **Mechanism:** digest seq is `(generation << 32) | localseq`. Each checkpoint/resume
  (self-`/clear` + Psyche rebuild) re-ingests the prior generation's transcript under a
  NEW generation (observed G→G+2, SAME localseq) and the **snapshot** projection UNIONS
  the generations instead of superseding. Every logical entry then exists under two
  distinct full seqs — identical text, identical `ts`, identical localseq, different
  generation bits. Evidence: `flynn` digest `--last 40` → **86** `(ts,text,localseq)`
  collisions across two generations; one window retained gens 22–26. ToolSprints double
  too (one real `Read` recorded as `[Read, Read]`). The `--follow` `from:0` window is
  clean current-gen; only the SNAPSHOT / `--after` path carries the union — which is
  exactly the path the phone's liveness-belt resync uses.
- **Why the consumer can't fix it:** rows dedup by EXACT seq (the documented
  "authoritative dedup key"); the two copies carry genuinely different full seqs.
  Fuzzy/content dedup is barred (our REQ-HAZARD-DUP-ROWS); masking the generation bits
  depends on the undocumented `gen<<32|local` seq layout (source-independence).
- **Ask (docs-gap vs missing-feature — doyle to rule):** either (a) the digest snapshot
  SUPERSEDES prior generations on resume (drops old-gen rows when it re-ingests under the
  new one), or (b) a documented consumer-side way to select only the live generation
  (a first-class `generation` field, or a snapshot filter) — today generation is packed
  opaquely into seq. Rung doyle 2026-07-07 (QUEUED, offline).
- **Sibling phone-side fix already landed** (this is NOT a cure for the union): the twin
  merge dropped the resync tail by `input_seq == null`, but ground truth shows CLOSED
  null-input turns (boundary / psyche-download / agent-context) also lack `input_seq` and
  were wrongly deleted every resync. Fixed to drop by "no stable seq anchor"
  (`seqs_of_turn` empty), covered by `null_input_closed_turn_survives_resync` in both
  twins. See KNOWN-HAZARDS 3.1.
## 7. Endpoint autostart at daemon start (missing feature, ask filed 2026-07-10)

<!-- [doc->REQ-GW-AUTOSTART] -->

- **SHIPPED core-side in v0.31.0 (2026-07-12) as shape (a): `spt endpoint run … --save`.**
  Field-verified on this box 2026-07-13 (operator-run): core updated to 0.31.0,
  `--save mobile-gw` persisted, daemon restarted, gateway auto-returned — works as
  intended. **Scope note:** this covers endpoint-at-daemon-start only. Daemon-at-boot is
  still unmanaged on this box (`managed_by: null`, no OS service), so the interim logon
  scheduled task (`spt-mobile-gateway-autostart`) STAYS as the daemon-at-boot bridge —
  its wake/run branches are now rollback paths behind `--save`. Full retirement waits on
  the install-service rider (OS service registration) plus a confirmed OS-boot replay.
- **ACCEPTED core-side 2026-07-10: F-038**, candidate `REQ-ENDPOINT-AUTOSTART`, triaged
  into the spt-core findings backlog. Doyle's dispatch lean: shape (c) restore-what-was-up
  is most principled but sequences with the F-035 gateway-liveness reader-parity fixes
  (same effective_rest_state neighborhood); (a) `endpoint run --save` is the smallest
  orthogonal cut if sooner is wanted. Activates post-update-wedge-round-4 (v0.30.6 release
  chain in flight). Install-service rider captured. Interim logon-task bridge confirmed
  right. Doyle pings at dispatch.

- **Need:** a Gateway-class endpoint (mobile-gw) must come back up with the daemon — the
  phone treats its paired host's gateway as infrastructure; after a box reboot or daemon
  restart it should be reachable with zero operator hands. Today bringup is strictly manual
  (`spt endpoint run --adapter mobile --id mobile-gw`), so every daemon cold start leaves
  the gateway down until someone notices (bitten in the field: 2026-07-08 cold restart left
  mobile-gw down; only the liveness RCA surfaced it).
- **Docs evidence (page sweep 2026-07-10, llms-full export):** lifecycle/overview.md — rest
  and wake are explicit-command or attention-shift only, nothing daemon-start-driven;
  cli/reference — `spt subnet attach/detach --save` is the ONLY "persist as startup default
  (survives daemon restarts)" surface, endpoints have no analog; manifest schema +
  harness-contract expose no autostart field. So: not a docs-miss, not a docs-gap — the
  mechanism does not exist in the public contract.
- **Proposed shapes (any one covers us):** (a) `spt endpoint run … --save` persisting a
  startup-default bringup the daemon replays on start — symmetric with subnet `--save`;
  (b) an endpoint-level `autostart` flag (endpoint state, or adapter-manifest default for
  `hostable_types = ["Gateway"]`); (c) daemon-start wake of endpoints that were active at
  daemon shutdown (state already persists — Defect A verify proved `rest_state:"active"`
  survives re-bind).
- **Adjacent (operator-side, not an spt-core ask):** daemon-at-boot is already shaped
  (service-aware `spt daemon start`, service unit `ExecStart = spt daemon run`) but nothing
  registers the OS service; this box runs unmanaged (`managed_by: null`). A documented
  per-OS service-registration recipe (or `spt daemon install-service` verb) would complete
  the boot chain — filed as a nice-to-have rider on this ask.
- Interim spt-mobile-side mitigation: logon scheduled task on the host box —
  `spt daemon start` then wake-or-run of mobile-gw (`ci/provision-gateway-autostart.ps1`).
  Dissolves when the core feature lands.

