WordloopWordloop
Decisions (ADRs)

A transcription failure after audio compose still closes the recording as completed

Once the final audio artefact is composed and sealed, a failed transcription write-back closes the recording as completed rather than failed, and provisional talking points are refused with 409 once the recording leaves the live phase.

0007 — A transcription failure after audio compose still closes the recording as completed

Status: Accepted Date: 2026-09-09 Deciders: core platform Supersedes: — Superseded by: —

Context

A live recording's meeting_recordings.status and a meeting's transcription (transcriptions.status) are two separate state machines. The recording's terminal states are completed and failed; the transcription's terminal states are completed and failed too, but they answer different questions:

  • Recording failed means Core could not produce a usable final audio artefact — the thing a user could still listen to.
  • Transcription failed means ML could not produce a transcript from that audio (provider outage, a permanent decode error, exhausted retries).

By the time ML's write-back (FinalizeFromTranscription in internal/core/service/recording.go) runs, the audio has already been composed and sealed into GCS — the recording's own durability job succeeded regardless of what ML does next. Before this delivery wave, a transcription failure in this call path was not consistently distinguished from an audio failure, so a review flagged the risk that a transcription-only failure could leave (or move) the recording in a failed state, which reads to the user as "your recording did not work" when in fact their audio is intact and only the transcript is missing.

A related question surfaced in the same review: once a recording has left the live phase, should Core still accept live-drafted talking points? Points created while active/stopping/draining_ml are provisional — only the final synthesis write-back (UpdateSynthesis) produces is_final=true points. If a late draft is accepted after that point (e.g. during awaiting_gap_upload or later), it would silently sit next to the final set with no path to ever be reconciled away.

Decision

Recording status is decided by audio, not by transcription outcome. FinalizeFromTranscription treats TranscriptionStatusCompleted and TranscriptionStatusFailed as the same signal for the recording's status: both close the recording as completed, because by that point the audio artefact is already final and sealed. recording.failed is reserved for "no usable final audio" — a transcription failure alone can never produce it. The transcription failure itself is not hidden: it stays visible on the transcriptions resource (status: failed, status_message) and in the recording's event history, so a client can tell the difference between "recording completed, transcript unavailable" and "recording completed, transcript available."

Provisional talking points are refused past draining_ml. liveArtefactsAccepted gates every live-artefact write (including CreateTalkingPoint) on the recording status being one of active, stopping, or draining_ml. Once the recording has moved to awaiting_gap_upload, composing_audio, post_processing, or a terminal state, a live-draft write is refused rather than silently accepted:

// internal/core/service/meeting_service.go
if !s.liveArtefactsAccepted(ctx, tp.MeetingID) {
    return nil, ErrRecordingNotLive // wraps domain.ErrConflict
}

The HTTP layer maps this to 409 Conflict (internal/entrypoints/api/routes/meeting.go): "recording is no longer live; provisional talking points are closed."

Consequences

Users see an accurate, if less alarming, status. A recording with a failed transcription reads as completed with a transcription resource that reports failed — the audio is there, only the transcript step needs a retry or manual attention. Client code that renders "recording failed" from meeting_recordings.status alone will never fire for a transcription-only failure; it must check the transcription's own status for that condition.

No orphaned provisional talking points. A talking point can no longer be created in a window where it would never be reconciled by the final synthesis write-back. A client that races a live-draft POST against the stop sequence gets a clear 409 instead of a write that silently vanishes from the user's view once synthesis replaces the draft set.

Clients must poll two resources for full recording health. A monitoring dashboard or the App's own recording-health banner cannot infer transcription failure from recording status; it must also read GET /meetings/{id}/transcription (or the transcription's status) to know a retry is needed.

Alternatives considered

  • Recording failed on any transcription failure. Rejected. Conflates "we lost your audio" with "we couldn't transcribe your audio," which are different severities and different recovery paths (the former has no remedy; the latter can be retried from the composed audio).
  • A third recording status for "audio-only completion." Rejected as unnecessary complexity for this delivery — the transcription resource already carries the distinction, and a new recording status would require every consumer (App, bet suite, ML) to learn it. Revisit if product wants a distinct UI state for "audio ready, transcript pending."
  • Silently accept late talking points and let final synthesis overwrite them. Rejected. UpdateSynthesis does not guarantee it clears every stray draft; a late-arriving draft could persist as a visible duplicate after the meeting is otherwise finalized.

Debt annotation

Principal: Low. The gate is a single status-set check reused by every live-artefact write path (talking points today; the same liveArtefactsAccepted helper should be applied to any future live-draft resource).

Interest: Low. The behavior is covered by service-level tests exercising both transcription terminal states and both sides of the liveArtefactsAccepted gate.

Multiplier: Number of client surfaces that read meeting_recordings.status as a proxy for "did everything work." Each new surface (App banners, admin tooling, alerting) must be told to check the transcription resource independently, or this decision's benefit (accurate status) becomes a source of confusing dashboards that only check one of the two resources.

Verification

  • internal/core/service/recording.go — FinalizeFromTranscription maps both domain.TranscriptionStatusCompleted and domain.TranscriptionStatusFailed to domain.RecordingStatusCompleted.
  • internal/core/service/meeting_service.go — liveArtefactsAccepted allows only active, stopping, draining_ml; CreateTalkingPoint refuses otherwise via ErrRecordingNotLive (wraps domain.ErrConflict).
  • internal/entrypoints/api/routes/meeting.go — domain.ErrConflict maps to 409 Conflict on the talking-points route.
  • Core's fix-wave regression suite (lane core, review 2026-09-09) exercises both terminal transcription states through the recording write-back.

On this page