Skip to main content

Why it exists

The default SessionManager 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 underlying StorageDriver: paddedSeq is a 10-digit zero-padded sequence so that list(ns, "<sessionId>:") returns entries in chronological order.

Lifecycle of an append

Read time is 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

Pick based on read/write ratio. For chatty voice agents, lower it. For batch summarizers that mostly read, raise it.

Tuning maxMessages

The default is unlimited (sessions grow forever). For long-lived sessions, set a cap:
When a snapshot runs and 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).
Each message gets its own sequence number. If snapshot frequency is hit mid-batch, the snapshot runs at the end.

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. Two appendMessage 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:
Each tenant’s sessions live under 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

See also