Repository navigation
fix(speakers): defer short live epoch labels and gate owner evidence - #23533
Git-on-my-level wants to merge 59 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
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>
…ping Failure-Class: FC-generated-presentation-contract-bypass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Failure-Class: FC-generated-presentation-contract-bypass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve trusted manual episode name projections without using a bound excerpt name as the fallback label for unbound speech of that provider voice. Failure-Class: FC-generated-presentation-contract-bypass 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Exercise enrolled and unenrolled guests through real idle reopen and a fresh listen matcher. Hydrate persisted wire rows, swap provider numbers, and require fresh acoustic evidence before names reappear. Anonymous cross-replica aliases remain an explicit follow-up. Failure-Class: FC-generated-presentation-contract-bypass Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep unplaced live rows unnamed and retain dense-label coverage for five-second voices after integrating the reconciled prerequisite regression suite. Failure-Class: FC-generated-presentation-contract-bypass 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>
The full preflight suite exposed an existing immediate getByTitle race. Framer Motion schedules onHoverStart through frame.postRender, so await the rendered snooze control before exercising the double-click contract. Product behavior and assertions are unchanged. Failure-Class: none Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
|
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. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ae989f5e6
ℹ️ 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".
| blocked = participant_names | {name for name in identity.contact_names if not identity.supported(name)} | ||
| blocked.discard('') | ||
| blocked = {name for name in blocked if not re.fullmatch(r'(?i)(?:a speaker|speaker [A-Z])', name)} |
There was a problem hiding this comment.
Limit contact-name scrubbing to actual identity claims
When an unrelated contact has a name that is also an ordinary word, such as Will or May, adding every unsupported contact spelling to blocked corrupts otherwise grounded notes. The case-insensitive replacement runs across all visible prose even when participants is empty: with those contacts, We will launch in May. becomes We “will” (as heard) launch in “May” (as heard)., and the title May launch plan loses its month. Restrict this repair to actual unsupported identity claims, rather than treating every occurrence of an account contact's name as a speaker attribution.
Useful? React with 👍 / 👎.
| defer_short_voices | ||
| and not name | ||
| and (cluster.speech_seconds if cluster else display_seconds.get(key, 0.0)) < MIN_EVIDENCE_SECONDS |
There was a problem hiding this comment.
Preserve qualification for voices already in the episode ledger
For successive episode moments, _record_moment_once supplies only segments newer than applied_until while carrying forward speaker_display_order. This duration check nevertheless requires five seconds again in each new delta, before consulting that ledger. A voice rendered as Speaker A after six seconds therefore becomes Unknown speaker when its next moment contains a one-second reply, despite retaining the same ledger token. Honor previously qualified tokens before applying the short-voice floor so established identities remain stable across incremental updates.
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.
Deduplicate named voices before adding anonymous speaker counts
When only some segments of a resolved voice have been named, such as after assigning a person to selected transcript lines, the new helper counts that voice twice: EpisodePresentation.participants includes the person's name, while ConversationDetailMeta.participants also includes the same speaker ID in unnamedIds for its remaining unbound segments. Adding these totals makes a single resolved voice appear as two speakers in the transcript heading; partially owner-labeled voices have the same problem. The previous implementation excluded owner IDs and resolved each remaining speaker ID through personIdsBySpeaker, avoiding this double count. Deduplicate the anonymous IDs against named and owner voices before calculating the heading count.
Useful? React with 👍 / 👎.
Treat an existing conversation-scoped voice ledger token as prior display qualification. Incremental moments contain only new speech, so a short reply must not erase an established ordinal. Regressions cover short and unplaced continuations, a new epoch, and a different conversation. Failure-Class: FC-generated-presentation-contract-bypass 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>
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>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ff289bfbec
ℹ️ 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".
| display = display_participants( | ||
| segments, owner_name=owner, voice_order=voice_order, display_scope=conversation_id, defer_short_voices=True | ||
| ) |
There was a problem hiding this comment.
Accumulate qualification evidence across live moment deltas
On the legacy live-moment path, a voice that has never qualified must now contribute five seconds within a single moment to receive a label. _record_moment_once filters out segments through applied_until, and the persisted ledger stores only already-qualified voices, so earlier sub-threshold speech is forgotten. For example, the same scoped voice contributing three seconds in each of three successfully applied moments remains Unknown speaker after nine seconds, while Flutter's whole-conversation resolver qualifies it. Calculate qualification from the conversation's accumulated placed speech, or retain bounded pending-duration evidence across moments, without replaying previously applied text.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve real helper imports and the isolated router cleanup fixture across the independent CI repairs. 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>
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>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 972cefe8b2
ℹ️ 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".
| kept = [ | ||
| person for person in people if person.get('is_ai_agent') or str(person.get('name') or '').strip() not in blocked | ||
| ] | ||
| repaired = scrub({**submission, 'participants': kept}) |
There was a problem hiding this comment.
Preserve supported bindings when rejecting another participant
When the same name appears in both a supported and an unsupported participant binding, blocked contains the name globally, so this filter removes both participants and the subsequent scrub erases even grounded references. For example, with resolved bindings c0 → Sarah and c1 → Phong, submitting Sarah for both speakers removes the valid c0 participant too and changes Sarah approved deployment to a speaker approved deployment. Reject participants by their individual binding rather than name alone, and preserve prose references to independently supported identities.
Useful? React with 👍 / 👎.
| if token not in order and len(order) <= MAX_UNNAMED_VOICES: | ||
| order.append(token) | ||
| labels[key] = labeler.label(token) if token in order[:MAX_UNNAMED_VOICES] else 'Others' |
There was a problem hiding this comment.
Allocate live labels in qualification order
When multiple voices qualify within one moment, this loop allocates labels by first appearance after computing each voice's total duration, rather than by when it reaches five seconds. For turns A at 0–1 seconds, B at 1–6, and A at 6–10, the backend assigns A Speaker A and B Speaker B; Flutter's _qualifiedLiveOrdinals assigns B the first ordinal because B qualifies first. Consequently, the episode prompt and persisted ledger identify different voices than the live transcript. Order newly qualifying voices by their qualification timestamp before appending their ledger tokens.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Short Soniox epochs produced consecutive Speaker 4–8 labels because provider/socket scopes each reserve a new conversation voice. This PR ships the bounded safe repair: live unknown voices spend no visible ordinal until five seconds of non-overlapping placed speech. Names still come from existing manual/acoustic authority. Raw identities stay distinct and available for correction.
This stacked PR depends on #23462 (
fix/speaker-identity-guardrails, integrated headd544d262e02b9be09682235b340ab98340a04224) and must merge after #23462. #23404 is already merged to main as6243071d9efeff253570749da4cece26ffcc7943. This branch merges the reconciled prerequisite and main, with no rebase or force push, and targets main as requested. It extends their shared display authorities rather than introducing another labeler.SpeakerNames' live qualification floor; qualified voices number in qualification order. Short/unplaced voices show the localized Speaker, with owner and person names preserved.CROSS_EPOCHthreshold,MATCH_MARGIN, complete-link agreement and different scopes. Weak/ambiguous evidence cannot remove an owner competitor. This grouping remains internal to owner arbitration; it does not rewrite transcript identities.Scope and remaining work
This is the authorized safe subset, not complete anonymous live linking. Anonymous voices with substantial evidence in multiple epochs can still acquire separate ordinals. Cross-listen anonymous identity needs a bounded encrypted, model/version/provenance-bound evidence handoff and reversible display aliases fenced by manual receipt generation, replica concurrency and finalization. The implementation/qualification checklist is in
backend/docs/listen_pusher_pipeline.mdx, “Remaining live anonymous linking”. Unplaced audio still bypasses matching; capture placement belongs to the timeline lane. No provider-label equality or weak short-turn vectors force an owner match.Prerequisite review follow-ups
The refreshed prerequisite fixes catalog-only ordinary-word contact scrubbing. One review finding remains in files unchanged from #23462: partially named/owner voices can be counted twice in the transcript heading. It belongs to the prerequisite's review; this lane does not edit that branch. The live ledger finding is fixed here with an incremental-renderer regression.
Failure-Class: FC-generated-presentation-contract-bypass
Line-Count-Exception: app/lib/widgets/transcript.dart | 1589 -> 1590 | Pass the live qualification option into the existing shared speaker naming primitive; no new widget behavior or duplicated labeler.
Product invariants affected
SpeakerNamesremains the Flutter naming authority. Live short-voice deferral extends that primitive and is documented inapp/docs/ux-contract.md; distinct wire/correction identities and named-participant counts remain intact.Trusted manual
person_nameprojections remain visible, while a name bound to one excerpt never labels an unbound excerpt from the same provider voice. Focused regressions cover both boundaries.Validation
Final publication source:
fe4adf5a57797e9173f774df4bdaf8039ca73405; merged main18fc1d6e114ff5a509b91c533949e24f6e2691c6and fix(speakers): add typed identity, count and presentation guardrails #23462d544d262e02b9be09682235b340ab98340a04224.Backend: 477 tests across 19 focused files passed after the final main/prerequisite reconciliation, including all 13 live epoch/moment cases. The ledger regression covers short and unplaced replies, a new epoch, and conversation isolation.
Flutter: 61 focused naming/rendered transcript tests passed; Dart analysis of six sources/tests found no issues.
Web: 154 files / 872 tests passed in the full suite. The web task-row regression now waits for Motion's post-render hover callback before double-clicking the snooze control. Its assertions and product behavior are unchanged; the focused two-test file passed.
Local
make preflightpassed all 58 checks after main/prerequisite reconciliation; PR metadata passed all five checks. Normal push hooks run before publication. A final main/prerequisite refresh is followed by focused backend/Flutter rechecks and frozen score replay; publication stops if the refreshed metrics differ.Remote CI: current published-head status is shown in the PR checks.
The acceptance fixture drives the real Soniox idle close/reopen adapter, epoch stamper and conversation allocator, deterministic short-turn embeddings through the live matcher, and a fresh listen matcher hydrated from serialized rows. After reconnect it swaps provider labels between owner and guest. Both enrolled and unenrolled guest cases must re-acquire evidence; an enrolled guest keeps the same name, an unenrolled guest stays separate and unnamed until qualification. Owner recognition never follows provider-number equality. These tests qualify the bounded safe repair; anonymous identity continuity across replicas remains deferred. No device or production acceptance is claimed.
Frozen local-quality scorecard
Executed the frozen #23435 scorer (
e7a2144f63edtooling) with the unchanged private corpus/word clocks, SHA-256b2d293306c74e1423ae41c70d0432ad5eea012a60224cf6f7dc88284105309cc. Historical inference source/audio/crop/payload hashes were verified, then this branch's realProjector.assign_parts, projection builder and conversation allocator ran with filesystem store/assignment boundaries. The split diagnostic uses the frozen split validator and the candidate's production split/build/allocation code. No raw identity mapping was fitted and no display collapse was counted as diarization improvement.Engineering gate: FAIL. All 10,541 scored words remain anonymous in this batch path: owner/enrolled accuracy is 0/4,415 (0%); wrong-name/false-owner rate is 0%. The live matcher is not exercised by this batch scorecard. This branch does not include the separate #23436 naming or #23437 batch voice-link changes that produced the supplied earlier 6/10/28/28/54 comparison, so the count differences (0/0/-1/-3/-3) are not like-for-like improvements attributable to this live repair. The batch identity producer files are byte-identical to main at evaluation.
The frozen five-speaker split case produced 27 unsplit speakers and 37/30 after the two cuts. Identity loss is 0.0/0.0 percentage points, versus the earlier approximately 3.6–3.8 points. Both split and unsplit owner accuracy are zero here: this is an all-anonymous floor, not a continuity gain, and the split gate remains FAIL. Anonymous cross-listen linking remains deferred as described above.
This was a current-projection replay with zero new model calls, zero production reads and no Firestore transaction qualification. Original inference receipts are retained; seven historical worker switches remain UNKNOWN, so full current-worker inference compatibility is INCOMPLETE. Non-English and real-device/production acoustic acceptance were not evaluated. Private persisted outputs, source hashes and aggregate reports remain outside Git.
UI-Evidence: none -- Naming policy changes only; focused naming and rendered transcript tests cover the visible labels. No layout or styling change.
Delivery
Backend/listen image deployment and a Flutter build are required for the corresponding changes. No merge, deploy, production data read, api.omi.me call, production config flip, or GCP tier used. No language-gate or firmware file touched. Rollback: revert the live-epoch feature changes while retaining the prerequisite PRs.
🤖 Generated with Claude Code