Skip to content

Live Mode

A session is an agent working on one job. Sometimes you want something else: to think out loud, ask what is going on, and kick off work as it occurs to you, without writing a prompt for each piece. That is what Live Mode is for. It is one continuous conversation with a fast model that answers quick things itself and hands anything slower to Zimmer’s own MCP surface. It starts a background session, says that it has, and checks back on it when you ask.

You can type or talk. Voice is not a separate mode with its own thread: Enable voice, next to the composer, turns voice on in the conversation you are in, and Type instead turns it off again. The thread carries both. Spoken turns are transcripts beside the typed ones.

TypedVoice on
Model (default; tunable)gpt-6-luna, Chat Completions, reasoning_effort: "none"gpt-realtime-2.1-mini, Realtime API
Where it runsa job on the serverthe browser’s WebRTC or the phone’s WebSocket connection to OpenAI
How it answerslonger answers, lists, snippets, links, light markdownshort spoken turns, nothing read out that is meant to be seen
Agent rootlive-modelive-mode

You enter it from the main screen: Live Mode beside New Session, or the chat button and the menu on a phone. /live has it as well, plus whether the OpenAI key is set.

Live Mode has one current conversation. Opening it resumes that conversation; New conversation ends it and starts a fresh one, typed. An ended conversation can be gone back to, which moves which one is current rather than opening a second. The database enforces this with a unique index over open conversations, so two starts racing cannot leave two current threads. Ended conversations stay readable, including those from the retired live-voice root. The migration that folded voice into Live Mode (UnifyLiveModes) ended any that were still open rather than deleting them.

mode on the record is text or voice: how the conversation runs right now. Switching it (POST /live_conversations/:id/mode) changes the model too, because each path has its own, and nothing else. The thread, the enabled MCP servers and the instructions stay as they were. While voice is on, typed messages are refused, because the call holds the conversation and would never hear a reply written behind its back. While it is off, no Realtime secret is minted. A call that is already connected when voice goes off elsewhere can still record its last lines and finish a tool call.

A conversation is its own record (LiveConversation, with LiveConversationTurn rows), not a session. A session is an agent process: it is spawned, heartbeated, swept when its start stalls, and reaped with its clone. A live conversation has no process for any of that to manage. What it shares with sessions is what it delegates to them.

Its lifecycle is short: preparing while Zimmer reads its agent root’s AGENTS.md, then active, then ended. It goes to failed when the instructions cannot be read, and the reason is shown on the page. ended is not final: going back reopens the same row, so its id, its thread and the sessions it touched are all still there. failed is final.

Say “go back to my last conversation” on a call, or press Go back to this conversation on an ended thread, on the web or the phone. The conversation being left ends, the target becomes current, and you can go back the same way, so starting a new conversation never loses the old one.

LiveConversations::Switch does this, and every surface goes through it: the live model’s switch_live_conversation, action_live_conversation’s switch, POST /api/v1/live_conversations/switch and the web button.

  • Which one. With no id, the conversation ended most recently that has anything in it. That is what “go back” means, so it needs no list, and a second “go back” undoes the first. An explicit id is any conversation from list_live_conversations.
  • One open, always. The switch locks the open rows and the target in one ordered statement, ends what is open and reopens the target in one transaction. The unique index catches whatever the locks cannot see. Two racing switches to one place come out as a switch and a no-op; to different places, the last one wins.
  • Refused, with nothing moved. A target that does not exist, a failed one, one from the retired live-voice root, or Live Mode being unavailable. So is a stale selection: a caller passes from_id, the conversation it believes is current, and if another one is current by then it is told so instead of ending something it cannot see (0 means it believes nothing is open, and a value it cannot read counts as stale). The target already being current is a no-op.
  • What comes back. The thread, and its own MCP servers less any the catalog has dropped. It takes the mode of the conversation being left, so a call stays a call. It is usable at once: the instructions and tool lists it had are good enough for a call that cannot wait, and they catch up with the catalog in the background (LiveConversationPrepareJob with refresh: true). A read that fails there changes nothing. One that ended before it ever read its instructions goes through preparing.
  • A reply in flight. The reply job looks before every round and every tool call, and stops once its conversation is no longer active. An answer already on its way lands on the old thread, where it is read on return. If the thread was left on a typed line nobody answered, going back to it typed gets that line its answer. A reply still running when you go back keeps its claim, so a second one cannot start beside it.
  • Background sessions are not touched. A session reports nothing into a conversation: the live model checks on it with get_session, whichever conversation it is in, so no result can land on the wrong thread or be lost by a switch. live_conversation_id on a delegated turn is provenance only. A call’s recap carries what was said and not the tool calls, so it also lists the sessions the thread has touched, which is how a reopened call knows what it set going.

