Incremental Session Manager
Why it exists
The defaultSessionManager writes the entire conversation array to storage on every appendMessage call. That works for short chats but breaks down on:
- Long sessions (200+ turns) where re-serializing 50KB on every turn becomes the dominant IO cost.
- Multimodal sessions where messages contain inline base64 images / audio.
- Voice agents that may produce hundreds of messages per minute.
IncrementalSessionManager writes one entry per turn and rolls up loose entries into a single snapshot every N appends.
Storage layout
Per session, three namespaces are used on the underlyingStorageDriver:
| Namespace | Keys | Contents |
|---|---|---|
sessions:meta | <sessionId> | Session metadata (userId, state, timestamps, nextSeq, appendsSinceSnapshot) |
sessions:snapshot | <sessionId> | Collapsed array of messages up to the last snapshot |
sessions:msg | <sessionId>:<paddedSeq> | One entry per loose (post-snapshot) message |
paddedSeq is a 10-digit zero-padded sequence so that list(ns, "<sessionId>:") returns entries in chronological order.
Lifecycle of an append
O(1 + N_loose) where N_loose ≤ snapshotFrequency.
Write amortizes to roughly (1 + 1/snapshotFrequency) storage operations per append vs the full-rewrite manager’s 1 write of a growing array.
Quick start
Configuration
Tuning snapshotFrequency
| Value | Trade-off |
|---|---|
5 | Very small loose-entry overhead (fast reads), more frequent snapshot rewrites |
25 (default) | Balanced — typical conversation patterns |
100+ | Few rebuilds, but reads do a larger list() of loose entries |
Tuning maxMessages
The default is unlimited (sessions grow forever). For long-lived sessions, set a cap:
total > maxMessages, the oldest messages are dropped to bring the count down to maxMessages. The trim happens during snapshot; between snapshots messages accumulate freely.
API
appendMessage(sessionId, message): Promise<void>
Single-message convenience. Equivalent to appendMessages(sessionId, [message]).
appendMessages(sessionId, messages): Promise<void>
Append multiple messages atomically (under one lock).
getOrCreate(sessionId, userId?): Promise<Session>
Returns the session. Creates an empty one if it doesn’t exist (writes meta only — no messages). The returned Session contains the full message history (snapshot + loose entries).
getHistory(sessionId, limit?): Promise<ChatMessage[]>
Fast path that returns only messages (no metadata round-trip).
limit returns the most recent N messages, equivalent to getHistory().slice(-N).
updateState(sessionId, patch): Promise<void>
Merge a partial state object into session.state. Useful for tracking conversation-level facts (mood, topic, last intent) outside the message history.
getState(sessionId): Promise<Record<string, unknown>>
Read the current state. Returns {} if the session doesn’t exist.
snapshotNow(sessionId): Promise<void>
Force a snapshot immediately, regardless of appendsSinceSnapshot. Useful right before a graceful drain or restart so the on-disk state is bounded.
deleteSession(sessionId): Promise<void>
Removes meta + snapshot + all loose message entries for the session.
Concurrency
Each method uses an internal per-session lock to prevent interleaved writes. TwoappendMessage calls on the same session ID serialize; calls on different sessions are independent.
The lock is in-process only. If you run multiple Agentium processes against the same storage, use a distributed lock layer (Redis, postgres advisory locks) above this — currently your responsibility.
Combine with ScopedStorage
For multi-tenant deployments, wrap the underlying driver:
tenant:<id>:sessions:meta, tenant:<id>:sessions:snapshot, tenant:<id>:sessions:msg. Disjoint key spaces, one underlying database file.
When to switch from the default SessionManager
| Symptom | Switch? |
|---|---|
| Sessions > 100 turns common | Yes |
| Voice agents (many short messages) | Yes |
| Multimodal messages (images inline) | Yes |
| Most sessions < 20 turns | No — overhead isn’t worth it |
| Need built-in summarization | Use the default, then layer on UnifiedMemoryConfig.summaries |
See also
- Sessions and Memory — overview of the session system
- Storage Drivers — pick the right backend (SQLite for single-process, Postgres for multi-process)
- Multi-Tenant — combine with ScopedStorage