# Live agent sessions

> How the live Sessions roster is built: session rows, agent states, presence expiry, and why the header counters ignore the filters.

The **Sessions (Live)** screen is a presence roster. It answers one
question: right now, who is signed in, and what is each of them doing?

Every row on the screen comes from a single call to
`adminSessions.list({ orgId })`. That procedure reads the TeleQuick
presence store in Redis and joins it to the `dim_agent` dimension, so
each row carries both the live state (which changes second to second)
and the durable agent identity (name, email, external id). The screen
polls the procedure and re-renders; it never mutates anything.

The query is scoped to one org and is disabled until an org is
selected. There is no cross-org roster.

## What a session row represents

One row is **one agent**, not one call. The row is keyed on `agentId`,
so an agent appears exactly once no matter how many calls they have
handled in the period you are looking at.

That has two consequences worth knowing before you read the screen:

- A row is a *current* snapshot. It has no history. If you need the
  sequence of states an agent moved through, this screen is the wrong
  tool — see [Related](#related).
- At most one call id is attached to a row (`currentCallId`). The
  roster describes the call the agent is on *now*.

## Session states

`state` is the field that drives the colour and the badge on the right
of each row. The screen's state filter enumerates the full set of
values the procedure returns:

| `state`   | Rendered as        | Companion field on the row |
| --------- | ------------------ | -------------------------- |
| `ready`   | Static tag, "ok" tone   | —                     |
| `ringing` | **Live pill**, "warn" tone | `currentCallId`    |
| `on_call` | **Live pill**, "warn" tone | `currentCallId`    |
| `acw`     | Static tag, "warn" tone | —                     |
| `aux`     | Static tag, "muted" tone | `auxReason`         |
| `offline` | Static tag, "muted" tone | —                     |

`ringing` and `on_call` are the only two states that render as a
*live* pill (the animated variant). That is deliberate: those are the
two states where the agent is attached to a call leg and where the
value is expected to change without you doing anything.

Note which states pair with which companion field, because it tells
you where the state came from:

- `ringing` and `on_call` carry a `currentCallId`. They describe call
  activity. They are not modes an agent picks from a UI — an agent
  becomes `ringing` because a call was offered to them.
- `aux` carries an `auxReason` (the free-form label the agent or your
  integration supplied, e.g. a break or training code). This is a
  *selected* state, which is why it has a reason attached.
- `acw` (after-call work) carries neither. It is the wrap-up window
  that follows a call, and it is distinct from `aux`: an agent in
  `acw` is finishing the previous interaction, an agent in `aux` has
  deliberately made themselves unavailable.

The screen shows no transition arrows and exposes no way to change a
state. The only transition evidence on the row is `stateSinceMs` — how
long the agent has been where they are.

## Row fields

These are the fields the roster actually reads off each row.

| Field             | Source                  | How the screen uses it |
| ----------------- | ----------------------- | ---------------------- |
| `agentId`         | Join key                | Row identity (React key). Not searchable. |
| `displayName`     | `dim_agent`             | Primary label on the row. |
| `email`           | `dim_agent`             | Not displayed. Searchable. |
| `externalAgentId` | `dim_agent`             | Searchable. Falls back to the primary label if `displayName` is absent. |
| `kind`            | Presence                | Secondary line. Defaults to `agent` when absent. |
| `state`           | Presence                | Badge and tone; the state filter matches on it exactly. |
| `stateSinceMs`    | Presence                | Rendered as an age on the right of the row. |
| `currentCallId`   | Presence                | Appended to the secondary line as `· call <id>`. |
| `auxReason`       | Presence                | Appended to the secondary line. |

The primary label degrades in a fixed order: `displayName`, then
`externalAgentId`, then the raw `agentId`. An agent showing a raw id
usually means the presence entry has no matching `dim_agent` row yet.

`stateSinceMs` is a Unix timestamp in milliseconds. The screen renders
it as an age relative to the browser's clock: seconds below a minute,
minutes below an hour, then hours. A missing or zero `stateSinceMs`
renders as `—`. Because the age is computed client-side, a skewed
workstation clock skews every age on the screen at once — if all the
ages look implausible, suspect the clock before the roster.

## How offline is detected

Presence is not a flag that an agent turns off on the way out. It is a
Redis entry that has to be kept alive. The gateway writes the entry as
the agent's session reports in, and the entry expires on its own if
those reports stop.

So `offline` on this screen means *there is no live presence entry for
this agent*, and the identity you see is coming from the `dim_agent`
side of the join. Three different real-world situations collapse into
that one value:

- the agent signed out cleanly,
- the agent's client or softphone died and the presence entry lapsed,
- the network between the agent and the gateway dropped long enough for
  the entry to lapse.

The roster cannot distinguish them. What it gives you instead is
`stateSinceMs`: an agent who has been `offline` for seconds is
interesting, an agent who has been `offline` for hours is just not
working today.

Expiry also explains a lag you will observe. An agent who yanks their
network cable does not flip to `offline` at that instant. They flip
when their presence entry lapses, and only then does the next poll show
it. Treat `offline` as "we stopped hearing from this session", not as
"this session ended at this moment".

The empty state on the screen — *"Agents appear here when they sign
in"* — is the other side of the same coin: an agent with no presence
entry and nothing to join against contributes no row at all.

## Refresh and polling behaviour

This screen polls. `adminSessions.list` is re-fetched on a fixed
8-second interval and the roster re-renders from whatever the latest
response says.

There is no push-based invalidation here. The original operator console
invalidates this query from a QUIC push when presence changes; this
Voice AI port has no realtime hook available, so it drops the push path
and uses a tighter poll instead. The practical difference:

- Worst-case staleness on this screen is one poll interval, not one
  round trip from the presence change.
- The ages on the rows advance only when a poll lands, because
  `stateSinceMs` is re-rendered from the response rather than ticked
  locally. An age that appears to "jump" several seconds is the poll
  landing, not a data problem.
- The poll runs whenever the screen is mounted and an org is selected.
  Leaving the screen open leaves the poll running.

## Why the counters ignore the filters

The header reads `N online · N ready · N on call · N aux`. Those four
numbers are computed from the **full** response, before the search box
and the state filter are applied. Filtering the table down to one
agent does not change them.

That is intentional — the counters are the org-wide picture and the
table below is your lens onto it — but it means the numbers do not add
up the way you might expect:

- `online` counts every row whose state is anything other than
  `offline`. So `ringing` and `acw` agents are included in `online`.
- `ready`, `on call`, and `aux` are exact-state counts.
- **There is no `ringing` or `acw` counter.** Those agents are visible
  in `online` but in none of the three breakdown numbers, so
  `ready + on_call + aux` is normally *less than* `online`. The
  difference is your `ringing` plus `acw` population.
- `offline` rows are counted internally but not surfaced in the header
  strip.

If you want the `ringing` or `acw` head-count, select that value in the
state filter and count the rows.

The filters themselves behave as follows. The state dropdown is an
exact match on `state`. The search box is a case-insensitive substring
match against `displayName`, `email`, and `externalAgentId` joined
together — so a query can span fields, and searching by raw `agentId`
will not match. The two filters combine with AND.

## Related

- [Telemetry](/platform/telemetry) — the metrics, trace, and CDR
  streams, for the per-call record behind a `currentCallId`.
- [Telephony Metrics](/glossary/metrics) — AHT, ASA, and the other
  contact-centre numbers that aggregate the states on this screen.
- [Authentication](/concepts/authentication) — how the org scope on
  this query is established.