A call follows the current conversation. Moving a call means reconnecting it. session.update can replace a Realtime session’s instructions and tools, but nothing removes what the session has already heard, so the old thread would stay in the model’s context. A new session starts from the new conversation’s own recap, the same way a dropped call comes back. A tool call that switched answers switched_to, and the client sends nothing more down that socket. While a call is up it also asks which conversation is current every 5 seconds, so it follows a move made somewhere else, including a new voice conversation started on another device. A call never turns voice on over someone typing: if the current conversation is typed, the call ends and says why. The car treats that as a drop and calls back, because in the car the driver is the one talking. The new call opens with a line nobody said, kept off the thread, so the model says where it picked up rather than leaving you in silence. Mute carries over and no hang-up sound plays. On the phone the microphone stays open and the call never goes idle.

A client that does not send reattaches on tool_calls is refused the switch, because it would carry on talking on a conversation that had ended. Web thread pages follow the same move from the header broadcast, unless a message is being typed, in which case the header links to the current conversation. A write that still reaches the old conversation is refused with a 409.

Live Mode has one agent root in the catalog, live-mode, whose AGENTS.md is the model’s instructions, typed or spoken. Starting a conversation fetches that file: a shallow, blobless clone with no checkout, then git show of the file, with the same stall detection as a session clone (LiveConversations::RootInstructions). CLAUDE.md is accepted when a root ships only that. The page says which commit the instructions came from.

The root can also ship one style file per mode beside it: modes/text.md for how to answer when typed, modes/voice.md for how to answer out loud. Both are optional and read in the same fetch, and they are kept on the conversation (mode_instructions), so a switch mid-conversation needs no second fetch. A blank file counts as missing.

The model is told, in this order:

  1. the root’s AGENTS.md;
  2. a short section Zimmer writes about mechanics only it knows: that the conversation can be typed or spoken and which it is now, the functions, the conversation’s URL — and, when the client that last drove the conversation is CarPlay, one line saying the person is in a car and naming the CarPlay fit filter and action. Zimmer states the fact; the root’s modes/voice.md is where what to do about it lives;
  3. Right now: typed or Right now: spoken. This is the root’s style file for the current mode, or Zimmer’s short default for that mode (LiveConversations::MODES) when the root ships none. It comes after AGENTS.md and says it takes precedence on style, so an AGENTS.md written with one mode in mind does not outvote the mode the conversation is in. The page’s header says when the default is in use;
  4. for a voice call only, a recap of the last thirty lines, because a new Realtime session remembers nothing. That is true whether it is reconnecting or the conversation was typed until a moment ago;
  5. the Live mode guidance, when there is any.

All of it is rebuilt every time it is used (LiveConversations::SystemPrompt). A typed reply builds it for each request, and a voice call builds it when its secret is minted and for every session.update. So a switch shows up in the very next answer.

The root’s default MCP servers are the conversation’s starting tool set. If the catalog has no live-mode root, Live Mode is off and says so: on /live, on its start page, in GET /api/v1/live and in get_live_conversations. It does not crash. Live Mode reads no other root, so a live-voice root left in a catalog is ignored.

