The permanently frozen viewfinder that aposemati-crop-and-framing recorded as an open defect is root-caused and fixed — and the reason it survived 546 tests, eight task reviews and a whole-branch review turns out to be a property of the harness rather than of anyone’s attention.
The finding, in one line
Over
awdl0, a QUIC datagram send into a dead peer SUCCEEDS — the sender learns nothing. Over loopback the same send fails immediately withPOSIXErrorCode(rawValue: 57): Socket is not connected. The harness takes the error path the field never takes, so a test written for this defect passes against unmodified code: loopback hands the sender the very error the radio withholds.
For Agents
Fixed in:
Sources/AposematiCore/NetworkPreviewChannel.swift(thestateUpdateHandler) andCameraAppModel.streamPreview(the retry loop). Correct prior art in the same file family:NetworkTransport.begin()— it has had the rightstateUpdateHandlerfor the control connection all along. Verified by: hand-run mutation — revert the source fix, keep the tests, watch two named expectations fail. Output quoted verbatim below. Everything here is measured on hardware or by hand-run mutation. Nothing is hypothesis.
The blind spot, stated so it can be reused
Loopback can reproduce only the quiet form of this defect class:
| Form | What it looks like | Reproducible on loopback? |
|---|---|---|
| Quiet — the peer leaves while nothing is being sent | no traffic, connection goes away | ✅ yes |
| Live sender — 30 fps of successful sends into a dead peer | every send() returns success forever | ❌ not at all |
The consequence, stated plainly
Anything that relies on send failure to detect peer death is untestable in-harness. It must be reasoned about, or found on hardware. There is no test you can write on loopback that distinguishes the fixed code from the broken code for the live-sender form — because on loopback the broken code also recovers, via an error that does not exist over the radio.
This is a stronger statement than the project’s existing two-roles-in-one-object lesson. That one said a badly shaped double hides defects. This one says a faithful two-process QUIC harness on loopback still cannot see this class, because the transport underneath behaves differently. The double is not the problem; the medium is.
The defect
Symptom in the field: focus away from the Mac app and the viewfinder freezes permanently — no recovery on refocus, no recovery on a camera switch. The phone’s own readout showed 30 fps throughout: it was still encoding and still sending. Both ends individually believed they were healthy.
Mac-side log, from the instrumentation added in 6607614 — the commit that made this findable
at all:
17:34:28.343 opening a preview flow, epoch 1
17:34:28.736 preview flow open, epoch 1
17:34:58.745 frame stream finished after 0 frames, epoch 1
17:34:58.745 retrying a preview flow in 2.0 seconds
A clean 32-second loop, forever. The Mac was correct throughout — it noticed, it gave up, it retried, on schedule. Nothing was wrong on the side that was doing the noticing.
The instrumentation is what turned a hypothesis into a diagnosis
The Mac had no instrumentation at all until this session — it was called “the cheapest high-value work remaining, and it blocks both diagnoses”. It was, and it did: the 32-second cycle above is the diagnosis, and it is unreadable without it.
Root cause — narrower than the previous handover claimed
NetworkPreviewChannel never installed a stateUpdateHandler on its NWConnection. It had
exactly two routes to shutDown():
- a receive error in
receiveNext()— reachable only viastartReading(), which is reachable only fromframes(); - a send error in
transmit()— which QUIC datagrams never report over the radio.
The phone only ever SENDS. It never calls
frames().So on the phone the channel had no path to
shutDown()whatsoever — not a slow path, not an unreliable path, none. The Mac survived only incidentally, because it happens to be the end that reads.
The asymmetry is the lesson
NetworkTransport.begin() had done the right thing for the control connection all along: a
[weak self] stateUpdateHandler tearing the connection down on .failed / .cancelled. The
preview flow was simply never given the equivalent.
A correct pattern existed in the same file and was not applied to the sibling connection. That is a different failure from “nobody knew how” — and it is the shape worth grepping for on any codebase that grew a second connection beside a first one.
The fix
Two changes, both needed.
| # | Change | Where |
|---|---|---|
| 1 | Install a [weak self] stateUpdateHandler calling shutDown() on .failed / .cancelled. shutDown() nils the handler before the cancel() that re-enters it. | NetworkPreviewChannel.init |
| 2 | Add the retry loop the Mac already had (while !Task.isCancelled, 2 s retry delay) | CameraAppModel.streamPreview, mirroring HostAppModel.runPreview |
Change 1 is one guard in the shared class both ends route through — so it fixes the Mac too, not only the phone.
Why change 1 alone settles the transport-cache question
NetworkTransport.previewChannel caches the in-flight open, and discardThePreviewFlowIfItDied()
clears that cache only when channel.isShutDown. Before the fix the cache handed back the DEAD
channel — verified, see the mutation output below. Change 1 makes isShutDown become true, so the
cache clears itself and no separate fix was needed.
Mutation evidence — the part that matters
Reverting only the source fix while keeping the tests:
send-only flow after its peer went away: shut down false
Expectation failed: (noticed → nil) == true
send-only flow reopening: peer offered a replacement, it handed back the dead one
Expectation failed: (reopened) !== (accepted)
Both fail after ~15 s of timeout. With the fix restored, both pass in 0.073 s.
The second line is independent confirmation of the cache defect
“peer offered a replacement, it handed back the dead one” is the transport cache returning a dead channel, observed rather than argued. That is what makes “change 1 alone is enough” a measurement and not a claim.
Why the mutation was run by hand rather than trusted from a report
This project has a precedent: commit
371b32a, where a fix was later disproved because its test passed identically with and without the change. Combined with the two opposite mutation failure modes already recorded in Mutation testing has two opposite failure modes, a reported mutation result is a claim. The run above was performed by hand for that reason.
A stale cross-reference that had been propagating
The repo’s HANDOVER.md cites “spec §4.3” three times for the rule that silence is
peer-controlled and must not be watched.
That section does not exist
docs/superpowers/specs/2026-08-25-preview-crop-design.mdhas 12 sections and none is 4.3 — the string4.3does not occur in the file at all. The rule actually lives atdocs/superpowers/specs/2026-08-18-aposemati-design.md:502, and in this vault at The one hard residual.
And its real scope is narrower than the citation implied. It binds the receiver-side reassembler stall watchdog, because keying that on silence reopens an on-demand backward watermark move four rounds were spent closing. It says nothing about connection state.
A
stateUpdateHandleris a local transport fact, not silenceWhich is exactly why the fix above is compliant with the rule. A miscitation was being used to argue against a category of fix the rule never covered — the citation had drifted to a document that has no such section, and the drift widened the scope in the retelling.
Open — needs hardware
- ⭐ Whether
awdl0delivers.failedat all, and how fast. On loopback the peer’scancel()propagated in ~76 ms. Over the radio the QUICCONNECTION_CLOSEmay be lost, in which case the phone falls back to the 30 s idle timeout and recovery becomes ~32 s instead of ~2 s. Still recovery, where today there is none — so the fix is worth shipping either way, but the number is unknown. CameraAppModelcompiles into no test target, so the retry loop is untested by construction — the same as the Mac’s. See aposemati-build-install-run onApps/being outside the SPM package.- The missing-shutter defect is untouched and still unresolved — one press in seven produced no file. See One shutter press in seven produced no file.
- Pre-existing and unchanged, but now more visible because it sits inside a retry loop: if the
preview task is cancelled while blocked in
PreviewRendezvous.claim, the loop swallows cancellation viatry?, so exit can take up to the 30 s handshake timeout. Neighbour of swift-uncancellable-continuation-trap in shape, though not the same mechanism.
Related
- aposemati-crop-and-framing — where this defect was first written down as open, and the instrumentation gap that blocked it
- [[aposemati-network-framework-gotchas#11-a-datagram-send-into-a-dead-peer-succeeds-over-awdl0-and-fails-on-loopback|11. A datagram send into a dead peer SUCCEEDS over
awdl0and FAILS on loopback]] — the API-level form of the transport fact - ⭐ The harness can only reproduce the quiet half of this defect class — the reusable method lesson
- aposemati-peer-restart-inference-boundary — the silence rule, its real location, and its real scope
- aposemati-phase2-live-preview — the preview flow this lives inside, and the hardware checklist this adds to
- aposemati — the project