Across Tasks 3 and 4 of Phase 2, the preview reassembler’s sender-restart handling was rebuilt seven times. Every mechanism satisfied its stated requirement, and every mechanism was exploitable. The answer was not a better predicate — it was to stop inferring.

The rule, in one line

Peer data can never move the watermark backwards. reset() is the only backward move, and the session layer calls it.

The general form: when a data path cannot distinguish an honest event from a spoofed one, do not build a fifth predicate — take the decision out of the data path and let the layer that already knows tell you.

For Agents

Types: PreviewReassembler (Sources/AposematiCore/PreviewReassembler.swift), PreviewStallWatchdog, PreviewViewfinder. Ledger evidence: .superpowers/sdd/2026-08-19-phase2-live-preview/progress.md, Task 3 fix rounds 1–4 and Task 4 fix rounds 1–3. Threat context: QUIC datagrams ride inside the authenticated mutual-TLS session with peer_authentication_required, and there is no other datagram path into ingest(). An off-path attacker cannot inject at all. Read every “attacker” below as “the authenticated paired peer, compromised or buggy” — this is defence in depth, not a network attack.

The requirement that created the problem

The brief demanded no stale delivery: a frame from before a restart must never be delivered as if it were current. Task 3 met it with a monotonic watermark that was only ever advanced, never reset.

Met so aggressively it created a worse failure

One flipped bit in the sequence field’s high byte jumps the watermark ~1 billion frames ahead, and every honest frame after it is refused forever — recovery needs the sender to reach 1,073,741,831, over a year at 30 fps.

Measured: 0 of 4,995 honest frames delivered after one spoofed sequence, with stats reporting discarded: 0 throughout. That inverts the phase’s own invariant verbatim — a lost fragment must cost one frame, never the stream — and it is the same defect shape the phase defends against (peer input deciding this process’s behaviour), spent on liveness instead of memory.

The four inference mechanisms, and how each one died

Each round produced a mechanism that passed its own tests and a reviewer that broke it by execution.

#MechanismHow it died
1Watermark gated by a time-derived forward window + outOfOrderRun restart detectionoutOfOrderRun is reset by any accepted fragment including the attacker’s, so one accepted spoof at newest+1 per honest frame holds the detector down forever. 0 / 200 delivered at 1:1, one 17-byte packet costing one honest 5 KB frame.
2Tightened so the run only clears on a fragment continuing an in-flight sequenceRegression, and worse than its predecessor: an honest stream of single-fragment frames never produces such a fragment, so the run is never cleared by honest traffic at all. A drip of far-future packets accumulates across unlimited time. 200/200 → 5/200 at 1 spoof per frame. Not hypothetical: a 1080p JPEG of a dark or uniform scene compresses under the 1184-byte single-fragment threshold, and Task 6 produces exactly such frames.
3Restart evidence moved off the accept path — time since the last delivered frame on the current lineMet its requirement and opened the worst hole of all: the first round where the attacker does not silence the honest stream but takes it over. adopt() set lastDeliveredAt = now for a line that had delivered nothing, so six refused 17-byte packets rewound the watermark onto the attacker’s line and refreshed the quiet timer. Honest 5/200, attacker delivered 195/200 — including a blind variant that knows nothing of the honest sequence. Also: a sender below the configured fps is hijackable (10/60 at 2 fps), because the quiet interval derives from the configured rate, not the observed one — and 2 fps is ordinary thermal throttling.
4(never dispatched)Ruled against — see below.

The structural point that ended it

At admission, an honest one-fragment frame at newest+1 and a one-fragment spoof at newest+1 are indistinguishable. No accept-path predicate separates them, so every further change to the condition trades one hole for another — which is exactly what rounds 1 and 2 each did, and round 3 showed the same holds for time-based evidence once a rewind can refresh the timer.

Four mechanisms failed the same way, and the common factor was never any one predicate: peer data was allowed to move the watermark backwards. The restart detector was asking the data who the sender is. The layer above already knows — a new CameraSession is a new preview stream — so the answer is to be told, not to infer.

What shipped

  • Keep the forward reachable gate (already proven to refuse far-future spoofs without moving the watermark).
  • Remove peer-driven rewind entirely.
  • Expose reset(); the session layer calls it.

Verified, and the verification is the point:

  • Hijack column: 200/200 honest, ZERO attacker frames, both shapes, all rates including 10:1 and 30:1 (the previous mechanism was 0/200 honest / 399 attacker).
  • Backward watermark movement is structurally impossible, not merely unreached: exactly three writes to newestFinished; the only peer-reachable one is guarded by a strict forward-only isAfter; the nil-seed is reachable only from a state that only reset() produces.
  • reset() clears in-flight state, watermark and newestDelivered, leaves lifetime counters, is idempotent, is safe mid-frame (one bounded orphan, counted not silent), and recovers immediately — first delivery on frame 0.
  • Far-future immunity holds including the slow-sender case that was 10/60.

The integration contract that came with it

Carried forward as a hard requirement on the receiving side

  1. Call reset() unconditionally on every transition into “previewing”, and after any transport reconnection or peer re-pairing (a new connection may carry a re-based sequence counter).
  2. Run a stall watchdog: if nothing is delivered for > 2 × deadline while stats.fragmentsRefused is increasing, call reset().
  3. Do not reset on ordinary loss or reordering — those never raise fragmentsRefused.

The conjunction in (2) is load-bearing. It is sound because ordinary loss never trips it: measured zero fragmentsRefused at 20% fragment loss with intra-frame shuffling, and outOfReach == 0 in all twelve loss × interleave configurations (0/10/20/30% × windows 1/2/4). That false-positive-free property was the thing most at risk of being lost quietly.