The model can call five kinds of function (LiveConversations::ToolBridge):

  • zimmer(tool, arguments), which calls one of Zimmer’s own MCP tools in-process. This is the delegation path: quick_router or start_session to start work, get_session to check on it, action_session to follow up. It exists when the conversation has one of Zimmer’s own catalog servers (zimmer, zimmer-*), scoped to the tool groups that server’s URL names. It is one gateway rather than one function per tool because the default surface’s definitions come to about 200 KB, too much for a realtime model to carry on every turn. The gateway lists each tool in one line, and describe_zimmer_tool returns any tool’s full schema.
  • list_mcp_servers, enable_mcp_server, disable_mcp_server, through which the model manages its own catalog servers. This is the self-enable path.
  • <server>__<tool>: every tool of every enabled non-Zimmer server, called directly.
  • get_live_settings, update_live_settings: its own settings and guidance, always present. “Talk faster” or “from now on, always…” becomes a change the live agent makes itself.
  • list_live_conversations, switch_live_conversation: going back, always present, whatever servers are enabled. The list is at most three short names to say aloud, taken from the first thing said in each thread, because nothing sets a conversation’s title. These are functions of their own because action_live_conversation is opt-in and withheld from the zimmer gateway: a model holding it could also end its own conversation for good.

The agent root’s AGENTS.md owns the policy. The intended shape, which Zimmer’s own section repeats in one line, is this: anything that takes more than about five seconds becomes a background session, and the live model stays responsive. The sessions it starts are ordinary sessions on the board.

Every session the live model starts, follows up or routes is told where the request came from. The turn-source note reads live_voice or live_text, from the conversation’s mode when the tool runs, and names the client that last drove the conversation (live_conversations.client_surface: web, ios, carplay or mcp). The background agent can then keep what it reports back short enough for the live model to say aloud.

When the live agent keeps reaching the same capability through a background session, it can call enable_mcp_server with that server’s catalog name and call the server’s tools itself. That costs one hop instead of a session. Enabling lists the server’s tools once and keeps them on the conversation (mcp_tool_cache), so building the function list never has to start the server. A voice client is handed the new tool list and sends a session.update. Live Mode picks it up on its next request. A conversation enables at most eight servers and exposes at most 80 direct tools.

The bridge runs on the server because nothing else can. Most catalog servers are npx stdio processes or need credentials, and neither the browser nor OpenAI can run or hold those.

  • Remote servers are reached with the URL, headers and OAuth token McpServerEndpoint resolves. These are the same ones MCP Apps uses.
  • Stdio servers are spawned for one call and closed after it. Their environment is the server’s own catalog env (interpolated through the secrets chain) plus PATH, HOME, LANG, LC_ALL, TMPDIR and USER. It is never the Rails environment, which holds the database password and every API key.

Every call is recorded as a tool turn with its arguments and output. The page shows it collapsed, for example as zimmer → quick_router.

A typed message becomes a user turn and enqueues LiveConversationReplyJob on the inference queue. The job claims the conversation (responding_since, under a row lock), replays the last 80 turns to Chat Completions with the bridge’s tools, and runs any function calls. It feeds the results back and repeats until the model answers in words, giving up after eight rounds. A message typed while a reply is running is answered by the same loop. Turns reach the page over Turbo Streams. Nothing is stored at OpenAI: the thread is replayed from Zimmer’s own rows every time.

Audio never passes through Zimmer:

  1. Enable voice switches the conversation to voice and starts the call in one press; Talk reconnects one that is already in voice. Either way the browser asks Rails for a client secret (POST /live_conversations/:id/client_secret). Rails calls OpenAI’s POST /v1/realtime/client_secrets with the whole session config: model, instructions, tools, gpt-4o-mini-transcribe transcription, semantic_vad turn detection, and the voice (marin, or LIVE_VOICE_VOICE). The secret lasts ten minutes and the real key never leaves the server.
  2. The browser opens WebRTC to OpenAI with that secret: an SDP offer to /v1/realtime/calls and the oai-events data channel.
  3. On response.function_call_arguments.done it relays the call to POST tool_calls and sends the output back as a function_call_output item. When that response’s response.done arrives and every result is in, it sends exactly one response.create. A response the person talked over (cancelled) gets no response.create. One cut short (incomplete) does: see the voice output cap.
  4. Finished transcript lines (conversation.item.input_audio_transcription.completed and response.output_audio_transcript.done) are posted as turns, so the thread on the page and in the API carries what was said.
  5. Talking over a reply needs nothing from the browser. Over WebRTC OpenAI holds the audio, knows how much has played, and stops and truncates it itself.

An open microphone hears more than the person talking to Zimmer: a passenger, the radio, a conversation across the room. Listening (a Live setting) picks what the call answers:

