The one producer, the two stores
- Producer:
voicegateway.register_worker(...)in every agent process. It posts a periodic presence payload to${VOICEGW_COLLECTOR_URL}/v1/agents/heartbeatwith the tenant’svk_ingest key as the bearer token. - Store A (hosted):
voicegateway-cloud’scloud_workerstable andPOST /v1/agents/heartbeat/GET /v1/agents, surfaced ondash.voicegateway.dev. - Store B (self-hosted): the engine’s own
voicegw serveworkerstable and the same/v1/agents/heartbeat//v1/agentsroutes, surfaced in the OpenOrca console via/openorca/snapshotand/openorca/events.
/v1/agents/heartbeat contract, the same register_worker heartbeat feeds either one. Point VOICEGW_COLLECTOR_URL at the cloud for the SaaS dashboard, or at a self-hosted voicegw serve for the OpenOrca console. That symmetry only holds if both ingest identically.
Agent environment variables
Each agent process needs three environment variables to participate in the fleet roster:The heartbeat payload (canonical)
register_worker’s presence() sends exactly this JSON:
dispatch_name is the LiveKit agent_name this worker dispatches under (defaults to agent_name; None for a worker with no LiveKit dispatch, e.g. Pipecat). The dashboard’s play-button probe dispatches by this field; it is not part of the (tenant, agent_id) identity key.
Ingestion rules (both stores must follow)
- Tenant is derived server-side from the
vk_key, never from the body. Thetenant_idin the payload is advisory only. A worker can only ever be written under the key’s tenant, so it can never appear under another tenant. - Identity is
(tenant, agent_id).agent_idis the node identity for upsert, roster keys, and any UI node id. Do not key identity onagent_name(it groups workers, it does not identify one). last_seenis stamped server-side at ingest (now()/time.time()on the receiving server). The payloadtsis informational metadata only and must not drive liveness: a client clock that is skewed or forged would otherwise read perpetually online or offline.- Upsert atomically on
(tenant, agent_id); a naive get-then-insert races two concurrent first beats into duplicate rows. A nativeON CONFLICT DO UPDATEworks when tenant is never NULL (key-authenticated writes). When tenant can be NULL (the self-hosted, no-credential operator),NULL != NULLunder the unique constraint breaksON CONFLICT, so select first, update or insert, and retry onIntegrityErrorfrom a concurrent first insert. - Offline TTL is 45 seconds (three missed ~15s beats). A worker whose server-stamped
last_seenis older than the TTL reportsstatus: "offline"andactive_sessions: 0, regardless of the last status it sent. - Status vocabulary is
idle | busy | offline. Constrain to this set on ingest; do not store or serve arbitrary client-supplied status strings.
Compatibility matrix
Field or behavior as of the two current implementations.cloud_workers is the reference; the engine workers table (introduced with the OpenOrca console) must satisfy the same rules, though not always with the same mechanism.
Keeping them from drifting
- This page is the single source of truth. A PR that changes ingestion in either store must update this page in the same change and satisfy every rule above.
- Prefer sharing semantics rather than re-deriving them: the offline TTL, the status vocabulary, and the payload field names should have one definition the engine owns (it is the producer), which the cloud consumes.
- The engine-side alignment items (rules 1, 2, 3, 6) are tracked against the OpenOrca console backend PR; the cloud side already satisfies the contract. Rule 4 (atomic upsert) is already satisfied by both, just via different mechanisms (see the compatibility matrix).
Related pages
Security model
Tenant isolation via vk_ ingest keys and server-side stamping.
Hosted cloud quickstart
Setting VOICEGW_COLLECTOR_URL, VOICEGW_API_KEY, and VOICEGW_AGENT_ID.
Guide: attach
Per-call attach for sub-tenant cost attribution.
Environment variables
Full reference for all fleet-related env vars.