It works. Phase 0 (transport measurement) and Phase 1 (link + shutter + stills) of Aposemati were built and proven on hardware on 2026-08-18. This note is the build record: what exists, what it produced, and what is still unverified.
The first live two-device run — 2026-08-18 23:47
An iPhone 16 Pro captured two photos to a Mac over a link the UI reported as
awdl0 — direct. Written to~/Pictures/Aposemati/2026-08-18/at 8064×6048 = 48.8 MP (6.3 MB and 5.2 MB HEIC).That is ~17× the pixels Continuity Camera can deliver — its ceiling was measured at 1920×1440 on this same phone at the start of the project (1. Continuity Camera’s hard ceiling).
Every premise the project rested on is now demonstrated rather than argued: AWDL carries it, the interface pin holds, the pairing gate works, capability negotiation reported the real 48 MP sensor, and a full-resolution photo landed where it should.
For Agents
Repo
/Users/levander/coding/aposemati. Branchphase0-phase1-link-and-shutter— 68 commits, 219 tests, zero build warnings, clean working tree, nothing merged tomain, nothing pushed. Both apps are installed and running on real hardware. The review loop is closed, and the final whole-branch review has now run (2026-08-19, six reviewers, entire branch at once) — verdict: no finding blocks the branch. Its findings are not yet applied: aposemati-whole-branch-review. Build/install/run commands: aposemati-build-install-run (andHANDOVER.md, which is current). Source of truth in-repo, in order:HANDOVER.md→.superpowers/sdd/2026-08-18-phase0-phase1-link-and-shutter/progress.md(419-line ledger, 37 rulings + 27 deferred minors) →docs/superpowers/specs/2026-08-18-aposemati-design.md→docs/superpowers/phase0-results.md. The ledger is the important one: every non-obvious choice is recorded there with its cost-if-wrong.
Status at a glance
| Branch | phase0-phase1-link-and-shutter, unmerged, unpushed, clean tree |
| Commits | 68 |
| Tests | 219, zero build warnings under -strict-concurrency=complete -warnings-as-errors |
| Phase 0 | PASSED on hardware — see aposemati-phase0-field-measurements |
| Phase 1 | Working end to end — shutter → 48 MP still → Mac library |
| Review loop | Closed. Ledger holds 37 rulings + 27 deferred minors — the review found the real count is 29, with no rulings for the last four commits |
| Whole-branch review | ✅ RAN 2026-08-19 — no finding blocks the branch. 3 Criticals; C1/C2 unreachable through the Phase 1 shutter path, C3 is the POSIX 57 root cause. Nothing applied yet — aposemati-whole-branch-review |
| Live preview | Not built. Phase 2 by design — not a bug |
| Remaining step | Apply the review’s ranked fixes (arm D, AuditGapTests.swift, the C2 hybrid), then hardware |
| Apps | Both installed and running on real hardware |
Architecture as built
Four channels, two connections. Preview (droppable datagrams) and control share one QUIC connection; stills and video get their own.
Why two connections and not one
QUIC eliminates head-of-line blocking between streams, but all streams on a connection share one congestion controller, and Network.framework exposes no cross-stream priority — every function in
quic_options.hwas enumerated and there is no congestion-controller selection, no stream priority, no pacing.nw_content_context_set_relative_priorityarbitrates only within a connection.A multi-megabyte still on the preview’s connection would spike viewfinder latency — reproducing the exact “Camo is laggy” failure this project exists to beat.
Three principles that survived the build unchanged:
- Never stream the thing you are keeping. Video records on-device at full quality and transfers afterwards, so recording quality is independent of link quality. Camo streams and records the stream, so its recording quality is capped by transport quality.
- Framing is metadata, never pixels. Crop, zoom, rotation, auto-framing are stored as a transform beside a preserved original.
- Nothing hardcodes a resolution. Capability is negotiated at pairing — see the corrected device capability matrix.
The wire-format change that mattered
Asset frames carry a 16-byte UUID prefix. Pairing an incoming payload to its announcement used to be positional FIFO — nothing on the asset frame identified its descriptor, and the only cross-check was byteCount. Two announced assets of equal size arriving out of announcement order silently misfiled one photo and dropped the other, while receivedAssets reported success.
The implementer found this itself and called it “the worst possible failure for a capture tool”. It was fixed before Task 12 ossified the wire format — correspondence is now structural, not incidental. Byte order is RFC 4122 network order, so the format is platform-stable.
Frame-size cap is decode-side only
FrameCodec.maxFrameSizeis 16 MiB and enforced only on decode.FrameCodec.encodeaccepts any payload and emits an over-cap frame with no local signal — the sender sees success while the peer’s decoder rejects it and tears the connection down. The sender-side guard lives in the transport, not the codec.16 MiB rejects ProRAW (25–75 MB). That is a scheduled Phase 4 revisit currently written as settled policy.
Security model
Both sides generate a self-signed identity in memory at launch — never persisted, never in the keychain. Verified: after the full suite plus ~5000 identity generations and a dozen handshakes, every file under ~/Library/Keychains/ was byte-for-byte and mtime-for-mtime identical to the pre-run snapshot.
After the QUIC handshake they run a commit-reveal nonce exchange and each derives the same six digits from both certificate fingerprints and both nonces. The user compares two screens.
The commit-reveal is not decoration — without it a reviewer ground two colliding certificates and ran a working machine-in-the-middle in 0.77 seconds. Full account: aposemati-pairing-security-model.
The four fixes that closed the review loop
All four landed after the live run and after the first round of documentation. Each is verified by execution, not by reading.
aef872b — the transport reads nothing until the application asks for the frames
This was the Critical. An unconfirmed stranger could take a listening iPhone from 18 MB to 196 MB RSS, and four strangers together reached 321 MB — jetsam territory on a phone. The earlier preConfirmationByteBudget did not bound it: it was a one-second sampler on an undrained, unthrottled AsyncStream, so exposure was ~1 second of line rate rather than unbounded-forever.
The fix makes the transport not read from a peer at all until the application asks for its frames.
Verified in a separate victim process, with a control
34.7 MB peak against four flooding strangers, versus 486 MB with the fix reverted. A control run against the pre-fix build is what makes the number mean something.
ed436a0 — the readiness bound raised to 30 s, and instrumented
The field bug from the first live run: could not open the direct link to iPhone: the phone did not finish the handshake in time. An earlier fix round raised a different bound on a premise that contradicted its own error table, so the bug survived a round.
The string the owner actually saw comes only from handshakeTimedOut, thrown from a single site — awaitReady(_:timeout:). Traced there and measured, it was still failing at 10.65 s against the old 10 s bound. Now 30 s, with readiness duration instrumented so the next two-device run measures it.
Reading the diff would have passed this
Only tracing the exact error string back to its one throw site, and then measuring there, caught it. See Trace a symptom to its single throw site, then measure there.
86fb3f1 — deferred reading must not become dropping
aef872b introduced a regression: a late reader lost frames that had arrived before the peer left. The fixed build delivered 0 where both the pre-fix build and LoopbackTransport delivered 1 — the one place the two transports had stopped being interchangeable, a property verified four separate times across the build. A late reader can now still collect what arrived before the peer left.
e1cd333 — the refusal-recovery button no longer loops
Previously the only button on the red “they do not match” screen re-handshook every ~2.1 s forever, making the phone display a new six-digit code every 2 seconds indefinitely, with no in-app recovery. Notable as a case where fixing one thing (making refusal actually stick) created another.
The whole-branch review — it ran, 2026-08-19
Verdict: no finding blocks the branch
Six reviewers, isolated clones, entire branch at once (
adc604b→961d996). The exploit claim that would have blocked it is refuted. Three handover open questions are closed with measurements. Two of the three Criticals (C1, C2) are real but unreachable through the Phase 1 shutter path — which is why the live run worked — and both sit directly under Phase 2 preview and Phase 4 video. The third, C3, is in the Phase 1 path: it is the POSIX 57 root cause below.Nothing from the review has been applied. Working artifacts are parked in
docs/superpowers/2026-08-19-review-artifacts/. Full account: aposemati-whole-branch-review.
SOLVED: the intermittent POSIX 57 during pairing
It was never a flake.
NetworkTransport.swift:369-372— the QUIC listener delivers twoNWConnections per incoming connection (streamIdentifier == 1is the one the listener was handed;0is the peer’s real stream).acceptcancels the sibling, and cancelling any one stream resets the whole QUIC connection.The shipped listener therefore tolerates under 250 ms between its connection arriving and the peer’s stream settling, and past that it fails totally, not intermittently (0/8 at 250 ms, 900 ms, 1100 ms, 1500 ms). The ~1.8% POSIX 57 rate was that cliff grazed by jitter. The margin is thin, not absent — the 2026-08-18 live run is positive evidence the gap came in under 250 ms on real AWDL.
Fix is arm D: park the sibling keyed by 4-tuple, release on the peer’s stream, reap orphans after
handshakeTimeout + pairingTimeout. 1800 pairings, zero failures. Do not use the naivereturn nil— it trades 1.8% instant failures for 0.56% thirty-second hangs. See 4. A QUIC listener hands you TWO connections per peer — and cancelling either resets both.
86fb3f1does not deliver its guarantee, and it cannot be made toThe late-reader test sends 10 bytes. The guarantee holds to 256 KB and not one byte further — a 4 MB photo vanishes silently while
carriedTraffic()reports the bytes acknowledged. Worse, C1 and C2 are mutually exclusive: a photo survives a late read iffbudget ≥ maxFrameSize(16 MB), so any budget that saves a 48 MP HEIC is one an unconfirmed stranger can hold. The recommendation is to drop the C1 guarantee, delete its test, and document thatLoopbackTransportandNetworkTransportare not interchangeable across a peer close — see ⭐ C1 and C2 are mutually exclusive — a decision, not a tuning problem.
Still untested on hardware
What needs a human
- Re-test the handshake at the new 30 s bound.
ed436a0is unverified in the field.- ⭐ Walk to the spot where Camo drops, with the apps. Phase 0 proved the probe holds
awdl0there at 156 Mbps — but that was two Macs moving zeros. The phone and the Mac pairing and transferring a real photo at that spot is the last untested product claim, and it is the entire reason the project exists.- ⭐ Log the stream-lag on that same run — the interval at the listener between the connection being delivered and the peer’s stream becoming ready. It is the exact quantity the 250 ms tolerance applies to, it is unmeasured on any real link, and it costs nothing. Single-digit ms ⇒ the shipped margin is fine; tens or hundreds ⇒ arm D is load-bearing.
- Deliberately press “They do not match”, then reconnect. It will offer the same Mac again — the refusal blacklist is per-process-lifetime by design (Known and accepted weaknesses).
Deliberately not built
Phase 1 was always shutter-and-stills. No live preview — that is Phase 2, and the four-channel design exists so it can be added without touching the transfer path. Also deferred: video recording (Phase 4), manual controls (5), framing (6), and the CoreMediaIO virtual camera (7), which is where “mirrors Camo” actually becomes true.
Sources/AposematiProbe/ is the Phase 0 measurement tool. It still carries the uncancellable-continuation shape that cost two fix rounds in the real transport — see swift-uncancellable-continuation-trap.
Corrected 2026-08-19 —
Probe's continuations are safeThe whole-branch review disproved this as a live defect: those continuations are never awaited directly, so they cannot hang anything.
HANDOVER.md:172-173gives the wrong reason for the same conclusion — what matters is not “it has a cancellation handler” but “is the continuation ever awaited directly”.Probe.swiftis separately 0.00% covered (0/666 lines) and got a grep rather than a bounds review.
How to build
swift test # 219 tests
cd Apps/AposematiCamera && xcodegen generate # .xcodeproj is not committed
cd Apps/AposematiHost && xcodegen generateTwo traps that cost real time
The iOS product is
Aposemati.app, notAposematiCamera.app— the scheme name and the product name differ. And thexcodebuild-destination id=is not thedevicectldevice identifier: the same phone has two different UUIDs.Full working commands: aposemati-build-install-run. Signing is Apple Development for team
NZZ28CZ49Hand does not work out of the box on a fresh machine — see aposemati-apple-signing-gotchas.
Related
- ⭐ aposemati-whole-branch-review — the final review of all 68 commits: POSIX 57 root cause, the C1/C2 decision, the ~70%-real test suite
- aposemati — project overview and phase plan
- aposemati-phase0-field-measurements — the throughput numbers, taken in the field
- aposemati-pairing-security-model — commit-reveal SAS and the 0.77 s MITM
- aposemati-network-framework-gotchas — AWDL, QUIC and Network.framework traps
- aposemati-build-process-lessons — verify-by-execution, mutation testing, commit-as-you-go
- aposemati-build-install-run — the build/install/run commands that actually work, and the two-UUID trap
- aposemati-transport-findings — the research that chose this transport
- camo-studio-teardown — the competitor this beats at range