ModeAnswersFor
Always (default)every line it hearsa private space, the car
Wake worda line that says “Zimmer”, anywhere in it, or a follow-up within ten seconds of the last line kept or spokena room with other people in it
Dynamicthe same, or a line a text model judges was said to Zimmerthe same, without the wake word

In Always, OpenAI’s turn detection starts each reply itself. In the two gated modes, Zimmer builds the call with create_response: false and interrupt_response: false, so turn detection does neither. Each finished user line goes to POST turns, and LiveConversations::Listening decides. A line that repeats Zimmer’s last reply is its own voice coming back through the speaker, and is dropped first. The wake word is a match on the transcript. The follow-up window means “yes” to a question, or the second half of a sentence split at a pause, does not need the wake word. Dynamic sends the line to the text model (reasoning_effort: "none", a three-token answer) along with the last six lines of the thread, so an answer to Zimmer counts and two people talking to each other don’t. If the judge cannot be reached (two seconds to connect, six to answer), the line is answered. A kept line is written to the thread, and the client sends response.create. A dropped line, or one that could not be transcribed, is not written anywhere, and the client deletes it from the call with conversation.item.delete, so it never colours a later reply.

Gating needs a client that does its half. A client says it does with listening_modes on client_secret, session_update, tool_calls and gated turns. Without it, an iOS build from before Listening for instance, the call answers everything whatever the setting says. Which mode a call is in follows from what OpenAI applied: client_secret answers listening, and after that the client reads create_response from each session.created and session.updated. A refused session.update does not leave the client thinking the call is gated when it isn’t.

The car always answers. A call whose requests say carplay is built as Always, whatever Listening says (LiveConversations::Actions). The car is one driver in a private space. A gated mode there drops lines without a sound, and the call sits on Listening as though it never heard. A call handed from the phone to the car mid-call becomes Always with the session_update the hand-over sends.

A call that has been on the car stays on Always until it hangs up, on the phone too (live_conversations.car_call). When the car disconnects, the call carries on through the phone with no word to the person, and they are still talking the way they were in the car. A gated mode arriving at that moment turns every line into a silent drop while the app says Listening. Nothing goes quiet this way: every client that re-applies Listening mid-call (the hand-over, the settings screen, the live model’s own update_live_settings) builds its session.update as Always for the rest of the call. The next call, which mints a new secret, starts on Listening as usual.

On the phone, a dropped line puts the call back to Listening, and the call log records it (listening.heard, dropped=true). listening on that line is the mode Zimmer decided in, which POST turns answers. A change to the mode during a call is logged too (listening.changed, with from and to), read off what OpenAI applied. session.updated says only whether a call is gated, so a call gated mid-call logs to=gated, and its next listening.heard names the mode. Every change to a Live setting, including one the live model makes itself, is logged at WARN as [LiveSettings], so it is in the shipped logs beside the calls it changed.

The call is owned by a dock in the layout (shared/_live_voice_dock, data-turbo-permanent), not by the conversation page. A Turbo navigation replaces the page body, and a peer connection living in it would drop on every click. The dock keeps the call, its mute and hang-up buttons, and a link back to the thread while you look around Zimmer.

The dock makes the same sounds as the phone and the car: heard, thinking, a tool working, failed, ended (the table), from lib/live_cues.js. While a cue sounds the microphone track is disabled, which sends silence. A browser that blocks audio has a call with no cues, and the call still works.

Type instead hangs up and switches back. Hanging up from the dock leaves voice on, and the conversation page offers Talk to reconnect or Type instead. When another device switches the mode, the header changes over Turbo Streams and the page re-renders its composer or call controls, unless a message is half-typed.

/live/settings holds what is worth tuning about how Live Mode thinks, sounds and listens. There is one set for the whole deployment, a single LiveSetting row. Reach it from the gear on a conversation’s page, from /live, or from Settings. Every field is blank until somebody sets it, and blank means the shipped default: the behaviour from before any of this was tunable. Reset settings to defaults writes blanks back. It leaves the guidance alone.

