# Findings on the relay spec, revision 3

Reviewed by **GPT-6 (Codex), 2026-09-07**, third pass, against revision 3 at `6b64d75`, with Paul's rulings treated as fixed. Unedited apart from this note. Revision 4 records how each finding was taken.

| Revision-2 finding | Disposition | Reason |
|---|---|---|
| R2-1 — Aggregate publication | Partial | ETag-first ordering fixes stale publication, but interrupted deletion escapes the specified reconciler. |
| R2-2 — Interrupted operations | Partial | Phases recover interrupted enable; checking `op` before a separate config write does not fence superseded writers. |
| R2-3 — Owner binding | Partial | Typed addresses distinguish transports, but the deletion callback lacks the relay address required by the text. |
| R2-4 — Rotation | Partial | Retirement and loaded-key heartbeats are specified; adoption evidence remains insufficient and other issuance paths bypass the gate. |
| R2-5 — Metering | Partial | Forwarding directions and rejected ingress are corrected; restart recovery loses the preceding-hour portion of the budget. |
| R2-6 — Config adoption | Partial | An acknowledgement is specified, but its identity and granularity cannot establish per-instance adoption. |
| R2-7 — Operational limits | Closed | Account and box admission limits and token buckets supply the bounds chosen by ruling 6. |
| R2-8 — Memory priority | Partial | The new arithmetic omits swap and page tables; local shedding does not establish the stated host-pressure behavior. |
| R2-9 — EIP isolation | Closed | Forwarding is restricted to live relay ports, with a real-EIP acceptance test. |
| R2-10 — Authoritative stage boundary | Partial | Paul’s decisions are recorded, but several contradictory roadmap passages remain. |
| R2-11 — Region ordering | Closed | Console order removes the placement-conversion ambiguity; the stale parent promise is covered below. |
| R2-12 — Incorrect references | Closed | The nonexistent section reference is removed and `writeEnv` now points to its definition. |

Reviewed commit `6b64d753c9e62d5a4b8550fa5d2cd317ec98c0f4`. Paul’s six rulings are treated as fixed. All three cryptographic vector values reproduce. No files were changed and no live TURN or EC2 tests were run.

## R3-1 — P1 — Interrupted deletion can leave authority enabled indefinitely

**Section:** §5, reconciliation and app deletion.

The reconciler processes existing records whose phase is not `done`. App deletion removes the record itself before publishing the aggregate. If deletion stops between those steps, no unfinished record remains for reconciliation to find.

The old aggregate can therefore continue authorizing the deleted app. Successful sixty-second reads renew the relay’s lease against that stale aggregate, so the fifteen-minute lease does not bound the failure. A holder of the derived key can keep minting credentials while that authority remains.

**Evidence:** The [reconciliation algorithm][reconcile] only resumes unfinished records, whereas [app deletion][deletion] claims that reconciliation catches a missing record within five minutes.

**Correction:** Require an unconditional aggregate rebuild at console start and every five minutes, including when no unfinished records exist, or preserve a deletion tombstone until publication completes. Specify recovery after deletion removes the record but before aggregate publication.

## R3-2 — P2 — The operation check still permits stale config publication

**Section:** §5, operation walk.

The following sequence satisfies the text:

1. Enable operation A re-reads `relay.json` and confirms its `op`.
2. A pauses before calling `appconfig.Apply`.
3. Disable operation B removes the credentials, publishes the aggregate and reaches `done`.
4. A resumes. `Apply` reads the current config bundle and successfully writes A’s credentials back.
5. A’s subsequent phase CAS fails, but its config write has already landed.

Reconciliation skips B because it is `done`. The wrong config can persist indefinitely. Re-enable or rotation can similarly leave an enabled app holding superseded credentials.

**Evidence:** The [operation check][reconcile] precedes the config write. [`applyAt`][apply] obtains its own config ETag and conditionally writes that object; it does not condition publication on the relay operation.

