# Supervising a live call

> How the active-call index decides a call is live, and how listen, whisper, and barge-in supervision work.

The **Active calls** screen lists in-progress calls for the current org and
offers a **Supervise** action on each row. This page explains what puts a call
on that list, what the three supervisor modes do, the token and scope the
gateway requires before it admits a supervisor, and which parts of the path are
wired up today.

Two queries back the screen:

| Panel        | Procedure                      | Source                        | Poll interval |
| ------------ | ------------------------------ | ----------------------------- | ------------- |
| Active calls | `telephony.listActiveCalls`    | Redis sorted set + session hashes | 8 s       |
| Live events  | `monitoring.agentEvents`       | In-process telemetry ring     | 3 s (pausable) |

## What counts as an active call

`telephony.listActiveCalls` reads a per-org sorted set at
`telequick:<orgId>:active_calls`, scored by call start time. A call
row appears on the screen only if it survives every one of the following
checks.

1. **Recency cutoff.** The query reads the index with `ZREVRANGEBYSCORE` from
   `+inf` down to `now - 60 minutes`. Anything scored older than that is never
   considered. Real calls finish in seconds to minutes; an entry older than the
   cutoff is almost certainly a stale index row left behind by a hangup path
   that never removed it — a carrier disconnect, a gateway restart mid-call, or
   a Redis blip.
2. **Row limit.** `limit` defaults to `100` and is capped at `500`. The limit is
   applied both to the initial index read and to the final sorted result.
3. **End-record gate.** For each candidate SID the procedure checks whether
   `session:<sid>:end` exists. `agent_runtime` writes that record on hangup (the
   RTP-listener silence-finalize / dialog-closure path). If the record exists,
   the call is over — even if `release()` failed to remove the SID from the
   index — and the call is treated as ended.
4. **Session hash present and tenant-matched.** For each surviving SID the
   procedure reads `tenant_id`, `agent_id`, `trunk_id`, `realm`,
   `started_at_ms`, and `node_id` from `session:<sid>`. A row whose read errored,
   whose `tenant_id` is missing, or whose `tenant_id` does not equal the
   requesting `orgId` is dropped.
5. **Start time re-check.** `started_at_ms` must parse to a finite number that is
   not older than the same 60-minute cutoff.

Surviving rows are sorted newest-first and truncated to `limit`.

### Self-healing eviction

Candidates that failed the end-record gate are removed from the sorted set
inline, as a best-effort pipelined `ZREM`. The cleanup is fire-and-forget: if it
fails, those SIDs are simply filtered out again on the next listing, so the
index converges without an external reaper.

### Fields on each row

| Field           | Meaning                                                   |
| --------------- | --------------------------------------------------------- |
| `call_sid`      | The call identifier. This is what **Supervise** acts on.   |
| `tenant_id`     | Always equal to the requesting org.                        |
| `agent_id`      | Empty string if the call did not route through an agent.   |
| `trunk_id`      | Empty string if unset.                                     |
| `realm`         | Defaults to `internal` when the session hash has no value. |
| `started_at_ms` | Drives the call-duration counter in the UI.                |
| `node_id`       | The gateway node handling the call. Empty string if unset. |

The duration shown in the row is computed client-side from `started_at_ms`, so
it keeps ticking between the 8-second refreshes.

## Supervisor modes: listen, whisper, barge-in

Clicking **Supervise** opens the supervise panel for that `call_sid`. The
operator picks one of three modes. The console sends the mode to the control
plane, which dispatches a `Supervise` op onto the per-tenant `rpc_bridge` queue;
the gateway then records the supervisor as a participant on that session's room.

| Mode         | Intent                                                                 |
| ------------ | ---------------------------------------------------------------------- |
| **Listen**   | The supervisor joins the session and hears the call without being heard. |
| **Whisper**  | The supervisor is heard by exactly one leg of the call.                 |
| **Barge-in** | The supervisor joins the conversation audibly to both legs.             |

### Choosing the whisper target

Whisper mode requires a `whisper_target` naming which leg hears the supervisor:

- `agent` — only the agent leg hears the supervisor.
- `caller` — only the caller leg hears the supervisor.

Listen and barge-in ignore `whisper_target`. The requirement is enforced in both
places: the control-plane procedure rejects a whisper request without a target,
and the C++ `rpc_bridge` handler validates the same condition, so a request
crafted against the gateway directly cannot bypass it.

## Minting a supervisor token and the required scope

Supervision is not covered by an ordinary session credential. The console mints
a short-lived, opaque **supervisor token** and hands it back to the gateway in
`SuperviseSubscribeRequest.auth_token`.