SettingPathDefaultChoices or rangeApplies
Text modeltypedgpt-6-luna (or LIVE_MODE_MODEL)gpt-6-luna, gpt-6-solnext turn
Text temperaturetypedOpenAI’s (1.0)0–2next turn
Text max output tokenstypedno cap256–128,000next turn
Voice modelvoicegpt-realtime-2.1-mini (or LIVE_VOICE_MODEL)gpt-realtime-2.1-mini, gpt-realtime-2.1, gpt-realtime-2next call
Voice reasoning effortvoicethe model’sminimal … xhighnext call
Voicevoicemarin (or LIVE_VOICE_VOICE)OpenAI’s ten voicesnext call
Speaking speedvoice1.00.25–1.5mid-call
Voice max output tokensvoiceno cap1,024–4,096 (why the floor)mid-call
Listeningvoicealwaysalways, wake_word, dynamic (Listening)mid-call, except on a call that has been on the car
Turn detectionvoicesemantic_vadsemantic_vad, server_vadmid-call
Eagerness (semantic)voiceautoauto, low, medium, highmid-call
Threshold, prefix padding, silence duration (silence)voice0.5, 300 ms, 500 ms0.1–0.95, 0–1,000 ms, 200–3,000 msmid-call
Noise reductionvoiceoffnear_field, far_fieldmid-call
Transcription modelvoicegpt-4o-mini-transcribethat, gpt-4o-transcribemid-call
Call telemetry (iOS app)voicedebugdebug, info, warn, off (Call telemetry)the app’s next batch

The list is what each path can actually use. The typed path calls Chat Completions with function tools, and OpenAI accepts those from the GPT-6 family only from gpt-6-luna and gpt-6-sol, only at reasoning_effort: "none". So there is no text reasoning-effort setting. The stronger GPT-6 models take tools only through the Responses API, and Live Mode does not use it yet. The ranges are narrower than the API’s where an extreme would silence the agent: a VAD threshold of 1.0 never hears speech.

When a change applies. The typed path reads the settings before every request. The voice path reads them when it mints a call’s secret. An open call is also sent a session.update with everything a Realtime session can change mid-call: instructions, tools, speed, turn detection, noise reduction, transcription, and the output cap. It is sent when the live model changes a setting itself (the tool-call response carries it), and when a person saves the settings page while the call dock has a call open (POST /live_conversations/:id/session_update). OpenAI fixes a call’s model, voice and reasoning effort for its life, so those apply from the next call.

Which turn detection settings apply. Turn detection decides which of its own settings reach OpenAI at all. The others stay saved and do nothing until the mode changes.

Turn detectionSent to OpenAISaved, no effect
Semantic (semantic_vad)EagernessThreshold, prefix padding, silence duration
Silence (server_vad)Threshold, prefix padding, silence durationEagerness

The save still goes through, so a setting can be tuned ahead of a switch, but no writer calls it a change that took. LiveConversations::SettingsUpdate works out what a write set that the mode now in force leaves unused, after the write, so a setting and the switch it needs can arrive together. Switching the mode names every setting it turned off. A reset, or putting one setting back on its default, names nothing.

  • update_live_settings answers Saved, but this changes nothing on a call: when nothing else changed, and tells the live model to say so out loud rather than confirm it. The conversation’s thread titles that note Live Mode saved a setting with no effect.
  • action_live_settings and PATCH /api/v1/live/settings put the same sentence in the answer, and REST also lists the keys in no_effect.
  • get_live_settings marks each unused setting NO EFFECT now. REST lists them in inactive.
  • The settings page shows an alert after the save. In the Voice section, each of these four settings carries an applies or no effect badge that follows the Turn detection select before you save.
  • The iOS app shows the sentence after a save.

An open call is still sent a session.update after such a write. OpenAI just receives the same turn detection as before.

A bad write cannot break Live Mode. Every writer goes through LiveConversations::SettingsUpdate: the page, REST, MCP, and the live model. An unknown setting, or a model, voice or mode not on the list, is refused and nothing is saved. A number outside its range is clamped, and the answer says so. If OpenAI still refuses what was saved (a model this key cannot use, say), the typed reply and the voice call each retry once on the defaults and say so: on the conversation’s page, and as settings_warning from client_secret.

