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

> Append-only session writes with periodic snapshots — for long conversations

# 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

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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).

```typescript theme={null}
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).

```typescript theme={null}
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).

```typescript theme={null}
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.

```typescript theme={null}
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.

```typescript theme={null}
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:

```typescript theme={null}
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

* [Sessions and Memory](/agents/sessions) — overview of the session system
* [Storage Drivers](/storage/overview) — pick the right backend (SQLite for single-process, Postgres for multi-process)
* [Multi-Tenant](/features/multi-tenant) — combine with ScopedStorage
