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