Skip to content

M1 Gate Review

The milestone gate review per docs/13 and docs/20, run after slice 7b closed M1’s last slice. Each slice already has its own retrospective review, so this one does not re-read the milestone’s code. It does something the slice reviews structurally cannot: take each promise in the M1 gate and ask what evidence exists that it is true. Two of them turned out not to be.

PromiseVerdictEvidence
demo-robot streams 2 tracks to 3 browsers through the sidecarmet, on CItests/stack/camera.spec.ts — three pages, both camera tracks, one producer each. Getting here took two corrections of this review’s own making (below)
the demo backend minting grants per ADR-0015met since slice 3bevery stack test connects with a grant the demo backend minted; role-scoped grants in introspect.spec.ts
the demo backend receiving webhooks per ADR-0015was not met — now metfinding 1: nothing had ever delivered one. tests/stack/contract.spec.ts now asserts signed session.started/session.ended arrive and that forged ones are refused
reconnect + ICE restart under fault injectionmetagent.spec.ts (ice-restart → session-close{retry:true} against the real agent), ladder.spec.ts (server restart, silent agent, peer-gone), faults.spec.ts (killed agent, server restart under the real agent)
latency harness reporting against budgetsmetslice 7b: tests/stack/latency.spec.ts, make latency, web/e2e/latency.csv
TS types conform to the golden fixturesmet, slice 2open question #15, closed 2026-09-16
written API-fit review for the remaining capabilitiesmet, M1 planningm1-api-fit-review.md
#SeverityFindingResolution
1highThe webhook half of ADR-0015 had never run. The sidecar implements HMAC-signed delivery with retries; the demo backend has had a receiver since M0. Nothing pointed one at the other: no FJARR_WEBHOOK_URL anywhere in compose. The receiver also did not verify the signature, so it was an open endpoint anyone on the network could post session events to. The gate has claimed this since M1 was writtencompose and make demo-up wire the sidecar to the demo backend; the receiver verifies x-fjarr-signature in constant time, refuses with 401, and de-duplicates by event_id (delivery is at-least-once); contract.spec.ts asserts both halves; CI sets the URL
2highTURN URLs in the standard form were silently ignored by the agent. turn:host:port is RFC 7065 and what every browser and every server config uses; webrtcbin wants turn://user:pass@host:port. Both webrtcbin consumers required the ://, and continued without a log otherwise — so a robot handed a perfectly ordinary TURN URL had no relay path and said nothing about it. It only surfaces behind the symmetric NAT that TURN exists for, which is the case docs/02 says to plan capacity forprotocol::turn_url_with_credentials accepts both forms, URL-escapes the credentials, and returns empty for anything else so the caller logs it instead of dropping it. Unit-tested over both forms, turns, a query string, base64 credentials and five rejects. With it, the relay-only simulator scenario connects for the first time: add-turn-server turn:coturn:3478 -> ok, connected in 134 ms over a relay candidate
3mediumThe fan-out acceptance assertion read /stats once and demanded exactly three subscribers. /stats is a global instantaneous view: sessions from an earlier run may still be draining (it read 5) or this run’s third viewer may not have subscribed yet (it read 2). It could only pass on a freshly started robot, which is CI and nowhere elseit polls until the hub settles at three subscribers per track with one producer each. The property is unchanged; the sampling is no longer a race
4mediumfaults.spec.ts is the only suite that stops the robot. An assertion failing between the kill and the restart would leave it down, so one real defect would be reported as a dozen fake ones in every later testan afterEach restores it whatever happened
5high(slice 7a, already fixed) a tier’s framerate was a target rather than a ceiling, so no camera below 30 fps could stream on any tierthe rate filter is a range; regression test verified to fail without the fix
6lowThe relay-only simulator scenario asserted that both ends use a relay candidate, which is a claim about the robot’s configuration rather than about the session’s pathit asserts the operator’s local candidate is relay — with the operator forced to relay, every packet traverses coturn regardless of the agent’s candidate — and reports the remote type as detail
CheckResult
Webhooks for one sessionrobot.online, session.started, session.ended, each signed and verified; unsigned and wrongly signed posts both refused with 401 and not recorded
TURN after the fixadd-turn-server turn:coturn:3478 -> ok; relay session connected in 134 ms; 34 frames in 1 s over the relay
Hub at rest0 subscribers on every track and tier with no sessions — the drifting counts in finding 3 were sampling, not a leak
Killed agentoperator saw peer-gone:agent-disconnected 629 ms after SIGKILL; streaming again 6.9 s later
Glass-to-glassclean 191/210 ms, lossy 198/263 ms, relay 173/205 ms on this laptop — recorded, and correctly refused as a verdict (below)

What this machine cannot check {#what-this-machine-cannot-check}

Section titled “What this machine cannot check {#what-this-machine-cannot-check}”

Several media suites pass one at a time and fail together on a developer laptop: three viewers decoding six streams while the robot software-encodes them saturates the machine. The frame stamp measures it rather than leaving it to opinion — 13 frames in 1.5 s with a counter gap of 8, or 1 frame where 15 are expected. The same starvation makes the simulator’s hot-plug scenario intermittent when run after others, while it passes alone.

This is recorded in docs/25, and it is why the latency harness refuses to hold a CPU-limited run to a budget.

Two corrections, both of this review’s own making. They are left here rather than tidied away, because the sequence is the lesson.

Finding 3 replaced a fixed two-second wait with a poll on the hub’s subscriber count. That was right, but the two seconds had been doing a second job nobody had written down: giving the frame-stamp watchers time to collect frames before the next assertion read them. On a fast machine the poll returns in milliseconds, so that assertion began reading empty watchers.

I then mis-read the resulting CI failure as hardware. It looked exactly like the starvation documented above, so I tagged the suite @heavy and moved it to the nightly — a workaround for a problem I had introduced, resting on a diagnosis that the evidence did not support. The tell was there and I walked past it: the suite failed in 3.7 seconds, far too fast to be a machine running out of breath, and it had been green on hosted CI before this review touched it.

The fix is to wait for the frames themselves rather than for a duration, which depends on no machine’s speed. With that, the suite passes on a four-core runner and on a laptop that genuinely does starve under load. The @heavy mechanism is gone: it existed only to serve the wrong diagnosis.

The starvation described above is real and still applies to the media suites run back to back. It simply was not this.

  • Add relay-only to the CI simulator set once it has been seen stable on CI hardware. It gates the path finding 2 shows was silently broken, but it is CPU-limited on this laptop and a gate that cannot be trusted is worse than none, so it is not added on a guess.
  • The agent’s own FJARR_ICE_POLICY=relay — a robot forced to relay — is still untested; finding 6 explains why the old assertion looked like it covered this and did not.
  • Input-to-photon lands with fjarr.desktop in M3, on the slice-7b rig.