a one-character `q` SHALL be rejected with `400`; `q` combines with every other filter of the list.
#### Scenario: Prefix match
- **GIVEN** sessions with identities `Alice`, `alice-2`, `malice` and `bob`
- **WHEN** the client sends `?q=al`
- **THEN** `Alice` and `alice-2` are returned and `malice` and `bob` are not
#### Scenario: One character rejected
- **WHEN** the client sends `?q=a`
- **THEN** the response is `400` with code `VALIDATION_ERROR`, `details.issues[0].path` equal to `["q"]` and a message naming the two-character minimum
#### Scenario: Prefix inside a room
- **GIVEN** identity `alice` with sessions in rooms `RM_a` and `RM_b`
- **WHEN** the client sends `?q=ali&room=RM_a`
- **THEN** only the sessions of `RM_a` are returned
#### Scenario: Agents by identity
- **GIVEN** agent sessions with identities `assistant-1` and `transcriber`, and a STANDARD session `assistant-fan`
- **WHEN** the client sends `GET /api/v1/agents?q=assist`
- **THEN** only the `assistant-1` agent session is returned
### Requirement: Exact SID lookup through the list
- **WHEN** the client sends `?q=PA_x1y2&from=now-24h&to=now`
- **THEN** the response has exactly one item with `sid: "PA_x1y2"` and `total: 1`
#### Scenario: Unknown SID
- **WHEN** the client sends `?q=PA_missing`
- **THEN** the response is `200` with `items: []`
#### Scenario: Agent SID
- **GIVEN** agent session `PA_ag1` joined 3 days ago
- **WHEN** the client sends `GET /api/v1/agents?q=PA_ag1&status=active`
- **THEN** the response has exactly one item with `sid: "PA_ag1"`, `total: 1` and both cursors `null`
### Requirement: Participant list item shape
Edition: CE. Each item of the participants and agents lists SHALL carry `sid`, `identity`, `kind`, `status`, `roomSid`,
`roomName`, `joinedAt`, `isPublisher`, `publishedSources`, the optional `name`, `leftAt`, `durationMs`,
`connectionType`, `nodeId`, the `stale` flag that `api-conventions` defines and a `client` summary, with unknown values
omitted as `api-conventions` requires.
#### Scenario: Complete row
- **GIVEN** a left session of `alice` from the JS SDK 2.6.1 on Chrome 130 / macOS 14 over UDP
- **WHEN** it is listed
- **THEN** the item has `status: "left"`, `leftAt`, `durationMs`, `connectionType: "UDP"` and `client: { sdk: "JS", version: "2.6.1", browser: "Chrome", browserVersion: "130", os: "macOS", osVersion: "14" }`
#### Scenario: No client information
- **GIVEN** a session whose `PARTICIPANT_JOINED` carried no `client_info`
- **WHEN** it is listed
- **THEN** the item has no `client` and no `connectionType` property
#### Scenario: Published sources
- **GIVEN** a session whose `tracks.bySource` is `{ CAMERA: 2, MICROPHONE: 1 }`, and a session that never published
- **WHEN** both are listed
- **THEN** the first item has `publishedSources` with `CAMERA` and `MICROPHONE` and no other source, and the second has `publishedSources: []`
#### Scenario: Stale active session
- **GIVEN** the default writer batch interval of 10 s and an active session whose `lastStatsAt` is 100 s old at the server's clock
- **WHEN** it is listed
- **THEN** the item has `stale: true`, and a left session with the same `lastStatsAt` has no `stale` property
#### Scenario: Sources and client summary
- **WHEN** an item of the participants or agents list is returned
- **THEN** `publishedSources` holds the track sources with at least one publication in `tracks.bySource` (empty when
none) and `client` carries `sdk`, `version`, `os`, `browser`, `browserVersion` and `osVersion` when known
### Requirement: Agents list endpoint
Edition: CE. `GET /api/v1/agents` SHALL list participant sessions of kind `AGENT` with the item shape, the
`status`, `room`, `q`, `from`/`to`, `sort` (with the ranked sorts), `order`, `cursor` and `limit` parameters and the
envelope of the participants list, without `kind` or `publisher`, in every edition.
#### Scenario: Active agents
- **GIVEN** two active agent sessions and one left agent session in range, and ten STANDARD sessions
- **WHEN** the client sends `GET /api/v1/agents?status=active`
- **THEN** exactly the two active agent sessions are returned with `kind: "AGENT"` and `nodeId`
#### Scenario: Agents on CE
- **GIVEN** the CE dashboard and a valid token
- **WHEN** the client requests `GET /api/v1/agents`
- **THEN** the response is `200` with the agent sessions of the default range, never `403``PRO_FEATURE`
#### Scenario: Agent search
- **GIVEN** active agent sessions `assistant-1` and `transcriber` in range
- **WHEN** the client sends `GET /api/v1/agents?q=tra`
- **THEN** only the `transcriber` session is returned
### Requirement: Index-backed participant queries
Edition: CE. Every query of this capability SHALL be served by an index of `data-model` with no
collection scan and with `totalDocsExamined` bounded by the page size plus the documents of the filtered
range, verified with `explain()` on a generated dataset; only the ranked sorts rank in memory, over the range scan
that the list's filters select.
#### Scenario: Explain plans
- **GIVEN** a database generated at scenario M
- **WHEN** the list with and without `q`, `room`, `kind`, `status` and `publisher`, the summary and the agents
list queries are explained
- **THEN** no winning plan contains a `COLLSCAN` stage and none sorts in memory, the ranked sorts aside
#### Scenario: Ranked sorts over 24 hours
- **GIVEN** a database generated at scenario M
- **WHEN** the participants and agents lists are explained with `sort=leftAt` and `sort=duration` over 24 hours, with and without `q`, `room`, `kind` and `status`
- **THEN** each plan scans the range index of its filters (`joinedAt_id`, `kind_status_joinedAt_id`, `roomSid_joinedAt_id`, `identityLower_joinedAt` or `roomSid_identityLower_joinedAt`), `totalDocsExamined` is at most the sessions joined in the range, and the p95 of a page is within 300 ms
#### Scenario: Publisher filter
- **WHEN** the list with `status=active&publisher=true` is explained
- **THEN** the plan uses `kind_status_joinedAt_id` with `isPublisher` as a residual filter
### Requirement: Participants summary endpoint
Edition: CE. `GET /api/v1/participants/summary` SHALL accept `from`, `to` and `tz` and answer `200` with
`{ active: { participants, services, byKind }, sessions: { participants, byKind }, approximate }`: active STANDARD
sessions, active sessions of the other kinds and active sessions per kind, then STANDARD sessions joined in the range
and sessions per kind joined in the range. Range counts follow the exact-or-approximate rule of the rooms summary.
#### Scenario: Short range
- **GIVEN** 31 active STANDARD, 3 active EGRESS and 2 active AGENT sessions, and 900 STANDARD, 40 EGRESS and 12 AGENT sessions joined in the last 24 hours
- **WHEN** the client requests `GET /api/v1/participants/summary?from=now-24h&to=now`
- **THEN** `sessions.byKind` holds the sums of `participants.byKind` of the global hourly rollups whose bucket starts inside the range, `sessions.participants` its `STANDARD` value, and `approximate` is `true`