max_output_tokens caps one Realtime response: its reasoning, its speech and its tool calls together. A response that reaches it stops where it is and ends incomplete, mid-sentence or mid-way through a function call’s arguments. So the cap has three guards:

  • A floor of 1,024. Every writer is clamped to it. At 180, a call lost the end of 12 of its 13 replies and all 4 tool calls on those responses.
  • It is a person’s to change. update_live_settings refuses it, and a reset from a call leaves it as it was, because the model would be capping the calls it needs to undo the cap. To make replies shorter, the model adds to the guidance. A person sets it on the settings page or through PATCH /api/v1/live/settings. action_live_settings can too, within the same range.
  • A cut-off tool call is not a silent one. All three clients treat incomplete, from the cap or OpenAI’s content filter, apart from a person talking over a reply. The call is relayed as usual, Zimmer refuses arguments that do not parse and runs nothing, and that answer goes back to the model with a response.create, so it can try again or say what went wrong. Three cut-off calls in a row stop the retries, and the client shows that OpenAI kept cutting the reply short.

When the cap costs a tool call, or a reply nobody heard a word of (it can run out in the model’s reasoning), the client lifts it for the rest of that call: max_output_tokens: "inf", on that session.update and every later one. A reply nobody heard is asked for again. The next call starts on the setting. A spoken reply the cap cut off just ends, with the failed cue, and the call goes back to listening.

The failure is written down in two places. Zimmer logs every failed tool call as a [LiveConversations::ToolBridge] WARN line with the reason and the size of the arguments, never the arguments. A catalog server’s own error text stays on the tool turn, and an exception logs its class. The iOS call log carries max_output_tokens on realtime.session_created and realtime.session_updated, as OpenAI applied it, and a response.done that ended incomplete is a warning, so it survives telemetry turned down to warn.

The guidance is free text, up to 8,000 characters, appended to the system prompt on every turn, typed and spoken. It comes after the root’s AGENTS.md and Zimmer’s section, last, so it reads as the newest word. It exists for fast iteration: tune it here, then lift what works into the live-mode root’s AGENTS.md. Copy puts the whole text on the clipboard.

The live agent can edit its own guidance. update_live_settings takes append_guidance for a standing instruction and guidance to rewrite it. That is deliberate, and every agent write is visible:

  • the thread shows the change as an amber note, not a folded tool call;
  • the History on the settings page records every change, field by field, before → after, and who made it: a person (the settings page, the iOS app) or an agent (a live conversation’s model, or a session through MCP);
  • any guidance edit in the history has Restore the guidance from before this edit.

The settings page posts only the fields you edited. A field you did not touch does not undo a change the live agent made while the page was open.

The iOS app has Live Mode behind the session list’s waveform button, over the same REST routes, with the same Enable voice and Type instead. The button lands in the current conversation with voice on and the call starting. When none is open it starts a new one with voice on. One tap on Type instead turns voice off in that thread. New conversation on the thread’s toolbar and its confirmation are the two taps to a fresh one, in whichever mode the thread is in. Back leads to the Live list. The web’s /live keeps its own entry, which opens the list. Starting a call on the phone turns voice on first (POST …/mode), so Enable voice, Talk and the car’s tiles all take the same path. A call on the phone opens a WebSocket to OpenAI rather than WebRTC. It uses the websocket_url the client-secret call returns, sends 24 kHz PCM16 as input_audio_buffer.append, and plays response.output_audio.delta. It relays function calls and transcript lines exactly as the browser does: the shared rules are RealtimeConductor in ZimmerKit. Over a WebSocket the phone holds the audio, so talking over a reply is the phone’s job: OpenAI sends a reply faster than it plays, and the phone keeps track of what is still queued (LiveReplyPlayout). When you start talking it stops whatever is still sounding, even if OpenAI finished generating it seconds ago, and sends conversation.item.truncate at the point you cut in. In the two gated Listening modes speech does not cut a reply off.

