---
name: v0350-midturn-spans-disjointness
description: "2026-08-29 SHIPPED + FIELD-VERIFIED v0.35.0 — mid-turn AGENT_OUTPUT spans (--mid) plus the disjointness fix that removed a second, cursor-blind transcript reader; Release A of the #22 compliance migration, B now unblocked."
metadata: 
  node_type: memory
  type: project
  originSessionId: c7810e19-679a-4f30-bebf-26af403899a0
  modified: 2026-08-30T02:04:27.521Z
---

**2026-08-29 — claude-spt v0.35.0 SHIPPED, installed, field-verified.** spt-core v0.66.0 landed
`spt-releases#238`, which unblocked `#22`. Release **A** of two.

## What shipped

`spt api state busy <id> --payload-stdin --mid` publishes a MID-TURN `AGENT_OUTPUT` span. Before
this, only the turn's *closing* assistant message was ever published, so everything written before a
tool call reached no io consumer at all — the measured 12.5% (4 of 32 tag sends over ~25h) that
`#22` was HELD on. Fed from `ScanOutcome.authored`, the same cursor-guarded batch that feeds the
PreToolUse now-signal poll.

**FIELD-VERIFIED through core's own instrument** (`spt api io-events perri --after 0 --json`):
`seq=21 kind=AGENT_OUTPUT mid=True` — the first span this adapter has ever published — with every
AGENT_OUTPUT before the install showing `mid=None` and the first after showing `mid=True`. A natural
before/after control. Then 5 AGENT_OUTPUT rows: 2 spans, 3 closers, **0 duplicate payloads**.

## ⭐ THE REAL WORK WAS A DEFECT THE FEATURE EXPOSED, NOT THE FEATURE

`turn_closing_output` read the transcript from **offset 0** and returned the last non-empty
assistant text — while `scan_and_dispatch` runs FIRST in `handle_stop` and had already advanced the
byte cursor past that same text. **Two readers of one transcript, one of them cursor-blind.** Free
while nothing parsed the payload; an overlap the contract forbids the moment spans ship. Fix: the
closing payload reads the same batch, so disjointness holds BY CONSTRUCTION from the cursor that has
prevented double tag-dispatch since v0.17.x. A dedup check would have been the wrong shape — it
re-derives at every Stop what the cursor already knows, and passes the day two texts differ by a byte.

## ⭐⭐ A COST THAT IS NOT CONSTANT ACROSS A MIGRATION EARNS A RELEASE BOUNDARY

Core: *"does not deduplicate and never will... Overlap is not refused; it is delivered twice."*
Before `[io] compliance = true` an overlap is a duplicated **record** (visible in `io-events`,
harmless). After the flip it is a duplicate **message delivered to a peer**, found by the recipient.
So spans shipped with compliance OFF — putting the first field test of disjointness on the NEAR side
of the flip that makes a mistake externally visible. Ship the risky property where it is still cheap
to be wrong.

## ⭐⭐⭐ A TEST WHOSE FIXTURE CANNOT REACH THE CODE READS EXACTLY LIKE A TEST THAT HOLDS

`only_the_ups_busy_report_carries_a_payload` passed `transcript_path: ""` → scan bails → batch empty
→ **the span leg is unreachable**. It kept passing when the leg landed, proving nothing. The full
suite went 543→550 green with the new leg completely uncovered. Renamed, given a real transcript,
and given a **positive control asserting the leg was actually reached**.

## ⭐⭐⭐⭐ MY "DEFINING" MUTATION TEST TESTED THE HAPPY PATH

M1 (restore the cursor-blind reader) **ESCAPED** `a_tool_bearing_turn_publishes_each_text_exactly_once`:
with a *distinct* closer, a cursor-blind read still yields one span + one different closer. The
overlap only reproduces when the turn **ends on the span** (no closing message). Added
`a_turn_ending_on_a_span_does_not_republish_it`; M1 then failed as intended. **Write the test for the
shape that breaks, not the shape that works** — and mutation-test the guard, or you never find out.

Also: my mutation harness classified `error: test failed` as a compile error, printing "DID NOT
COMPILE" for three mutations that had actually been **caught**. Instrument bug reading as a result.

## ⭐⭐⭐⭐⭐ Two self-inflicted traps worth not repeating

- **Overwrote `IO-COMPLIANCE-PLAN.md` with `Write` without reading it** — destroying findings 1–5
  (the `tag_scan` deletion inventory, the settle trap, the schema-staleness note). Recovered with
  `git show HEAD:` and amended instead. Look at the target before overwriting, always.
- **A CHANGELOG `Edit` replaced the previous version's heading** instead of inserting above it, so
  0.34.0's notes sat under 0.35.0 and the release body would have carried both. Caught only by
  grepping `^## \[0\.3` for the notes extraction. When inserting a new section, assert the OLD
  heading still exists afterwards.
- Leak guard caught `claude-spt-bs#22` (the PRIVATE home repo) in a new `hook.rs` doc comment.
  Private issue refs belong in `traceable-reqs.toml` (excluded), never in mirrored source.
- A background `cmd; echo EXIT=$?; tail` reported **exit 0 while the gate had failed** (`tail`'s
  code). Read the output; the harness's exit code is the pipeline's last command.

## State / next

Repo clean at `d7b68c6`, tag `v0.35.0` on both remotes (public mirror commit `8344199`), asset
verified at the consumer, installed on this node. Floor moved to core **0.66.0**.

**Release B is now UNBLOCKED** (gated on A7, which passed): declare `[io] compliance = true` and
delete `tag_scan`'s peer-dispatch half **in the same change** (the published ordering rule — no
version may exist where both parse). Plan + the surviving deletion inventory live in
`IO-COMPLIANCE-PLAN.md`. One open question recorded there: our confirm UX (`sptc_tag_send`, the
parked turn-end confirm, `REQ-TAG-CONFIRM-NEXT-HOOK`) is ours and rich, while `DISPATCH_RESULTS` is
declared *"the only channel, by design"* — B must not keep a second confirm channel alive beside it.

New requirements: `REQ-IO-MIDTURN-SPAN-REPORT`, `REQ-HAZARD-IO-SPAN-OVERLAP` (hazard 8.2 in
`docs/KNOWN-HAZARDS.md`). See [[v0330-io-funnel-hold]] and [[v0340-nowsignal-pretool-agent-output]].
