---
name: v0251-interrupt-watch-heal-fix
description: "2026-07-18 v0.25.1 SHIPPED — deployah RCA: interrupt-watch buried-marker defect (--last-1 too narrow) fixed via window scan; a --json-trailer 'D1' was a self-inflicted 2>&1 misdiagnosis, corrected by doyle"
metadata: 
  node_type: memory
  type: project
  originSessionId: 01a548be-ed5c-45bf-a7c7-2da0bacbd94c
  modified: 2026-07-19T05:00:29.640Z
---

**2026-07-18 — v0.25.1 SHIPPED** (0.25.0→0.25.1, binary-only, floor 0.27.0). Commit a11fc3b; tag v0.25.1; GH release live on BigscreenVR/claude-spt-bs (adapter.spt asset); public mirror 62b8fa0. From an operator `/diagnose`: **deployah stuck Esc-interrupted, never self-healed to idle → black-holed inbound**; the anti-stuck measure (interrupt-watch) failed.

**RCA — the REAL and SOLE defect: BURIED MARKER.** interrupt_watch.rs (the translate-binary heartbeat, REQ-HAZARD-INTERRUPT-STUCK-BUSY) read only `spt endpoint digest <id> --last 1 --json` every 15s and healed only if the LATEST turn's `input` IS the interrupt marker. deployah's 3 post-interrupt notifies (spt-update) arrived between the Esc and the tick, became newer digest turns (`entries:[]`, parked), and BURIED the marker → the heal saw a notify (not the marker) → `heal_decision` concluded "agent moved on" → never healed. doyle/todlando healed fine because NOTHING arrived to bury their marker (their latest turn WAS the marker). **FIX**: scan a WINDOW (`--last 12`) and heal when the window CONTAINS a marker AND the latest turn has NO agent entries (empty `entries` = agent not producing = stuck); re-arm when the latest turn HAS entries (agent genuinely resumed). New `DigestView{latest_has_entries, window_has_interrupt}` + rewritten `heal_decision(Option<&DigestView>, healed)`. Discriminator: stuck latest turn `entries:0`; resumed `entries>0` (deployah showed 18 after recovery).

**CORRECTION — the "node-wide --json-trailer outage" I first reported was WRONG (doyle grounded it).** I claimed a `DIGEST:<id> version=N` trailer on `digest --json` broke the adapter's strict `serde_json::from_str` fleet-wide. FALSE. That trailer is on **STDERR** (`eprintln!` cli.rs:1619, present since 2026-06-03 commit 16f4c8e, every release incl. v0.38.x), and `interrupt_watch::spt_capture` (line 188) reads **`out.stdout` ONLY** — it never merges stderr. I only "saw" the trailer because I put `2>&1` in my OWN diagnostic shell commands; the adapter's parser never receives it. Verified: `spt endpoint digest <id> --json 2>/dev/null` ends in a pure `}` and parses clean. So the trailer was NEVER the cause. The streaming-deserializer "first-JSON-value" tolerance I added is HARMLESS defensive hardening (doyle: keep it — defends an N-1 core or a future stderr interleave), **not a fix for a real outage**.

**LESSON (feedback-loop hygiene): never diagnose a subprocess-capture bug through a `2>&1` shell probe.** `2>&1` fabricates a stderr-in-stdout symptom the real capture never sees. Match the probe to the actual capture path (here: stdout-only). This cost a wrong RCA framing that shipped in the v0.25.1 changelog/commit/comments before doyle caught it (corrected after).

**RECOVERY (live, before the fix):** `spt api --adapter claude-spt state idle deployah --session-id <sid>` → `IDLE:deployah (gate-armed)` + `IDLE_PARKED_DRAIN:deployah: injected 3 parked message(s)` → deployah resumed (18 entries, worked the v0.38.1 release backlog). sid from carrier `state/session/deployah.sid` (= 0cc427d2…, matches sessions.log ordinal 15). Operator pinged on mobile-gw as requested. **Proven manual recovery for a stuck-interrupted/ACTIVE endpoint: mark idle with the carrier sid — drains the spool.** (Same recipe as [[stuck-active-idle-blackhole]].)

**doyle's core follow-up (v0.38.2 candidate, he's driving):** make `digest --json` self-contained — emit `version` as a FIELD in the object (recommended shape: top-level `"version": N`) AND suppress the stderr `DIGEST:` trailer under `--json`. The adapter does NOT consume the digest cursor today (tag_scan uses a transcript byte-offset `.pos`; interrupt-watch reads only turns[].input + entries), so no hard constraint — but the in-object `version` future-proofs any json consumer.

**Gates:** 323 tests green (9 interrupt_watch), clippy clean, traceable-reqs green (hardens existing REQ-HAZARD-INTERRUPT-STUCK-BUSY, no new REQ). The window-scan FIX is correct and is the real fix. Narrative corrected post-ship (changelog/GH-notes/comments) — binary unchanged (comments-only correction commit on main, no re-tag).

**PROPAGATION:** running translate binaries are long-lived (spawned at each session start) = still the OLD interrupt-watch until each endpoint bounces. `spt adapter update claude-spt` + bounce endpoints to pick up v0.25.1. deployah recovered manually; its NEXT interrupt still needs the update or another manual heal until bounced.

Related: [[stuck-active-idle-blackhole]] (stuck-ACTIVE class + manual recovery recipe), [[v0250-symmetric-event-postentersettle]] (same-day prior ship).
