---
name: digest-json-consumption-contract
description: "REFERENCE 2026-07-20 — rules for consuming `endpoint digest --json` (version/seq pair, follow deltas, depth-as-shared-state, teardown trap), learned across an 8-round blind-read of spt-core's contract page; includes OUR OWN shipped contamination defect."
metadata: 
  node_type: memory
  type: reference
  originSessionId: 5327083b-be0b-4d72-8796-780ec5f26dcb
  modified: 2026-07-20T08:22:29.240Z
---

Operational rules for any claude-spt code that reads `spt endpoint digest`. Earned across an 8-round
acceptance read of spt-core's `json-shapes.md` (PR #41, publishes at spt-core **v0.39.0** — read the
PUBLISHED page from then on; the pre-release exception is retired).

**Change detection.** `version` is **process-lifetime** — resets on daemon restart, so a `>` compare
stalls forever and a bare `!=` can *silently skip* on a collision. Rule: version gating is an
optimization, never a correctness boundary. Compare the pair `(version, highest agent-produced seq)`
**at a constant window depth**, and *also* poll unconditionally on a slow cadence — the pair is blind
to any change that moves no committed agent-produced `seq` (open-turn content AND injected entries,
on closed turns as much as open ones).

**seq.** `seq` = `(ledger_ordinal << 32) | line index`, projected from on-disk records, so it
**survives daemon restart**. `version` is the only process-lifetime number on the contract.
`Boundary`/`Context` never carry `seq` at all; open-turn entries carry none until they commit.

**Identity / de-dupe.** Follow: `(turn index, entry index)`, trailing entry is **replace-in-place**
(a growing `ToolSprint` overwrites its own seq/ts — a content hash counts one sprint many times).
Snapshot: key on `input_seq` / `seq`; a leading preamble turn has no `input_seq` (and is where
`Boundary` entries live); injected entries have no durable id at all.

**Depth is SHARED STATE — the big one.** The daemon holds one projected digest per endpoint; every
request publishes its own projection into it. A non-default `--last` is therefore a **write**: it
collapses every concurrent follower to that depth via a `from == 0` full replace and oscillates
`version` for everyone, with no content change. Programmatic consumers use the **default depth**;
`--last` is a human/one-shot flag. To get a deeper window, an adapter sets `window_turns` in its
manifest `[digest]` — **also the only way to deepen a `--follow` stream**, which always uses the
effective default. Profiles are sparse **leaf-replace** and the **overlay wins**; the layer is
**per endpoint** (`spt api endpoint-info <id>` → `adapter` = `<adapter>[:profile]`, absence = base).
Make every layer your endpoints *can* resolve against declare the depth you need — the endpoint that
breaks you is the next one created.

**Liveness — never from digest.** A torn-down endpoint does **not** look empty: the digest projects
from on-disk records, so one *with history* serves real frozen content indefinitely; only a
record-less one is empty. Silence and content both tell you nothing. Establish liveness out-of-band
(`endpoint list` / `api endpoint-info`). A `--follow` stream ends observably on daemon stop, exits
**0** either way (never branch on it), resubscribe **with backoff**, and a stream ending before its
base update never established. Every subscription opens with a full base at `from == 0`.

## OUR OWN DEFECT — FIXED, SHIPPED v0.25.5 (2026-07-20)

`interrupt_watch.rs` pulled `--last 12` every 15s **per hosted live endpoint** against a manifest
`[digest] window_turns = 6` — claude-spt contaminated the hub continuously on every node (caught
live: a passive follow showed `from=0` frames alternating depth 6↔12). Shipped fix: **`window_turns`
6 → 12 in the manifest, `--last` dropped from the watcher** (pull at the effective default). Pinned
by a unit test that reads `adapter/claude-spt.toml` and asserts `window_turns >= WINDOW_TURNS` AND
that no `[profiles.ccs.digest]` leaf exists (a leaf would leaf-replace the base table and drop the
depth). That test first *failed on its own substring check* — the manifest prose names the leaf — so
match a real table header (`line.trim() == "[profiles.ccs.digest]"`), never `contains`.

**Rule this leaves:** an adapter never asks for depth per-pull. Declare it in the manifest; every
programmatic consumer takes the default. The queued `--follow` migration is now UNBLOCKED (it was
blocked because follow always uses the effective default and would have silently narrowed marker
detection 12 → 6).

Filed spt-core seeds carrying what docs could not fix: hub contamination (ships with my repro
frames), `DIGEST-FOLLOW-NONEXISTENT-ENDPOINT-INDISTINGUISHABLE`, `DIGEST-INTERRUPT-HAS-NO-KIND` (our
interrupt detection rests on a harness marker STRING in `Turn.input` — a wording change silently
stops healing), run-identity for the version collision, and the exit-0-on-abnormal-end lie.
