Skip to main content

Watch Roster

The Watch Roster is the live presence board the UI shows before a lead dispatches work. It tracks whether every agent is reachable, busy, blocked on an approval, or offline. Transitions emit agent.status_change into the Crew Journal so the full history replays alongside every other event.

States

The strings align with the agent_status.status CHECK constraint in migration 52. Changing them requires a migration.

Upsert semantics

  • Idempotent on same-status. A second Upsert(busy -> busy) refreshes since but does NOT emit a journal entry — the journal is a transition log, not a heartbeat.
  • Emits on transition. online -> busy -> blocked -> online -> offline produces four agent.status_change entries, each with summary "agent <id>: <prev> -> <new>".
  • MissionID is not persisted on the roster row (a single agent can legitimately be between missions). It IS threaded into the journal entry so the per-mission timeline doesn’t drop transitions.

Sweeper

presence.SweepOffline(ctx, db, j, 5*time.Minute) flips rows whose since is older than the threshold (default 5 min) to offline and emits the transition. The server wires this on a 60s ticker so idle agents don’t linger as “online” forever. Payload on the timeout emit: {"reason": "idle_timeout"}.

Wiring

The orchestrator tracks presence via the presenceAdapter in internal/server/orchestrator_adapters.go:
This replaced a prior path that emitted journal entries directly but never wrote the agent_status row — so /crows-nest and /api/v1/presence/roster always returned empty. The adapter now calls presence.Upsert which atomically writes the row and emits the matching journal entry. Presence updates are best-effort: a DB blip logs a warning but does not abort an agent run.

Read endpoints

  • GET /api/v1/presence/roster[?crew_id=...] — workspace-scoped list of roster rows. Optional crew filter narrows further.
Response:
Cross-tenant crew IDs return 404 (same shape as “no such crew”) so existence isn’t leaked. See Presence API for schemas.

CLI

--crew accepts either a crew slug or a CUID — slugs are resolved against /api/v1/crews before each render. --watch re-renders the table on an interval (short-polling — the server has no presence SSE stream yet); --interval sets the poll cadence (default 5s, floored at 1s). See crewship presence.

Journal entries

  • agent.status_change — one per real transition. Summary: "agent <id>: <prev> -> <new>" (or "agent <id>: <new>" on first-ever transition).
Payload includes status, prev, and any details dict (e.g. current_task_id, blocked_reason).

Gotchas

  • since is wall-clock. If the server clock jumps, sweeper decisions can look weird. This is not load-bearing enough to warrant monotonic tracking.
  • Details are typed loosely. details map[string]any is serialised as JSON; callers are responsible for schema. Common fields: current_task_id, blocked_reason, reason.
  • Offline is eventual. An agent that crashes hard is not flagged offline until the sweeper next ticks (up to 60s) AND 5 min have passed since the last heartbeat. Don’t rely on offline for liveness probes — query the container state directly.