ServicesPresence

Presence

Low-latency "who is active here right now" tracking. Subjects - users, agents, services, devices - heartbeat into named contexts (a room, a document, a world shard), and liveness is a TTL: stop heartbeating and the session evaporates on its own. On top sit agent cognitive states, server- computed activity states, capacity policies with overflow behavior, geo queries, and durable last-seen summaries.

When to reach for it: presence avatars in a document or room, live occupancy counts for capacity-limited spaces, agent cognitive-state dashboards, "last seen 5 minutes ago" labels, nearby-session queries.

When not to: uptime monitoring of services and devices - "is this thing healthy" rather than "who is in this place" - belongs to Liveness; ephemeral broadcast messaging between clients belongs to Pub/Sub.

Concepts

  • Context - a named place where presence is tracked, with a kind like room, document, or world. Contexts are created by admins and carry the policy: heartbeat thresholds, capacity, metadata schema, geo. See Context policies.
  • Session - one subject's presence in one context, created by its first heartbeat. A subject can hold several sessions (one per tab or device) and be present in many contexts at once.
  • The subject is the caller. Sessions belong to the JWT subject - you cannot heartbeat on another subject's behalf. The kind (user, agent, service, device) is inferred from the principal and overridable per beat.
  • Cognitive state vs activity - state is what the subject reports (available, thinking, busy, offline); activity is what the server computes from heartbeat age (active, idle, stale); both are filterable.
  • TTL is the source of truth - miss heartbeats past the context's ttl_seconds and the session disappears. No disconnect handling needed.
  • Last-seen is durable - live sessions evaporate, but a per-context last-seen summary persists for "last seen at" queries.

Heartbeats

snug presence heartbeat --context docs4-lobby --session-id docs4-s1 \
  --state available --device-id dev-laptop

Send the same session_id to refresh; omit it and the server generates a fresh prs_<uuid> - which creates a new session per beat, so clients that refresh must send a stable id. --active false reports the session as idle without dropping it. Over HTTP the same call is:

curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"session_id": "docs4-s1", "state": "thinking"}' \
  http://localhost:4000/api/v1/presence/docs4-lobby/heartbeat
{
  "status": 200,
  "msg": "OK",
  "data": {
    "session_id": "docs4-s1",
    "context_id": "docs4-lobby",
    "state": "thinking",
    "activity": "active",
    "admission": "admitted",
    "observer": false,
    "expires_at": "2026-08-28T05:02:03.937773Z",
    "occupancy": {
      "active": 1, "idle": 0, "stale": 0, "total": 1,
      "observers": 0, "available_agents": 0,
      "capacity_remaining": null, "occupancy_state": "open"
    }
  }
}

Every heartbeat returns the context's occupancy summary for free. Heartbeating a nonexistent context is 404 context_not_found - contexts are never auto-created. The CLI heartbeat command has no metadata or geo flags; send those over HTTP or in a multiplex file.

The activity lifecycle

Verified against a context with ttl_seconds: 4, idle_after_seconds: 1, stale_after_seconds: 2 - one heartbeat, then watched:

Seconds since beatactivity
0active
1.3idle
2.5stale
4.5gone - session expired

Transitions are applied lazily when sessions are listed and by a background cleanup worker (every 30 seconds by default), so a list always reflects current heartbeat age.

Who is here

snug presence list docs4-lobby
snug presence list docs4-lobby --state available --kind agent
snug presence list docs4-lobby --activity idle
snug presence list docs4-lobby --aggregate-by-subject   # collapse multi-tab
snug presence count --context docs4-lobby

list returns full sessions (subject, device, state, activity, metadata, expiry). With two device sessions for one subject, plain list returned count: 2 and --aggregate-by-subject returned count: 1. count returns the same occupancy summary heartbeats carry, plus per-cognitive-state counts:

"by_state": { "available": 1, "busy": 1, "offline": 0, "thinking": 0 }

Cognitive state

state moves a session between available, thinking, busy, and offline without resending the full heartbeat payload:

snug --output json presence state --context docs4-lobby \
  --state busy --session-id docs4-s1
{ "session_id": "docs4-s1", "previous_state": "available", "state": "busy", "context_id": "docs4-lobby" }

Leaving, and who was here

snug presence leave --context docs4-lobby --session-id docs4-s1
snug presence leave --context docs4-lobby     # all of my sessions there
snug presence last-seen --subject docs-wave

last-seen survives session expiry and leave - it returns the overall last_seen_at plus one summary per context (context_id, context_kind, last_state, last_seen_at). It is self-or-admin: querying another subject with a regular token was 403 insufficient_permissions ("cannot act on another subject"), verified with a second user's token.

Multiplexed heartbeats

One request keeps many contexts alive (up to 25 by default). Items are processed independently and failures are reported per context, verified with one valid and one missing context:

snug presence multiplex --file multiplex.json
{ "items": [
  { "context_id": "docs4-lobby",   "heartbeat": { "session_id": "docs4-s1", "state": "available" } },
  { "context_id": "docs4-missing", "heartbeat": {} }
] }

The response carried succeeded: 1, failed: 1, with the full heartbeat result for the first item and error: "context_not_found" for the second.

Limits and configuration

Defaults: 45-second heartbeat TTL, idle after 15 seconds, stale after 30 (all overridable per context), 4 KB of metadata per session, 25 multiplex contexts, and list pages of 100 (max 500) - all tunable via the PRESENCE_* variables in the Presence CONFIG reference.

Reference

  • Context policies - capacity and overflow, metadata schemas, geo and nearby queries, threshold tuning, the admin sweep
  • Presence API - every endpoint, callable
  • Related: Liveness for entity uptime monitoring, Pub/Sub for ephemeral messaging, Geo for persistent location tracking and geofences

On this page