Session management
This page explains what a session is in Omnia, where a conversation lives while it runs, and what happens to it when a client reconnects, goes quiet or lands on a different replica.
What is a session?
Section titled “What is a session?”A session is one conversation between a client and an agent. Two stores hold it, and they answer different questions:
- The context store holds the conversation the model sees: the turns that are concatenated into each provider call. The runtime owns it, and
spec.contexton the AgentRuntime configures it. It decides whether a conversation can continue. - The session archive is the row and the records that session-api keeps for the same session: messages, tool calls, provider calls, tags, token counts and cost. The facade writes it off the conversation as it happens. It decides what you can read back and report on afterwards.
The split lets the archive be optional. An agent in a workspace with no session-api still holds a full multi-turn conversation, because the conversation never depended on the archive. The reverse also holds: an archive row only proves a conversation once existed, not that its turns are still in the context store.
The fields of an archive row are listed in the Session API reference.
How it works
Section titled “How it works”The WebSocket facade mints a session ID when the socket opens and announces it in the connected message. The session-api row is created on the first message.
graph LR C((Client)) -->|WebSocket /ws| SVC[Agent Service] SVC --> F subgraph Pod[Agent pod] F[Facade<br/>cmd/agent] -->|gRPC Converse, HasConversation| R[Runtime<br/>cmd/runtime] end R --> CS[(Context store<br/>memory or Redis)] F -->|HTTP /api/v1/sessions| SA[session-api] SA --> PG[(PostgreSQL)]The runtime talks to the context store. The facade never reads the conversation back from session-api, and it never asks session-api whether a conversation can continue.
Lifecycle
Section titled “Lifecycle”A session starts when a client connects. The first message creates the archive row, and each message and tool call is recorded against it. When the connection closes and the turns still running on it have finished, the facade marks the session completed. An error frame sent to the client records the error as a message and marks the session error. Session-api never overwrites a terminal status.
A turn outlives its socket. If the client disconnects mid-turn, the turn finishes and its reply is recorded before the session is marked completed.
Resumption
Section titled “Resumption”A client resumes a conversation by sending a message that names a session_id it did not receive on this connection. The facade checks that the caller owns the session, then asks the runtime whether the context store still holds it.
flowchart TD M[Message names a session_id] --> Q{Minted by this connection?} Q -->|yes| O[Open the session without probing] Q -->|no| W[Check the caller owns the session] W --> H[Runtime HasConversation] H -->|resumable| OK[Conversation continues with its history] H -->|context gone| E[SESSION_EXPIRED, message dropped] H -->|store unreachable| I[INTERNAL_ERROR]A resume for an ID that the context store has dropped does not start a fresh conversation. The facade answers SESSION_EXPIRED and drops the message, because answering it without history would let a session appear to resume into an empty model context. The connection stays open, and the client starts a new conversation by sending without a session_id. A store that cannot be reached is reported as an internal error rather than an expiry, because the context may be intact.
A session ID is not a bearer secret. Only the user the archive attributes a session to can resume it, and that user is established by a credential, never by a caller-supplied parameter such as device_id. Any other caller gets SESSION_EXPIRED.
Realtime audio sessions add a second mechanism. When a duplex audio socket drops unexpectedly, the facade parks the provider connection for a grace period, and a client that reconnects with ?resume=<session_id> reattaches to it. This is separate from the conversation resume above. It is resolved when the socket opens, before any message arrives.
Context stores and TTL
Section titled “Context stores and TTL”spec.context.type selects the backend. The default is memory, and redis is the other option.
The in-memory store needs no infrastructure and is fast. Its contents belong to one runtime process, so a pod restart loses every conversation, and a second replica cannot see a conversation held by the first. A client that resumes onto a different replica gets SESSION_EXPIRED, as if the conversation had timed out.
The Redis store keeps conversations across restarts and across replicas, so a resume lands correctly whichever pod receives it. The cost is a Redis deployment to run and a Secret holding its URL, which spec.context.storeRef points to. The context store how-to covers the setup.
spec.context.ttl defaults to 24 hours. It is a sliding window measured from last use: a read or a write of the conversation extends it. When it passes with no activity, the store drops the conversation and the next resume answers SESSION_EXPIRED.
The TTL governs the context store only. It does not delete the archive: the session-api row, its messages and its cost records remain until a SessionRetentionPolicy removes them. See Session retention for how that side works.
Choosing a TTL
Section titled “Choosing a TTL”The TTL should outlast the longest pause you expect between a user’s messages, since a conversation idle for longer than that is gone when they return. A longer TTL holds more in the store. Each turn adds to the conversation, and tool calls carry their full arguments and results, so a long-lived session grows. The agent’s truncation strategy bounds what reaches the provider, but it does not shrink what the store holds.
Scaling
Section titled “Scaling”With one replica, the in-memory store works. Conversations are lost on a pod restart and the agent cannot scale out.
With several replicas, use Redis. Every replica then shares one conversation store, and any of them can serve any session.
Session affinity is the alternative when Redis is unavailable. Setting sessionAffinity: ClientIP on the agent’s Service routes one client address to one pod, so the in-memory store sees the same clients repeatedly. It does not survive a pod restart, it loads pods unevenly when many clients share an address, and a client that changes address loses its conversation. Scale agents shows the Service setting. Treat affinity as a stopgap, and use Redis for production.
User opt-out
Section titled “User opt-out”User opt-out is an Enterprise feature, enforced when a SessionPrivacyPolicy applies to the session. When a user opts out, session-api still records the session but replaces its content with [OPTED_OUT]. A user who opts out asks that what they said is not kept. The operator still needs the conversation’s cost and outcome, so tokens, cost, model, latency, tool names, statuses and timings stay in the record. The SessionPrivacyPolicy reference lists exactly which fields are replaced.
Function invocations
Section titled “Function invocations”A function-mode agent serves HTTP instead of a WebSocket. Each POST /functions/{name} creates one archive row tagged function, and the messages, tool calls, provider calls, eval results and runtime events of that invocation are recorded against it. The row closes as completed or error when the response is written. A SessionRetentionPolicy treats function sessions like any other row. See Define functions.