live/* JSON-RPC methods. To use a live channel, create a session on a realtime-capable model (for example gpt-realtime-2) and call live/open to start the channel explicitly. Channel creation, status, refresh, input, interruption, truncation, and close are all caller-initiated through the live surface.
This guide covers how to open live channels, send input, and observe channel state.
What this guide is for
Use this guide when you want to:- open a live audio/text channel on a session
- send audio, text, or model-supported image input to a live channel
- understand live channel lifecycle and capabilities
- reason about live channels in the context of the normal session model
ModelCapabilities.realtime remains the capability bit that gates whether live/open succeeds. Image input is a separate per-channel capability: check LiveOpenResult.capabilities.image_in before sending an image. The --live-ws <addr> flag on rkat-rpc enables the WebSocket audio transport.
For WebRTC, the binary must be compiled with the live-webrtc feature and
started with --live-webrtc. At least one of these runtime transport flags is
required for the live/* methods to be advertised.Mental model
A session has exactly one conversation history. The session’s LLM client is the active delivery mechanism for that history; most models deliver via request/response (e.g. Anthropicclaude-opus-4-8, OpenAI gpt-5.6-sol), a small class delivers via a persistent bidirectional socket (e.g. OpenAI gpt-realtime-2). The only thing a realtime-capable model changes is how the model is reached — the session still owns history, tools, context, and turn boundaries.
- One canonical history. The session is the source of conversational truth. Committed audio and text join the same history as non-live turns at turn boundaries. A provider-acknowledged image is materialized and persisted as canonical context before the channel emits its redacted
user_content_committedreceipt. - Capability gates channel open.
ModelCapabilities.realtimeis the signal that determines whetherlive/opensucceeds. No channel is opened automatically. - Channel lifecycle is caller-initiated. Call
live/opento start a channel,live/closeto end it, andlive/send_input/live/commit_input/live/interrupt/live/truncateto control flow. - Refresh without history replay. For ordinary OpenAI Realtime channels,
live/refreshapplies supported mutable settings such as visible tools. A canonicalSystem-sequence change, model/provider swap, or canonical transcript or user-content-registry rewrite requireslive/close+live/open. The adapter validates PCM at 24 kHz mono; refresh does not support arbitrary audio-format changes, and reopening does not make an unsupported format valid.
Realtime-capable models
ModelCapabilities.realtime: bool is set per model in the curated catalog (meerkat-models; the ModelCapabilities type lives in meerkat_core::model_profile). Capability is catalog data, not prefix inference:
- OpenAI Realtime:
gpt-realtime-2— the model for the ordinary shipped Realtime adapter, with still-image input advertised ascapabilities.image_in == true. - Public GPT Live:
gpt-live-1also advertisesrealtime == true, but requires the host-composedopenai-live/ execution-identity / WebRTC path described in GPT Live, not the Realtime WebSocket factory. - Private experimental GPT Live:
gpt-live-1-codexis a separate experimental catalog row on the deprecatedexperimental-gpt-livepath, not an ordinary Realtime model. - Gemini: reserved for future
*-live*endpoints — no production models today. - Anthropic: no realtime-capable models today.
- Self-hosted:
realtime = falseby default.
GET /models/catalog (REST) or models/catalog (RPC) to inspect which models advertise realtime == true in the running runtime.
The transports in this guide belong to a native RPC host; a browser can be its
media/signaling client. This is not the embedded @rkat/web WASM runtime.
Switching the model, enabling --live-ws, or compiling openai-live alone does
not install the public GPT Live host authority.
Opening a live channel
A live channel is opened by creating a session on a realtime-capable model and then callinglive/open. These are separate lifecycle steps.
Create, then open
Create a session on a realtime-capable model, then calllive/open:
live/open returns a LiveOpenResult containing a discriminated transport
bootstrap, WireLiveChannelCapabilities, and WireLiveContinuityMode, plus
advisories (left out when empty) naming what the open went ahead without,
such as staged_prompt_refused. The advisory vocabulary may grow; an unknown
advisory is informational, never an error. Omitted
transport prefers WebSocket when its listener is configured and otherwise
selects WebRTC when that is the only enabled transport. Request
"transport": "websocket" or "transport": "webrtc" when the choice must be
stable. WebSocket requires rkat-rpc --live-ws <addr>.
The direct WebSocket input path accepts JSON text chunks and negotiated raw
PCM audio only, with a 2 MiB aggregate and per-frame ceiling. It does not
accept inline images. Send every image through JSON-RPC
live/send_input;
the JSONL control plane accepts frames up to 64 MiB (excluding the newline),
which accommodates the documented 20 MiB decoded-image ceiling plus base64
and envelope overhead.WebRTC SDP exchange
WebRTC is a feature-gated runtime transport. A binary without thelive-webrtc Cargo feature does not expose --live-webrtc or
live/webrtc/answer. Start a capable binary with:
JSON-RPC
Result shape
meerkat.live data channel before createOffer() so the SDP contains a media
section, set the local description, and wait for ICE gathering to finish. Send
that complete offer through JSON-RPC and install the returned answer:
Browser signaling sketch
opened is the live/open result and rpc is the application’s normal
JSON-RPC request helper. The bootstrap token expires after 60 seconds, is
single-use, and is bound to the returned channel. Replay, expiry, or a channel
mismatch fails typed. JSON-RPC is the canonical signaling path; the optional
http_url field in the transport union is only a convenience when a host
provides one.
Bounding the initial seed
By default,live/open projects the full canonical history into the
new realtime provider session. Long-lived sessions can request a smaller seed
with the optional positive seed_max_chars parameter:
System and SystemNotice rows, and selects a recent whole-turn
suffix. It retains adjacent instruction/context prefixes with their turns
rather than slicing an individual turn to fill the budget. Older instruction
rows may therefore be omitted along with older dialogue; a System larger
than seed_max_chars is not exempt from the window. An existing compaction
summary may be retained as the head before the suffix when it fits.
If selection omits any history, LiveOpenResult.continuity reports
mode: "degraded". Omitting seed_max_chars preserves the full-seed behavior.
The value must be positive; the server rejects zero.
The full canonical System sequence is retained separately as a drift witness,
not as a guarantee that every instruction is replayed to the provider.
Canonical image identity, tombstone, and aggregate-accounting sidecars remain
complete outside the seed-message window. Provider replay uses the selected
messages; there is no separate full-transcript instruction replay sidecar.
Configuration defaults
For ordinary session creation without a model hint, setagent.model in the
applicable effective realm configuration:
live/open. See the
Configuration guide.
Observing channel status
Uselive/status to read the current state of a live channel:
Channel close events
Every committed live channel close publishes one{"type": "live_channel_closed", "session_id": ..., "channel_id": ..., "reason": ..., "reopen_recommended": ...} event (AgentEvent::LiveChannelClosed)
on the owning session’s event stream, so observers never poll live/status to
learn that a channel closed or why. reason is one of:
reopen_recommended is true only for a media_fault whose session may still
reopen with its retained context.
Live channel methods
Live observations and durable history
The media transport and the session event log answer different questions. WebSocket or WebRTC carries low-latency channel input, output, and observations; committed turn content still lands in the canonical session transcript. Both transports forward each observation as it arrives, so a transcript frame can reach the client before its row commits (see Transcript rows while the member’s turn runs); theuser_content_committed receipt for an image is still sent only once the
image is persisted.
The associated session-level session/subscribe event stream is a bounded
live subscription. A lagging consumer receives a typed stream_truncated
event with reason.kind: "stream_lagged" and reason.dropped, then continues
from the retained tail. This reports a gap in that subscriber’s view, not a
truncated transcript or durable event log.
A response the provider ends without completing it arrives as a
turn_failed observation. Route on failure.status (failed, incomplete
or in_progress; a status without a wire name arrives as
{"unknown": {"debug": ...}}); the provider’s reason, code and message
are diagnostics only. It is not terminal: the channel stays open and the next
turn proceeds. Nothing of the failed response is committed to the transcript,
a WebRTC peer stops playing the audio still queued for it, and Meerkat does
not retry it; the client decides whether to ask again. Its
usage, when present, is charged to the session; when it is absent, the
session event stream carries turn_usage_accounting_unmeasured instead.
Built-in realm-backed persistent sessions can feed a separate durable event
audit projector through an unbounded queue independent of the UI ring. The
audit projection is asynchronous derived state, so when event evidence matters,
resume from the last available durable cursor with events/list_since and
reconcile against current session or channel state. Media frames and event
projection are not session commit authority; the committed transcript and
The RuntimeStore/backend carrier owns restart authority. SessionStore rows
are component/content state, not a second currentness owner.
Sending image input
Image input is turn context, not a response trigger by itself. Check the channel’simage_in capability, submit the image with a caller-stable
idempotency_key, wait for its durable receipt, then follow it with dependent
text or audio. On an explicitly committed channel, live/commit_input can
request a response from image-only context after that receipt arrives.
image_input_idempotency_conflict.
The data value contains the encoded image bytes as standard base64; do not
include a data: URL prefix. OpenAI Realtime currently admits PNG and JPEG,
verifies that the byte signature agrees with the declared MIME type, and
enforces a 20 MiB decoded-image safety ceiling before provider send. Invalid
keys, malformed base64, unsupported MIME types, content mismatches, and oversized images use the
typed rejection reasons image_input_idempotency_key_invalid,
image_input_invalid_base64, image_input_unsupported_mime,
image_input_content_mismatch, and image_input_too_large. Before provider
send, Meerkat also checks the canonical session’s cumulative decoded image
history. A new image that would take that history above 40 MiB is rejected as
image_input_history_budget_exceeded; it is not sent or persisted. A binding
without image support uses image_input_not_implemented.
Do not place an image behind uncommitted text or audio. Commit that input first,
then submit the image as the first content in the fresh sequence; otherwise the
adapter rejects it with image_input_requires_commit. This keeps the durable
predecessor identity unambiguous.
Acceptance, rejection, and durability
live/send_input has deliberately layered outcomes:
The same rejection reason can be immediate or asynchronous depending on which
layer detects it. For example, malformed base64 or an invalid key is rejected
before queue acceptance, while a content conflict discovered against durable
session identity is reported after the command drains. A terminal live error
or a missing receipt is never evidence of persistence.
For a WebRTC or direct-WebSocket channel, send the image through JSON-RPC
live/send_input, not the WebRTC data channel or direct WebSocket. The data
channel’s fixed message ceiling is suitable
for control/audio coordination but not full images. An image envelope delivered
whole and decoded within the effective 65,535-byte ceiling receives the scoped
image_input_transport_unsupported rejection, and that data channel remains
open. A larger envelope may be rejected by the browser or SCTP transport before
Meerkat can classify it, so it cannot receive a server-side typed rejection.
Route every image through JSON-RPC; the data channel carries the receipt
described below.
If the adapter’s bounded image queue or provider-ack window is full,
image_input_backpressured reports the byte ceiling without retaining another
caller payload. Retry the same key after earlier image receipts arrive.
The public transport does not echo image bytes back. After Meerkat has applied
the image to canonical session history, it emits a redacted
user_content_committed observation containing the item identity, content
index, media type, and caller idempotency_key. WebRTC callers must wait for
that receipt before sending RTP audio that depends on the image.
Reopening the same session hydrates blob-backed user images and replays the
typed images to the provider. Canonical live image history has a 40 MiB
aggregate decoded-image ceiling (two maximum-size images); every history
occurrence counts, including repeated references to the same blob. The live
input gate enforces that same ceiling before accepting each new image, so a
successfully committed live image cannot make an otherwise valid session
unreopenable later. Existing legacy or out-of-band history above the ceiling,
a missing blob, or bytes that do not match the durable content-addressed
identity still fail live/open instead of trimming or silently changing
visual context. Reduce canonical history explicitly or start a fresh session;
reconnect never substitutes placeholders for accepted images.
End-to-end example
This example usesexplicit_commit so the client owns audio turn boundaries.
The ordinary default is provider_managed; do not call live_commit_input
for that provider-managed VAD flow.
Live channels and mobs
Each mob member has its own session, so live channels are per-member by construction. To make a member live-capable, set its profile’smodel to a realtime-capable model (for example in the MobDefinition TOML):
tools.comms = true independently of whether
the profile’s model is live-capable.
Open a member’s live channel through its mob after the member is spawned:
mob/member_live_open over JSON-RPC, rkat mob live open, Python
Mob.member_live_open or TypeScript Mob.memberLiveOpen. The mob checks the
member first (not retiring, in cleanup or revoke, and restored), then the
member’s own live host serves the requested transport or refuses it typed.
A WebSocket bootstrap names the owning host’s advertised URL. With
transport: "webrtc" the result is a WebRTC bootstrap: post the browser’s
offer with live/webrtc/answer on the same JSON-RPC server, as for a session
channel. A host that serves no WebRTC refuses it with
LiveTransportUnsupported before it connects a provider; a member host
started by rkat mob host serves WebSocket only, so a placed member’s channel
is WebSocket. The session-scoped live/open refuses a mob member’s session
(data kind mob_member_session_addressed_by_id), and controller session/*
methods do not proxy a placed member’s host-local session in multi-host v1.
GPT Live (public OpenAI Live API)
gpt-live-1 is OpenAI’s continuous voice model on the public Live API. It is a
different protocol from Realtime: audio runs continuously with provider-owned
speech scheduling, and the voice model does not run tools. Instead the session is
started with a client delegation: when the voice model decides a request
needs tools, files, or reasoning, Meerkat runs the work through the channel’s
ordinary durable executor and returns the result as commentary that the voice
model speaks from. The canonical conversation persists through Meerkat’s normal
transcript authority.
The path is compiled with the openai-live feature (meerkat, meerkat-rpc,
meerkat-mob, meerkat-mob-mcp; meerkat-openai/live). The host composes
ExperimentalGptLiveOpenAuthority::new_public(PublicGptLiveOpenAuthorityConfig)
with an OpenAI API-key binding in the realm, the model gpt-live-1, a voice,
and optional trusted session instructions. No operator, Gate0, or realm
admission applies: the catalog row and the configured binding are the
admission. The gpt-live model family never selects the Realtime WebSocket
factory.
Known released public Live voice choices include alloy, ash, ballad,
beacon, bossa, cedar, cinder, coral, delta, echo, gleam,
marin, meridian, quartz, ripple, sage, shimmer, stone, tempo,
verse, vesper, willow. This is not a closed set enforced by Meerkat:
the broker warns for other nonempty names and forwards them to the provider.
Acceptance still depends on the service and account; the warning does not
prove that a custom voice is available. For HTTP 403 “Voice session access
denied”, check both the selected voice and the organization’s Live access
rather than ruling out an entitlement failure.
Consumers open the channel through the existing execution-identity seam:
live/webrtc/answer and apply the
answer in the peer. The browser must not send session.start; the server
already started the session. Capability atoms are live.execution_identity.v1
and live.execution.client_context.v1, exactly as for the deprecated
experimental profile.
A mob member’s voice channel opens through its mob with the same selector,
after the mob’s member checks:
live/webrtc/answer on the same connection, as above.
The server’s execution-identity authority serves it exactly as it serves
live/open, and mob/member_live_close closes the channel through that
authority. An execution-identity authority’s refusal (access denied, an
identity it does not admit, a member it finds ineligible) answers -32026
(host unavailable) with the authority’s reason, where live/open answers
-32020 or INVALID_PARAMS. The member must be local to the JSON-RPC
server: a placed member refuses execution_identity with Unsupported in
0.9.5, before anything reaches its host.
Public Live has no authoritative turn identifiers, completed-turn events, or
delegation task text. Meerkat synthesizes turns from transcript role
alternation and joins each delegation to the open or most recent user turn,
frozen to the transcript prefix observed before the delegation offset; the
resulting delegated transcript is a Meerkat-derived fact. Transcript
boundaries never prove playback completed.
Assistant output may begin on a fresh channel from startup context or later
canonical updates, without microphone input. Generated runtime authority keeps
an existing foreground correlation when present; otherwise it admits a fresh
assistant-only interaction with ProviderInitiated attribution. That attribution
does not claim which context append caused the speech or prove provider
consumption. It never creates a user transcript row. Later user input cannot
rewrite an already admitted assistant output’s interaction or playback handle.
Missing transcript events do not establish silence or completion. In particular,
Meerkat does not finish assistant output after a quiet interval or after a
delegation-result acknowledgement: a delayed readout retains its output identity.
Applications must keep transcript grouping, playback evidence, and backend task
completion separate; see OpenAI’s session guidance.
While the user’s latest utterance is unanswered, Meerkat holds a delegation’s
result (and its narration) behind it, so the voice model answers the user
first; the model’s next output, the utterance’s own delegation, or 1.6 s of
the user’s silence releases it. Each delegation carries a trusted in-progress
instruction until its result arrives, so a result held back that way is
finished work the model has not been told about. Meerkat therefore sends a
notice bound to the delegation as soon as it holds a result, always before
the result itself can be released: the work has
finished and its result has not arrived, so the model may say it has just
finished but must not state, guess or imply the outcome before the result
reaches it. The notice carries no result content, and the result itself is
still released only by those triggers. A finished delegation whose result
Meerkat cannot deliver yet, for example while another delegation’s result
holds the channel’s one result slot or while results wait for the provider to
acknowledge a concurrent historical summary, gets the same notice. While the
model is speaking, that notice waits until the response ends or the user takes
the floor, and it is dropped if the result arrives first.
On open and reopen, the selected canonical dialogue is supplied in native
session.input at provider session creation. User text and visible assistant
text retain their order and roles; executor System rows, SystemNotice rows, tool
calls, and raw tool results are not voice-session instructions or dialogue.
History is not injected as a speech-prompting session.commentary.append.
The provider allows up to 128 startup messages and 8,192 combined tokens.
Meerkat does not silently trim the selected seed to fit: select a bounded history
window at the shared open-projection seam, or resolve an oversize-seed rejection
before retrying. The canonical session history itself is unchanged.
Generated context summaries for an existing agent
A dedicated host can opt in to a generated factual summary, rather than a replayed history window. This is a Rust host composition seam, not a caller prompt field and not transcript compaction:LiveContextSummarizer::summarize(LiveContextSummarySnapshot<'_>)
as an asynchronous, tool-free model call returning Result<String, LiveContextSummaryError>. The snapshot exposes session_id(), messages(),
llm_identity(), canonical_message_cursor(), and max_output_bytes().
messages() contains the entire current model-boundary context window,
including context and tool-result rows and canonical voice transcript blocks
marked TranscriptSource::SpokenUnmeasured. Media remains typed references
rather than newly hydrated images. Observed speech is not a new user request
or proof of playback. Treat all source text as data; produce facts, decisions, ongoing
work, and uncertainties, not executable instructions or assurances that the
user heard unmeasured speech.
llm_identity() is the existing background text identity before the voice
override. A host can build a separate client using
AgentFactory::build_llm_client_for_identity(&config, snapshot.llm_identity()).
This resolves the configured binding through the factory/auth owner without
changing the agent’s model, running its tools, or adding a transcript turn.
An independently configured summary model must likewise resolve through that
factory seam.
Use the ordinary open_with_execution_identity method with seed_window: None.
The host policy also regenerates summaries for both canonical-context and
delegation-result replacement opens. Combining a summary policy with a replay
window is rejected. Input overflow, empty or oversized output, timeout, source
change during generation, and producer errors refuse opening without falling
back to raw history. Limits count bytes, not characters or provider tokens;
the provider’s independent startup token limit still applies.
The shared owner seals the summary to the source session, exact body revision,
rewrite generation, text identity, canonical cursor, and complete projected
context digest. The deferred provider open and seed acknowledgement recheck the
exact canonical source prefix. Observed transcript rows also advance the
canonical revision/cursor and follow ordinary append-only catch-up. A
conflicting body or rewrite cannot be disguised by an unchanged row count.
The summary enters public Live
as explicitly factual, unprivileged startup session.input, never
session.instructions. Continuity is degraded, not a claim of lossless replay.
ExperimentalGptLiveWebrtcTransport::bound_context_summary(channel, session)
exposes read-only, channel-scoped summary provenance until unbinding.
Concurrent historical context
BeforeOpen remains the default. A dedicated public Live host can instead
start native media without waiting for summary generation:
context_preparation() on the existing receipt-validated
ExperimentalLiveChannelCustodyStatus. Its NotRequested, Preparing(stage),
ProviderAcknowledged, and Failed(reason) values are independent of the
channel’s media Pending/Active phase. A host may display “Listening, loading
context” only after actual media activation. ProviderAcknowledged proves an
exact provider receipt, not model consumption, spoken output, or playback.
Summary failure remains visible while media can remain active; it never
silently changes to ready or falls back to raw history.
The provider owner bounds the total append and fragments it within the public
500-token limit. A complete acknowledgement requires every exact fragment
receipt. A native rejection can follow partial delivery and is therefore
ambiguous, not permission to replay the append blindly; actual channel-close
interruption remains a separate outcome.
Concurrent preparation is channel-owned. Exact close, identity change, or
replacement cancels and fences that channel’s job. A late old result cannot
populate a new channel. The same opt-in policy applies to replacement opens;
their independent pending and activation receipts remain mandatory.
For an availability indicator, call
ExperimentalLiveOpenAuthorityProvider::probe_execution_readiness(session, execution_identity). Unlike catalog capability atoms, this checks the exact
durable source and configured credential binding. It prepares and drops an
unbound factory: no summary generation, text-model call, channel reservation, or
provider session is started. Auth resolution can acquire or refresh its normal
lease/credential, so this is not a zero-I/O status getter. Opening always
revalidates; a previous successful probe is not an admission receipt.
Delegate to the selected existing member
The normal live delegation composer continues to create an isolated durable fork. A console host that intentionally wants the selected agent to perform voice-delegated work in its existing session can opt in at the mob feature boundary:Transcript rows while the member’s turn runs
A live channel commits its transcript rows (the user’s finalized speech, the assistant’s spoken segments) one at a time, in their order on that channel, and each row waits for the session’s turn boundary: the same boundary a running turn of the member holds until that turn’s own rows commit. While such a turn runs (for example voice-delegated work in the existing member, above), later live rows wait behind it, so the persisted transcript can lag the conversation by up to that turn’s length. Once the turn’s boundary commits, the waiting rows commit in their original order. On a GPT Live channel they all commit then, before a member turn queued behind them (a typed message, a second delegation) starts: the transport holds the boundary across the rows it already has, for at most 64 rows or about a second at a time, so a queued turn waits at most that long for them. A WebSocket or WebRTC live channel queues its waiting rows in arrival order off the media path, so its voice, its other observations and the client’s input keep flowing while a member turn runs, and holds the boundary the same way across consecutive transcript rows. A turn completion or interruption row takes the boundary on its own, so while member turns queued on the session run back to back, the rows after it wait for the next of those turns. Such a channel forwards each observation as it arrives, so a frame can reach the client before its row commits; if the session refuses a row, the channel closes with an error frame (reason: "projection_error") naming the
row’s kind (row), its provider item id (item_id) and how many later rows
were not applied (unapplied_rows). Once 16 MiB of rows wait, the channel
closes with reason: "projection_backlog_full"; every row it already queued
still commits. These ordering guarantees hold per channel, not across channels
of the same session, and the rows of a closed channel that are still
committing can interleave with a reopened channel’s rows.
A channel that closes while the member’s turn runs does not wait for the turn.
The rows still waiting are handed to the close, which commits them once the
turn’s boundary frees. If the turn ends with an error, they commit at once; if
it keeps the boundary past the close settlement’s ten-minute hang guard, they
still commit once it frees it, with no further deadline. Rows handed to a close
this way are held in memory only, so they are lost if the process exits before
they commit. If the turn’s boundary frees while the close is still settling, a
later row can commit ahead of a row the close holds, and a channel reopened on
the same session can commit its rows ahead of rows a closed channel still
replays, so the transcript order can differ from the conversation; no row is
lost. A close that fails before it commits leaves the rows it found deferred on
the channel for its retry. On a WebSocket or WebRTC channel the rows its
transport queued commit after the close in the same way, in order, also when
the client disconnected, and are held in memory the same way.
A row the session refuses for any other reason (for example an assistant output
whose provider turn never reached the runtime) is not recorded. The session’s
transcript then carries a RuntimeNotice system notice with category
live_transcript_row_unrecorded whose payload names the channel, the row kind,
its provider item id, its position among the rows the close held and the
refusal class; it never carries the row’s content. A refused assistant output
start is not reported on its own while later rows that settle the same
provider item’s speech are still to be applied: a playback terminal (such as a
segment’s release), a playback completion or truncation, or text the item
stages (its final transcript or spoken transcript deltas) whose response then
completes normally (not cancelled, not a tool call) with no barge-in interrupt
discarding it first. The session applies those rows by its usual rules, or
refuses one, and then that row is reported itself. Staged text is recorded only
when its response completes normally, and a closed channel completes nothing
on its own, so a start followed only by text whose response never completes
that way is still reported. Text that arrives after its response’s completion
is recorded at once, yet its start is still reported, so that notice
over-reports. A refused completion’s own notice names no item or response, so
the text it would have recorded cannot be attributed from the notice. Staged
text is also recorded only after the item before it in the transcript, so text
whose earlier item has not landed waits while its start is not reported; if
that item never lands, the text is lost without a notice. The notice is
recorded the same way as other transcript writes, so a session that refuses
every write refuses it too, and only a warning remains.
Continuous media without fabricated playback completion
Direct public Live WebRTC provides no per-utterance rendered-completion event or mapping from media samples to Meerkat’s opaque output handles. Neither transcript grouping,response.done, audio-element ended, analyser silence,
nor output-address publication establishes playback completion.
For continuous console voice, opt in on the same public authority used by
initial and replacement opens:
CallerConfirmedSnapshots mode remains available for hosts that
can genuinely measure playback. In ProviderManagedUnmeasured, generated
document authority settles each observed segment internally as unmeasured; it
does not publish an actionable playback output handle or wait for a browser
playback_complete call. Strict media-owner registration and activation still
apply. Ordinary observation transport acknowledgement is never playback or
hearing evidence.
Observed assistant text is retained as canonical Transcript
blocks with TranscriptSource::SpokenUnmeasured, through a distinct
AssistantUnmeasuredSnapshotCommitted event. Committed-live provenance advances
coverage without echo before segment advancement. Ordinary
messages_for_model_boundary(), summaries, compaction, and reload therefore
consume the same canonical history; there is no separate observation-position
ledger or messages_for_live_context() overlay. The source marker must remain
unmeasured when rendering or summarizing these rows. An observed snapshot has
BlockAssistantMessage.stop_reason: None: it does not manufacture
StopReason::EndTurn. Independently observed provider stop evidence remains
optional throughout persistence and wire projection. Normal completed model
responses still use Some(stop_reason); consumers must not substitute a
completion for an absent value.
Public admission diagnostics include the failing stage and typed cause (for
example, missing_default_binding, lease_absent, or
openai-live-openai-api-backend) without logging credentials, provider payloads,
or connection URLs. These diagnostics do not change admission policy.
Barge-in playback hint
The gpt-live protocol has no client command that cancels or clears assistant audio the provider has already queued. When the user starts speaking over the assistant, that tail keeps playing until the provider stops it. Meerkat already measures both sides of the floor (the reflected input energy and the assistant output’s silence run), so it tells the client when to silence its own playback:live/assistant_playback_hint { channel_id, hint: "duck" }is sent once when the user’s speech and audible assistant audio overlap, whichever side started: the user speaking over the assistant, or the assistant starting a response while the user keeps talking (at the user’s next voiced frame). A reply that starts after the user has stopped is never ducked.live/assistant_playback_hint { channel_id, hint: "restore" }follows when the assistant output goes quiet for 1.6 s (the response ended) or when the user goes quiet for 1.6 s (including a false trigger: the user stops without taking the turn), whichever comes first. On the user’s quiet, the restore depends on what the duck covers. A burst the user interrupted, one the assistant was voicing when the user started speaking, is restored at its next pause of 600 ms (at once if it is already pausing that long), so the stale tail the provider voices before it yields is not replayed. That burst ends at its first 600 ms pause, even while the user is still talking: audio after that pause, like any response that starts into or after the user’s speech, is an answer and is restored at once, also in the middle of a sentence, so the answer is heard. An answer spoken entirely while the user is still talking stays ducked to its end, because the user held the floor for all of it. Meerkat tells the two apart by onset order on the provider audio clock (gpt-live-1 output frames carry no response id). A restore follows every duck, so a duck never leaves playback muted for the next turn.
rkat-rpc writes it on the JSON-RPC connection that owns
the channel, beside the other live/* notifications. The TypeScript SDK
delivers them typed through MeerkatClient.onLiveNotification(listener) and
the Python SDK through MeerkatClient.on_live_notification(callback) (both
return an unsubscribe function). Those clients drive rkat-rpc from a server
process, so the page that plays the channel receives the hint over whatever
connection already carries its live control traffic. The web SDK has no live
transport. Hosts that compose their own surface on
ExperimentalLivePublicObservationPublisher receive hints through its
publish_playback_hint method; the default implementation drops them.
Recommended client policy: route the remote assistant track through a gain
node (with the media element muted so audio plays only through the graph),
duck to silence on duck, and return to unity gain on restore, with a short
ramp to avoid clicks. Place any meters or analysers after the gain node so
they report what the user hears. The TypeScript SDK ships this policy:
applyLiveAssistantPlaybackHint(gate, hint, now, { duckedGain: 0.2 }); the
restore timing is the same either way.
applyLiveAssistantPlaybackHint takes any object with a
gain.setTargetAtTime method, so it stays independent of the DOM types. The
constants LIVE_ASSISTANT_PLAYBACK_DUCKED_GAIN (0),
LIVE_ASSISTANT_PLAYBACK_UNITY_GAIN (1) and
LIVE_ASSISTANT_PLAYBACK_GAIN_TIME_CONSTANT_S (0.01) are exported for
clients that implement the gate themselves.
Media health of the first output
A public Live WebRTC session can occasionally carry silent media for its whole life while the model speaks: transcripts arrive, but the remote audio track decodes as silence. The runtime catches this on each channel’s first assistant output, inProviderManagedUnmeasured mode:
- When that output’s first segment commits with a non-empty transcript, the
runtime sends
live/media_health_requested { channel_id, output_id }. Theoutput_idis only a report key (that output is already settled; it is not a playback handle). - The client answers with
live/media_health { channel_id, output_id, decoded_frames, audible_frames, max_rms }: raw counters from the channel’s media start to now (nothing was audible before the first output).audible_framescounts decoded frames whose RMS reached the client’s non-silent floor, andmax_rmsis linear amplitude (0.0 to 1.0). The client never sends a verdict. - The runtime judges the counters.
audiblekeeps the channel open. Amedia_fault(no audible frame and a peak below 0.002) means the runtime has already closed the channel through the ordinary close (retained summary custody included) before it answered.reopen_recommendedis true for the session’s first media fault, so the client can reopen with the retained context. A second fault on the same session closes without the recommendation, so a broken media path never loops.
live/status also reports
{"status": "closed", "reason": "media_fault", "reopen_recommended": true}.
A plain close still reports {"status": "closed"}. The request is made once
per channel. A report for an output the runtime did not request, a repeated
report, or a report for an inactive channel is refused. A client that never
answers leaves the output unjudged: no fault and no reopen.
For public Live, live/playback_complete is an explicit caller-confirmed
snapshot cut: it commits the transcript prefix observed at that ordered
checkpoint, without waiting for the next user turn or a close. It does not
claim that the provider finished its turn or that a person heard the text.
Later continuation remains in the same interaction and provider group but
receives a fresh one-use playback output handle after the cut settles. A late
group final cannot replay a committed prefix or extend an earlier playback
report to cover later text. Callers may await playback completion and then
close sequentially.
Explicit live/truncate prefixes use the same ordered snapshot checkpoint and
must match its exact prefix; they do not wait for a provider final. Omitting a
prefix remains unmeasured. Every terminal kind, including unmeasured and legacy
complete/prefix reports, records exact settlement facts before its target is
retired. Internal retry after a lost projection receipt reuses those facts,
never a new caller report, changed accounting, or an expanded prefix.
Closing drains provider observations, canonical transcript projection, and the
bound control consumer before channel terminality and resource retirement.
Already-admitted output publications retain access to their lifecycle custody
during drain; terminal close takes its exclusive lifecycle lease afterward.
Failed projection or publication retains the exact observation and completed
steps. Retrying close resumes those steps without re-admitting output or
duplicating already committed transcript text.
Exactly correlated pending-append errors during close become
InterruptedByClose: completion failed, but partial provider consumption is
unknown. They are neither acknowledgements nor proof of zero consumption;
generated authority permits no retry or automatic replacement. They do not
prevent draining the subsequent close confirmation.
An independently reported playback completion can join a final transcript
that arrives during close; closing itself never proves playback or hearing.
Output without playback evidence remains explicitly unmeasured.
Canonical context appends are claimed by generated authority. A concurrent
drain of the same pending edge defers without reminting send authority; an
already acknowledged edge is not resent. A provider turn that wins the
boundary also produces a typed deferral, not a projection failure. Rows from
the session’s other activity, such as a typed message and its reply, reach
the voice model’s context only between provider turns: while the provider
holds a turn open, including a user turn it has not finished, they wait in
order and are appended once that turn ends, and they remain in the session
transcript meanwhile. Local
cursor/row bookkeeping is serialized only during projection and receipt
realization, never while waiting for a provider acknowledgement.
Enqueue acknowledges store-sealed canonical staging, not provider delivery.
An owned, channel-scoped drain retains submission and error custody independently
of the committing turn or any cancelled observer; exact close flushes that
worker’s quiescence before committing terminality. A historical post-commit
delivery/recovery failure remains observable and is reported during close, but
cannot block cleanup once its worker stopped and physical retirement is proven.
This does not bypass failed canonical transcript projection: that still blocks
the physical drain. A streamed service turn that promotes
its provisional tail still projects the exact promoted store boundary.
Provider turn-finish callbacks wake that owned task without awaiting it, including
empty or idempotent transcript completions. Generated close revocation defers
new sends; a generated ambiguity recovery realizes its own old-channel close
without trying to join the worker that is performing that recovery.
Assistant rows attributed by generated authority to an existing voice worker
are executor trace for generic mirroring: their result has the separate
delegation-specific delivery path. Later ordinary background interactions
remain eligible for generic context delivery.
Ordinary runtime-backed turns, including RPC turn/start and the mob-owned
executor used for asynchronous peer work, notify an owned
context-projection task after the CoreExecutor acknowledges the committed
boundary. That task reads the store-sealed canonical body after executor locks
are released; it never reconstructs history from turn results or UI events.
It rechecks the target channel after each read, so a delayed read cannot publish
through an old channel after close/reopen. A failed post-commit source read or
classification is retained for explicit drain observers and reported during
close; a later commit notification retries the canonical scan. Close does not
wait for a source read held behind unrelated background work. This derived
context delivery is distinct from committing incoming live transcript evidence:
a failure of the latter still blocks the physical drain.
Mirroring classifies each committed row independently. User speech retains
session-, channel-, and row-scoped domain provenance before canonical hashing,
including when materialization is deferred behind another transcript item.
Thus a live callback cannot swallow preceding ordinary text rows, and a text
callback cannot echo speech already present on that channel. Generic transcript
rewrites clear replacement-row provenance; another session, channel, or row
cannot reuse it. Attributed realtime sidecar events and snapshots use schema 2;
new readers verify both schemas 1 and 2, while older readers reject schema 2.
This domain provenance is used with the store witness and generated channel
binding, not as a standalone transport authorization token.
Both channel-addressed RPC close and exact-receipt host close revoke generated
append and recovery custody before physical retirement. A delayed committed
source read therefore cannot turn an explicit close into a fresh replacement
channel. Context and result recovery use separate sealed close paths that
preserve their own replacement authority; they are not ordinary explicit closes.
Both recovery activations require playback-owner readiness, retain the original
pending receipt, and mint a fresh activation receipt in the generated answer
commit. An activated replacement remains exactly closeable. Revoked or closed
replacements cannot be published by a late registration callback.
Provider ingress routes append receipts before the control consumer, so a
control task committing its final user transcript cannot wait on its own
receive loop. Duplicate high-volume transcript notifications stay on the
adapter projection path rather than filling the control queue. Canonical
delegation-final commits include live-provenance context projection before
releasing their evidence to the worker. The MobSessionService method
commit_live_delegation_final_transcript requires the same MeerkatMachine
reference explicitly; decorators must forward it, not inherit a default.
A failed projection, missing drain receipt, or unconfirmed physical retirement
retains the exact channel custody and returns an error. Retry the same channel
after the blocking work settles; a timeout does not cancel or repeat an
in-flight provider close.
A terminal provider read failure (including a dropped sideband socket without
session.closed) is different: the shared owner drains the known observation
prefix, retires the failed local transport, commits generated channel close,
then projects the retained terminal fault before releasing registration.
This can return Closed without a graceful remote close acknowledgement; it
does not assert a successful model turn, final transcript, or playback.
If fault projection fails after generated close, exact close must still be
retried: the owner retains the reporting obligation and replays it without
duplicating a completed publication. Do not skip exact close just because
status already says Closed, and do not infer terminality from a generic
transport error in a surface handler.
Pending fault reporting pins its transport projection past the normal
post-close cache TTL. Once cleanup completes, exact receipt replay relies on
generated custody, not that cache. The generated-close/host-commit handoff
continues in an owned task if the close observer is cancelled.
The sideband owner declares ProviderWebrtcEofEvidence: EOF defaults to
Unconfirmed, while ProviderConfirmed requires its explicit protocol receipt.
An unconfirmed clean EOF is retained as a ConnectionLost fault, not a graceful
provider close. The public OpenAI broker rejects both pre-close and mid-close
WebSocket EOF lacking session.closed as transport failure. Known terminal
drain evidence can retire a hung close request locally; an unfinished
projection or control consumer still blocks successful cleanup.
An ordinary session turn
holding pending tools may return a busy close-settlement error; closing a live
channel does not authorize cancellation or fabricate those tools’ results.
Ambiguous-delivery replacement bootstraps retain their machine-issued
pending_receipt. Register the replacement playback owner with that receipt
before answering its offer, just as for an initial strict open.
The shared ExperimentalLiveReplacementRequired value exposes open(),
pending_receipt(), and canonical_seed_cursor(). Project it to a strict pending
handle, register the new media owner, answer that replacement’s offer, publish
answer-delivery custody, and obtain its active receipt from status. Do not issue
a second fresh open or reuse the old channel’s activation receipt. Ordinary text
updates do not switch the durable text model; a lost replacement notification
can be recovered by pulling the same pending bootstrap until its answer binds
or that exact channel closes.
Explicit user close must call the shared exact-receipt close even when the
known channel already reports Closed or Revoked. Recovery can have closed
its old transport while a summary or credential preparation is still pending.
close_experimental_live_pending_channel accepts the original pending receipt;
close_experimental_live_active_channel accepts its activation receipt. Generated
close authority cancels only that channel’s outstanding recovery candidates,
refuses their late admission or activation, and retires any admitted candidate.
It cannot cancel a later independent open on the same session. The internal
recovery handoff close does not cancel its own replacement. Retained replacement
bootstraps are mechanical publication custody: closing a never-activated
replacement removes its entry, and delayed old close feedback cannot remove a
newer entry.
For the default BeforeOpen policy, cancellation invalidates generated recovery authority; it does not forcibly
drop an arbitrary host summary or credential future. Until that bounded
preparation finishes or times out, its existing projection-memory reservation
may temporarily backpressure another open. Hosts should surface that refusal
and retry within their own bound, not infer a new channel or bypass the shared
reservation.
A process admits live opens one at a time and queues none. An open keeps that
admission until its provider has acknowledged its seed: for a provider-brokered
WebRTC channel (the provider answers the client’s offer), through that offer
and answer. A second open during that setup is refused before anything opens,
as the typed
RealtimeSessionOpenProjectionError::Backpressured (SessionBusy, -32002, on
rkat-rpc live/open; MemberLiveError::Unavailable from a member live
host). The refusal is retryable: retry the open with backoff.
The private ChatGPT-brokered path (experimental-gpt-live, gpt-live-1-codex)
is deprecated and will be removed after the public path has shipped for at
least one release.
Limitations and known gaps
- Ordinary/default composition uses OpenAI Realtime. Its provider integration is OpenAI’s Realtime API (
gpt-realtime-2), not Azure OpenAI (azure_openai) or other providers. Publicgpt-live-1uses the separate host-composedopenai-live/ execution-identity / WebRTC path above; the private experimental path is deprecated. - One live channel per session. A session has at most one live channel at a time. For per-member live channels in mobs, open channels against individual member sessions.
- Deferred sessions are model-gated.
live/openmay materialize a deferred session whose resolved model is realtime-capable; a deferred non-realtime session is rejected before channel creation. When the open materializes a deferred session, or opens one whose create-time prompt is still staged, the prompt reaches the realtime model as the session’s first user row: the run-start hooks admit it once, and a later text turn does not deliver it again. A hook that refuses it keeps it staged for the first text turn; the open goes ahead without it and reportsstaged_prompt_refusedin the result’sadvisories. - Live rows wait for a running member turn. Transcript rows commit in arrival order behind a running turn of the member, so the persisted transcript can lag the conversation by that turn’s length (on a WebSocket or WebRTC channel, by each member turn queued behind it while turns run back to back; that channel’s voice and events keep flowing meanwhile); rows handed to a channel close are held in memory until they commit and are lost if the process exits first, and a row released mid-close can commit ahead of an earlier held row (see Transcript rows while the member’s turn runs).
- Ordinary Realtime refresh is not instruction/history replay. Supported mutable settings such as visible tools can refresh; canonical
System-sequence changes, model/provider swaps, and canonical transcript or user-content-registry rewrites requirelive/close+live/open. The adapter remains fixed to PCM 24 kHz mono, including after reopen. - GPT Live may delegate a question asked over its own playback. Public
gpt-live-1sessions use client delegation for the whole session, and the provider’s delegation events carry no typed reason, so Meerkat cannot keep a single turn native. A question that starts while the assistant is still speaking, before the cue for a delegated result has been sent, can be routed to the executor even when it asks about the conversation itself. The user then hears the answer late, in the same voice, once the executor’s turn has finished and its result has been spoken after at least 1.6 s of output silence; occasionally the voice instead resumes its earlier readout and never answers, and the question has to be asked again.
See also
- Mobs guide — spawning members, profiles, realtime-capable per-member models
- Configuration guide — setting
agent.model - JSON-RPC API —
session/create,live/*methods - REST API — REST routes for session creation
