Skip to content

fix(meeting): gate transcript-idle close on ASR partials, not just finals - #39

Merged
WingedGuardian merged 1 commit into
mainfrom
feat/meeting-partials-idle
Jul 16, 2026
Merged

fix(meeting): gate transcript-idle close on ASR partials, not just finals#39
WingedGuardian merged 1 commit into
mainfrom
feat/meeting-partials-idle

Conversation

@WingedGuardian

Copy link
Copy Markdown
Owner

Why

The #38 transcript-idle close keyed on the committed-turn count. Day-one real use dropped ~15 min of a live meeting: on quiet/far-field audio Speechmatics can go many minutes without committing a FINAL, so a live meeting read as "idle", the session closed, and the dormant state (correctly refusing to reopen on room noise) kept it closed until a quiet gap finally re-armed it.

What

Evidence-based idle signal, verified against the live Speechmatics API:

  • ambient_bridge/active_session.py (additive; ambient path unaffected — nothing there reads the new surface): last_activity (monotonic) stamps ASR speech evidence. Two live-API behaviors shape the predicate, both observed with a direct probe:
    • Speechmatics re-emits the same trailing partial for as long as audio flows → a partial counts only when non-empty AND changed vs the previous text.
    • Speechmatics emits empty finals every ~2s (commit heartbeat) while audio flows → a final counts only when it actually commits a turn (len(acc.committed) grew).
  • meeting_bridge/server.py: the idle close now tracks last_evidence_ts fed by last_activity (primary) with the turns count as fallback for backends without it; both absent ⇒ idle-close disabled for that backend. Seeded at session open; dormant/re-arm machinery unchanged.
  • MeetingSession protocol documents the optional liveness attributes.
  • Tests: 3 ambient evidence tests (frozen partials ≠ evidence, empty finals ≠ evidence, committing final = evidence + tracker reset), meeting lifecycle test for partial-evidence keep-open; 50 meeting + 13 ambient pass.
  • Docs: config comment, README, meeting.env.example, CONTRACTS §1c.

Verification

E2E against real Speechmatics on the edge (isolated instance, synthesized-speech PCM):

Direct-probe transcript of the live API (empty finals every ~2s during tone; frozen partial re-emission) is what drove the predicate — both behaviors are pinned by unit tests.

Reviewed twice: full review (DONE, no findings) + focused delta re-review of the post-E2E predicate changes.

Rollout

Edge currently runs with MEETING_TRANSCRIPT_IDLE_CLOSE_S=0 (idle-close disabled as the stop-the-bleed mitigation). After this deploys, the env override is removed to re-enable at the 300s default on the new signal.

🤖 Generated with Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b7b77c6b5

ℹ️ 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".

Comment on lines +158 to +160
if len(self._acc.committed) != before:
self._last_activity = time.monotonic()
self._last_partial_text = "" # partial restarts after a commit — the next one counts anew

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count merged finals as ASR activity

When Speechmatics sends another final for the same speaker, TranscriptAccumulator.add_final() merges it into the existing committed run instead of appending a new entry, so len(self._acc.committed) does not change even though new transcript text was committed. In that single-speaker/merged-turn case, neither last_activity nor the turns fallback advances, and the meeting bridge can hit MEETING_TRANSCRIPT_IDLE_CLOSE_S and finalize/dormant a still-active session if partials are absent or not changing. Track whether add_final actually added text/results rather than using only the committed-list length.

Useful? React with 👍 / 👎.

…nals

The #38 idle-close used the committed-turn count as its "meeting still
going?" signal. Day-one real use showed that fails on quiet/far-field audio:
Speechmatics can go many minutes hearing speech it never commits as a final,
so a LIVE meeting read as idle — the session closed, went dormant, and ~15min
of a real meeting were silently dropped until a quiet gap re-armed it.

Switch the idle signal to ASR SPEECH EVIDENCE: ActiveSession now stamps
last_activity (monotonic) on every NON-EMPTY partial and every committed
final, and the meeting server gates the idle close on that — with the turn
count kept as fallback for backends without it. Partials fire whenever the
ASR hears anything speech-like, so a hard-to-hear meeting stays open while
a post-meeting noisy room (no partials) still closes and stops billing.

ambient_bridge/active_session.py change is additive (a timestamp + property;
the partial lambda became a bound method with identical accumulator/flush
behavior) — ambient bridge behavior is unchanged. Empty/keep-alive partials
don't count as evidence (same extraction as set_partial). Tests cover the
evidence rules on ActiveSession and the keep-open/close/dormant lifecycle on
the server; docs updated (config comment, README, meeting.env.example,
CONTRACTS 1c).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@WingedGuardian
WingedGuardian force-pushed the feat/meeting-partials-idle branch from 0b7b77c to 5944d5b Compare July 16, 2026 21:18
@WingedGuardian
WingedGuardian merged commit b51ba6f into main Jul 16, 2026
4 checks passed
@WingedGuardian
WingedGuardian deleted the feat/meeting-partials-idle branch July 16, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant