# Sessions

> Persist conversation history across runs in a provider-neutral format.

A session stores the conversation as provider-neutral messages. zidane saves each turn when it completes. After a crash, you keep the history up to the last completed turn and can resume. To continue the interrupted run itself, from its last model or tool call, add a durable runtime ([crash recovery](https://zidane.si/docs/durability.md)).

## Create and load a session

```ts
import { createAgent, createSession } from 'zidane'
import { basic } from 'zidane/presets'
import { anthropic } from 'zidane/providers'
import { createSqliteStore } from 'zidane/session/sqlite'

const provider = anthropic()

const store = createSqliteStore({ path: './sessions.db' })
const session = await createSession({ store })

const agent = createAgent({ ...basic, provider, session })
await agent.run({ prompt: 'hello' })
await session.save()
```

Restore with `loadSession(store, id)`. On a session with turns, `agent.run()` without a `prompt` resumes it, for hosts that save the user message before the run starts.

`session:start` and `session:end` (with `status` and `turnRange`) fire at run boundaries ([session hooks](https://zidane.si/docs/hooks.md#session-and-background-tasks)). The apps keep every session in one SQLite file, `sessions.db` ([where](https://zidane.si/docs/files.md#config-and-data-files)), and add forks, titles and spaces ([app sessions](https://zidane.si/docs/apps.md#sessions)).

## Storage backends

| Store | Import | Durability | Concurrency |
| - | - | - | - |
| `createMemoryStore()` | `zidane` | Lost on exit | Single process |
| `createSqliteStore({ path })` | `zidane/session/sqlite` (Bun only) | Per-turn flush, WAL | Safe in one process. Never attach two `Agent`s to one session |
| `createRemoteStore({ url })` | `zidane` | Server-side (HTTP) | Delegated |
| `createFileMapStore(adapter)` | `zidane` | `turns.jsonl` + `meta.json` | Wraps any `{ get, save, delete }` backend |

`createSqliteStore` migrates v3 databases in place on first open (idempotent), after copying the database to `<db>.zidane-pre-v4.bak`. To roll back, stop every process using it and copy the `.bak` over it.

## Compaction

Compaction summarizes older turns when the conversation nears the model's context window, so a long session keeps going. A model writes the summary. zidane saves it as a `compact-summary` block in place of the older turns, adds back recently read files and active skills, and continues.

| Setting | Library (`behavior.autoCompact`) | Terminal and desktop apps |
| - | - | - |
| Default | Off | On (Auto-compact) |
| Fires at | 85% of the usable window (`threshold: 0.85`) | 80% (`autoCompactThreshold`: 0.6, 0.7, 0.8 or 0.9) |
| Summary model | The run's model, or `autoCompact.provider` and `model` | The session's model, or Compaction model (`compactionModel`) |
| By hand | `agent.compact()` | Session details: Compact now in the desktop app, `k` in the terminal app. It runs `agent.compact()` on the session's model. |
| Show the summary | | Compaction summaries (off by default) |

```ts
createAgent({
  provider,
  behavior: { autoCompact: { threshold: 0.8, keepTurns: 4, model: 'claude-haiku-4-5' } },
})
```

| `autoCompact` option | Default | Does |
| - | - | - |
| `threshold` | `0.85` | Fraction of the usable window (window minus the output reserve) that triggers it |
| `contextWindow` | the provider's | The model's window in tokens |
| `keepTurns` | `compactKeepTurns`, else 4 | Recent turns kept word for word |
| `restore` | on | Add recently read files and active skills back after the summary |
| `minGrowthFraction` | `0.1` | Share of the window new content must fill before another compaction |
| `maxFailures` | `2` | Failures in a row before the run stops trying |
| `provider`, `model` | the run's | Where the summary runs |

When the history still starts with the last request a run sent, the summary call resends that request with the summary instruction added as a [system note](https://github.com/Tahul/zidane/blob/main/docs/SKILL.md#keeping-the-prefix-stable), so the provider reads the conversation from its prompt cache. If that fails, or the reply has no summary, zidane sends a separate summary request.

### Compact by hand

```ts
const result = await agent.compact({ keepTurns: 4 })
if (result.compacted)
  console.log(`${result.replacedCount} turns replaced`)
```

| Option | Default | Does |
| - | - | - |
| `keepTurns` | `autoCompact.keepTurns`, then `compactKeepTurns`, then 4 | Recent turns kept word for word |
| `model` | The model the last run ended on | The model that writes the summary. Another model cannot reuse the cached request. |
| `signal` | | Cancels the compaction. `agent.abort()` cancels it too. |

When it compacts, the result has `compacted: true`, the `summaryTurnId`, `replacedCount`, `droppedCount`, `restoredFiles`, `restoredSkills`, the `model`, the `usage` of the summary call and `effectiveTokens` (the estimated context size on the next turn). Otherwise it has `compacted: false` and a `reason`: `'nothing-to-compact'`, `'no-session'` or `'aborted'`. The first two make no provider call.

`agent.compact()` needs an idle agent. It rejects while a run or another compaction is in progress, and `run()` rejects while it works. It also rejects when the summary call fails. It fires the same hooks as auto-compaction, with `reason: 'manual'`. The lower-level `compactConversation()` summarizes turns outside an agent.

Compaction fires `compact:start`, `compact:progress` and `compact:end`. A context overflow mid-run stubs older tool output and media instead ([errors](https://zidane.si/docs/errors.md)). `compactConversation()` throws `CompactInvalidInputError` when there is nothing to summarize (no turns, or `keepTurns` covers them all), and `CompactPromptTooLongError` when the summary request is still too long after its retries.

In the apps, the experimental setting Compact before the cache expires (`compactBeforeCacheExpiry`, off) compacts an idle long session about a minute before its prompt cache is estimated to expire. A session counts as long when its last request had at least 100k tokens, or half the token count at which auto-compaction fires, if that is more. Any activity in the session cancels it.

`behavior.compactStrategy: 'tail'` costs less. It hides older tool results on the wire and writes no summary ([output budgets](https://zidane.si/docs/hooks.md#output-and-context-budgets)).

## Message format

Providers convert to and from `SessionContentBlock`:

| Block `type` | Fields |
| - | - |
| `text` | `text`, optional `origin` |
| `image`, `audio`, `video`, `document` | `mediaType`, `data` (base64) or `ref`, optional `name`. Documents add `encoding` |
| `tool_call` | `id`, `name`, `input` |
| `tool_result` | `callId`, `output: string \| ToolResultContent[]`, `isError?`, `metadata?` |
| `thinking`, `redacted_thinking` | `text` and `signature?`, or opaque `data` |
| `provider_reasoning` | Opaque OpenRouter or vLLM reasoning, replayed to the same provider |
| `server_tool_use`, `server_tool_result` | Anthropic server tools such as `web_search` |
| `compact-summary` | Summary that replaces compacted turns |

A text block's `origin` (`TextOrigin`) says who sent it when that is not the session's own user: `kind` (`'user'`, `'signal'` or `'event'`), `source` (such as `'slack'`), and optional `sender` and `messageId`.

An assistant turn with `aborted: true` holds model output that a process death cut off, rebuilt when its durable run resumed. The transcript shows it, the model context leaves it out, and the request is sent again.

`ToolResultContent` is a text, image, audio, video or document part. Providers with `imageInToolResult: true` get image results natively. Other providers get them in a companion user message. `toolResultToText(output)` flattens any output to text.

## Build provider requests yourself

Use `session.toMessages({ provider })`, not `session.turns`. Raw turns can hold orphaned `tool_use` / `tool_result` blocks (from an interrupted run, a partial write or a compaction boundary), which providers reject with a 400. `toMessages()` applies the loop's pairing repair and, given a `provider`, the user-tail rule some models need. It never changes `session.turns`. Do not write its result back with `setTurns` or `appendTurns`.

| Helper | Input |
| - | - |
| `toWireMessages(turns, opts?)` | Raw `SessionTurn[]` from `store.getTurns()` |
| `ensureToolResultPairing(messages, opts?)` | Messages |
| `ensureEndsWithUserMessage(messages, provider)` | Messages |

Converters for other frameworks, from `zidane`: `fromAnthropic`, `toAnthropic`, `fromOpenAI`, `toOpenAI`, `autoDetectAndConvert`.

## Reference

- [`zidane/session`](https://zidane.si/docs/reference/session.md)
- [`zidane/session/sqlite`](https://zidane.si/docs/reference/session-sqlite.md)
- [`zidane/durable`](https://zidane.si/docs/reference/durable.md)
- [`zidane/restate`](https://zidane.si/docs/reference/restate.md)
