The Agents page (CE) that lists agent participant sessions with their room, status, published sources and node, a
live counter and links to the participant and room details, which stay locked in CE like those of the other lists.
## Requirements
### Requirement: Agents route
Edition: CE. The `/agents` route and its sidenav entry SHALL be available in every edition, never locked and without a
required feature; the page loads its data from `GET /api/v1/agents` and registers the header refresh control like the
other lists.
#### Scenario: Agents in CE
- **GIVEN** the CE dashboard with `features: []`
- **WHEN** the user opens `/agents`
- **THEN** the sidenav entry shows no lock icon, the page renders its tiles, filters and table, the header shows the refresh control and the request `GET /api/v1/agents?…` is sent
### Requirement: Agents page state and filters
Edition: CE. The Agents page SHALL read `from`, `to`, `tz`, `q`, `status`, `room`, `sort`, `order`, `limit` and
`cursor` from the URL, offer the shared list search box labelled "Identity or SID" (sent as `q`; an exact `PA_` SID
shows only that session ignoring the range and every filter, and Enter opens it keeping the range), a status select
(All, Joining, Active, Left, Aborted), the global range and the shared room chip, and drop `cursor` on any change.
#### Scenario: Active agents filter
- **WHEN** the user selects status Active
- **THEN** the URL gets `status=active` without `cursor` and the list request is `GET /api/v1/agents?status=active&…`
#### Scenario: Search by identity
- **WHEN** the user types `agent-` and the debounce elapses
- **THEN** the URL gets `q=agent-` without `cursor` and the list request is `GET /api/v1/agents?q=agent-&…`
#### Scenario: Exact SID
- **GIVEN** the status filter reads Left and the agent session `PA_z9` is active
- **WHEN** the user types `PA_z9`
- **THEN** the table shows only that session with the hint "Exact ID: range and filters ignored · Enter opens it", and Enter navigates to `/participants/PA_z9` keeping `from`, `to` and `tz`
#### Scenario: Room chip
- **GIVEN** `/agents?room=RM_abc`
- **WHEN** the page loads
- **THEN** a chip reads "Room: demo-room" with a link to `/rooms/RM_abc` and a remove button, and removing it drops `room` and `cursor`
### Requirement: Agents table
Edition: CE. The table SHALL show, in order, SID (participant detail link with copy button), Identity (copy button),
Name, Room (link to the room detail), Status (stale cue), Joined, Left, Duration (live), Published (source icons) and
- **THEN** the cells read, in order, `PA_z9` once, linking to `/participants/PA_z9` with its copy button, `agent-1` with its copy button, "Support bot", `demo-room` linking to `/rooms/RM_abc`, "active", the join time, "—", a duration counting up live and the microphone source icon, and the Node column is hidden until chosen in the Columns menu
- **WHEN** the user follows the SID link `PA_z9` or the room link `demo-room` of an agent row
- **THEN** `/participants/PA_z9` or `/rooms/RM_abc` renders the full-page lock of its detail, as from the Participants and Rooms lists
#### Scenario: No agents
- **GIVEN** `?status=active&q=zzz`
- **WHEN** the list is empty
- **THEN** the empty state reads "No agents match" with the hint "Change the filters or widen the time range.", "Clear filters" (drops `q`, `status` and `room`) and "Widen range" (opens the range picker)
#### Scenario: Dates as on the other lists
- **GIVEN** today is 2026-10-18 in the time zone `Europe/Madrid`, an agent that joined today at 12:31:05 and one that joined yesterday at 09:00:00
- **WHEN** the rows render with the default 24 h range
- **THEN** the Joined cells read "12:31:05" and "2026-10-17 09:00:00", and the Joined and Left headers carry the time-zone hint `Europe/Madrid`
#### Scenario: Stale agent
- **GIVEN** an active agent session that the list response marks `stale: true`
- **WHEN** the row renders
- **THEN** its status badge carries the stale cue, judged by the API and never by the browser clock
sessions joined in the global range, read from the participants summary (`sessions.byKind.AGENT`, "≈" and the rollup
tooltip when approximate) and refreshed with the page.
#### Scenario: Sessions in range
- **GIVEN** the participants summary answers `sessions.byKind.AGENT: 12` with `approximate: false`
- **WHEN** the page renders
- **THEN** the tile reads 12
#### Scenario: Approximate sessions
- **GIVEN** a 30-day range and `approximate: true`
- **WHEN** the tile renders
- **THEN** it shows "≈" before the value and the rollup tooltip on that mark
### Requirement: Agents ranked sort limit
Edition: CE. The Left and Duration sorts of the Agents table SHALL be offered only while the resolved range is at most
`limits.participantRankedSortMaxRangeMs` of `GET /api/v1/info` (24 h by default), or with any range while `room` is
set; otherwise their headers SHALL be disabled with the tooltip "Narrow the range to <n> to sort by <column>", and a URL
asking for one of them SHALL fall back silently to Joined descending. Joined is sortable with any range.
#### Scenario: Sort by duration
- **GIVEN** the default 24 h range
- **WHEN** the user activates the Duration header
- **THEN** the URL gets `sort=duration&order=desc` without `cursor` and the request is `GET /api/v1/agents?sort=duration&order=desc&…`
#### Scenario: Range beyond the limit
- **GIVEN** a 7-day range and no `room`
- **WHEN** the table renders
- **THEN** the Left and Duration headers are disabled, hovering Duration shows "Narrow the range to <n> to sort by Duration" with <n> naming the 24-hour limit, and Joined stays sortable
#### Scenario: Room filter lifts the limit
- **GIVEN** a 7-day range and `room=RM_abc`
- **WHEN** the user activates the Left header
- **THEN** the URL gets `sort=leftAt&order=desc` without `cursor` and the request carries `room=RM_abc`