# Agent activity rail and transcripts

> How the Voice AI activity rail selects, orders, and labels AI-handled calls, and what the transcript drawer shows.

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.

| Included | Excluded |
| -------- | -------- |
| Calls whose handler was an LLM voice agent | Calls handled by a human agent |
| Calls for the currently selected org (`orgId`) | Calls in other orgs |
| The first page of results | Anything past the first page |

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](/platform/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.

| Displayed | Source field | Fallback behaviour |
| --------- | ------------ | ------------------ |
| Caller → callee (monospace) | `caller`, then `callee` or `dialed_num` | Either side renders as `—` when absent |
| Handler name (caption) | `last_handler_display_name` | Literal `AI agent` when absent |
| Disposition tag | `last_disposition` | Tag is omitted entirely when absent |
| Duration | `total_duration_sec` | Rendered `m:ss`; a missing or non-numeric value renders `0:00` |
| Row identity | `call_id` | Also the key passed to the transcript drawer |

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:

| Part of the card | Source field, in precedence order |
| ---------------- | --------------------------------- |
| Speaker tag | `handler_kind`, then `role`, then `speaker`; falls back to the literal `segment` |
| Body text | `transcript`, then `text`, then `content` |
| Per-segment disposition caption | `disposition`, shown only when present |

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](/platform/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:

| Surface | Question it answers |
| ------- | ------------------- |
| Call rows | Did calls happen, who handled them, how did they end? |
| Relay panel | Is media moving through the voice namespace *right now*? |

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.

## Related

- [Telemetry](/platform/telemetry) — CDRs, metrics, and traces for the same calls
- [Telephony Metrics](/glossary/metrics) — definitions for the numbers behind call outcomes
- [Authentication](/concepts/authentication) — how the console's org scope (`orgId`) is established