fragmentsLate must never reach the watchdog

It is attacker-drivable without bound at zero memory cost — 10,000 fragments at distance exactly 12 gives late = 10000 with no buffering. It is diagnostic only.

Related, and the same shape one layer up: fragmentsRefused originally conflated restart-refusal with malformed-fragment-refusal, so a peer sending garbage could drive the watermark backwards on demand — reconstituting, in the integration contract, the attacker-reachable restart signal that four rounds had just closed.

The counter leak that the fix path opened, twice

The same “each change is locally correct” pattern recurred in Task 4 on the counters:

  • Attacker direction. The over-budget guard called drop() → finish(), which parks newestFinished on the malformed frame’s own sequence and removes its partial — so every remaining fragment of that frame misses partials, fails isAfter(S,S) and lands in fragmentsOutOfReach. Measured: 50 garbage frames of 1024 fragments → outOfReach = 45,650 at zero delivery, 913 per attack frame — the exact stall signature, free and repeatable, and reachable through the public channel surface with no hostile fragmenter (20 sends of a 400,000-byte frame → 4,540).
  • Honest direction, created by that fix. standing treated a behind-watermark sequence as .late while newest &- sequence <= ceil(elapsed × fps) + inFlightLimit. The window grew with idle time, which is backwards — a longer silence makes a restart less detectable exactly when it makes one more likely. A 300-frame run, a 15 s outage and a restart gave outOfReach = 0, malformed = 0 and frozen delivery the watchdog could never clear.

The backward bound must be a small constant, not a mirror of the forward one

The forward reachable window is right to grow with silence — a sender could plausibly have advanced that far. The backward direction is not symmetric: a genuinely late or reordered fragment is only slightly behind (within reordering distance), whereas a restart is arbitrarily far behind. Mirroring a time-derived window backwards imports the forward case’s justification where it does not apply.

After the fix: idle sweep flat at 5/5/5/5/5/5/5/5 out to 3600 s (was 30/30/30/30/21/0), outage-restart outOfReach 0 → 250, all three attacker shapes still 0.

An accepted contradiction in the dispatcher’s own brief, recorded rather than quietly resolved: “a restart with gap ≤ inFlightLimit must now fire” cannot hold once the backward bound is the reordering tolerance and reorderingTolerance == inFlightLimit. The tolerance was accepted. Measured cost of a single such restart: exactly 12 frames (~400 ms at 30 fps), self-healing, with delivered still climbing so the watchdog conjunction is never met and no spurious reset fires.

The one hard residual

A peer that restarts numbering cyclically inside the reordering tolerance freezes delivery invisibly

Restarting every 12 frames freezes delivered at 12 with outOfReach = 0, discarded = 0, late = 3000 over ~20 s. A permanently dead preview the watchdog can neither see nor clear.

Keying the watchdog on silence alone would fix it and must not be done — that re-opens the on-demand backward watermark move four rounds were spent closing, because silence is peer-controlled. The delivered-flat AND fragmentsRefused-climbing conjunction is load-bearing.

This is a genuine hard residual, not a bug to fix. It is documented with the reason the obvious fix is wrong, which is the part that has to survive into Phase 3.

The rule’s real scope — and a miscitation that was widening it (2026-08-26)

The silence rule above is narrower than it was being cited as, and the citation itself had gone stale.

HANDOVER.md cites "spec §4.3" three times. That section does not exist.

docs/superpowers/specs/2026-08-25-preview-crop-design.md has 12 sections and none is 4.3 — the string 4.3 does not appear in the file at all. The rule actually lives at docs/superpowers/specs/2026-08-18-aposemati-design.md:502, i.e. in the design spec, not the crop spec, and in this vault at The one hard residual above.

What the rule binds: the receiver-side reassembler stall watchdog, and only that. Keying that watchdog on silence reopens the on-demand backward watermark move four rounds were spent closing, because silence is peer-controlled and therefore attacker-controlled.

What the rule does not bind: connection state. It says nothing about it.

A stateUpdateHandler on an NWConnection is a local transport fact, not silence

The local QUIC stack telling you your own connection has entered .failed is not the peer declining to send. A miscitation was being used to argue against a category of fix the rule never covered — and the fix it was arguing against was the correct one for a permanently frozen viewfinder. See aposemati-loopback-blind-spot.

The generalisable half: a rule that travels by citation drifts in scope. This one had migrated to a document with no such section, and every retelling widened it — from “do not key this watchdog on silence” to “do not watch for peer death at all”.

Why this generalises

For any agent building an accept path

  1. Name the boundary before choosing a predicate. “Honest restart vs spoofed restart” is not a distinction the receiver’s data contains. Once that is stated, four rounds of predicate-tuning are visibly a dead end rather than progress.
  2. A mechanism that satisfies its requirement is not a mechanism that is safe. All four here passed their own tests; each was broken by a reviewer running an attack. See aposemati-build-process-lessons.
  3. Attack the replacement, not only the original bug. Rounds 2 and 3 were each caught because the re-reviewer attacked the new mechanism instead of confirming the old attack was fixed.
  4. A root cause can live in the brief. The dispatcher’s own ruling: “Root cause was a BRIEF defect — my plan text demanded stale-frame rejection that the accept path cannot distinguish from a spoof.”
  5. Count the rounds, then stop. The structural call was made at round 4 of a 5-round cap rather than spending the remaining budget on a fifth mechanism.