Recording a direction from Andrew (2026-08-26) plus the measurements that support it, because it decides #35, #38 and #39 and none of those can be settled without it.
The change in premise
Two facts reframe every open schema question:
- echoreplay is going away. The archive is converted once and then deleted.
- Tape generation is moving into the game engine — sourced from the broadcaster and the game state machine, not from polling
/session.
Andrew's proposal:
maintain precision for echoreplay translation, but optimize for the real message/event/values that the echoreplay /session endpoint fields are derived from. tape v2 can be a transition to tape v3... which would completely shed the unoptimized, delta-unfriendly, or non-deterministic/non-atomic nature of an echoreplay file.
Why /session is the wrong thing to model
/session is a projection of engine state into hand-parseable JSON, sampled by polling. Three consequences, each measurable:
It renders derived values next to their inputs. Corpus measurements:
| Relationship |
Result |
team stats == sum of player stats |
68,680 / 68,680, zero mismatches |
pot_speed_from_rot / rot_per_sec |
one constant, 0.7539821…, spread 3.6e-08 across 3,600 samples |
arm_speed vs speed_from_arm |
54,312 bit-identical, 2,400 differing |
That constant is ~2*pi*0.12 — tangential speed at the disc radius. It is physics computed from rot_per_sec, shipped so a client does not have to. v2 already drops team stats and rebuilds them with sumTeamStats (reconstruct.go:449), so the pattern is established; it simply was not applied to the rest.
It renders orientation into a lossy form. The engine's own wire protocol carries a quaternion:
broadcaster/v1/r15net.proto:229 spatial.v1.Quat rotation = 6;
(now living in nevr-server-rs/rad-protocol/proto/, and it does import "spatial/v1/types.proto" — so the blast-radius constraint on spatial.v1.Pose still holds.)
/session renders that quaternion as forward/left/up: nine numbers for three degrees of freedom. Measured across 740,588 bases, they are orthonormal only to ~1e-4 — about three orders of magnitude worse than float32 epsilon — and uniformly left-handed. The deviation is rendering noise, not information, and it is what the round-trip's 2.4e-3 orientation tolerance has actually been measuring.
It is a sample, not a transaction. Events between polls are inferred by diffing snapshots, and the proto already records where that fails (echo_arena.proto:580-600): the engine reported 0 stuns and 0 catches for a player across a whole match while tape's sensors emitted 3 and 5. Engine-native generation makes events authoritative instead of inferred. This is the strongest argument for v3.
The contract that separates the two
I asked earlier whether v2 is semantically lossless or byte-lossless. The answer is per-provenance, not global:
|
source |
contract |
governing risk |
| v2 echoreplay lane |
the archive, converted once |
byte-lossless |
the source is deleted; a wrong call is unrecoverable |
| v3 engine lane |
broadcaster + state machine |
semantically lossless |
none — the source is live and re-derivable |
What that implies for v2 right now
Be maximally conservative. 'Derivable' is not the same as 'bit-reproducible', and the precision data shows the gap is real: speed_from_arm matches arm_speed 96% of the time and differs 4%; the pot_speed_from_rot ratio is constant only to float32 resolution. Dropping either as redundant would be a one-way loss against an archive that no longer exists to check.
So: do not apply the redundancy audit to v2. It applies to v3.
What that implies for v3
Carry the inputs, not the projections: quaternions rather than bases, rot_per_sec rather than pot_speed_from_rot, engine events rather than diffed snapshots, per-player stats rather than team sums.
Effect on the open issues
Open question
Whether the v2 archival lane must be byte-exact on float values or only on number formatting. The repo currently asserts byte-compatibility for the writer's formatting (FixProtojsonUint64Encoding, FixEngineFloatFormatting) which is the narrower claim. If float values must also be reproduced exactly, the basis noise has to be stored as a residual. That is the one thing still to settle before the archive is converted.
Recording a direction from Andrew (2026-08-26) plus the measurements that support it, because it decides #35, #38 and #39 and none of those can be settled without it.
The change in premise
Two facts reframe every open schema question:
/session.Andrew's proposal:
Why
/sessionis the wrong thing to model/sessionis a projection of engine state into hand-parseable JSON, sampled by polling. Three consequences, each measurable:It renders derived values next to their inputs. Corpus measurements:
stats== sum of player statspot_speed_from_rot/rot_per_secarm_speedvsspeed_from_armThat constant is ~
2*pi*0.12— tangential speed at the disc radius. It is physics computed fromrot_per_sec, shipped so a client does not have to. v2 already drops team stats and rebuilds them withsumTeamStats(reconstruct.go:449), so the pattern is established; it simply was not applied to the rest.It renders orientation into a lossy form. The engine's own wire protocol carries a quaternion:
(now living in
nevr-server-rs/rad-protocol/proto/, and it doesimport "spatial/v1/types.proto"— so the blast-radius constraint onspatial.v1.Posestill holds.)/sessionrenders that quaternion asforward/left/up: nine numbers for three degrees of freedom. Measured across 740,588 bases, they are orthonormal only to ~1e-4 — about three orders of magnitude worse than float32 epsilon — and uniformly left-handed. The deviation is rendering noise, not information, and it is what the round-trip's 2.4e-3 orientation tolerance has actually been measuring.It is a sample, not a transaction. Events between polls are inferred by diffing snapshots, and the proto already records where that fails (
echo_arena.proto:580-600): the engine reported 0 stuns and 0 catches for a player across a whole match while tape's sensors emitted 3 and 5. Engine-native generation makes events authoritative instead of inferred. This is the strongest argument for v3.The contract that separates the two
I asked earlier whether v2 is semantically lossless or byte-lossless. The answer is per-provenance, not global:
What that implies for v2 right now
Be maximally conservative. 'Derivable' is not the same as 'bit-reproducible', and the precision data shows the gap is real:
speed_from_armmatchesarm_speed96% of the time and differs 4%; thepot_speed_from_rotratio is constant only to float32 resolution. Dropping either as redundant would be a one-way loss against an archive that no longer exists to check.So: do not apply the redundancy audit to v2. It applies to v3.
What that implies for v3
Carry the inputs, not the projections: quaternions rather than bases,
rot_per_secrather thanpot_speed_from_rot, engine events rather than diffed snapshots, per-player stats rather than team sums.Effect on the open issues
Basismessage I proposed is withdrawn. It would reconstruct RAD's JSON rendering artifact. v2 keeps basis fidelity for the archival lane; v3 carries the quaternion the engine already has. The dense/keyframe/hydrated reframing is unaffected and still stands.possession_timeis derived outright.last_throw's v2 home around/sessionshape. In v3 it is a throw event from the engine with the thrower attached, not a session-level scalar that happens to describe whoever is local.Open question
Whether the v2 archival lane must be byte-exact on float values or only on number formatting. The repo currently asserts byte-compatibility for the writer's formatting (
FixProtojsonUint64Encoding,FixEngineFloatFormatting) which is the narrower claim. If float values must also be reproduced exactly, the basis noise has to be stored as a residual. That is the one thing still to settle before the archive is converted.