---
title: "Sessions & Memory"
description: "Understand session isolation, streaming completion, memory persistence, channels, and the fetch-native runtime."
---

> Documentation Index
> Fetch the complete documentation index at: https://xsaf.ilha.build/llms.txt
> Use this file to discover all available pages before exploring further.

# Sessions & Memory

Every `XsafAgent` owns a Hono application. XSAF mounts routes and exposes a web-standard fetch handler, but it never opens a network listener.

## Fetch and channels

### Built-in routes

| Route          | Behavior                                                                     |
| -------------- | ---------------------------------------------------------------------------- |
| `GET /health`  | Returns `{ "ok": true }`; it is not a dependency health check.               |
| `POST /invoke` | Accepts `{ prompt, sessionId? }` and returns a collected model result.       |
| `/mcp`         | Mounted only after `.serve()`.                                               |
| `/chat`        | Default route of `@xsaf/agent/channel/http` when that channel is registered. |

Blank prompts and malformed JSON sent to `/invoke` return `400`. The default HTTP session ID is `"http"`.

### Deploy the fetch handler

```ts
await bot.start();

export default {
  fetch(request: Request) {
return bot.fetch(request);
  },
};
```

Node.js deployments need a server adapter that translates incoming requests to web-standard `Request` and `Response` objects. XSAF intentionally does not choose or start that listener.

### Compose with Hono

```ts
bot.app.get("/ready", (context) => {
  return context.json({ ready: true });
});

await bot.start();
```

The same Hono app hosts user routes, channel routes, and MCP. Mount custom middleware after `bot.start()` and before accepting requests when route order matters. Hono's MCP host and Origin protections remain enabled for the MCP endpoint.

### Channel contract

```ts
interface XsafChannelDriver {
  name: string;
  listen(context: ChannelContext): void | Promise<void>;
  send(target: ChannelTarget, payload: ChannelPayload): Promise<void>;
  close?(): Promise<void>;
}
```

Inbound channel messages dispatch through the normal request path. The result is sent back through that channel and `message.sent` is emitted.

### HTTP channel

```ts
import { agent } from "@xsaf/agent";
import http from "@xsaf/agent/channel/http";

const channel = http({ name: "web", path: "/chat", apiKey: process.env.API_KEY });
const bot = agent(config).channel(channel);
```

```http
POST /chat
Accept: text/event-stream
Authorization: Bearer <api-key>
Content-Type: application/json

{"sessionId":"customer-1","text":"Hello"}
```

Requests accepting `text/event-stream` receive typed events for response chunks and tool/delegate activity. Other clients retain JSON responses for string and object results. A missing session ID is generated with `crypto.randomUUID()`. When `apiKey` is configured, the route requires the matching bearer token.

The HTTP channel tracks pending responses by session in FIFO order. Its `send()` target must be a session ID, and closing the channel rejects pending requests. Use `@xsaf/agent/channel/mock` for deterministic in-process tests; see [Testing](/recipes/testing).

### Chat SDK channel

The Chat SDK channel bridges the universal `chat` package to natively support Slack, Teams, Discord, Telegram, Google Chat, and other platforms.

```ts
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { agent } from "@xsaf/agent";
import chatSdk from "@xsaf/agent/channel/chat-sdk";

const bot = new Chat({
  userName: "mybot",
  adapters: { slack: createSlackAdapter() },
  // ... state adapter, etc.
});

// Wire webhooks to your HTTP framework separately:
// app.post("/webhooks/slack", bot.webhooks.slack);

const assistant = agent(config).channel(chatSdk(bot));
```

The driver registers inbound handlers on `bot` at `listen()` time and dispatches each incoming message into the XSAF request path. Outbound payloads are posted back to the originating platform thread.

## Sessions and streaming

Session IDs define both memory history and concurrency ordering. Direct invocation defaults to `"default"`; the built-in `/invoke` route defaults to `"http"`.

### Session serialization

```ts
await Promise.all([bot.invoke("First", "customer-1"), bot.invoke("Second", "customer-1")]);
```

The second request waits for the first request's memory and model work to finish. Calls for different session IDs can run concurrently. `ask()` participates in the same session locking.

### Consume a stream

```ts
const result = await bot.invoke("Write a summary", "customer-1");

if ("textStream" in result) {
  for await (const chunk of result.textStream) {
process.stdout.write(chunk);
  }

  const completed = await result.completed;
  console.log(completed.usage);
} else {
  console.log(result.text);
}
```

