Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.agentium.in/llms.txt

Use this file to discover all available pages before exploring further.

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:
NamespaceKeysContents
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

appendMessage("chat-1", msg)


1. lock(sessionId)             // prevents interleaved appends per session


2. write sessions:msg/chat-1:0000000007  =  msg


3. appendsSinceSnapshot += 1


4. if appendsSinceSnapshot >= snapshotFrequency:
       │      readMessages = snapshot ∪ all loose entries
       │      apply maxMessages trim
       │      write sessions:snapshot/chat-1 = collapsed array
       │      delete every sessions:msg/chat-1:* entry
       │      appendsSinceSnapshot = 0


5. write sessions:meta/chat-1  (updated counters)
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

import { IncrementalSessionManager, SqliteStorage } from "@agentium/core";

const sessions = new IncrementalSessionManager(new SqliteStorage("sessions.db"), {
  snapshotFrequency: 25,  // roll up every 25 appends
  maxMessages: 1000,      // trim oldest beyond 1K on snapshot
});

await sessions.appendMessage("chat-1", { role: "user", content: "Hi" });
await sessions.appendMessage("chat-1", { role: "assistant", content: "Hello!" });

const history = await sessions.getHistory("chat-1");

Configuration

interface IncrementalSessionConfig {
  snapshotFrequency?: number;  // default 25 — rebuild snapshot every N appends
  maxMessages?: number;        // default unlimited — trim oldest when snapshot runs
}

Tuning snapshotFrequency

ValueTrade-off
5Very 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
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:
new IncrementalSessionManager(storage, { snapshotFrequency: 25, maxMessages: 200 });
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).
await sessions.appendMessages("chat-1", [
  { role: "user", content: "What's 2+2?" },
  { role: "assistant", content: "4" },
]);
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).
interface Session {
  sessionId: string;
  userId?: string;
  messages: ChatMessage[];      // hydrated
  state: Record<string, unknown>;
  createdAt: Date;
  updatedAt: Date;
}

getHistory(sessionId, limit?): Promise<ChatMessage[]>

Fast path that returns only messages (no metadata round-trip).
const last20 = await sessions.getHistory("chat-1", 20);
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.
await sessions.updateState("chat-1", { mood: "frustrated", lastIntent: "complaint" });

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.
process.on("SIGTERM", async () => {
  for (const sessionId of activeSessions) {
    await sessions.snapshotNow(sessionId);
  }
  process.exit(0);
});

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:
import { IncrementalSessionManager, ScopedStorage, SqliteStorage } from "@agentium/core";

const raw = new SqliteStorage("sessions.db");

function sessionsForTenant(tenantId: string) {
  const scoped = new ScopedStorage(raw, { tenantId });
  return new IncrementalSessionManager(scoped, { snapshotFrequency: 25 });
}
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

SymptomSwitch?
Sessions > 100 turns commonYes
Voice agents (many short messages)Yes
Multimodal messages (images inline)Yes
Most sessions < 20 turnsNo — overhead isn’t worth it
Need built-in summarizationUse the default, then layer on UnifiedMemoryConfig.summaries

See also