# Digest double-vision — diagnosis (2026-07-07, flynn)

**Symptom:** flynn endpoint view in spt-mobile shows every recent row twice
(screenshot `Screenshot_20260707-041744_spt-mobile.png`): "v1.0.5 released" agent
message doubled (both closed); voice user-msg doubled (one closed, one `(working…)`).

## Root cause — UPSTREAM in spt-core's digest projection (NOT the phone merge)

A single one-shot `spt endpoint digest flynn --json` snapshot **already contains the
duplicates** — the phone never enters the picture.

spt-core seq = `(generation << 32) | localseq`. On each **checkpoint/resume**
(self-`/clear` + Psyche rebuild), spt-core re-ingests the prior generation's transcript
under a **new generation** (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.

Evidence (`spt endpoint digest flynn --json --last 40`), 86 collisions of
`(ts, text, localseq)` under two generations. Exact screenshot rows:

| row | gen A seq | gen B seq | ts | note |
|-----|-----------|-----------|-----|------|
| `**v1.0.5 released**…` | (23,208) | (25,208) | 11:14:05 | bug 1 (both closed) |
| voice `<EVENT user-msg mobile-gw>` (turn input) | (23,209) | (25,209) | — | bug 2 |
| `**It landed.**…` | (23,210) | (25,210) | 11:15:30 | in voice turn |
| `Screenshot shows…` | (23,215) | (25,215) | 11:20:33 | |
| `Checkpoint written…` | (23,217) | (25,217) | 11:21:11 | |

Generations retained in one window: gen22×10, gen23×93, gen24×14, gen25×167, gen26×8.
Pairing is G↔G+2 with identical localseq. ToolSprints also duplicate (one real `Read`
recorded as `[Read, Read]`).

**Both screenshot doublings are the SAME defect** — bug 1 vs bug 2 differ only in whether
the newer-generation copy was already closed (bug 1) or still open/replaying (bug 2,
`(working…)`) at snapshot time.

## Why the phone can't fix it within contract

Digest rows dedup by **exact seq** (REQ-HAZARD-DUP-ROWS, the documented "authoritative
dedup key"). The two copies carry genuinely different full seqs → nothing collapses. The
only phone-side workarounds are both barred:
- **fuzzy (ts/content) matching** — explicitly forbidden by REQ-HAZARD-DUP-ROWS.
- **mask the generation bits (dedup by localseq)** — depends on the undocumented
  `gen<<32|local` seq structure (source-independence violation) and localseq can legitimately
  recur across genuinely different turns.

The `--follow` `from:0` window is scoped to the CURRENT generation and is clean; it's the
**snapshot path** (`EndpointViewModel.resync` at open + every 15s liveness-belt Timeout)
that re-pollutes the view with the multi-generation buffer.

## Ask for doyle (spt-core)

Triage (AGENTS.md): docs-gap vs missing-feature. Either:
1. digest snapshot should **supersede** prior generations on resume (drop the old
   generation's rows when it re-ingests under the new one), OR
2. 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.

## Secondary (phone-side, real but NOT the screenshot cause)

`DigestViewState.applySnapshot` / `digest.rs::apply_snapshot` drop the open tail with
`inputSeq == null`. Ground truth: **null-input turns (Boundary / Context / agent-context)
are CLOSED yet carry NO `input_seq`** — 9 such turns in the live window. The correct
open-turn discriminator is `partial == true`, not `input_seq == null`. Current code would
wrongly drop legitimate null-input closed turns on every resync. Fixtures (`partialTurn`)
must also model a null-input CLOSED turn to lock this. Fix app-side after the spt-core
ruling.