```ts
interface AgentStreamResult {
  textStream: AsyncIterable<string>;
  completed: Promise<AgentResult>;
}
```

XSAF wraps rather than eagerly buffers the provider iterable, preserving backpressure. It collects the final text while the consumer advances the stream, then persists the assistant message and resolves `completed`.

> **Always finish a stream**
>
> A same-session request remains locked until `completed` settles. If a caller abandons `textStream` without consuming or closing it, that session can remain blocked. Alpha does not provide a separate public stream-cancel method.

`POST /invoke` consumes the model stream internally and returns a single JSON response. The bundled HTTP channel returns an `AsyncIterable` as Server-Sent Events, so clients receive chunks as they are consumed.

Tool calls receive a combined timeout and caller signal through `ToolExecutionContext.signal`. Cancellation is cooperative: adapters and tool code must observe the signal. Timed-out executions are not retried, but host-process code that ignores its signal can continue running.

## Memory

XSAF stores inbound user messages and completed assistant responses through one memory driver. The default is process-local in-memory storage.

### Use the default

No configuration is required. For an explicit instance:

```ts
import { inMemory } from "@xsaf/agent/memory/in-memory";

const memory = inMemory();
```

The in-memory driver is non-durable. `get()` returns a new array, but message objects are not deep-cloned.

### Use db0 (durable SQL)

```ts
import { createDatabase } from "db0";
import sqlite from "db0/connectors/bun-sqlite";
import { db0 } from "@xsaf/agent/memory/db0";

const memory = db0(createDatabase(sqlite({ name: "xsaf-memory" })));
const bot = agent(config).memory(memory);

const hits = await memory.search({ query: "weather", limit: 10 });
const scoped = await memory.search({ query: "weather", sessionId: "customer-1" });
```

`db0` is an optional peer dependency. Messages are stored as rows in `xsaf_messages` (`session_id`, `seq`, `role`, `content`, …). `.search()` runs a parameterized `LIKE` query across all sessions unless `sessionId` is set; corrupt or invalid rows fail the request. Call `db0(database, { dispose: false })` when another owner still needs the Database after agent shutdown.

On Nitro, enable `experimental.database` and pass `useDatabase()` into `db0(...)`.

### Use unstorage (durable KV)

```ts
import { createStorage } from "unstorage";
import fsDriver from "unstorage/drivers/fs";
import { unstorage } from "@xsaf/agent/memory/unstorage";

const memory = unstorage(
  createStorage({
driver: fsDriver({ base: "./data/memory" }),
  }),
);

const bot = agent(config).memory(memory);
```

`unstorage` is an optional peer dependency. Session history is stored as a JSON `Message[]` under `xsaf:session:<encodedSessionId>`. Loaded payloads are validated; corrupt data fails the request. Call `unstorage(storage, { dispose: false })` when another owner still needs the Storage after agent shutdown.

### Supply a driver

```ts
interface XsafMemoryDriver {
  get(sessionId: string): Promise<Message[]>;
  append(sessionId: string, message: Message): Promise<void>;
  clear(sessionId: string): Promise<void>;
  close?(): Promise<void>;
}
```

```ts
const bot = agent(config).memory(durableMemory);
```

Only one memory driver can be configured. XSAF treats memory failures as request failures rather than silently continuing with incomplete context.

```ts
interface Message {
  role: "system" | "user" | "assistant" | "tool";
  content: string;
  name?: string;
  toolCallId?: string;
  meta?: Readonly<Record<string, unknown>>;
}
```

The agent persona is inserted as a system message in each model request and is not persisted in memory. Channel metadata and stored content are untrusted data; a durable driver should apply its own storage security and retention policy.

For non-streaming requests, XSAF persists the assistant response before invocation resolves. For streaming requests, persistence occurs after successful stream consumption. Memory append failures reject `completed` and keep the request failure visible.

Alpha ships `@xsaf/agent/memory/db0` for SQL backends via [db0](https://db0.unjs.io/) (searchable) and `@xsaf/agent/memory/unstorage` for KV backends via [unstorage](https://unstorage.unjs.io/). Custom drivers remain structural integrations and must implement the contract above.

## See also

- [Agent lifecycle](/xsaf/start)
- [Testing](/recipes/testing)
- [MCP](/xsaf/mcp)
- [Public driver contracts](/reference#public-contracts)
- [Roadmap](/roadmap)

Source: https://xsaf.ilha.build/recipes/sessions-memory/index.mdx
