# spt-mobile

An Android app that gives its user a first-class presence on SPT subnets from a phone: browse endpoints, watch live digests, exchange messages, and pipe Pebble voice transcripts to agents. The phone is a **Gateway** in spt-core's sense — the intelligence at this endpoint is the user. Design grilled 2026-07-06; spt-core's `CONTEXT.md` remains authoritative for all subnet-side meaning (endpoint, subnet, digest, instance, `user-msg`, spool).

## Language

### Identity & topology

**Mobile Gateway**:
The ONE gateway-type endpoint that is the user's phone-presence on the subnet, with an **instance per paired host** (spt-core Instances model). Agents address a single id; replies resolve to the active instance.
_Avoid_: phone endpoint, mobile node, per-host endpoints

**Host binary**:
The spt-mobile adapter's endpoint binary running on a subnet node. Owns that node's Mobile Gateway perch instance, its own iroh node key (distinct from the daemon's), the device-link listener, the host spool, and the conversation history log. Talks to its daemon over the local `api` surface only.
_Avoid_: server, bridge, proxy, gateway daemon

**Paired host**:
A subnet node running the host binary that the phone has completed pairing with. Paired hosts hold a user-assigned **host priority** (an ordered list); the first *reachable* paired host serves any given operation.
_Avoid_: gateway host (lecturn's term for the same role — fine in cross-project talk, not canonical here)

**Device link**:
The phone-initiated iroh connection between the app's embedded Rust core and one host binary, addressed by the host binary's node key (never an IP). Authenticated by the pairing token.
_Avoid_: tunnel, socket, API connection

**Pairing**:
The one-time ceremony binding phone↔host binary: desktop shows a QR carrying the binary's node key + a per-phone pairing token; phone scans and dials forever after. Distinct from spt-core's **subnet join** — the phone never becomes a subnet member.
_Avoid_: login, registration, subnet join

**Reachable**:
A paired host whose device link dials successfully right now. Reachability is a property of the HOST, never of a target endpoint — a reachable host relaying to an offline endpoint still succeeds (spt spools it, `QUEUED` = success).

### Messaging & history

**Star (starred endpoint)**:
A per-paired-host pointer to exactly one endpoint. The voice pipe routes to the first reachable host's star, walking host priority. One star per host; re-star is instant retarget. Stars live on the phone; the subnet never sees them.
_Avoid_: favorite, pin, default target

**Voice pipe**:
The chain webhook receiver → phone spool → `user-msg` send to the current star. Every transcript traverses it; none are dropped.

**Webhook receiver**:
The localhost HTTP listener inside the app's foreground service that accepts the Pebble Index-webhook POST (multipart: `transcription`, `recordedAt`, `client`). Always answers 200 and enqueues; Pebble has no persistent retry, so the receiver's availability is the loss boundary.

**Phone spool**:
Persistent on-phone FIFO of not-yet-forwarded outbound sends (voice transcripts and typed messages alike), drained in order to the first reachable host. The phone-side mirror of spt's own offline spool, one hop earlier.

**Host spool**:
The host binary's per-phone durable queue of inbound messages awaiting a live device link. Drained on reconnect; drain feeds the conversation history cache, so nothing is lost, only late.

**Conversation history**:
The host binary's append-only per-endpoint log of both directions (inbound agent messages + the phone's own sends), keyed by msg-id. Source of truth; the phone holds a synced cache that is the UNION across paired hosts (msg-id collapses overlap). Not the digest — history is an archive, the digest is a rolling glance.

**msg-id**:
A client-stamped identifier unique to each phone-originated send. Enables exact dedup between history rows and digest echoes. Rides inside the `json="…"` metadata attribute of spt-core's existing `--json-payload` send axis.

**Interlaced view**:
The per-endpoint timeline the app renders: conversation history merged with digest rows, ordered by timestamp, collapsed by msg-id (own sends) and digest `seq` (digest rows).

**Voice tag**:
Metadata marking a transcript's origin — `origin:"voice"` + `captured` (ISO-8601 from Pebble's `recordedAt`) — carried in the envelope's `json="…"` attribute (spt-core `--json-payload`). Late-flushed dictations read correctly at the receiving agent.

## Flagged ambiguities

- **"starred" ≠ favorites list.** A star is a routing target, one per paired host. If a favorites/sort concept arrives later it gets its own word.
- **The phone is never a node.** Same resolution lecturn made: no daemon on the device in v1; the hybrid path is Android becoming an spt-core build *target* later, at which point the device link swaps for local IPC — app logic unchanged.
- **Failover keys on host reachability only.** A reachable host whose starred endpoint is offline does NOT trigger failover; the send spools subnet-side (`QUEUED` = success).
- **History ≠ digest.** spt-core CONTEXT.md:493 defines the digest as a rolling glanceable buffer. spt-mobile's archive need is met by conversation history, a spt-mobile concept.

## Example dialogue

> **Dev:** Pebble fired a dictation while I was on the subway — where is it?
> **Domain expert:** The webhook receiver 200'd it into the phone spool. No paired host was reachable, so it sat there; when you surfaced, the app dialed host priority in order, host 2 answered, and the transcript went as `user-msg` to host 2's star, voice-tagged with the capture time.
> **Dev:** But host 2's star is doyle, and doyle's node was down.
> **Expert:** Doesn't matter — host 2 was reachable, so no failover. spt spooled the send; doyle gets it on wake. `QUEUED` is success.
> **Dev:** And why do I see the transcript once, not twice, in doyle's timeline?
> **Expert:** Your own send carries a msg-id. When doyle's digest later echoes it, the interlaced view collapses the digest row into your history row.
