Problem
There is no way to ask a live room whether its member_info holds two records for the same member at the same version. That check is a pre-migration gate for any change to the equal-version tiebreak (#571 / #572 is the immediate case), because a surviving equal-version duplicate resolves to a different winner under a new tiebreak rule — which flips ban authority at the re-key PUT.
Every existing surface falls short:
| command |
why it can't answer |
debug room-state |
aggregates only (member_count, ban_count, deputy grants) — no per-record data |
member list |
canonicalized, one entry per member. Duplicates are invisible by construction: it routes through MemberInfoV1::canonical |
debug contract-get |
performs the GET but prints a summary; no raw state, no --raw |
debug config / debug bans |
wrong subtrees |
There is also no read-only command that refreshes the local store. ~/.local/share/river/rooms.json does hold state.member_info with per-record version, but running room list, member list, debug room-state and debug contract-get all left its mtime untouched (verified 2026-07-30). So the only machine-readable copy of the data is whatever a past mutating command happened to persist.
Concretely, on 2026-07-30 the freshest available Official-room snapshot was from 2026-07-29 20:58 and reported 477 member_info records / 477 distinct member_ids / 0 equal-version duplicates — while a live debug room-state reported member_count: 118. The result is reassuring but its provenance is stale, and it cannot be refreshed on demand.
Suggested fix
Either of these closes it; the first is smaller.
-
riverctl debug member-info [--json] <room_owner_key> — dump the raw member_info vector as fetched, one row per record: member_id, version, signature digest (or its first bytes), deputies. Explicitly not canonicalized — showing duplicates is the entire point. Then the gate is a one-liner:
riverctl -f json debug member-info <owner> \
| jq -e '[.[] | {m:.member_id, v:.version}] | (length) == (unique | length)'
-
--raw on debug contract-get — emit the fetched state bytes so any consumer can decode with river-core. More general, more useful for future gates of this shape, slightly more work.
A --refresh flag on the read-only commands (persist what was just fetched) would also be worth having independently — the current behaviour makes the local store quietly and unboundedly stale.
Why this is worth a command rather than a script
The natural workaround is a purpose-built binary that GETs the contract and decodes ChatRoomStateV1. That means a second implementation of riverctl's connect/fetch path, maintained separately, used once per contract re-key — the kind of tool that rots between uses and is trusted anyway. The check belongs where the fetch path already lives.
Raised from #572, where the review asked for "a one-line riverctl duplicate check before migrating". It turned out not to exist.
[AI-assisted - Claude]
Problem
There is no way to ask a live room whether its
member_infoholds two records for the same member at the sameversion. That check is a pre-migration gate for any change to the equal-version tiebreak (#571 / #572 is the immediate case), because a surviving equal-version duplicate resolves to a different winner under a new tiebreak rule — which flips ban authority at the re-key PUT.Every existing surface falls short:
debug room-statemember_count,ban_count, deputy grants) — no per-record datamember listMemberInfoV1::canonicaldebug contract-get--rawdebug config/debug bansThere is also no read-only command that refreshes the local store.
~/.local/share/river/rooms.jsondoes holdstate.member_infowith per-recordversion, but runningroom list,member list,debug room-stateanddebug contract-getall left its mtime untouched (verified 2026-07-30). So the only machine-readable copy of the data is whatever a past mutating command happened to persist.Concretely, on 2026-07-30 the freshest available Official-room snapshot was from 2026-07-29 20:58 and reported 477 member_info records / 477 distinct member_ids / 0 equal-version duplicates — while a live
debug room-statereportedmember_count: 118. The result is reassuring but its provenance is stale, and it cannot be refreshed on demand.Suggested fix
Either of these closes it; the first is smaller.
riverctl debug member-info [--json] <room_owner_key>— dump the rawmember_infovector as fetched, one row per record:member_id,version, signature digest (or its first bytes),deputies. Explicitly not canonicalized — showing duplicates is the entire point. Then the gate is a one-liner:--rawondebug contract-get— emit the fetched state bytes so any consumer can decode withriver-core. More general, more useful for future gates of this shape, slightly more work.A
--refreshflag on the read-only commands (persist what was just fetched) would also be worth having independently — the current behaviour makes the local store quietly and unboundedly stale.Why this is worth a command rather than a script
The natural workaround is a purpose-built binary that GETs the contract and decodes
ChatRoomStateV1. That means a second implementation of riverctl's connect/fetch path, maintained separately, used once per contract re-key — the kind of tool that rots between uses and is trusted anyway. The check belongs where the fetch path already lives.Raised from #572, where the review asked for "a one-line riverctl duplicate check before migrating". It turned out not to exist.
[AI-assisted - Claude]