---
name: io-funnel-two-release-wave
description: "2026-08-28 SHIPPED claude-spt v0.32.0 + pacer v0.6.0 — pacer reads codes off the IO funnel (AGENT_OUTPUT) instead of parsing its owner's digest. A published kind with no emitter; a new flag on a safety-critical call; my class-rule comment that was still a list."
metadata: 
  node_type: memory
  type: project
  originSessionId: 5bc82780-5ceb-4afe-8302-0a2ffad05c64
  modified: 2026-08-29T00:29:14.420Z
---

**BOTH SHIPPED + FIELD-VERIFIED same day**, spt-pacer-tool#14. pacer retired its bespoke
agent-output detection in favour of spt-core 0.64.0's `AGENT_OUTPUT` io kind.

⭐ **A PUBLISHED KIND IS NOT AN EMITTED KIND — check the emitter before consuming.** `AGENT_OUTPUT`
rides the activity report an adapter already makes (`spt api state idle --payload-stdin`), and
**claude-spt passed no payload at any of its eight `state idle` call sites** (validated scan: 0
payload sites vs 151 control hits for `state busy|idle`). The kind existed, was documented, and
nothing on this harness had ever produced one. Shipping pacer#14 as written would have made pacer
**deaf to every control code**. Hence two ordered releases: claude-spt v0.32.0 first, verify, then
pacer v0.6.0. *When adopting a new upstream surface, find the code that PRODUCES the data, not just
the contract that describes it.*

⭐⭐ **`env.spt` IGNORES THE EXIT CODE — adding a flag to a safety-critical call there is a silent
strand.** The Stop `state idle` does two jobs: publish AGENT_OUTPUT (new) and mark the endpoint IDLE
(the thing that keeps inbound flowing). On a core predating `--payload-stdin` the call is REFUSED and
takes the idle mark with it → stuck ACTIVE, spooling everything unseen
([[stuck-active-idle-blackhole]]) — invisible, because that seam never reads exit codes. Fix was
BOTH: manifest floor 0.27.0 → 0.64.0 (the loud front door, enforced at registration) and a new
`spt_strict_stdin` seam that falls back to a payload-free re-report, loudly, on any refusal (the
seatbelt for a core downgraded under an installed adapter). *Neither `spt` (stdin, exit-blind) nor
`spt_strict` (exit-aware, stdin-null) could do this job — the missing combination WAS the hazard.*

⭐⭐⭐ **DO NOT TRUNCATE ADAPTER-SIDE.** Bodies cap at 16 KB; core cuts on a char boundary and stamps
`truncated="1"`, which is the whole mechanism by which a consumer learns to follow `seq` to the
digest. Cutting first to "stay under the cap" delivers the same bytes with the flag ABSENT — a
silent loss wearing the shape of a complete payload, the [[v0310-attr-passthrough]] lesson in a new
costume. Core's cut is announced; ours would not be. For pacer this is load-bearing rather than
cosmetic: **the cut lands at the END of a turn, which is exactly where a pacer code sits.**

⭐⭐⭐⭐ **THE WATERMARK WAS A PROPERTY OF THE OLD SOURCE, NOT A SAFETY FEATURE.** pacer deduped codes
on the digest `seq` because the digest WINDOW is re-read every poll. io frames are drained
destructively and "each frame is its own event… never a resend", so keeping the dedup could only
ever swallow a real second instruction (an agent writing `!!wait=5!!` twice means it twice). Settled
the drain's destructiveness EMPIRICALLY, not by reading: `pause` arrives on the same drain and one
`pause` does not re-apply forever. Retired the frozen `last code acted` from `status` too — left in
place it reports a value seeded from durable state and never moving, which **reads like live state
and is not.**

⭐⭐⭐⭐⭐ **MY OWN "EXCLUDED AS A CLASS" COMMENT WAS STILL A LIST.** `3201839` (the v0.31.0 fix) wrote
that the root JIT-plan class was "excluded as a class instead of one file at a time" while the code
under it stayed EIGHT ENUMERATED LITERALS — so `IO-FUNNEL-PLAN.md` would have tripped the leak guard
exactly as `ATTR-PASSTHROUGH-PLAN.md` did. [[list-vs-predicate-assertions]], committed by the very
commit that was supposed to close it. Now a real predicate, and **two traps found by measuring, both
of which fail by PASSING**: `:(glob)` is what stops `*` at the `/` (bare `*-PLAN.md` selects 42 files
where the root class is 8 — it would have silently unpublished all of `docs/plans/`, which IS public
product), and `set -f` is what stops the SHELL expanding the pattern against the WORKING TREE before
git ever sees a pathspec. The belt-and-suspenders loop was ALSO vacuous: it interpolated each entry
into a `grep` regex, which a `:(glob)` pathspec can never match — it would have passed by testing
nothing. Verified in production: the public v0.32.0 tree carries 0 root plans and no `IO-FUNNEL`.

⭐ **MUTATION TESTING CAUGHT A TEST PASSING FOR THE WRONG REASON.** Deleting `parse_io`'s
`type="io"` check failed NOTHING — because no other frame type carries a `kind` attribute today, so
the required-field parse rejected impostors by accident. Re-pinned on a `sensory` frame that DOES
carry `kind="AGENT_OUTPUT"`. *A green test proves the behaviour only if you have seen it go red for
the right mutation.* Also: two of my mutation runs initially applied NOTHING (bad anchors) and
printed no failures — which reads identically to "caught". Assert the anchor inside the mutation.

**Field proof (the gate, not the green suite)** — after a real turn, `spt shell cmd PACER-0 status`:
`io: 3 frame(s), 1 AGENT_OUTPUT, 1 code(s); truncated 0 followed / 0 blind, 0 pointer(s) missed`.
The 1 code was a deliberately INERT probe (`!!classify=zzzz00-9:io_probe!!` — a stretch id that
matches no live board), so it was scanned and counted, then refused at apply: registry still 5
classes. *Design a field probe that exercises the path and cannot mutate state.* A broker-launched
resident's stderr is discarded, so the counters ride the `status` verb — the only field instrument
there is; the two miss shapes are always shown, including as zeros, because a counter that appears
only when non-zero is indistinguishable from a build that never counted it.

Scope kept honest: the digest read STAYS for context boundaries (`clear`/`compact`/`boot`) — no io
kind covers them, the vocabulary is closed. Operator slated **spt-bs-releases#239** to surface
boundaries as shell frames; that is when the digest call can go. Incidental win: `endpoint digest
--last N` is a WRITE into shared per-endpoint follow state ([[digest-json-consumption-contract]]),
so the old loop mutated its own owner's digest state every tick merely to read codes.

Forward flag: **releases#238** adds mid-turn framed emission — so **never assume one AGENT_OUTPUT
per turn**; nothing built here does.
