Skip to main content

Incremental Session Manager

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