In the car you choose with one tap: Continue conversation calls the open conversation with voice on, and New conversation ends it and calls a fresh one. It is still one thread, and the ended conversation stays readable. With nothing open the tile is only New, and going back is by voice, with no screen for it: “go back to my last conversation” needs no list, and the call stays on its one call screen, reading Connecting… for the moment it takes to reconnect (Going back to a conversation). Either way it is the same call, with Mute and End while it runs, and a gear to the voice settings by touch, for a call that has stopped working. The car has no other voice: a call that isn’t up in 15 seconds gives up, the tile says Live Mode unavailable, retrying…, and the car retries with no voice, playing the falling notes the first time and when it stops. Unplug with a call up and it carries on on the phone, which goes to the call’s thread. GET /api/v1/live_conversations/:id/sessions returns the sessions the conversation’s tool calls have named, newest first (LiveConversation#referenced_session_ids). See CarPlay. Every TestFlight build signs in the CarPlay entitlement that makes it reachable.

The gear on the app’s Live screens opens the same Live settings over /api/v1/live/settings, guidance and history included. A save there sends an open phone call a fresh session.update.

The app logs every step of its voice calls, and the setting Call telemetry decides how much of that reaches the shipped logs. It is a threshold, and each level keeps everything above it:

LevelKeeps
debug (the default)every step: speech starting and stopping, each response, each cue, each state change
infomilestones (a call starting, connecting and ending; the audio session, route and turn detection in use; the timings) plus warnings and errors
warnproblems only: interruptions, microphone stalls, retries, and errors
offnothing

An error is kept at every level but off. The default is verbose. To turn it down, open Live settings (Settings → Live settings, or the gear on a Live conversation) and set Call telemetry. It applies from the app’s next batch, because Zimmer filters every batch against the setting and answers with the level, which the app adopts. The app also reads the level from each call’s client secret and keeps it across launches. The REST route is PATCH /api/v1/live/settings {"voice_telemetry": "info"}, and the MCP tool is action_live_settings. The live model cannot change it from a call, as with the voice output cap: update_live_settings refuses it, and a reset from the call leaves it as it was. What is logged, and how to read it, is under CarPlay and observability.

Live Mode needs LIVE_OPENAI_API_KEY, typed or spoken, resolved through the secrets chain (Parameter Store, Rails credentials, environment). Without it Live Mode is off, with that as the stated reason. Zimmer does not write the key: set it in the secrets console that administers Zimmer’s store, as a secret parameter at /zimmer/<env>/secrets/static/LIVE_OPENAI_API_KEY, or with the gcloud commands in Adding a secret. A new value reaches Zimmer within a minute, with no redeploy. /live’s “OpenAI API key” panel shows whether the key is set, which link of the chain holds it, and that path, and links the console when ZIMMER_STORE_CONSOLE_URL is set. It shows no value and no fingerprint. See Secrets Zimmer itself depends on. The key has its own name rather than OPENAI_API_KEY because store-resolved variables can reach session environments, and Codex and Pi read OPENAI_API_KEY on their own. A key set for Live must not change how those sessions authenticate or bill.

VariableDefaultWhat it does
LIVE_OPENAI_API_KEYnoneThe key. Required.
LIVE_MODE_MODELgpt-6-lunaThe typed default model, when the Live settings name none
LIVE_VOICE_MODELgpt-realtime-2.1-miniThe voice default model, likewise
LIVE_VOICE_VOICEmarinThe default voice it speaks in, likewise
LIVE_OPENAI_BASE_URLhttps://api.openai.com/v1An OpenAI-compatible endpoint instead
ZIMMER_STORE_CONSOLE_URLnoneThe console over Zimmer’s own store, linked from the key panel. Unset, the panel shows the path without a link
ZIMMER_STORE_CONSOLE_STORE_NAMEnoneWhat that console calls the store, shown beside the link
  • MCP: get_live_conversations (whether Live Mode can start, the current and recent conversations, what going back would return to, one thread) and action_live_conversation (start, switch, send_message, set_mode, finish); get_live_settings and action_live_settings (update, reset, restore_guidance) for the Live settings. All four are in the opt-in live_conversations group, which nothing carries by default: a live model can reach the whole catalog, so a session must not be able to steer one just by holding sessions. See the MCP server.
  • REST: /api/v1/live, /api/v1/live/settings and /api/v1/live_conversations. An API key may read them. Starting, going back, messaging, switching mode, finishing, the voice relay and changing the settings take the iOS app’s sign-in acting as you, for the same reason. An agent changes the settings through MCP, where the change is recorded as an agent’s. See the REST API.
  • Supervisor: /supervisor/live_conversations and /supervisor/live_conversation_turns, both read-only.

What is not proven yet, and the known rough edges, are on Known limitations.