[Backend] Disambiguate speaker labels — nicknames, collisions, and the snowflake fallback #344
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM. Found in the August 2026 session lifecycle review (#319).
The defect
Three ways a speaker label goes wrong before the LLM ever sees the transcript:
recording.py:717-723resolves names viabot.get_user(...).display_name, which isglobal_name or name— the guild nickname is never consulted. Players whose server nick is their character name lose that identity. Worse, the live recording dashboard does prefer the guild member (services/recording_status.py:28), so the name the GM sees during the session differs from the one in the transcript.character_mapis keyed by display name (reminder_tasks.py:2077-2113), so both map to whichever entry won a last-write-wins dict insert. Two players merge into one speaker.[00:12:01] 184623...: textwhich the LLM will merge into, or guess as, a named player.reminder_tasks.py:2059does the same for a WAV missing fromspeakers.json.Proposed fix
Resolve names from the guild member first, falling back to the global user. Key the character map by Discord user id, not by display name — the id is already in the filename. On a display-name collision, disambiguate in the rendered label. Never emit a bare snowflake: fall back to a stable readable form and flag the session so the GM can correct it.
Acceptance criteria
Verified; the two remaining gaps fixed in
dd8f305and7a689a9.Criteria
character_mapand_apply_character_namesno longer exist.resolve_speakersis keyed onowner_idthroughout, and persistence carries the id._disambiguatequalifies with an id suffix and loops on a second-order collision.The snowflake guard was fixed at the producer only
2114f42stopped the bot writing a bare id, and its own message notes thatspeakers.get(stem) or defaultcatches only a missing key. The backend consumer kept exactly thator, so a snowflake present as the value is truthy and walked straight through into the transcript — where the LLM reads it as a person's name and merges it into a real player, which is this issue's original symptom.That is a live path, not a leftover. Every
speakers.jsonwritten before that commit has exactly that shape; CLAUDE.md states the per-session audio directory is never deleted automatically; and reprocess is one of four documented routes back intoprocess_audio. An old recording reprocessed on a new backend reproduces the bug in full.resolved_speaker_namenow checks symmetrically against the stem and mirrors the bot'sunknown_speaker_label, so a track handled by either side reads the same to a GM."Flags the session" — recovered instead
Looking at where the gap actually is, flagging turned out to be the weaker answer.
session.attendance_unmatched_speakers— the attendance screen, which is exactly where someone goes to say who an unrecognised speaker was. That is the flag, and it already existed.So the linked account's display name now fills in — but only when the bot produced a placeholder, because the bot reads Discord directly and prefers the guild nickname, which is normally what a table calls each other. Its answer wins whenever it has one.
Recovering the name is strictly better than flagging that it is missing, so the criterion is met in substance rather than in form. If you would still like an explicit session-level marker on top of this, that is a small follow-up and worth its own issue rather than being assumed.
One residual the sweep missed
recording_status.pystill hadgetattr(member, "display_name", str(member_id))foruncaptured_members— the last bare-snowflake fallback. Lower stakes than the transcript path (no LLM reads it as a person) but the same unreadable string in front of the same GM, and there was already a helper for it.Closing.