# 0048 — The idle-edge contract: activity observation and digest seal authority

Date: 2026-07-24
Status: accepted
Amends: ADR-0008 lineage (session digest, via CONTEXT.md's revised digest section); extends the shell frame vocabulary (REQ-SHELL-FRAME-VOCAB)

<!-- REQ set minted at IDLE-EDGE W1 activation 2026-07-24 (doyle): REQ-ACTIVITY-{LINK-PUSH,INFO-PULL,LIST-JSON} + REQ-DIGEST-{SEAL-ON-IDLE,CROSS-NODE-PULL}; plan = IDLE-EDGE-JIT.md -->

## Context

Two independent field consumers converged on the same seam in the same week —
the **idle transition**, the adapter-reported moment an endpoint stops working:

- **perri's spt-rebound-tool** (a `kind="shell"` adapter that nudges its idle
  owner) hard-depends on observing its owner endpoint's busy/idle *transitions*.
  No surface existed: the idle state is a perch sentinel written by the
  adapter's report command, readable by the daemon's delivery routing, exposed
  to nobody. perri's non-core build completed against a mock seam and the core
  seam became their critical-path blocker on 2026-07-24.
- **liam's spt-progress-tool Liaison** hit the **mint-then-idle deadlock**: a
  digest turn was *sealed* (assigned the stable `seq` a scanner keys on) only
  when the **next user input** arrived, so a finished-but-idle turn sat
  `partial` and seq-less indefinitely — a scanner could never see a tag in the
  owner's latest turn until the owner happened to be prompted again. The
  workaround ("drive one more command after any tag") is a discipline no
  consumer should have to know.

The model already holds the answer to "when is a turn finished": the
activity/idle detection entry rules that idleness is **adapter-reported, never
inferred** (not PTY-quiescence, not heuristics). This ADR makes that one
signal authoritative for two new consumers — and deliberately keeps the digest
out of the activity business.

The operator ruled all decisions below on 2026-07-24 (rebound grill). CONTEXT.md
carries the canonical terms (activity observation; digest turn sealing;
cross-node instance resolution); this ADR carries the why and the rejected
shapes.

## Decisions

### 1. Activity observation is a first-class surface with two avenues — and the digest is not one of them

<!-- [doc->REQ-ACTIVITY-INFO-PULL] --> <!-- [doc->REQ-ACTIVITY-LIST-JSON] -->

**Push (shells only):** an owned Shell observes its owner's busy/idle
transitions as an **activity frame** on the **existing shell-link event
stream**. Link-scoped: the owner is implied by the link token — no addressing,
no new authorization surface, no new verb.

**Pull (everyone else):** current activity state is readable via
`spt api endpoint-info` (single endpoint, point-in-time) and as a per-endpoint
`activity` (busy|idle) key on `endpoint list --json` (many-at-once roster
survey).

**Rejected — riding the digest structured-delta stream.** The digest is a
*content* surface sourced from normalized session logs; activity truth is a
*perch sentinel*. Wedging state-about-the-endpoint into a content stream
dilutes the glossary term, and the delta stream is unbuilt — the activity need
would have dragged a whole subscriber machinery in by the ear. perri's original
"idle/busy in `endpoint digest --json`" rider dissolved into the pull avenues.

**Rejected — a new subscription verb.** A whole new surface for one consumer
class, when the shell link already carries owner↔shell traffic in both
directions and its lifecycle gives the restart story for free.

### 2. Activity frames are drive-class: ephemeral, latest-wins, current-state-carrying

The shell channel taxonomy already distinguishes **command** (discrete,
durable) from **drive** (continuous, ephemeral, latest-wins). An activity frame
is drive-class: it carries the *current* state, a redundant same-state resend
is a harmless no-op (the consumer derives edges), and a missed frame is
superseded by the next. **Never spooled, never replayed** — replaying a stale
transition after a relink would be actively wrong. Current state is emitted on
**every link establishment and re-link**, so a restarted consumer (or daemon)
resynchronizes with zero extra machinery.

