Sessions are terminal instances backed by Docker containers (cloud mode) or direct PTY (local mode). Each session belongs to a project and runs in an isolated environment with resource limits.
POST /projects/{id}/sessions
│
▼
┌──────────┐ container ready ┌─────────┐
│ Starting │ ────────────────── ▶ │ Running │
└──────────┘ └─────────┘
│ │
│ spawn failure │ POST /stop
▼ ▼
┌──────────┐ ┌─────────┐
│ Failed │ │ Stopped │
└──────────┘ └─────────┘
▲ │
│ resume/retry failure │ POST /resume
│ ▼
└──────────────────────── ┌─────────┐
│ Running │
└─────────┘
States:
- Starting — container is being created, image pulled if needed
- Running — container running, exec attached, PTY available for I/O
- Stopped — container stopped gracefully (user-initiated or idle timeout)
- Failed — container failed to start or crashed;
error_reasoncontains details
See REST API for full endpoint documentation. Summary:
| Endpoint | Method | Description |
|---|---|---|
/api/v1/projects/{project_id}/sessions |
POST | Create session in project |
/api/v1/projects/{project_id}/sessions |
GET | List sessions for project |
/api/v1/sessions |
GET | List all sessions (global, paginated) |
/api/v1/sessions/{id} |
GET | Get session by ID |
/api/v1/sessions/{id}/stop |
POST | Stop session |
/api/v1/sessions/{id}/resume |
POST | Resume stopped session |
/api/v1/sessions/{id}/retry |
POST | Retry failed session |
/api/v1/sessions/{id} |
DELETE | Delete session record |
/api/v1/images |
GET | List available Docker images |
/api/v1/profiles |
GET | List session profiles |
Managed by DockerOrchestrator (runner/src/docker.rs):
- Image pull — pulls the image if not locally available
- Container create — creates with resource limits, seccomp profile, and labels
- Container start — starts with idle entrypoint (container stays alive)
- Exec attach —
docker execwith TTY for interactive shell - Resize — resizes the exec's TTY window
- Stop/Remove — graceful stop, then removal
All Relay-managed Docker resources carry labels:
relay.managed=true— identifies Relay-managed resourcesrelay.session_id=<uuid>— links container to session
Configured in [docker] section of config.toml:
| Setting | Default | Description |
|---|---|---|
memory_limit |
2g |
Memory limit per container |
memory_swap |
2g |
Memory+swap (equal = no swap) |
cpu_limit |
2.0 |
CPU cores per container |
exec_timeout_secs |
30 |
Timeout for exec operations |
Each session gets a dedicated Docker network, preventing cross-session network access.
Configure docker.allowed_images to restrict which images can be used. Empty list allows all images.
Profiles are predefined terminal initialization templates:
[[profiles]]
name = "claude-default"
init_command = "claude"
env_vars = { "NODE_ENV" = "production" }Available via GET /api/v1/profiles. When creating a session, specify a profile name to use its initialization command and environment configuration. The Docker image is specified separately at session creation time (see domain model §5 for image resolution chain).
CloudSessionManager enforces per-project session limits via tokio Semaphore:
- Each project gets a semaphore with
max_sessionspermits - Session creation acquires a permit; stop/fail releases it
- Exceeding the limit returns HTTP 429 / gRPC
RESOURCE_EXHAUSTED
Source: runner/src/cloud_session_manager.rs
On server start, before accepting connections, a reconciliation pass syncs SQLite state with Docker:
- Dead sessions —
Starting/Runningsessions whose container no longer exists or has exited are markedStopped - Orphan containers — containers labelled
relay.managed=truewithout a DB session are force-removed - Orphan networks — Relay-managed networks not attached to any managed container are removed
All three steps run concurrently via tokio::join!. Errors are logged but tolerated — partial reconciliation is better than refusing to start.
See ADR-009 for the design rationale.
Source: runner/src/reconciliation.rs
Sessions are stored in SQLite via the Store layer:
| Field | Type | Description |
|---|---|---|
id |
UUID | Primary key |
project_id |
UUID | FK → Project |
container_id |
String? | Docker container ID |
state |
Enum | Starting / Running / Stopped / Failed |
profile |
String | Profile name used |
created_at |
DateTime | Creation timestamp |
updated_at |
DateTime | Last state change |
stopped_at |
DateTime? | When stopped/failed |
error_reason |
String? | Error message when Failed |
Optimistic concurrency: state updates include a WHERE clause matching the last-known updated_at, preventing lost updates from concurrent writers.
State changes emit RelayEvent::SessionStateChanged events with session ID, new state, and optional error message. These propagate to SSE subscribers for real-time UI updates.