Skip to content

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.

Every conversation surface produces a session, but they live in two separate stores:

  • Gateway (dashboard) sessions — created by the /ws/chat WebSocket. These are the chats you see in the dashboard. They are stored under a key with the gw_ prefix and persisted to SQLite when [gateway] session_persistence is 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_persistence is enabled.

The GET /api/sessions endpoint surfaces both: gateway sessions always, and channel sessions when channel persistence is on.

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.

SurfaceSession ID you passStorage keyNotes
Gateway / dashboarda UUID (auto-created) or a chosen name like operator-maingw_<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, omit session_id to have the gateway mint a new UUID, or pass an existing one (or a stable name like operator-main) to resume.
  • The generic /webhook endpoint scopes its run to a session via the optional X-Session-Id header.
  • The inter-agent session tools (below) take the full session key as session_id — for channel sessions the convention shown in those tools is channel__identifier (for example telegram__alice).

Persistence is controlled independently for the two stores. Both default to enabled.

[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
KeyTypeDefaultMeaning
session_persistencebooltrueWhen 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_hoursu320If greater than 0, sessions older than this many hours are archived on daemon start. 0 disables TTL cleanup.
[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
KeyTypeDefaultMeaning
session_persistencebooltruePersist channel conversation history across restarts.
session_backendstring"sqlite""sqlite" (FTS5 search, metadata, TTL) or "jsonl" (legacy flat files).
session_ttl_hoursu320Auto-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>
GET /api/sessions

Returns 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.

GET /api/sessions/running

Returns 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.

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 /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.

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 /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-…" }

Files are attached to a specific session and referenced by ID in subsequent WebSocket messages:

POST /api/sessions/{session_id}/attachments
Content-Type: multipart/form-data

The 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.

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_BUSY WebSocket error code. The error reads Session <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 varDefaultMeaning
REVKA_GATEWAY_SESSION_LOCK_TIMEOUT_SECS300Seconds a queued turn waits for the session lock before timing out. Invalid or zero values log a warning and fall back to the default.

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.

Lists active sessions with channel, message count, and last-activity time.

  • limit (integer, optional, default 50) — max sessions to return.

Returns a plain-text listing, one line per session: - <key>: channel=<channel>, messages=<N>, last_activity=<ts>.

Reads the recent message history of a specific session.

  • session_id (string, required) — the session key, e.g. telegram__user123.
  • limit (integer, optional, default 20) — 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.

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.

Agent cron jobs choose where their turn runs with session_target:

ValueBehavior
isolated (default)Each run starts a fresh, blank context. Recommended for most automated jobs.
mainThe 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.

  1. A client connects to /ws/chat with an existing session_id, or omits it to mint a new UUID. The gateway sends session_start with resumed and message_count.

  2. The client sends a message. The actor queue acquires the session lock; the session state moves to running with a turn_id.

  3. The agent streams chunk frames, plus tool_call / tool_result events, then a final done frame. State returns to idle.

  4. If the lock cannot be acquired, the client receives a SESSION_BUSY error. On failure mid-turn, state becomes error.

  5. With persistence on, the transcript is written to SQLite so GET /api/sessions/{id}/messages returns it after a restart.