Sessions & pods
a long-lived, multi-service pod you drive interactively — quota-counted like a runWhat a session is
A run executes your code once and ends. A session keeps a pod — one or more services started together — alive so a human can drive it live (teleop a sim, poke a bridge, watch a viewer).
A session is just a Mode=session job carrying the pod, so it is tenanted and quota-counted exactly like a run: it reserves against the policy ledger at submit and releases on stop. keepalive pings + an idle-TTL reaper bound its lifetime, so nothing runs forever by accident.
Live video (WebRTC) is deferred — see the last card. Interactive control today is the bidirectional /data teleop channel.
Launch a pod
POST /v1/sessions (needs runs:submit). A pod is a list of services; each declares an image or a stack, optional needs (start order), and an optional health gate. Services start in topological order — a service waits until every service it needs is healthy.
curl -X POST $BASE/v1/sessions \
-H "Authorization: Bearer slt_…" -H "content-type: application/json" \
-d '{
"name": "teleop-lab",
"project": "wing-study",
"idle_ttl_sec": 600,
"pod": {
"services": [
{ "name": "sim", "stack": "mujoco",
"cpu_cores": 4, "ram_gb": 8,
"health": { "command": ["curl","-sf","localhost:8080/healthz"],
"period_sec": 5, "timeout_sec": 2, "retries": 3 } },
{ "name": "bridge", "image": "ghcr.io/acme/teleop-bridge:1.2",
"needs": ["sim"] }
]
}
}'Returns 202 with the session view — note phase: “launching” and the teleop_stream you drive it through:
{
"id": "ses_9f21…", "job_id": "job_…", "org_id": "org_…",
"name": "teleop-lab", "project": "wing-study",
"phase": "launching", "idle_ttl_sec": 600,
"created_at": "2026-07-15T10:00:00Z",
"services": [ { "name": "sim", "stack": "mujoco", "image": "…", "needs": [] },
{ "name": "bridge", "image": "…", "needs": ["sim"] } ],
"teleop_stream": "data"
}The pod DAG is validated up front — a cycle, an unknown needs, or a service with neither image nor stack is a 400/422 before anything is scheduled. Per-service cpu_cores/ram_gb/gpu_count are summed into the pod’s quota footprint, so an over-cap pod is a 429 at submit (see Policy & quotas).
Lifecycle: keepalive & reap
Poll GET /v1/sessions/{id} until phase is ready (all services healthy). Phases:
launchingreadyidlestopping · stopped · failedA live client must keepalive to hold the pod open. With no ping within idle_ttl_sec (default 300s, or the deploy’s POLYGON_SESSION_IDLE_TTL_SEC), the reaper stops the session and frees its reservation — no leak, no forgotten pod.
# hold the pod open (call well within idle_ttl_sec) — 409 if it is no longer live
curl -X POST $BASE/v1/sessions/ses_9f21…/keepalive -H "Authorization: Bearer slt_…"
# list your org's sessions, newest first
curl $BASE/v1/sessions -H "Authorization: Bearer slt_…"
# stop it yourself (needs runs:cancel) — tears the pod down, releases quota
curl -X DELETE $BASE/v1/sessions/ses_9f21… -H "Authorization: Bearer slt_…"Drive it: bidirectional teleop
Interactive control rides the run’s realtime /data stream (the session’s teleop_stream). It is two-way: read telemetry over SSE, write control back over the teleop endpoint. Both halves are gated by a short-lived HMAC ticket minted from subscribe after a tenancy check — a ticketless or cross-org send is 401/404, never the bearer directly.
# 1) mint a stream ticket for the session's pod job (60s, single stream)
TICKET=$(curl -sX POST "$BASE/v1/jobs/$JOB/streams/data/subscribe" \
-H "Authorization: Bearer slt_…" | jq -r .ticket)
# 2) READ telemetry (SSE, ticket in the query — EventSource can't set headers)
curl -N "$BASE/v1/jobs/$JOB/streams/data/data?ticket=$TICKET"
# 3) WRITE a control frame back to the pod (downstream teleop)
curl -X POST "$BASE/v1/jobs/$JOB/streams/data/teleop?ticket=$TICKET" \
--data '{"cmd":"set_target","x":1.0,"y":0.5}'
# → 202 { "delivered": true, "stream": "data", "seq": 1721037600000000000 }The node running the pod writes each frame to /streams/data.teleop for a service to consume. A send to a pod that isn’t running yet is a 409.
POST /v1/sessions/{id}/signal is a documented stub that returns 501 — the SFU-lite media relay + coturn are infra-gated and not wired in this environment. The signaling seam (session.signal over the node channel) is in place for when the media plane lands; teleop over /data is the interactive path today.503 sessions disabled. All routes are org-scoped: a cross-tenant or unknown id is 404.