**Correction:** Define a fence enforced at config publication, covering API handlers and reconciliation. The config write must reject a superseded operation, rather than discovering supersession afterward. Merely adding another record read leaves the same race.

## R3-3 — P2 — Allocation deletion requires an argument Pion does not provide

**Section:** §3, owner binding.

The spec requires `OnAllocationDeleted` to remove only the binding whose relay address matches the event’s. Pion v4.1.4 supplies no relay address in that callback. It supplies source and destination addresses, protocol, username and realm. The creation callback does supply the relay address.

This matters because Pion removes the allocation from its manager before closing it and issuing the deletion event. Cleanup must not erase a replacement binding created while the old allocation is closing.

**Evidence:** [Owner-binding requirement][binding]; Pion’s [callback signatures][events] and [deletion sequence][pion-delete].

**Correction:** Associate cleanup with the registered relay-socket wrapper, which knows its allocation identity, or specify another available discriminator that cannot remove a replacement binding. Correct the callback claim.

## R3-4 — P2 — The config acknowledgement cannot prove the rotation cutoff

**Section:** §2, config delivery and credential lifetime; §5, config adoption and rotation.

The proposed object is keyed by box, account and app, but adoption happens per route. One app can have multiple routes and environments on one box. If one route restarts successfully while another remains on old config, the successful acknowledgement can make the box appear adopted without identifying the stale instance.

The generation string also lacks the required relationship to rendered content. The existing `config-gen.json` is a separate change notification across all bundles. Its update can fail after the manifest write, and concurrent touches can coalesce. It is not an immutable identifier embedded in the config being rendered.

There is also a direct delivery contradiction: §2 says exec routes opting into file delivery restart, while §5 says they are not restarted. Existing socket-exec routes receive files and are bounced; opted-in TCP-address routes receive files but are never bounced.

These distinctions determine whether the last issuer actually stopped using the old key. They cannot be collapsed into “a config-ack for that generation exists.”

**Evidence:** [Acknowledgement schema and status rules][ack]; [delivery and lifetime promises][delivery]; existing [per-route/environment synchronization][sync], [generation semantics][generation], and [exec delivery contract][syncexec].

**Correction:** Define acknowledgements per route, including the effective relay key identity actually adopted and the result of application/restart. Distinguish running adoption, hibernating delivery, failed restart and externally managed processes. Bind the acknowledgement to the rendered config identity rather than relying on the app-wide notification alone. Retirement must wait for evidence covering every issuer; file delivery alone must not satisfy that condition.

## R3-5 — P2 — Enable and reconciliation can issue the new key before the rotation gate passes

**Section:** §5, enable, reconciliation and rotation.

Rotation updates the console’s master secret before invoking the command that checks relay heartbeats. During that interval, a first enable, region addition or resumed config operation derives credentials from the region’s current console master.

Those paths have no heartbeat gate. They can distribute the new `kid` while a relay has failed to load it—even while `rotate` correctly refuses to proceed. A member then receives credentials that an advertised relay rejects.

**Evidence:** [Enable selects the current master][enable]; [reconciliation derives from the record and masters][reconcile]; [rotation changes the console secret before checking acknowledgements][rotation].

**Correction:** Separate staged masters from the active issuance master, and switch issuance only after the heartbeat gate succeeds. Every credential-writing path must use that gated selection, including reconciliation and region additions.

## R3-6 — P2 — Restart recovery drops part of the rolling-hour budget

**Section:** §3, limits and metering.

The spec explicitly recovers only the current hour’s metrics object. At 12:01, the rolling-hour budget still includes traffic from 11:01 onward, most of which resides in the preceding object.

A restart therefore forgets almost the entire window and can immediately readmit an app or account whose budget remains exhausted. This contradicts the stated recovery behavior.

**Evidence:** [Rolling-hour limits and current-hour recovery][meters].

**Correction:** Load every hourly object intersecting the rolling window—normally the previous and current hour—and rebuild budgets from the relevant samples. Include a restart across an hour boundary in the acceptance tests.