The gateway resolves the token at `sa:<token>` in Redis and admits the subscribe
only if both conditions hold:

- the token's `scopes` contain `telephony:supervise`, **and**
- the token's `tenant_id` matches the tenant of the call being supervised.

Token properties:

| Property     | Value                                                                    |
| ------------ | ------------------------------------------------------------------------ |
| Format       | Opaque string, resolved server-side. Not a self-describing JWT.           |
| Binding      | Bound to a single `call_sid`, so a leaked token is narrowly scoped.        |
| TTL          | 10 minutes. Reissue if the supervisor stays connected past it.             |
| Who may mint | Org **owners** and **admins** only.                                        |

The regular agent role is deliberately **not** granted `telephony:supervise`.
Supervising moves live call audio outside the per-call participant set, so it is
modelled as an explicitly elevated action rather than something any agent
identity picks up implicitly.

## What works today and what is pending

The supervisor path ships in stages, and the Active calls screen is honest about
which stage you are in.

**Working today**

- The active-call listing, liveness gating, and inline eviction described above.
- Token minting with the role gate, `call_sid` binding, and 10-minute TTL.
- Gateway admission: `sa:<token>` resolution, scope check, and tenant check.
- Dispatch of the `Supervise` op onto the tenant's `rpc_bridge` queue, and
  registration of the supervisor as a participant on the session room.
- Mode and `whisper_target` validation, in the control plane and again in the
  C++ handler.

**Pending**

- **Audio routing is a no-op.** Until the `codec_engine` mixer follow-up lands,
  joining as a supervisor does not move audio in either direction, for any of the
  three modes. The call returns `SUPERVISOR_JOINED` or `SUPERVISOR_UPDATED` so
  the console can confirm that the intent was accepted and render the pending
  state. Treat the Active calls screen as a read-only monitor for now.

## Live engine telemetry ticker

The lower panel on the screen renders recent engine telemetry from
`monitoring.agentEvents`, which reads an in-process ring buffer rather than the
call index. The procedure takes a `limit` (default `50`, maximum `200`; the
screen requests `40`) and returns an `enabled` flag alongside the events. When
the telemetry consumer is not enabled, the event list comes back empty.

Each rendered line shows the event's receive timestamp, its channel, and a
flattened summary of the first few fields of the raw payload. `tenant_id`,
`org_id`, `ts`, and `timestamp` are omitted from that summary because they are
either redundant with the row context or already displayed. Payloads that do not
parse as JSON are shown truncated.

**Pause** stops the 3-second refresh entirely; the last fetched batch stays on
screen until you resume.

## Troubleshooting an empty or stale list

**The list is empty but calls are in progress.**

- Confirm the session hash's `tenant_id` matches the org you are viewing. Rows
  whose `tenant_id` differs from the requested `orgId` are filtered out silently.
- Confirm the call was added to `telequick:<orgId>:active_calls` with a
  start-time score. A call absent from the index is invisible to this screen
  regardless of its real state.
- Check for a premature `session:<sid>:end` record. Its presence is treated as
  proof the call ended, and the SID is evicted from the index on the spot.

**A call that ended is still listed.**

- It will disappear on the next listing once `session:<sid>:end` is written, or
  once its start time passes the 60-minute cutoff. Eviction happens during a
  listing, so the self-heal requires at least one more poll.
- Eviction is best-effort. If the cleanup pipeline fails, the entry remains in
  the index but stays filtered out of the response.

**A long-running call disappeared.**

- Anything scored older than 60 minutes is outside the window and will not be
  listed, even if the call is genuinely still up. The 60-minute ceiling replaced
  a much wider window that surfaced hours-old hung-up calls to operators.

**Rows look truncated.**

- The default `limit` is 100 and the maximum is 500. The limit bounds both the
  index read and the final result, so a very busy org sees the most recently
  started calls first.

**Supervise accepted but nobody can hear anything.**

- Expected. Audio routing is pending the mixer work; see
  [What works today and what is pending](#what-works-today-and-what-is-pending).

**Supervise is rejected before the panel connects.**

- The minting identity must be an org owner or admin.
- The token must carry `telephony:supervise` and a matching `tenant_id`.
- The token is bound to one `call_sid` and lives 10 minutes; a token minted for
  a different call, or an expired one, is refused at the gateway.

## Related

- [Telemetry](/platform/telemetry) — metrics, traces, CDRs, and SIP capture
- [Authentication](/concepts/authentication) — API keys, scopes, and short-lived tokens
- [Telephony Metrics](/glossary/metrics) — definitions for the numbers on live calls
