Sessions & conversation state
How sessions are scoped, persisted, serialized, and shared across the gateway, channels, and cron jobs.
A session is a single conversation thread: an ordered list of messages plus a small amount of state (who is talking, whether a turn is in flight, when it was last touched). Revka keeps sessions so the agent can resume context after a restart, so the dashboard can list and reopen past chats, and so a scheduled job can drop its output into the same thread you are already reading.
This page explains how sessions are scoped and named, how they are persisted, how concurrent turns are serialized, and the REST endpoints, tools, and config keys you use to work with them. If you are building a chat UI on top of the gateway, also read Realtime: WebSocket, SSE & Live Canvas and Cron, sessions & attachments API.
Where sessions come from
Section titled “Where sessions come from”Every conversation surface produces a session, but they live in two separate stores:
- Gateway (dashboard) sessions — created by the
/ws/chatWebSocket. These are the chats you see in the dashboard. They are stored under a key with thegw_prefix and persisted to SQLite when[gateway] session_persistenceis enabled. - Channel sessions — created when a message arrives from Telegram, Discord, Slack, Matrix, and the other messaging channels. Their keys are prefixed with the channel name (for example
discord_…,telegram_…). They are persisted under{workspace}/sessions/when[channels] session_persistenceis enabled.
The GET /api/sessions endpoint surfaces both: gateway sessions always, and channel sessions when channel persistence is on.
session_id format
Section titled “session_id format”A session is addressed by a session ID, but the persistence layer stores it under a session key that adds a surface prefix. Understanding the difference matters when you read raw storage or use the inter-agent tools.
| Surface | Session ID you pass | Storage key | Notes |
|---|---|---|---|
| Gateway / dashboard | a UUID (auto-created) or a chosen name like operator-main | gw_<id> | The REST API hides the gw_ prefix; you pass the bare ID in the path. |
| Channel (no thread) | derived from sender | <channel>_<sender> | e.g. telegram_alice |
| Channel (threaded) | derived from thread + sender | <channel>_<thread>_<sender> | Slack/Discord threads include the thread ID. |
Key facts:
- On
/ws/chat, omitsession_idto have the gateway mint a new UUID, or pass an existing one (or a stable name likeoperator-main) to resume. - The generic
/webhookendpoint scopes its run to a session via the optionalX-Session-Idheader. - The inter-agent session tools (below) take the full session key as
session_id— for channel sessions the convention shown in those tools ischannel__identifier(for exampletelegram__alice).
Session persistence config
Section titled “Session persistence config”Persistence is controlled independently for the two stores. Both default to enabled.
Gateway sessions — [gateway]
Section titled “Gateway sessions — [gateway]”[gateway]# Persist gateway WebSocket chat sessions to SQLite. Default: true.session_persistence = true
# Auto-archive stale gateway sessions older than N hours. 0 = disabled. Default: 0.session_ttl_hours = 0| Key | Type | Default | Meaning |
|---|---|---|---|
session_persistence | bool | true | When false, the gateway keeps no session backend. Session list/messages/state endpoints return empty results with session_persistence: false, and chats do not survive a restart. |
session_ttl_hours | u32 | 0 | If greater than 0, sessions older than this many hours are archived on daemon start. 0 disables TTL cleanup. |
Channel sessions — [channels]
Section titled “Channel sessions — [channels]”[channels]# Persist channel conversation history so sessions survive daemon restarts.# Files are stored in {workspace}/sessions/. Default: true.session_persistence = true
# Backend: "sqlite" (default) or "jsonl" (legacy). SQLite adds FTS5 search,# metadata tracking, and TTL cleanup.session_backend = "sqlite"
# Auto-archive stale channel sessions older than this many hours. 0 disables. Default: 0.session_ttl_hours = 0| Key | Type | Default | Meaning |
|---|---|---|---|
session_persistence | bool | true | Persist channel conversation history across restarts. |
session_backend | string | "sqlite" | "sqlite" (FTS5 search, metadata, TTL) or "jsonl" (legacy flat files). |
session_ttl_hours | u32 | 0 | Auto-archive stale channel sessions older than N hours. 0 disables. |
You can toggle gateway session persistence from the dashboard Config page; see Cron, cost & config pages.
Chat sessions — list, messages, state, delete, rename
Section titled “Chat sessions — list, messages, state, delete, rename”All session endpoints require a pairing bearer token. See Pairing & authentication for how to obtain one.
Authorization: Bearer <token>List sessions
Section titled “List sessions”GET /api/sessionsReturns all sessions (gateway plus channel) sorted by most recent activity, together with the IDs of archived sessions.
{ "sessions": [ { "id": "8d43b6ef-0f18-4c3f-b04c-3a03f79e2c72", "channel": "dashboard", "started_at": "2026-06-18T09:00:00+00:00", "last_activity": "2026-06-18T09:14:22+00:00", "status": "active", "message_count": 12, "name": "Release planning" }, { "id": "telegram_alice", "channel": "telegram", "started_at": "2026-06-17T20:10:00+00:00", "last_activity": "2026-06-17T20:31:05+00:00", "status": "idle", "message_count": 4 } ], "archived_session_ids": ["3f0c…"]}The status field here is a coarse recency flag: active when the last activity was under 5 minutes ago, otherwise idle.
List running sessions
Section titled “List running sessions”GET /api/sessions/runningReturns only sessions whose live state is running (a turn is currently in flight). Each entry includes session_id, created_at, last_activity, and message_count.
Load a transcript
Section titled “Load a transcript”GET /api/sessions/{id}/messages{ "session_id": "8d43b6ef-…", "messages": [ { "role": "user", "content": "Plan the next release" }, { "role": "assistant", "content": "Here is a draft plan…" } ], "session_persistence": true}Get session state
Section titled “Get session state”GET /api/sessions/{id}/state{ "session_id": "8d43b6ef-…", "state": "running", "turn_id": "f1c2…", "turn_started_at": "2026-06-18T09:14:00+00:00"}state is one of idle, running, or error. turn_id and turn_started_at appear only while a turn is active. A missing session returns 404.
Rename a session
Section titled “Rename a session”PUT /api/sessions/{id}Content-Type: application/json
{ "name": "Release planning" }The name must be non-empty. The session must already exist (404 otherwise). Response: {"session_id": "…", "name": "Release planning"}.
Delete (archive) a session
Section titled “Delete (archive) a session”DELETE /api/sessions/{id}Deleting archives (soft-deletes) the session rather than erasing it, so history stays recoverable and the ID appears under archived_session_ids in the list response.
{ "deleted": true, "archived": true, "archived_at": "2026-06-18T10:00:00+00:00", "session_id": "8d43b6ef-…" }Upload an attachment
Section titled “Upload an attachment”Files are attached to a specific session and referenced by ID in subsequent WebSocket messages:
POST /api/sessions/{session_id}/attachmentsContent-Type: multipart/form-dataThe response returns a file_id; include it in the attachments array of your next /ws/chat message. Images become [IMAGE:…] markers for vision models; other files become [DOCUMENT:…]. Max 25 MiB per file. See Cron, sessions & attachments API for the full attachment flow.
Per-Session Actor Queue
Section titled “Per-Session Actor Queue”A session must never run two turns at once — overlapping writes would corrupt the SQLite transcript and scramble state transitions. The gateway enforces this with a per-session actor queue: each session allows at most one in-flight turn, and additional requests queue in FIFO order behind it.
How it behaves on /ws/chat:
- One turn per session. A second message for a busy session waits for the current turn to finish.
- Bounded queue. When the queue depth limit is reached, new requests are rejected with the
SESSION_BUSYWebSocket error code. The error readsSession <id> queue full (<N> pending requests). - Lock timeout. A queued request that waits longer than the lock timeout fails with
Previous message is still being processed — please wait, or retry once it completes (timeout: <N>s). The session ID is intentionally omitted from this message. - Idle eviction. Session slots that go untouched past their idle TTL are dropped from memory (the persisted transcript is unaffected).
The lock timeout defaults to 300 seconds and is set with an environment variable:
| Env var | Default | Meaning |
|---|---|---|
REVKA_GATEWAY_SESSION_LOCK_TIMEOUT_SECS | 300 | Seconds a queued turn waits for the session lock before timing out. Invalid or zero values log a warning and fall back to the default. |
Inter-agent session tools
Section titled “Inter-agent session tools”Three agent tools let one agent read from and write to another conversation’s session — the basis for inter-agent communication. They operate on the session backend directly using the full session key.
sessions_list
Section titled “sessions_list”Lists active sessions with channel, message count, and last-activity time.
limit(integer, optional, default50) — max sessions to return.
Returns a plain-text listing, one line per session: - <key>: channel=<channel>, messages=<N>, last_activity=<ts>.
sessions_history
Section titled “sessions_history”Reads the recent message history of a specific session.
session_id(string, required) — the session key, e.g.telegram__user123.limit(integer, optional, default20) — most recent messages to return.
Requires Read tool permission under the security policy. An empty/unknown session returns a “No messages found” notice rather than an error.
sessions_send
Section titled “sessions_send”Appends a message to another session’s history as a user message, so the target agent picks it up on its next turn.
session_id(string, required) — the target session key.message(string, required) — the content to send; must not be blank.
Requires Act tool permission. See Tools overview and the Security model for how Read vs Act gating works.
Routing cron output into a session
Section titled “Routing cron output into a session”Agent cron jobs choose where their turn runs with session_target:
| Value | Behavior |
|---|---|
isolated (default) | Each run starts a fresh, blank context. Recommended for most automated jobs. |
main | The job runs in the same session as the user’s primary interactive chat, so it can see recent conversation context. |
{ "schedule": { "kind": "cron", "expr": "0 9 * * 1-5", "tz": "America/New_York" }, "job_type": "agent", "prompt": "Summarize overnight alerts", "session_target": "isolated", "delivery": { "mode": "announce", "channel": "discord", "to": "1234567890" }}session_target is available wherever you create agent jobs — the cron_add tool, the [[cron.jobs]] config block, and the POST /api/cron body.
For the full scheduling surface, see revka cron and the cron guides under Agent jobs & delivery.
How a turn flows through state
Section titled “How a turn flows through state”-
A client connects to
/ws/chatwith an existingsession_id, or omits it to mint a new UUID. The gateway sendssession_startwithresumedandmessage_count. -
The client sends a
message. The actor queue acquires the session lock; the session state moves torunningwith aturn_id. -
The agent streams
chunkframes, plustool_call/tool_resultevents, then a finaldoneframe. State returns toidle. -
If the lock cannot be acquired, the client receives a
SESSION_BUSYerror. On failure mid-turn, state becomeserror. -
With persistence on, the transcript is written to SQLite so
GET /api/sessions/{id}/messagesreturns it after a restart.