## R3-7 — P2 — The OOM arithmetic does not establish the claimed ordering

**Section:** §3, hardening and memory; §7, flagship gate.

The stated tenant maximum of 1000 follows from RSS alone. Linux’s score also includes swap entries and page tables, and global scoring uses RAM plus swap. This repository permits a tenant to use swap equal to its memory cap, so a 1 GiB memory ceiling does not establish the claimed scoring bound. [Linux scoring implementation](https://raw.githubusercontent.com/torvalds/linux/v6.12/mm/oom_kill.c); [tenant swap allowance][swap].

There is a separate eligibility problem: exhausting the edge’s 80% cgroup ceiling invokes a kill within that tree. The relay, in its own unit, is not a candidate. Shedding only when the relay exceeds its own 160 MiB threshold does not respond to pressure created elsewhere while the relay remains small.

**Evidence:** [The arithmetic and shedding trigger][memory]; [the edge’s cgroup boundary][edge-memory]; [the relay-first gate][relay-gate].

**Correction:** State which pressure conditions the guarantee covers and correct the scoring envelope to include swap and page tables. If relay-first shedding is required under host-wide pressure, specify a host-pressure trigger. Align the gate with that scope; the fixed defaults alone do not establish it.

## R3-8 — P2 — The roadmap still contradicts the approved relay contract

**Sections:** Roadmap stage 4a and §11; relay §§2–5 and §9.

The amendments resolve TLS, credential duration and the main metering gate, but leave these conflicting instructions:

| Topic | Remaining roadmap text | Revision-3 contract |
|---|---|---|
| Master exposure | The custody paragraph describes exposure of enabled apps’ derived keys and calls the boundary no worse than existing secrets. | Ruling 3 explicitly exposes every region’s master through console sealed config, including authority to derive future app keys. |
| Region order | Placement puts the instance’s own region first. | Every instance receives console order. |
| Peer policy | Permissions refuse the relay’s own addresses. | The EIP is permitted, with forwarding restricted to live relay ports. |
| Authority lifecycle | Authority is synced from the route table; disable bumps incarnation. | Authority derives from per-app relay records; disable removes authority and re-enable creates the new incarnation. |
| Accounting amendment | §11 assigns three relay meters to stage 4a. | Ruling 5 assigns two meters to 4a and moves the inter-region meter to stage 5. |

**Evidence:** Roadmap [authority and custody][roadmap-authority], [peer policy and placement][roadmap-policy], and [unit-economics amendment][roadmap-accounting].

**Correction:** Update these passages to reflect the fixed rulings and revision-3 mechanisms. No further policy decision is needed; the parent must stop giving the implementation plan conflicting instructions.

**Verdict: request changes.** One P1 and seven P2 findings remain.

[reconcile]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:389
[deletion]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:505
[apply]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/store.go:146
[binding]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:199
[events]: /home/paulca/.claude/jobs/f1f6099c/tmp/gomodcache/github.com/pion/turn/v4@v4.1.4/internal/allocation/event_handler.go:20
[pion-delete]: /home/paulca/.claude/jobs/f1f6099c/tmp/gomodcache/github.com/pion/turn/v4@v4.1.4/internal/allocation/allocation_manager.go:150
[ack]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:455
[delivery]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:108
[sync]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/sync.go:30
[generation]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/generation.go:28
[syncexec]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/appconfig/syncexec.go:1
[enable]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:442
[rotation]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:481
[meters]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:242
[swap]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/cgroup/cgroup.go:313
[memory]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:157
[edge-memory]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/internal/host/units/carlos-edge.service:57
[relay-gate]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-relay-stun-turn-design.md:560
[roadmap-authority]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:620
[roadmap-policy]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:656
[roadmap-accounting]: /home/paulca/amadan.net/carlos/platform/.claude/worktrees/media-roadmap-spec/docs/superpowers/specs/2026-09-07-media-platform-roadmap-design.md:979