Skip to main content
Keep structured turns as your source of truth. Preserve each returned role, text, order, and any IDs or timestamps through storage and analysis. Never reconstruct speakers from punctuation, sentence count, alternating rows, or literal Agent: / User: text. Consecutive turns may have the same speaker.
A display string is a derived view, not an interchange format. Flattening turns into labelled text and later guessing their speakers can corrupt a correct transcript. Keep analysis and summaries in separate fields; do not let them replace the original turns. This prevents speaker-label corruption; it does not correct speech recognition, conversation reasoning, or interrupted playback.

Streaming text and stored turns have different meanings

Read the wire protocol for framing and lifecycle. Unknown speaker information from other sources must remain unknown or be rejected for review, never assigned by alternation. Only interpret caller deltas as caller speech when their transport actually guarantees that meaning.

One pipeline: events → storage → analysis → UI

This Node.js 20+ example is the transcript portion of your existing Omni connection. Keep its audio, configuration, tool, and interruption handlers. It makes no new call or recording. Run authenticated transcript retrieval on your server with an omni:read key; never expose that key in a browser. Use the opaque PyAI call_id from the call list/detail API, not a carrier call ID. Save the following as transcript-pipeline.mjs. recordEvent accepts your own append-only event-store callback; it does not turn live deltas into durable turns. readTranscript fetches the stored response after the call is available.
transcript-pipeline.mjs
Wire onTranscript: frame => recordEvent(appendEvent, frame) into your SDK connection, where appendEvent is your bounded, access-controlled event writer. Store lifecycle/interruption/playback receipts alongside those events with their original identities. Ensure asynchronous writes are drained and errors handled before closing your event writer. Do not register both onEvent and onTranscript as duplicate transcript writers. After the call, use the same module on your backend:
transcriptStore, analysisStore, analyzeConversation, and your event writer are application adapters. Send the analyzer structured turns as data, preserving roles and order; quoted text is not an instruction or a speaker separator. Protect transcript storage with your tenant access and retention policies. A 404 can mean the transcript has not arrived yet or no transcript exists; retry only under your application’s bounded completion policy. Never fall back to guessed speakers, or overwrite an existing structured record with a summary. The currently returned messages document can contain only role and text per turn. Turn IDs and timestamps are not guaranteed. Preserve them when provided; do not fabricate provider IDs. If your database needs row keys, use application-owned keys scoped to a saved snapshot and array position, clearly separate from provider IDs. Do not deduplicate identical text: repeated words or consecutive same-speaker turns can be real speech.

Interrupted speech is a separate dimension

On flush or barge_in, clear queued playback as described in the protocol. Preserve the text’s speaker and the interruption receipt independently. Neither an assistant final: true event nor a stored assistant turn proves full playback. Only associate confirmed playback with text when your transport supplies a reliable matching identity and progress. Without that evidence, mark delivery unknown; do not infer that the entire stored greeting was heard, or arbitrarily trim it using punctuation.

Regression fixtures to keep

Use synthetic conversations in integration tests, including:
  • Two consecutive caller turns, followed by two consecutive assistant turns.
  • Multiline text and several sentences inside a single turn.
  • Literal quoted Agent: and User: text, including HTML-like characters.
  • Repeated identical utterances: preserve both turns and their order.
  • Partial caller deltas and a cancelled assistant reply: no false finality or playback claim, no speaker reassignment.
  • Optional IDs/timestamps preserved when present; absent fields left absent.
  • Missing or unrecognized roles rejected for review, never guessed.
Copy this synthetic fixture into your tests. The expected output is six turns with unchanged roles, order, and text. The two Yes. turns remain separate; quoted labels inside text remain text.
transcript-fixture.json
Our documentation checks execute the published module against this fixture and the interruption, missing-role, and optional-metadata cases without network or model calls.