### 3. The latency promise is bounded observation, anchored by a transition timestamp

The contract promises **a frame per transition, promptly — sub-second class,
never hard-real-time**. Each frame carries the **transition timestamp**: when
the state *took effect* (the sentinel flip), not when the frame was emitted.
Edge-anchored consumers (rebound's idle-countdown) anchor to that timestamp,
so emission latency self-corrects out of their arithmetic.

**Rejected — promising synchronous-with-report delivery.** That would weld the
contract to the report command's plumbing. Bounded observation leaves the
implementation free to start with daemon-side observation and later upgrade to
report-path triggering with **no contract change**; the timestamp makes the
difference invisible to consumers either way.

### 4. The idle transition seals the digest turn

<!-- [doc->REQ-DIGEST-SEAL-ON-IDLE] -->

A turn's records gain their stable `seq` when the turn **finishes**, and the
authoritative finish signal is the **idle transition** — the same
adapter-reported signal decisions 1–3 instrument. Next-input arrival, the old
seal trigger, is demoted to a harmless no-op fallback. Sealing is
**idempotent and seq-stable**: it assigns `seq` to what the log holds at idle,
and a late-flushing straggler record folds into the sealed turn without
changing the assigned `seq`. The mint-then-idle deadlock dies at the source.

**Which seq that names** (clarified 2026-07-24, A2 ruling — wording, not a
decision change): "the assigned `seq`" that never changes is the turn's
`input_seq`, the cursor anchor. Entry-level seqs inside the turn follow
last-idx-wins and MAY advance FORWARD as a straggler folds into a trailing
sprint — which is how that straggler reaches an `--after` consumer at all.
Forward-only is the invariant; a backward or arbitrary move is a defect.

**Rejected — extractor-observed log completion.** "The log looks done" is a
heuristic — the same inference class the activity model explicitly bans
(PTY-quiescence redux, one layer up).

**Rejected — provisional seqs on partial turns.** Seq stability is the entire
point of seq; a seq that can change is a scanner bug factory.

### 5. Cross-node digest reach is pull-first

<!-- [doc->REQ-DIGEST-CROSS-NODE-PULL] -->

The `CROSS_NODE_M4` refusal on qualified addresses lifts for the **snapshot
pull** (`endpoint digest <id@node>`, including `--after <seq>` incremental
polling) under the **existing address gate** — the access model was ratified
long ago; this is routing, not policy. The **delta stream stays node-local**
until a real cross-node subscriber exists (the same defer-until-consumer
discipline applied to #70's spawn-on-named-node leg the same day). Note the
composition: seal-on-idle's stable seqs are precisely what make cross-node
`--after` polling trustworthy — a remote consumer polling a sealed turn can
never observe its seq shifting under them.

## Consequences

- The idle sentinel graduates from delivery-routing input to a **published
  contract signal** with two external consumer classes (rebound's edge loop,
  the digest sealer). Its write path is now load-bearing for latency: the
  bounded-observation promise caps how lazily the daemon may notice a flip.
- The shell frame vocabulary gains the activity frame; the published vocabulary
  doc (REQ-SHELL-FRAME-VOCAB lineage) must carry its name, attrs (state +
  transition timestamp), and drive-class semantics — adapter authors must
  never be tempted to treat it as a durable event log.
- `endpoint-info` and `endpoint list --json` gain additive keys — N-1 parsers
  unaffected per the additive-evolution posture.
- Scanners keyed on `seq` (alchemy-class) may drop their drive-one-more-command
  discipline once seal-on-idle ships; liam re-verifies the
  scanner-sees-latest-turn case at ship.
- perri's locked contract (message SENT 2026-07-24) is binding: any deviation
  from decisions 1–3 during build is a contract break and goes back through
  the operator.
