# Blind-read: `endpoint digest --json` contract page (W4 acceptance gate)

**Reader:** perri (claude-spt adapter author) · **Surface:** `docs-site/src/reference/json-shapes.md`
§"Session digest — `endpoint digest --json`" @ `d039636` (raw, pre-release) · **Date:** 2026-07-19

**Disclosure up front — I am not a clean-room reader.** claude-spt already consumes this surface
(`tools/claude-spt/src/interrupt_watch.rs`), and two shipped RCAs (v0.25.1, v0.25.2) were caused by
exactly the asymmetries this page now documents. I answer as asked — from the page alone — but where
my prior scars agree or disagree with the page I say so explicitly, because that comparison is worth
more to you than a pretend-naive read.

---

## 1. What I would build from this page alone

**Shape:** poll `spt endpoint digest <id> --json`, parse stdout as ONE JSON object
(`{turns[], version}`). No `2>&1` merge needed — the page states stderr carries no trailer under
`--json`, so stdout-alone and merged parse identically.

**Poll loop, as the page leads me:**

1. Timer tick → `digest <id> --json --after <cursor>` (cursor omitted on first pull).
2. Read top-level `version`. If unchanged from the last pull, **stop — nothing changed.** The page
   sells `version` as the cheap "is a pull worth making" check, so I treat it as a change-detector
   only, never as a cursor.
3. If `after_predates_window: true` is present, I missed rows: discard incremental state and treat
   this pull as a full-window refresh (log the gap — a silent hole is worse than a loud one).
4. Walk `turns` oldest → newest, `entries` in stream order.
5. **Advance the cursor to `max(seq)` over `Agent`/`ToolSprint` entries ONLY.** The page is explicit
   that this is the number space `--after` filters on, and that `Boundary`/`Context` never carry
   `seq`. I never write `version` into `--after` — the page calls that out as a trap that forces a
   full-window refresh every tick.
6. Order injected entries (`Boundary`/`Context`) by `ts` (RFC3339-UTC), since they have no `seq`.

**Provenance handling** — the table drives this directly: `Agent`/`ToolSprint` = the agent did
something; `Boundary`/`Context` = spt bookkeeping, the agent did *not* act. Any "is the agent
working?" question keys on the agent-produced kinds alone. The page's warning — *never key "newest
thing seen" on the presence of injected entries* — is the single most load-bearing sentence on the
page for me.

**What I would NOT build from this page:** a `--follow` consumer (see §2).

---

## 2. What I had to INFER rather than read

Ranked by how much it would have cost me:

1. **`--follow --json`'s delta shape is undocumented.** The page says it "streams per-update deltas
   instead of the snapshot shape above" and stops. I cannot parse a shape I have never seen: is it
   NDJSON, one JSON value per line? Is each delta a `Turn`, an entry, or a patch? Does it carry
   `version`? Is there a resync/heartbeat frame? **Consequence: a blind author cannot use `--follow`
   at all** and silently falls back to polling — which is what I would do. This is the page's
   biggest hole.
2. **`--last <n>` does not exist on this page.** My shipped watcher uses `--last 12` (a window),
   not `--after`. A blind reader would build a pure `--after` cursor and never learn a window flag
   exists. Either it belongs here or the page should say windowing is spelled `--after`-only.
3. **`version`'s lifetime.** "Monotonic counter that bumps each time the projected digest changes" —
   monotonic *within what*? Per endpoint, presumably. Does it survive a daemon restart, or reset to
   0/1? A consumer that skips pulls on `version == last` **stalls forever** if it reset lower, and
   re-pulls forever if it jumped. I inferred "persistent per endpoint" with no basis.
4. **Cursor progress during an open turn.** `seq` is "blanked on an open/partial turn", and `partial`
   marks the open trailing turn. So while a turn stays open, `version` bumps but `max(seq)` cannot
   advance — my cursor stands still while the digest visibly changes. The page implies this; it never
   says it. A poller that assumes "version bumped ⇒ my cursor will advance" livelocks on a long turn.
5. **Whether injected entries can land on an ALREADY-CLOSED turn.** "Stream order" reads as
   append-to-newest. From scars I know a `Context`/`echo_mirror` can be filed onto an *older* turn
   after the fact — which means a turn you already consumed can change. The page's warning covers the
   symptom, not this mechanism.
6. **`ts` vs `seq` consistency.** Injected entries order by `ts`, agent entries by `seq`. Are the two
   guaranteed consistent (no clock skew / no reordering) when interleaved? Inferred yes.
7. **One JSON value per invocation.** Stated as "self-contained", which I read as exactly one value
   on stdout — but not said outright. My shipped parser defensively reads only the FIRST value.
8. **Interrupts are invisible.** There is no `Interrupt` entry kind. An agent interrupted by Esc
   produces no distinguishable entry — detecting it means string-matching harness marker text inside
   `input`/`text`. That is exactly what my watcher does, and it is unreadable from this page. If
   interrupt detection is a supported consumer use case, the enum owes it a kind; if it is not, the
   page should say interrupts are out of scope so nobody builds on a string match by accident.

---

## 3. Surprises against what claude-spt does today

- **The provenance table is my v0.25.2 bug, pre-solved.** My interrupt-heal treated *any* non-empty
  `entries` on the latest turn as "agent resumed". Core files `Context`/`echo_mirror` entries onto
  the interrupt-marker turn seconds after the Esc — so a stuck agent read as resumed, the watcher
  stood down, and a live agent black-holed inbound for 37+ minutes. Had this table existed, that
  release would not have shipped. **This page is worth its weight for the provenance column alone.**
- **The stdout/stderr parity paragraph settles a false alarm I filed.** In v0.25.1 I reported a
  "node-wide trailer outage" that turned out to be a `2>&1` observation artifact (doyle corrected me).
  This paragraph now makes the human-vs-`--json` split unambiguous. Please keep the parenthetical
  about the human path keeping `DIGEST:<id> version=N` on stderr — that sentence is what makes the
  earlier confusion impossible to repeat.
- **`--after` is not what my adapter uses.** I window with `--last`; the page teaches cursoring. Two
  different consumption models. Mine is deliberately dumb (a window is immune to cursor stalls on
  open turns), but a blind reader has no way to arrive at it — see §2.2/§2.4.
- **`after_predates_window` is better than what I built.** I have no "I missed rows" signal at all;
  I re-scan a fixed window and hope. I will adopt this.
- **Mild surprise:** `version` bumping only when the projection *changes* means it is a content hash
  in effect, not a write counter. Good for me (idle polls are free) — but it makes the reset-on-restart
  question in §2.3 sharper, not softer.

---

## Verdict

The page is buildable for a **polling snapshot consumer** — I could write that loop from this text
with no source access and get the cursoring right, which is the gate's question and the answer is
yes. It is **not** buildable for a `--follow` consumer, and it leaves a poller exposed to two silent
stall modes (`version` lifetime, open-turn cursor freeze) that read as "working fine" until they are
not. Fixing §2.1–2.4 would close the gap; §2.8 is a scope call I cannot make for you.
