The Activity screen in the Voice AI console is the read-only view of what your voice agents have been doing. It pairs two independent signals: a list of recent calls that an LLM handler took, and a live panel showing whether media is actually moving through the TeleQuick voice relay right now. The screen is observe-only. It has no wrap-up codes, no ACD controls, and no supervisor barge — those belong to the contact-center suite. Everything here reads from the same call-reporting data that the contact-center reports read.

What the activity rail includes and excludes

The rail calls reportsAllCalls.list with handlerKind: 'llm'. That filter is fixed in the screen and is not user-adjustable. Consequences worth internalising before you use this screen as evidence:
  • A call missing from the rail is not a call that did not happen. It may have been handled by something other than an LLM handler, or it may have fallen off the first page.
  • The rail is not a search tool. There is no date range, no filter box, and no pagination control. For arbitrary queries over call history, use the call reporting surfaces in the contact-center suite or query CDRs directly — see Telemetry.

The 25-row window and refresh cadence

The query is issued as { orgId, handlerKind: 'llm', page: 1, perPage: 25 } and re-runs every 15 seconds. Only page 1 is ever requested. The counter pill in the card header shows rows.length — the number of rows currently rendered, not a total call count for the org. When the org has more than 25 AI-handled calls in range, the pill reads 25 and stays there. The header chevron collapses and expands the list body. Collapsing hides the rows; it does not stop the query, so the rail stays current while collapsed and the count in the pill keeps updating. When the query returns no rows, the card shows the empty state “No calls yet”.

Row fields

Each row is a button. The left side identifies the call, the right side summarises its outcome. Duration is formatted by flooring to whole seconds and zero-padding, so 95 renders as 1:35. There is no separate talk / hold / wrap breakdown on this screen — total_duration_sec is the only duration the rail reads.

Dispositions and colour tones

The rail does not define a disposition vocabulary of its own. It takes the last_disposition string exactly as the call-reporting pipeline recorded it, renders that string verbatim as the tag label, and passes the same string to the shared statusTone helper to pick the tag’s colour tone. Two practical rules follow:
  1. Read the label, not the colour. The tone is a hint derived from the string. A disposition that statusTone does not recognise still renders — it just renders with the neutral default tone. A neutral tag means “unmapped”, not “neutral outcome”.
  2. A missing tag is not a disposition. If last_disposition is empty or absent, the row shows no tag at all. That is a gap in the call record, not an outcome value.
If you want a tone applied to a disposition your pipeline emits, the mapping lives in statusTone in the console’s shared UI layer, not in this screen.

How agent selection reorders the list

When an agent is selected in the console, the rail reorders the rows it already fetched so that the selected agent’s calls appear first. A row counts as the selected agent’s when either:
  • last_handler_id equals the selected agent’s id, or
  • last_handler_display_name equals the selected agent’s name.
The sort is a stable partition on that boolean: matching rows move to the top, and the pipeline’s original ordering is preserved inside each group. This is a client-side reorder of the 25 rows already on screen. It is not a server-side filter. Selecting an agent does not re-query, does not narrow the list to that agent, and cannot pull in that agent’s older calls from beyond the first page. If the agent has no calls in the current 25 rows, selecting it changes nothing visible. The name-based fallback exists because some records carry a display name without a handler id. It matches on exact string equality, so an agent renamed after a call was recorded will not match its own historical rows by name.

Reading a transcript drawer

Clicking a row opens a right-hand drawer and issues reportsCallDetails.byId with { orgId, callId }. The drawer header shows the call_id in monospace so you can correlate with CDRs and traces. Clicking the backdrop or the close button dismisses it; clicks inside the drawer do not. The drawer tolerates three response shapes, in this order:
  1. detail.segments
  2. detail.turns
  3. the response itself, when it is an array
Whichever resolves first becomes the segment list. Each segment is rendered as a small card: The speaker tag is tinted with the ok tone when the resolved speaker value is exactly llm; every other speaker value renders with the default tone. That is the only colour signal in the drawer — it distinguishes agent turns from everything else, not agent turns from caller turns specifically. If a segment resolves no body text from any of the three fields, the drawer falls back to printing the raw segment object as JSON, truncated to 200 characters, in monospace. Seeing that fallback means the segment exists but uses field names this drawer does not know about — it is a schema mismatch, not an empty turn. While the query is in flight the drawer shows Loading….

When a call has no captured segments

If the resolved segment list is empty, the drawer shows:
No transcript — This call has no captured segments.
What that message does and does not tell you:
  • It means reportsCallDetails.byId returned a record whose segments / turns were empty or absent for that call_id.
  • It does not distinguish “the call was never transcribed” from “the transcript was captured elsewhere” from “the record no longer carries segments”. This screen surfaces the call-details record as-is; it does not create, backfill, or re-request segments, and it exposes no retention or capture setting.
Because the rail and the drawer read different procedures, a row can be present with a full duration and a disposition while its drawer is empty. The row proves the call was reported; only the drawer speaks to segments. To investigate further, correlate on the call_id shown in the drawer header against your CDRs and traces — both carry a call identifier and the tenant, so the same call is addressable from Telemetry.

Reading the voice relay data-plane panel alongside call rows

Above the Activity card sits a relay panel fed by observability.relayHealth with ns: 'voice', polled every 5 seconds. The two surfaces answer different questions, and reading them together is the point of this layout: An empty rail with a healthy relay reads very differently from an empty rail with a silent relay — the first is “no traffic”, the second is “traffic that may hear nothing”.

How the panel handles failure

relayHealth invokes the relay.metrics service with a 3 second timeout. The short timeout is deliberate: the panel polls every 5 seconds, and a slow engine must read as one missed tick rather than stacking up requests. If that invocation throws or times out, the procedure returns { configured: false, text: '' } and the panel renders as No data. Read that state carefully. configured: false is returned on any RPC failure, including a control-plane problem that has nothing to do with the relay. A “No data” panel is a signal to check the metrics path itself; it is not evidence that the relay is down.
  • Telemetry — CDRs, metrics, and traces for the same calls
  • Telephony Metrics — definitions for the numbers behind call outcomes
  • Authentication — how the console’s org scope (orgId) is established