# DESIGN — Frame-Authority Token (the authority spine)

> **SHIPPED vs DESIGNED (read first).** This note designs a four-field token
> `{ presenceAge, witnesses, founderId, epoch }` with `witnesses` as a secondary tie-break and a
> `≥k`-distinct **corroboration gate**. **That is NOT what shipped.** The implemented token is
> `{ simulationAge, peerId }` (`Recovery.js` / `SimulationEngine.js`), and the property the gate and
> `witnesses` were meant to provide — *a minority cannot move an established session* — is instead
> provided by **support-weighting** in `compareFrameRank` (rank by first-hand live-peer headcount,
> then by seniority `compareAuthority`, with a **maturity gate** so a fresh majority can't hijack a
> mature frame). Consequences:
> - **`witnesses` did not ship and is not needed.** It was only an equal-age secondary key; `peerId`
>   already breaks age ties deterministically, and support-weighting subsumes "prefer the
>   better-connected frame" as the *primary* key.
> - **The corroboration gate did not ship.** Support-weighting defends the same in-scope property
>   (lone/minority dissenter can't move a group); both designs equally fall to a colluding/forged
>   *majority*, which stays out of scope.
> - **`presenceAge` (time-since-founding from `epoch`) shipped as `simulationAge`**, decoupled from the
>   tick epoch (it tracks held-frame run time and re-anchors to 0 on abandon-and-resync).
> - The doc's framing of *seniority* as "the spine" is inverted in the shipped code: the spine is
>   **support + maturity**; `compareAuthority` seniority is only the final tie-break.
> The reasoning below is preserved as the design record; treat the four-field token and gate as
> *designed-not-shipped*.

**Covers: EDGE 1 (cold-start tie-break), EDGE 2 (group merge / longer-present wins), EDGE 4 (seniority beats readiness).**

These three EDGEs share one mechanism: replace today's scalar `epoch` min-register
(`SimulationEngine.js:_adoptEpoch`, strict-min) with a richer, gossiped **frame-authority token** so
that "which frame the network runs on" is decided by *seniority*, not by the numerically-smallest raw
wall-clock instant. EDGE 1 is the degenerate (all-equal) case that needs only the tie-break; EDGE 2 is
the same election re-converging after a partition heal; EDGE 4 is the same election refusing to treat a
freshly-`isReady()` clock as authoritative. Correction-application (what happens to the tick when a peer
adopts a different frame) is delegated to `DESIGN_EDGE3_EDGE6.md`; staleness/forgery defenses to
`DESIGN_EDGE5A.md`.

## Rule
Each peer advertises a **frame-authority token** `authority = { presenceAge, witnesses, founderId, epoch }`
and adopts the lexicographically-greater of (its own token, any token it hears), compared as: larger
**quantized `presenceAge`** wins; ties broken by larger **`witnesses`**; final ties broken by **lowest
`founderId`** — the winning token's `(founderId, epoch)` becomes the frame this peer derives its tick
from. `presenceAge` is the *duration since the frame was founded* (`= floor((syncedNow - epoch)/1000)`
seconds), measured in elapsed time and therefore comparable across two groups whose absolute clocks
differ, and advertised as `0` until the peer's clock is both `isReady()` and corroborated (so readiness
alone never manufactures seniority).

## Message / Field
The token rides the **existing attendance** (`MSG_ATTENDANCE = 'em-attend'`, emitted every `tickMs` by live
peers at `SimulationEngine.js:408`). We **replace the current `beat.epoch`** (added at line 409 only when
`syncedTick`) with a single object field **`beat.authority`** carrying the four named subfields
`beat.authority.presenceAge`, `beat.authority.witnesses`, `beat.authority.founderId`,
`beat.authority.epoch`. The receive path that today calls `this._adoptEpoch(msg.epoch)` in `_onAttendance`
(`SimulationEngine.js:851`) is replaced by `this._adoptAuthority(msg.authority)`, which runs the
lexicographic compare above and, on a strict win for the incoming token, re-anchors the local frame. The
same `beat.authority` object is also attached to the bootstrap payload `MSG_BOOT` (today `boot.epoch` at
line 734) and to `MSG_BOOT_DECLINE` (today carries `epoch` at lines 703/708), so a joiner learns the
authoritative frame from whichever message it gets first — no new message type and no new round.

## Monotonicity Argument
Adopting a winning token can change `epoch`, which can change the *derived* tick either up or down; the
sacred no-rewind invariant is preserved by never letting adoption *lower* the local tick. When
`_adoptAuthority` yields a frame whose epoch implies a smaller present tick, the peer does **not** snap
backward — it hands the delta to the correction policy (`DESIGN_EDGE3_EDGE6.md`), which **stalls** the
tick (holds it constant while real time catches up) for a small delta or **abandon-and-resyncs**
(catching-up → re-bootstrap → resume at `max(currentTick, target)`) for a large one; in both paths the
counter is non-decreasing. The members of the *winning* frame never adopt anything (their own token is
always maximal), so a senior group's tick cadence is provably undisturbed — exactly the EDGE 2/EDGE 4
requirement that A's tick keeps tracking `elapsed/tickMs` and is never yanked toward a junior peer. Thus
no peer's in-game tick ever decreases under this rule.

