Repository navigation
fix(speakers): add typed identity, count and presentation guardrails - #23462
Git-on-my-level wants to merge 38 commits into
Conversation
…diarization ids
A live episode's "So far" bullets read "Speaker 379 mentions..." because the
legacy live-moment renderer wrote the raw per-conversation diarization counter
into the prompt ("Speaker 379: ...") and the summarizer copied the label
verbatim into user-visible notes (#23381). The finished-notes v2 path already
renders cluster keys, so it never had the problem.
Add utils/speaker_display.SpeakerLabeler and route every LLM-facing or
user-facing transcript renderer through it: the owner renders as "You" or the
owner's name (live moments now resolve the account's display name), named
people keep their names, and every unnamed voice gets a dense per-transcript
"Speaker A/B/..." label in first-appearance order, keyed by
(speaker_id_scope, speaker_id) so merged multi-source transcripts stay
distinct. Raw diarization ids no longer reach any prompt or payload:
- live moments: incremental.speaker_label/_legacy_moment (+ moment rules now
forbid copying voice markers or placeholder headings like "New moment")
- TranscriptSegment.segments_as_string (titles, memories, summaries, chat)
- transcript_for_llm action-item speaker labels
- shared-chat bounded transcripts, smart-merge question excerpts, retrieval
chunks and MCP match snippets, review quotes
- conversation analytics and populate_speaker_names (MCP/integration/webhook
payloads)
Bump smart-merge QUESTION_VERSION: cached adjacent-merge answers were keyed to
the old raw-id excerpts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two guards so "Speaker 379" cannot come back:
- unit: run each LLM-facing formatter with the incident's raw ids (378/379,
across scopes) and assert the digits never survive a render; covers
segments_as_string, live and v2 moment renderers, cluster transcripts,
smart-merge lines, shared-chat bounded transcripts, MCP snippets, retrieval
chunks, action-item labels, populate_speaker_names and analytics.
- static: check_speaker_label_interpolation.py rejects any new interpolated
"Speaker {speaker_id}" (f-string, .format, %, concatenation) outside the
sanctioned SpeakerLabeler; registered in checks-manifest with a self-test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f79f136297
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if field(row, 'speaker_label_source') == 'manual': | ||
| name = ( | ||
| owner_name | ||
| if field(row, 'is_user', False) | ||
| else _clean_name(names.get(str(field(row, 'person_id') or ''))) | ||
| ) |
There was a problem hiding this comment.
Preserve projected person names when resolving manual labels
For a manually named non-owner segment in live moments, this branch discards the valid person_name resolved above and looks only in names. However, incremental.render_moment calls this helper without a names map: _named_segments supplies the trusted name through person_name instead. Consequently, a manually corrected speaker becomes anonymous again in the legacy-serving live-notes prompt. Preserve that trusted projection here, or pass the resolved names map from the episode caller.
Useful? React with 👍 / 👎.
| elif name: | ||
| labels[key] = name |
There was a problem hiding this comment.
Keep anonymous fallback labels separate from segment-bound names
When voice_order is supplied and the first segment of a raw voice has a resolved name, this stores that person's name as the fallback label for the entire voice. A later segment with the same raw identity but no person binding then receives that name through labels[key], even though its DisplayParticipant.name is None. Episode rendering always supplies voice_order, so this can falsely attribute an unbound excerpt to a person in live notes. Allocate an anonymous fallback independently and apply names only to the individually supported rows.
Useful? React with 👍 / 👎.
| rows = [ | ||
| {'id': cluster.segment_ids[0] if cluster.segment_ids else '', 'speaker_id': cluster.speaker_id} | ||
| for cluster in evidence.clusters | ||
| ] | ||
| names = {str(person.get('id')): str(person.get('name') or '') for person in people} | ||
| display = display_participants(rows, evidence=evidence, names=names) |
There was a problem hiding this comment.
Share collapse decisions with the transcript evidence renderer
When manual segment bindings affect the unnamed-voice threshold, rebuilding the projection from these synthetic rows produces a different keyspace from render_cluster_transcript, because the rows omit person_id and manual-label provenance. For example, nine one-second evidence clusters with one manually named segment remain nine separate transcript keys (only eight are unnamed), while this evidence packet collapses all nine into others. The notes prompt then receives contradictory voice identities for the same segment IDs. Reuse the transcript's projection or provide the same binding inputs before deciding which clusters collapse.
Useful? React with 👍 / 👎.
| final participants = ConversationDetailMeta.participants(conversation.transcriptSegments, | ||
| you: you, personName: (id) => personById(people, id)?.name, speakers: conversation.speakerResolution); | ||
| if (participants.uncounted) return null; | ||
| final count = participants.named.length + participants.unnamed; |
There was a problem hiding this comment.
Count distinct people rather than distinct display names
For two different people with the same name, the transcript heading now undercounts speakers: ConversationDetailMeta.participants delegates to EpisodePresentation.participants, whose name set deduplicates both people into one entry. An owner plus two separate people named Ada therefore displays two speakers instead of three. The existing transcript_heading_test.dart case “the same person across participant ids counts once, another person still counts” explicitly covers this input. Keep the name-list helper for presentation, but calculate the heading count using person identities and supported unnamed voices.
AGENTS.md reference: app/AGENTS.md:L5-L5
Useful? React with 👍 / 👎.
|
|
||
| def sanitize_speaker_text(text: str) -> str: | ||
| """Generated prose and replayed state cannot reintroduce old numeric placeholders.""" | ||
| return re.sub(r'(?i)\b(?:speaker|spk)[ _](?:c?\d+)\b', 'a speaker', text) |
There was a problem hiding this comment.
Sanitize the newly introduced alphabetic speaker placeholders
When the live-notes model copies an anonymous dialogue label, the sanitizer now misses the labels actually supplied by this commit: Speaker A, Speaker B, and so on. It only recognizes numeric forms. For example, parse_update accepts {"people":["Speaker A"],"title":"Speaker A follow-up"} unchanged, and to_wire preserves those values through the same sanitizer. This allows anonymous voice markers to appear as named participants and in user-visible headings despite the new prompt prohibition. Extend the presentation sanitization to cover the alphabetic label format as well.
Useful? React with 👍 / 👎.
| @lru_cache(maxsize=1) | ||
| def _counter(): | ||
| return Counter('omi_speaker_display_count_guard_total', 'Display projections collapsing excess unnamed voices') |
There was a problem hiding this comment.
Make lazy metric registration safe for concurrent first calls
If two rendering threads trigger the count guard for the first time in a worker, lru_cache can execute _counter concurrently before either result is cached. Both calls then register the same Prometheus timeseries, causing one to raise ValueError. Because display_participants does not catch this, telemetry can fail an otherwise valid transcript or analytics request; the synchronous conversation-analytics endpoint is one affected caller. Serialize initialization or reuse an already registered collector, as the existing helpers in utils/llm/notes_refactor_metrics.py do.
Useful? React with 👍 / 👎.
…n's evidence A People record, lexicon spelling, profile, memory or old meeting can no longer authorize a participant name. Calendar attendees overlapping the conversation, this conversation's roster/tile names and affirmative owner corrections still do. Enforced in the persisted notes contract independent of packet rollout; agent output is repaired, unsupported names become "a speaker" or hedged heard names. Failure-Class: notes-identity-leak Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load the real speaker presentation modules in the isolated date-validation harness, expect canonical c0 keys in wake-word rendering, and remove the duplicate Dart test import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 53249b58e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| named = {key: value.casefold() for key, value in self.speaker_names if value} | ||
| # An explicit resolved binding cannot be overwritten by a calendar lead. | ||
| if speaker is not None and speaker in named: | ||
| return named[speaker] == name.casefold() | ||
| if speaker is None and name.casefold() in named.values(): |
There was a problem hiding this comment.
Preserve policy-approved likely names during identity repair
When speaker evidence authorizes a likely-band candidate, display_participants supplies a name such as likely Ada, and the prompt prefix copies that value into speaker_names. This comparison does not recognize the underlying contact name Ada, so repair_identity blocks it and changes a compliant note such as “Likely Ada proposed a release” into “Likely a speaker proposed a release.” This occurs even without a participant manifest and contradicts SPEAKER_RULES, which explicitly allows likely-band names when hedged. Preserve those authorized hedged mentions without treating them as confident participant bindings.
AGENTS.md reference: backend/AGENTS.md:L3-L3
Useful? React with 👍 / 👎.
| if len(tokens) > 1 and _pattern(name).search(str(submission)): | ||
| for token in (tokens[0], tokens[-1]): | ||
| if len(token) > 2 and not any(token.casefold() in value.casefold().split() for value in supported): | ||
| aliases.add(token) |
There was a problem hiding this comment.
Avoid replacing ordinary words with inferred name aliases
When an unsupported full name contains a common word, adding its first and last tokens to a case-insensitive replacement pattern corrupts unrelated prose throughout the notes. For example, with contact Will Smith mentioned in the submission, repair_identity changes “We will ship on Friday” into “We “will” (as heard) ship on Friday” and similarly rewrites action descriptions. The full-name occurrence does not establish that every matching token elsewhere refers to that person. Restrict alias replacement to identifiable name references rather than globally scrubbing ordinary words.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y light notes_packet imported known_spellings at module scope, pulling the dream store and People database into every conversation_processing import; the isolated action-item test then failed at collection. Import it at the call, and pre-load the speaker display modules the models now import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Load the real known-spelling helper with explicit empty database adapters so notes-packet imports do not pull the dream-agent runtime into action-item date tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deduplicate named participants by People record ID so repeated diarization IDs count once while different people with the same display name retain separate participant counts. Preserve the existing transcript heading regression assertion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c518a37184
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| participant_names = { | ||
| str(person.get('name') or '').strip() | ||
| for person in people | ||
| if not person.get('is_ai_agent') | ||
| and not identity.supported(str(person.get('name') or '').strip(), person.get('speaker') or None) |
There was a problem hiding this comment.
Reject invalid speaker bindings without globally blocking valid names
When a submission assigns an already-resolved name to the wrong speaker, adding that name to blocked also removes its valid participant entry and scrubs correctly attributed prose throughout the notes. For example, with authoritative bindings c0 → Ada and c1 → Bob, participant entries for both Ada/c0 and Ada/c1 cause both entries to disappear and “Ada approved the release” to become “a speaker approved the release.” Reject the unsupported participant binding individually, while preserving names independently authorized by the speaker authority.
AGENTS.md reference: backend/AGENTS.md:L3-L3
Useful? React with 👍 / 👎.
| final participants = ConversationDetailMeta.participants(conversation.transcriptSegments, | ||
| you: you, personName: (id) => personById(people, id)?.name, speakers: conversation.speakerResolution); | ||
| if (participants.uncounted) return null; |
There was a problem hiding this comment.
Preserve the resolution guard on transcript speaker counts
When resolution is missing or unavailable but every segment belongs to the owner or a named person, ConversationDetailMeta.participants returns uncounted: false, so this replacement now displays an exact speaker count. Previously _speakerCount returned null whenever resolution was not countable. For example, an unavailable conversation containing only owner-labelled segments now shows a one-speaker count, contrary to the UI contract’s requirement to omit transcript counts for unavailable resolution. Keep an explicit resolution-countability check before deriving the count from the participant list.
AGENTS.md reference: app/AGENTS.md:L3-L3
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The published notes packet repair now defers its database-dependent spelling import until it is used. Keep the date harness minimal instead of eagerly loading the helper with extra database mocks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the identity-aware display authority at overlapping renderers; preserve main boundary regression coverage in its own suite, both static scan interfaces and the legacy scoped-key helper. Remove stale labeler scaffolding and keep analytics compatible with lightweight segment objects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the active worker count in the parent shell and update it with the existing PID arrays. Every 20ms polling iteration previously forked twice for command substitution merely to count workers. File isolation, status handoff, crash detection and timing budgets remain intact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The canonical identity authority passes the full raw-speaker scan without an extra exemption. Keep main full-scan coverage unchanged alongside the broader changed-file identity ratchet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rtion Main changed this regression to render all segments in one transcript. Retain its stronger A/B and six-distinct-output assertions; the old per-segment A/A expectation was kept incorrectly during the speaker-authority merge. Digest and attribution checks remain intact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A feature shard still exceeded the unchanged 720-second wall guard at 762s after counter-fork removal. Bash 5.1+ can wait for any tracked child; retain the atomic status-file and liveness reaper, with polling on older Bash. This removes the remaining periodic jobs/sleep subprocesses from Linux CI while preserving per-file isolation and failure detection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8999b65973
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if speaker is not None and speaker in named: | ||
| return named[speaker] == name.casefold() |
There was a problem hiding this comment.
Honor owner-confirmed identity corrections before stale bindings
When an owner-confirmed correction contradicts an existing speaker binding, this early return rejects the corrected name before checking _corrected_name. For example, with c0 → Michael Liebmann and the stored correction “It was Phong, not Michael Liebmann,” a submission identifying Phong/c0 loses its participant entry and “Phong proposed agents” becomes “a speaker proposed agents.” This affects corrections saved by edit_tools, which deliberately update the summary without rewriting transcript bindings; confirmed_corrections_context explicitly requires those facts to be used in regenerated notes. Allow an affirmative owner correction to override the stale binding while retaining the restriction on calendar-only overrides.
Useful? React with 👍 / 👎.
| if "speaker_name" in extra_keys: | ||
| item["speaker_name"] = display.label(segment) |
There was a problem hiding this comment.
Preserve the enriched owner name in MCP transcripts
For an owner with a configured profile name, the REST conversation-detail endpoint calls populate_speaker_names to resolve that name, but this assignment replaces it with You: the new projection is constructed without owner_name, and its names map only includes rows with a person_id. Consequently, an owner segment previously returned as speaker_name: "David" now loses the resolved name even though the endpoint still performs the enrichment read. Pass the resolved owner name into the projection or preserve the trusted enriched speaker_name instead of overwriting it with the default.
Useful? React with 👍 / 👎.
| retained = sorted(unknown, key=lambda key: -seconds[key]) | ||
| retained = set([key for key in retained if seconds[key] >= MIN_VOICE_SECONDS][: MAX_UNNAMED_VOICES - 1]) | ||
| collapsed = set(unknown) - retained |
There was a problem hiding this comment.
Preserve accumulated voice evidence across live moments
Once an episode's persisted voice ledger contains nine entries, overflow remains true on subsequent moments, but this retention threshold uses only speech durations from the current moment. _record_moment_once passes only previously unapplied segments, so an established voice can lose its label merely because its next excerpt is short: nine voices with ten seconds each initially retain Speaker A through Speaker G, but a subsequent one-second segment from the first voice renders as Others with the same ledger. This discards an already-supported voice distinction in incremental notes despite no new voice arriving. Persist cumulative speech evidence or the retained-voice decision so short continuations do not repeatedly collapse established voices.
Useful? React with 👍 / 👎.
The drain test assumed a 50ms sleep meant renewal had begun; a loaded runner returned the response before the heartbeat reached renewal. Wait for an asyncio.Event set at the blocked renewal entry. Keep the renewal-reached, cancellation and sub-5-second drain assertions unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A full feature shard still spent 638s in isolated pytest sessions and exceeded the unchanged 720s step budget. Keep core xdist and asyncio, skip the unused loop-on-failure plugin, and load pytest-httpserver only in files referencing its fixture. Preserve unified/batched plugin behavior and explicitly test both isolated and unified plugin states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ty-guardrails Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tely Retain None and non-string input guards without strict typecheck errors. Keep the public tool schema unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xture Keep all four error-sanitization assertions and the unchanged CPU limit. Measure the status behavior in the call phase rather than charging its cold dependency import to one test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…endencies normally Keep the input guards and output contract, remove production file-loader fallbacks added for the isolated test, and register the real pure modules in that fixture so dataclass annotations resolve correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ture Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retain main notes and header-chip fixes. On overlapping retrieval/Wrapped guards, keep the validated accurate annotations, canonical imports and normalized text rather than reintroducing type ignores or test-only production loaders. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve fresh per-test isolation and every proration/402/no-write assertion. Keep endpoint calls under the unchanged one-second CPU guard without charging cold router imports and mock construction to the call phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restrict identity repair to unsupported participant bindings, exempt owner names and protect owner aliases. Add a default-on identity repair kill switch across notes paths and remove repair-only People catalog reads. Preserve developer API participant IDs, wire shape and speaker names; retain MCP opaque display identities and the owner's real speaker name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Use deterministic timestamps in serialized presentation fixtures so clock microseconds cannot look like leaked raw speaker IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retain the same real-module imports, isolated IO setup and behavioral assertions while removing textual conflicts with the current main fixtures. No production code change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raw diarization ids reached live notes ("Speaker 379"), Review context, search excerpts and speaker analytics. Resolve all touched display/prompt boundaries through one participants authority, preserve wire/correction identities, and make raw-label interpolation fail locally and in CI.
display_participantsresolves owner/names, scoped voice keys and small first-appearance labels. Notes turns and the evidence packet use the same opaque keys; manual excerpt labels never name an entire cluster.omi_speaker_display_count_guard_total. This mitigates presentation, without changing diarization or stored identities.speaker_idfield as null and suppliesspeaker_key/speaker_label; its ownerspeaker_namekeeps the real name. The public developer API preserves its pre-PR participant shape and rawspeaker_idvalues, matching transcript segment IDs, plus real owner names and existing speaker-name values.SpeakerNamesfor labels andEpisodePresentation.participantsfor named counts; adds opaqueDiarizationLabel, Review localization and the same over-split mitigation. Uses existing UI primitives; new Others copy is translated in all 49 locales and has a changelog fragment.Integrates #23404, squash-merged to main as
6243071d9efeff253570749da4cece26ffcc7943. The current published main/CI integration atb6bd0a8852b81bb7833a7627381c5e75db95c660is retained. The typeddisplay_participantsauthority, opaque prompt keys, evidence-aware counts and notes identity contract supersede the earlier display layer while retaining main's formatter coverage and smart-merge cache-version bump.The published integration also retains main's full-scan speaker guard alongside the fingerprint ratchet, preserves isolated real-module preloads and the deferred spelling import, counts distinct named person IDs separately, and waits for tracked test-worker completions without busy polling. Test isolation and the 720-second shard budget are unchanged.
FIX-FIRST review repair
participants(including model voice bindings). A People contact spelling in ordinary prose is not a speaker identity claim. Sarah Chen mentions, birthday tasks/events and titles keep their corrected People spelling, even when the transcript says "sara chen". Unsupported Michael participant bindings are still removed and attributed references repaired.NOTES_IDENTITY_REPAIR_ENABLEDdefaults on. Setoff,false,0ornoon every notes-processing service to restore notes output without identity rejection/repair. The shared switch covers agent acceptance/salvage, one-shot, serving and legacy output paths, independently ofNOTES_PACKET_V2_MODE.search_people; packet serving and People spelling correction retain their existing scoped reads.speaker_id,person_id,is_user), populated raw speaker IDs, and original segment speaker-name values. MCP keeps opaque keys/display labels as described above.378/379even when speaker labels are correct. The raw-label assertions remain unchanged.BACKEND_UNIT_TEST_FILE_LIST=... BACKEND_PYTEST_WORKERS=1 bash backend/test.sh(isolated pytest process per file). Coverage exercises Sarah spelling preservation across packet modes, unsupported Michael bindings (including agent salvage), unbound owner protection, David collisions, identity kill-switch values across modes, real list/detail API responses, and MCP owner-name/character-budget behavior. Shared metadata validation passed all 5 checks. Full preflight, zero-error backend typechecking, 31 selected backend files and generated-contract hooks passed before publication reconciliation. Both review-repair patches are unchanged on top of the concurrent main/CI integration; final combined-head checks and remote CI are tracked on this PR.Failure-Class: FC-generated-presentation-contract-bypass
Product invariants affected
SpeakerNamesremains the app's naming primitive; Review, transcript and exports share it.EpisodePresentation.participantscontinues to count only named people.Validation
nice -n 10, the backend file-list harness and one backend worker.UI-Evidence: none -- Changes are naming/identity helper logic, not widget layout or styling; focused transcript and Review widget tests exercise the rendered labels.
Delivery
Backend changes need the backend/listen/sync notes-processing images deployed; Flutter behavior needs an app build. No deploy, production data read, api.omi.me request or merge is part of this PR. No GCP tier was used. Rollback: revert this PR; raw storage and correction keys are unchanged. No firmware or record-lane projection/assignment/diarizer files changed.
🤖 Generated with Claude Code