## Determinism Argument
The compare is a pure lexicographic order over `(presenceAge_quantized, witnesses, founderId)` with a
total, reproducible tie-break: **lowest `founderId` wins** when the first two fields tie, and ids are
unique, so there is always a unique winner and never a coin-flip. `presenceAge` is **quantized to whole
seconds** before comparison so that millisecond jitter from latency/sampling cannot make a younger frame
spuriously out-rank an older one and cannot make the winner depend on *when* a beat happened to arrive;
genuine equal-age cases (EDGE 1 cold-start, EDGE 2's equal-age groups) fall deterministically through to
`founderId`. The adoption operation is a **join over a total order** (associative, commutative,
idempotent — a CRDT-style max), so regardless of message reordering, duplication, or the partition/heal
pattern, every peer converges to the *same* maximal token; insertion order and link latency change the
*timing* of convergence but not its *result*. In EDGE 1 two simultaneous peers both quantize to
`presenceAge = 0`, both have `witnesses` ≈ their own count, and the lower `founderId` founds the frame —
identical on both peers, every run.

## Failure Modes
1. **Unconverged clock inflates `presenceAge`.** A peer whose synced clock has not yet settled could
   compute `syncedNow - epoch` wrongly and advertise a too-large age, spuriously winning. Mitigation:
   advertise `presenceAge = 0` (an explicit "unsynced" token) until `clock.isReady()` **and** at least
   one distinct peer has corroborated a sample — this is the same gate EDGE 4 needs (readiness ≠ age).
2. **Quantization-boundary flapping.** Two frames whose ages straddle a one-second bucket boundary could
   oscillate which one wins as each ticks over the boundary at slightly different instants, causing the
   network to thrash between frames. Mitigation: **hysteresis** — once a peer has adopted a frame, it
   only switches to another if that other's token exceeds the current one by **≥1 full bucket** (or wins
   on `founderId` at exactly-equal age), so equal/near-equal ages stay pinned to the `founderId` winner.
3. **Founder departs.** If `presenceAge` were "time since I last saw the founder", losing the founder
   would reset the frame's age and let a younger frame out-rank it. Mitigation: `presenceAge` is derived
   from the frame's `epoch` (data carried in the token), not from founder liveness, so surviving members
   keep advertising a correctly-growing age after the founder disconnects — the frame outlives its
   founder.
4. **Witness double-count / churn.** `witnesses` could be inflated by counting the same peer twice or by
   transient reconnects, perturbing the secondary tie-break. Mitigation: `witnesses` = count of *distinct
   peer ids* seen corroborating within a sliding window, and it is only a **secondary** key — the primary
   `presenceAge` and final `founderId` keys decide almost all cases, so witness noise cannot flip a clear
   seniority gap.

## How This Stays Sparse
The token adds **four numbers to a attendance the peer already sends every `tickMs`** — there is no new
message, no new round, and no per-tick flood; the per-beat cost is O(1) bytes and the receive-side
`_adoptAuthority` compare is O(1) per beat. `presenceAge` is computed locally from `epoch` and the
synced clock (no extra sampling or probe traffic), `witnesses` is maintained incrementally from the
corroboration events the clock layer already exchanges, and adoption touches only local state. In a mesh
of N peers the message count is unchanged from today (each live peer beats to its neighbours as before);
we have widened a field, not added a protocol. Bootstrap/decline already carried `epoch`, so attaching
the token there is also free. Nothing in this rule scales with the tick rate — convergence happens over
attendance, which are emitted at the existing cadence regardless of how the election resolves.

## How a Peer Could Exploit or Break This
A peer (malicious or merely buggy with a wildly-wrong clock) can advertise a **huge `presenceAge` with a
tiny `epoch`** — the EDGE-1 raw-epoch hijack reborn at the token level — and, undefended, win the
election and drag the whole network thousands of ticks to its frame. The first-line consequence is a
fully-synced, healthy group being yanked off its cadence by a single lying newcomer. This is contained
by three designed-for defenses (full coverage of the *malicious* variant, EDGE 5b, is OUT of initial
scope and flagged): (a) the **corroboration gate** in `DESIGN_EDGE5A.md` — a peer already in a healthy
≥k-peer agreement refuses to adopt a foreign frame unless ≥k *distinct* peers corroborate that token, so
a lone claimant cannot move the group regardless of its claimed age; (b) `presenceAge` counts only
**confirmed-synced** time, and a fresh liar has no corroborators to back its age (its token is treated as
`0`/unsynced); (c) **self-demotion** (EDGE 5a) ensures even a genuinely-old-but-stale token decays. A
cryptographically-forged, *corroborated* age would still defeat this and requires signed attestation —
explicitly out of scope, recorded as an open risk.
