# zidane/session

> Sessions, session stores and the provider-neutral message format that persists a conversation.

Import: `import { … } from 'zidane/session'`. Source: [src/session/index.ts](https://github.com/Tahul/zidane/blob/main/src/session/index.ts).

Use `zidane/session` to keep an agent's history across runs: `createSession` starts a conversation on a `SessionStore` and `loadSession` restores one. The built-in stores are `createMemoryStore`, `createRemoteStore` and `createFileMapStore`. The entry also holds the artifact helpers and the converters between zidane's message format and the Anthropic and OpenAI formats. See [Sessions](https://zidane.si/docs/sessions.md).

```ts
import { createAgent } from 'zidane'
import { anthropic } from 'zidane/providers/anthropic'
import { createMemoryStore, createSession, loadSession } from 'zidane/session'

const store = createMemoryStore()
const session = await createSession({ store })
const agent = createAgent({ provider: anthropic(), session })
await agent.run({ prompt: 'Remember that my favorite color is green.' })
await session.save()
const restored = await loadSession(store, session.id)
console.log(restored?.turns.length)
```

## Functions

### `autoDetectAndConvert`

Function.

Converts a wire message of unknown provider into a [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage). Block types decide between [`fromAnthropic`](https://zidane.si/docs/reference/session.md#fromAnthropic) and [`fromOpenAI`](https://zidane.si/docs/reference/session.md#fromOpenAI). Anthropic is the default.

```ts
function autoDetectAndConvert(msg: { content: unknown; role: string }): SessionMessage
```

**Parameters**

- `msg` (`{ content: unknown; role: string }`): Anthropic or OpenAI wire message.

**Returns** `SessionMessage`

Source: [src/session/messages.ts:1105](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L1105)

### `createFileMapStore`

Function.

Create a single-session `SessionStore` backed by a file-map adapter.

```ts
function createFileMapStore(
  adapter: FileMapAdapter,
  options?: FileMapStoreOptions,
): SessionStore
```

**Parameters**

- `adapter` (`FileMapAdapter`): Host adapter that loads, saves and deletes the file map.
- `options?` (`FileMapStoreOptions`, default `{}`): File names to use. See [`FileMapStoreOptions`](https://zidane.si/docs/reference/session.md#FileMapStoreOptions).

**Returns** `SessionStore`

**Example**

```ts
const session = await createSession({
  store: createFileMapStore(hostSessionStore),
})
```

Source: [src/session/file-map.ts:174](https://github.com/Tahul/zidane/blob/main/src/session/file-map.ts#L174)

### `createMemoryStore`

Function.

Creates an in-memory [`SessionStore`](https://zidane.si/docs/reference/session.md#SessionStore). Nothing is persisted.

```ts
function createMemoryStore(): SessionStore
```

**Returns** `SessionStore`

**Example**

```ts
import { createMemoryStore, createSession } from 'zidane/session'

const session = await createSession({ store: createMemoryStore() })
```

Source: [src/session/memory.ts:36](https://github.com/Tahul/zidane/blob/main/src/session/memory.ts#L36)

### `createRemoteStore`

Function.

Creates a [`SessionStore`](https://zidane.si/docs/reference/session.md#SessionStore) that talks to a REST session API over HTTP. See the module comment for the endpoints it calls.

```ts
function createRemoteStore(options: RemoteStoreOptions): SessionStore
```

**Parameters**

- `options` (`RemoteStoreOptions`): API base URL, headers and timeout.

**Returns** `SessionStore`

**Example**

```ts
import { createRemoteStore, createSession } from 'zidane/session'

const store = createRemoteStore({ url: 'https://api.example.com', headers: { authorization: 'Bearer token' } })
const session = await createSession({ store })
```

Source: [src/session/remote.ts:44](https://github.com/Tahul/zidane/blob/main/src/session/remote.ts#L44)

### `createSession`

Function.

Create a new session.
Async so stores that generate IDs server-side (e.g. a database) can be supported.

```ts
function createSession(options?: CreateSessionOptions): Promise<Session>
```

**Parameters**

- `options?` (`CreateSessionOptions`, default `{}`): Session identity, metadata and store. See [`CreateSessionOptions`](https://zidane.si/docs/reference/session.md#CreateSessionOptions).

**Returns** `Promise<Session>`

**Example**

```ts
import { createMemoryStore, createSession } from 'zidane/session'

const session = await createSession({ agentId: 'reviewer', store: createMemoryStore() })
console.log(session.id, session.turns.length)
```

Source: [src/session/index.ts:1223](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L1223)

### `currentExtensionSession`

Function.

Returns the session bound by [`withExtensionSession`](https://zidane.si/docs/reference/session.md#withExtensionSession) for the current async context, or `undefined` when none is bound.

```ts
function currentExtensionSession(): Session | undefined
```

**Returns** `Session | undefined`

Source: [src/session/index.ts:60](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L60)

### `deleteExtensionSessionState`

Function.

Removes one value from an extension namespace in the session metadata and saves the session when it has a store. Does nothing when the value does not exist.

```ts
function deleteExtensionSessionState(
  session: Session | undefined,
  extensionName: string,
  key: string,
): Promise<void>
```

**Parameters**

- `session` (`Session | undefined`): Session to modify, if any.
- `extensionName` (`string`): Name of the extension that owns the value.
- `key` (`string`): Key to remove.

**Returns** `Promise<void>`

Source: [src/session/index.ts:133](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L133)

### `demoteStaleRunningStatus`

Function.

Demote a persisted `'running'` status to `'idle'` (in place) and return the
data. A persisted `'running'` is necessarily stale: the run that set it lived
in another process (crash, kill -9, power loss) and cannot still be executing
in a freshly-loaded session. Call this on the result of a raw `store.load()`
so consumers don't render a ghost in-progress state or refuse a new run.
`loadSession` applies it automatically; direct `store.load()` callers (e.g.
the TUI) should run loaded data through this.

Run RECORDS get the same treatment: the agent persists each record at
run start with `status: 'running'` (crash durability — see the
`SessionStore.updateRun` contract), so a freshly-loaded session may
carry `'running'` runs that died with their process. They demote to
`'aborted'` so replay renders a truthful "aborted" state instead of a
perpetually-in-flight run.

```ts
function demoteStaleRunningStatus(data: SessionData): SessionData
```

**Parameters**

- `data` (`SessionData`): Loaded session data. It is modified in place.

**Returns** `SessionData`

Source: [src/session/index.ts:1476](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L1476)

### `dropInvalidToolPairings`

Function.

Drop-only normalization of persisted history: delete structurally invalid
tool blocks so the transcript never re-triggers wire-time pairing repairs
turn after turn.

Unlike [`ensureToolResultPairing`](https://zidane.si/docs/reference/index.md#ensureToolResultPairing) — which SYNTHESIZES placeholder
content and must therefore stay wire-only (fabricated results would poison
persisted history and training data) — this pass performs pure deletions of
blocks that are provably invalid regardless of interpretation:

| Corruption | Action |
| - | - |
| duplicate `tool_call` id within one run lane (across or within turns) | drop the later instance |
| duplicate `tool_result.callId` within one turn | drop the later instance |
| `tool_result` with no matching `tool_call` in the immediately preceding same-lane assistant turn | drop the block |
| turn emptied by the drops above (reasoning-only remainders count as empty) | drop the whole turn |

Duplicate detection is scoped to the WIRE-VIEW lane: tool_call ids only
collide when both instances reach one model request. By default a turn's
lane is its raw `runId` — correct for parent/subagent interleaving, where
providers minting per-conversation counter ids (`call_0`, `call_1`)
legitimately reuse the same id across lanes. Callers that know the run
topology (see `createSession`) pass `laneForRun` to widen lanes to true
wire views: all depth-0 runs share the main conversation view, and a
resumed subagent shares its resume-chain ancestors' view — duplicates
across those runIds DO reach a single request (wire-time
`ensureToolResultPairing` mode 4 would strip them on every send) and are
healed here.

Orphan `tool_use` blocks (a call whose result was never persisted —
interrupted run) are deliberately KEPT: they are authentic model output,
and the wire-time pass patches them with a synthetic result on every send
without touching history.

Deletions cascade by design: dropping a duplicate `tool_call` orphans its
`tool_result`, which is then dropped too; a turn emptied by either is
removed entirely.

Safe to persist: deterministic, idempotent (re-running on healed output is
a no-op returning the same reference), and fabricates nothing.

Pure: does not mutate input turns or their content arrays.

```ts
function dropInvalidToolPairings<T extends { content: SessionContentBlock[]; role: string; runId?: string }>(
  turns: T[],
  options?: { laneForRun?: (runId: string | undefined) => string; onDrop?: (drop: PairingDrop) => void },
): T[]
```

**Type parameters**

- `T` (`extends { content: SessionContentBlock[]; role: string; runId?: string }`)

**Parameters**

- `turns` (`T[]`): Persisted turns, oldest first. They are not modified.
- `options?` (`{ laneForRun?: (runId: string | undefined) => string; onDrop?: (drop: PairingDrop) => void }`, default `{}`): `onDrop` is called once per deletion. `laneForRun` maps a `runId` to its wire-view lane.

**Returns** `T[]`

Source: [src/session/messages-pairing.ts:526](https://github.com/Tahul/zidane/blob/main/src/session/messages-pairing.ts#L526)

### `fromAnthropic`

Function.

Converts an Anthropic wire message into a [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage).

```ts
function fromAnthropic(msg: { content: unknown; role: string }): SessionMessage
```

**Parameters**

- `msg` (`{ content: unknown; role: string }`): Anthropic message with a string or block-array `content`.

**Returns** `SessionMessage`

Source: [src/session/messages.ts:205](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L205)

### `fromOpenAI`

Function.

Converts an OpenAI wire message into a [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage).

```ts
function fromOpenAI(msg: { content: unknown; role: string }): SessionMessage
```

**Parameters**

- `msg` (`{ content: unknown; role: string }`): OpenAI message with a string or part-array `content`.

**Returns** `SessionMessage`

Source: [src/session/messages.ts:320](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L320)

### `getExtensionSessionState`

Function.

Reads one value an extension stored in the session metadata. Returns a copy, or `undefined` when the session, extension or key is missing or a name is unsafe.

```ts
function getExtensionSessionState<T = JsonValue>(
  session: Session | undefined,
  extensionName: string,
  key: string,
): T | undefined
```

**Type parameters**

- `T` (default `JsonValue`)

**Parameters**

- `session` (`Session | undefined`): Session to read, if any.
- `extensionName` (`string`): Name of the extension that owns the value.
- `key` (`string`): Key under the extension namespace.

**Returns** `T | undefined`

Source: [src/session/index.ts:71](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L71)

### `interruptedToolResultOutput`

Function.

Output of the synthetic result that closes a tool call interrupted by a
process death. Keeps [`SYNTHETIC_TOOL_RESULT_PLACEHOLDER`](https://zidane.si/docs/reference/index.md#SYNTHETIC_TOOL_RESULT_PLACEHOLDER) as a prefix
so consumers that reject fabricated results still match it.

```ts
function interruptedToolResultOutput(toolName: string): string
```

**Parameters**

- `toolName` (`string`): Name of the interrupted tool, quoted in the message.

**Returns** `string`

Source: [src/session/index.ts:1612](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L1612)

### `loadSession`

Function.

Load an existing session from a store.

```ts
function loadSession(
  store: SessionStore,
  sessionId: string,
  options?: LoadSessionOptions,
): Promise<Session | null>
```

**Parameters**

- `store` (`SessionStore`): Store to load from.
- `sessionId` (`string`): Id of the session to load.
- `options?` (`LoadSessionOptions`, default `{}`): Load options. See [`LoadSessionOptions`](https://zidane.si/docs/reference/session.md#LoadSessionOptions).

**Returns** `Promise<Session | null>`: The session, or `null` when the store has no session with that id.

Source: [src/session/index.ts:1518](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L1518)

### `matchesArtifactFilter`

Function.

Tests an artifact against a filter. An undefined filter matches everything. `limit` is not applied here.

```ts
function matchesArtifactFilter(
  artifact: SessionArtifact,
  filter: ArtifactFilter | undefined,
): boolean
```

**Parameters**

- `artifact` (`SessionArtifact`): Artifact to test.
- `filter` (`ArtifactFilter | undefined`): Filter to apply.

**Returns** `boolean`

Source: [src/session/artifacts.ts:222](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L222)

### `mergeArtifact`

Function.

Apply an upsert to an existing row: identity fields (`id`, `runId`,
`turnId`, `createdAt`) stay, `label` refreshes, `meta` and `projectRoot`
refresh when the incoming artifact has them, and `updatedAt` becomes the
later of the two. Shared by every store so the
merge semantics can't drift.

```ts
function mergeArtifact(
  existing: SessionArtifact,
  incoming: SessionArtifact,
): SessionArtifact
```

**Parameters**

- `existing` (`SessionArtifact`): Stored artifact.
- `incoming` (`SessionArtifact`): Artifact being upserted.

**Returns** `SessionArtifact`

Source: [src/session/artifacts.ts:180](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L180)

### `mergeSessionMetadata`

Function.

The metadata a `saveMeta` should store: `incoming` wholesale without
`keys`, otherwise `stored` with only `keys` taken from `incoming`.

```ts
function mergeSessionMetadata(
  stored: Record<string, unknown> | undefined,
  incoming: Record<string, unknown>,
  keys: readonly string[] | undefined,
): Record<string, unknown>
```

**Parameters**

- `stored` (`Record<string, unknown> | undefined`): Metadata the store holds now; when absent, `incoming` is stored as is.
- `incoming` (`Record<string, unknown>`): Metadata the writer sends.
- `keys` (`readonly string[] | undefined`): Keys the writer changed (`SaveMetaOptions.metaKeys`); an `undefined` value in `incoming` removes the key.

**Returns** `Record<string, unknown>`

Source: [src/session/metadata-merge.ts:23](https://github.com/Tahul/zidane/blob/main/src/session/metadata-merge.ts#L23)

### `normalizeArtifactInput`

Function.

Validate + normalize a raw input (typically from tool-result metadata,
i.e. an untrusted boundary). Returns `null` when the entry is unusable
— missing kind/uri/label or oversized in a way truncation can't fix.
`label` is clipped, `meta` is dropped (not the artifact) when it exceeds
the byte bound.

```ts
function normalizeArtifactInput(raw: unknown): SessionArtifactInput | null
```

**Parameters**

- `raw` (`unknown`): Untrusted value, typically from tool-result metadata.

**Returns** `SessionArtifactInput | null`

Source: [src/session/artifacts.ts:131](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L131)

### `resumeChainRunIds`

Function.

Transitive `resumedFromRunId` chain starting at `runId` (inclusive).

A resumed run only persists the turns IT produced — the turns it was
seeded with still carry the original run's id. So reconstructing a
resumed child's full conversation means collecting turns from every run
in its resume lineage, not just the latest link. Cycle-guarded (a
malformed chain terminates instead of looping).

```ts
function resumeChainRunIds(runs: readonly SessionRun[], runId: string): Set<string>
```

**Parameters**

- `runs` (`readonly SessionRun[]`): Run records of the session.
- `runId` (`string`): Run to start from.

**Returns** `Set<string>`

Source: [src/session/index.ts:169](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L169)

### `selectArtifactEvictions`

Function.

Which artifacts to evict so a session stays at or under `cap` after
one more insert. `file` artifacts go first (noisiest, cheapest to
regenerate), oldest first within a kind; ties keep the caller's order
(stable sort), so pass rows in insertion order. Pure — callers delete
the returned ids.

```ts
function selectArtifactEvictions(
  current: readonly SessionArtifact[],
  cap?: number,
): string[]
```

**Parameters**

- `current` (`readonly SessionArtifact[]`): Artifacts currently stored for the session, in insertion order.
- `cap?` (`number`, default `ARTIFACTS_PER_SESSION_CAP`): Maximum number of artifacts per session.

**Returns** `string[]`

Source: [src/session/artifacts.ts:199](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L199)

### `setExtensionSessionState`

Function.

Stores a JSON value under an extension namespace in the session metadata and saves the session when it has a store. Does nothing without a session.

Throws when a name is unsafe or the value exceeds the size limits (16 KiB per item, 64 KiB for all extension state).

```ts
function setExtensionSessionState(
  session: Session | undefined,
  extensionName: string,
  key: string,
  value: JsonValue,
): Promise<void>
```

**Parameters**

- `session` (`Session | undefined`): Session to write, if any.
- `extensionName` (`string`): Name of the extension that owns the value.
- `key` (`string`): Key under the extension namespace.
- `value` (`JsonValue`): JSON value to store. It is copied.

**Returns** `Promise<void>`

Source: [src/session/index.ts:102](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L102)

### `sortArtifacts`

Function.

Newest first, stable on id.

```ts
function sortArtifacts(artifacts: SessionArtifact[]): SessionArtifact[]
```

**Parameters**

- `artifacts` (`SessionArtifact[]`): Artifacts to sort. The array is sorted in place.

**Returns** `SessionArtifact[]`

Source: [src/session/artifacts.ts:246](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L246)

### `toAnthropic`

Function.

Converts a [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage) into an Anthropic wire message. Foreign reasoning blocks are dropped following [`reasoningWireAction`](https://zidane.si/docs/reference/index.md#reasoningWireAction).

```ts
function toAnthropic(msg: SessionMessage): { content: unknown; role: string }
```

**Parameters**

- `msg` (`SessionMessage`): Message to convert.

**Returns** `{ content: unknown; role: string }`

Source: [src/session/messages.ts:566](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L566)

### `toOpenAI`

Function.

Converts a [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage) into an OpenAI wire message.

```ts
function toOpenAI(msg: SessionMessage): { content: unknown; role: string }
```

**Parameters**

- `msg` (`SessionMessage`): Message to convert.

**Returns** `{ content: unknown; role: string }`

Source: [src/session/messages.ts:665](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L665)

### `toWireMessages`

Function.

Build a wire-ready `SessionMessage[]` from raw persisted `SessionTurn[]`.

**The canonical "I want to send these to a provider" projection.** Use
this — not raw `session.turns` / `store.getTurns()` — when constructing a
provider request outside `agent.run()`. The agent's own wire pipeline
(`executeTurn` in `src/loop-turn.ts`) applies the same repair just-in-time;
downstream consumers that bypass the loop hit `tool_result must be
preceded by a tool_call` 400s precisely because they shipped raw turns.

Pipeline:

1. Filter out turns that never reach the model (`system` and `command`
   roles).
2. Resolve content refs with `options.resolveContentRef`.
3. [`ensureToolResultPairing`](https://zidane.si/docs/reference/index.md#ensureToolResultPairing) — repairs all six orphan / dup
   corruption modes: synthetic placeholders for orphan tool_use blocks
   (preserves the model's tool_use shape), strips for duplicates and
   orphan tool_results.
4. [`ensureEndsWithUserMessage`](https://zidane.si/docs/reference/index.md#ensureEndsWithUserMessage) when `options.provider` is set —
   guards against assistant-tail prefill rejection (opus 4.7, o-series).

Pure + idempotent. Does not mutate the input turns. Re-running on the
output is a no-op.

**Do not feed the result back into `session.setTurns` / `appendTurns`.**
The repair can insert `SYNTHETIC_TOOL_RESULT_PLACEHOLDER` / `[Orphaned
tool result removed …]` markers; round-tripping those into persisted
state contaminates the session's reasoning history and defeats the
"don't rewrite the past" invariant the wire-only repair was designed
around.

```ts
function toWireMessages(
  turns: readonly { content: SessionContentBlock[]; role: 'system' | 'user' | 'assistant' | 'command' }[],
  options?: ToWireMessagesOptions,
): Promise<SessionMessage[]>
```

**Parameters**

- `turns` (`readonly { content: SessionContentBlock[]; role: 'system' | 'user' | 'assistant' | 'command' }[]`): Persisted turns, oldest first.
- `options?` (`ToWireMessagesOptions`, default `{}`): Repair and tail options. See [`ToWireMessagesOptions`](https://zidane.si/docs/reference/session.md#ToWireMessagesOptions).

**Returns** `Promise<SessionMessage[]>`

Source: [src/session/messages.ts:1084](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L1084)

### `withExtensionSession`

Function.

Run `fn` with `session` bound as the active extension session for the
duration of the call (and every async continuation it spawns). Uses
`AsyncLocalStorage.run` rather than `enterWith` so concurrent agents in the
same process (e.g. the GUI's multi-session host) never observe each other's
session through `currentExtensionSession()`.

```ts
function withExtensionSession<T>(session: Session | undefined, fn: () => T): T
```

**Type parameters**

- `T`

**Parameters**

- `session` (`Session | undefined`): Session to bind. When `undefined`, `fn` runs without a binding.
- `fn` (`() => T`): Function to run with the binding in place.

**Returns** `T`

Source: [src/session/index.ts:51](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L51)

## Constants

### `ARTIFACT_KINDS`

Constant.

Built-in kinds. Open set — extensions add their own.

```ts
const ARTIFACT_KINDS: { readonly branch: 'branch'; readonly commit: 'commit'; readonly file: 'file'; readonly pullRequest: 'pull_request' }
```

Source: [src/session/artifacts.ts:28](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L28)

### `ARTIFACT_META_MAX_BYTES`

Constant.

Maximum size of an artifact `meta` value, in bytes of its JSON encoding. Larger values are dropped.

```ts
const ARTIFACT_META_MAX_BYTES = 16_384
```

Source: [src/session/artifacts.ts:43](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L43)

### `ARTIFACTS_METADATA_KEY`

Constant.

Reserved tool-result metadata namespace. Tools (and extension
`detect` hooks) emit `metadata.artifacts = ArtifactInput[]` and the
loop lifts each entry into the session's artifact store.

```ts
const ARTIFACTS_METADATA_KEY = 'artifacts'
```

Source: [src/session/artifacts.ts:40](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L40)

### `ARTIFACTS_PER_SESSION_CAP`

Constant.

Maximum number of artifacts kept per session. Stores evict the oldest `file` artifacts first.

```ts
const ARTIFACTS_PER_SESSION_CAP = 200
```

Source: [src/session/artifacts.ts:51](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L51)

### `SEND_REQUEST_HISTORY`

Constant.

Request ids kept per session; a claim past it drops the oldest.

```ts
const SEND_REQUEST_HISTORY = 256
```

Source: [src/session/send-requests.ts:18](https://github.com/Tahul/zidane/blob/main/src/session/send-requests.ts#L18)

## Interfaces

### `AppendTurnsOptions`

Interface.

Options of `SessionStore.appendTurns`.

```ts
interface AppendTurnsOptions
```

**Properties**

- `dropPartials?` (`{ runId: string }`): Drop every stream partial of run `runId`, in the same write as the
  turns: the aborted assistant turn `Agent.resume` joined them into.
  Honoured like `pruneSteps`.
- `pruneSteps?` (`{ runId: string; throughTurn?: number }`): Drop the durable step records of run `runId` for its loop turns up to
  `throughTurn` (all of them when unset), in the same write as the turns:
  those turns are what the steps produced, so a resumed run starts from
  them and never asks for the steps again. Honoured by the stores of
  checkpoint durable runtimes (`zidane/durable`); other stores have no
  step records and ignore it.
- `run?` (`SessionRun`): Upsert this run record in the same write as the turns (a checkpoint
  run's `loopTurn` and `wallMs` must never lag its turns). Honoured like
  `pruneSteps`.

Source: [src/session/index.ts:180](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L180)

### `ArtifactFilter`

Interface.

Filter for listing artifacts. Fields combine with AND.

```ts
interface ArtifactFilter
```

**Properties**

- `kind?` (`string`): Only artifacts of this kind.
- `limit?` (`number`): Maximum number of artifacts to return.
- `projectRoot?` (`string | null`): Tri-state like `SessionStore.list`: string → that project only,
  `null` → artifacts with no project, omitted → axis ignored.
- `sessionId?` (`string`): Only artifacts of this session.
- `since?` (`number`): Only artifacts with `updatedAt >= since`.
- `uri?` (`string`): Exact locator match — reverse lookup ("who produced PR #123?"). Pair with `kind`.

Source: [src/session/artifacts.ts:105](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L105)

### `CreateSessionOptions`

Interface.

Options for [`createSession`](https://zidane.si/docs/reference/session.md#createSession).

```ts
interface CreateSessionOptions
```

**Properties**

- `_data?` (`SessionData`): @internal: restore from existing data (bypasses id/agentId/metadata
  options). The session ALIASES this object for its lifetime — every
  mutation (appendTurns, run records, updatedAt) writes through to it,
  and the load-time pairing heal may replace its `turns` array in place
  before the session is returned. Callers must not retain independent
  ownership expectations.
- `agentId?` (`string`): Agent ID label
- `id?` (`string`): Session ID. If omitted and store provides generateSessionId, that is used.
- `metadata?` (`Record<string, unknown>`): Initial metadata
- `onPairingDrop?` (`(drop: PairingDrop) => void`): Observability hook for the load-time pairing heal: fired once per block
  or turn `dropInvalidToolPairings` deleted while restoring `_data`.
  Deletions are permanent once the session is saved, so hosts that alert
  on wire-time `pairing:repair` events should wire this to the same sink.
  Only fires on the `_data` restore path; fresh sessions never heal.
- `projectRoot?` (`string`): Project tag — see `SessionData.projectRoot`. Stamped once on
  creation; ignored when `_data` is set (restoring an existing
  session preserves whatever was already persisted there). The chat
  layer's session factory stamps `projectRootOf(cwd)` so sessions
  started from the same repo — any subdir, any linked worktree — share
  one tag.
- `store?` (`SessionStore`): Storage backend (optional, enables save/load)

Source: [src/session/index.ts:996](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L996)

### `FileMapAdapter`

Interface.

Host-provided file-map adapter. Three methods exchanging `Record<string, string>`
payloads — the whole persistence surface the wrapper needs.

```ts
interface FileMapAdapter
```

**Properties**

- `delete` (`() => Promise<void>`): Delete all persisted state.
- `get` (`() => Promise<{ files: Record<string, string> }>`): Load the current file map. Returns an empty `files` record when nothing is persisted.
- `save` (`(files: Record<string, string>) => Promise<void>`): Replace the persisted file map. Full-rewrite semantics.

Source: [src/session/file-map.ts:48](https://github.com/Tahul/zidane/blob/main/src/session/file-map.ts#L48)

### `FileMapStoreOptions`

Interface.

Options for [`createFileMapStore`](https://zidane.si/docs/reference/session.md#createFileMapStore).

```ts
interface FileMapStoreOptions
```

**Properties**

- `metaFile?` (`string`): Filename for the metadata JSON. Default: `meta.json`.
- `sendRequestsFile?` (`string`): Filename for the claimed send request ids. Default: `send-requests.json`.
- `turnsFile?` (`string`): Filename for the JSONL turns log. Default: `turns.jsonl`.

Source: [src/session/file-map.ts:58](https://github.com/Tahul/zidane/blob/main/src/session/file-map.ts#L58)

### `LoadSessionOptions`

Interface.

Options for [`loadSession`](https://zidane.si/docs/reference/session.md#loadSession).

```ts
interface LoadSessionOptions
```

**Properties**

- `clock?` (`Pick<AgentClock, 'now'>`): Time source for the stale `'running'` check. Default: `Date.now`. Pass a
  replay-stable clock inside a replaying run (Restate:
  `{ now: () => ctx.date.now() }`) so a replay takes the same branch.
- `onPairingDrop?` (`(drop: PairingDrop) => void`): Forwarded to `createSession` — see `CreateSessionOptions.onPairingDrop`.
- `persistHeal?` (`boolean`): Write a load-time pairing heal back to the store (default true). Pass
  `false` from READERS — a window mirroring a session another process
  runs (daemon mode) — because the persist is a bulk turn overwrite and
  the owner's next load heals and persists it anyway.

  No load closes interrupted tool calls: the process holding the run may
  still be running the tool. A run does that once it owns the session,
  through `Session.healInterruptedToolCalls`.

Source: [src/session/index.ts:1489](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L1489)

### `PairingDrop`

Interface.

One deletion made by [`dropInvalidToolPairings`](https://zidane.si/docs/reference/session.md#dropInvalidToolPairings).

```ts
interface PairingDrop
```

**Properties**

- `callId?` (`string`): Tool-call / tool-result id the drop concerns. Absent for empty-turn drops.
- `index` (`number`): Zero-based index into the INPUT `turns` array.
- `kind` (`PairingDropKind`): What was dropped. See [`PairingDropKind`](https://zidane.si/docs/reference/session.md#PairingDropKind).

Source: [src/session/messages-pairing.ts:472](https://github.com/Tahul/zidane/blob/main/src/session/messages-pairing.ts#L472)

### `RemoteStoreOptions`

Interface.

Options for [`createRemoteStore`](https://zidane.si/docs/reference/session.md#createRemoteStore).

```ts
interface RemoteStoreOptions
```

**Properties**

- `headers?` (`Record<string, string>`): Optional headers (e.g. for authentication)
- `timeoutMs?` (`number`): Per-request timeout in ms. Default 30s — bounds a hung backend.
- `url` (`string`): Base URL of the session API

Source: [src/session/remote.ts:20](https://github.com/Tahul/zidane/blob/main/src/session/remote.ts#L20)

### `RunEstimate`

Interface.

A run's expected duration (`SessionRun.estimate`). Times count from the
run's `startedAt` and leave out time spent waiting on the user
(`SessionRun.userWait`).

```ts
interface RunEstimate
```

**Properties**

- `at` (`number`): When this estimate was made, epoch ms.
- `calibration?` (`number`): Calibration factor applied to the model's answer, when one was.
- `highMs` (`number`): Working time past which the run counts as over its estimate, in ms.
- `initialMs` (`number`): The first estimate's total before calibration, in ms: what calibration compares the actual time to.
- `overrunChecked?` (`true`): The run went over its estimate and was estimated again: it is not asked again.
- `revisions` (`number`): How many estimates the run has had, this one included.
- `totalMs` (`number`): Expected working time of the whole run, in ms.

Source: [src/session/index.ts:369](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L369)

### `RunEstimateFailure`

Interface.

Why a run's estimate failed (`SessionRun.estimateFailure`).

```ts
interface RunEstimateFailure
```

**Properties**

- `at` (`number`): When the attempt failed, epoch ms.
- `message?` (`string`): Failure detail, when there is one.
- `reason` (`string`): The decision's failure reason (`DecisionFailureReason`).

Source: [src/session/index.ts:387](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L387)

### `RunOwner`

Interface.

Who wrote a stored `running` marker.

```ts
interface RunOwner
```

**Properties**

- `hostId` (`string`): Machine, pid namespace and boot the owner ran in: pids only compare within one.
- `pid` (`number`): Owner process id.
- `startedAt?` (`number`): When the owner process started (epoch ms, as `processStartedAt` reads
  it): tells a live owner from a later process that got its pid. Absent
  when it could not be read, and on owners written by older code.
- `token` (`string`): Random per-process id, telling the owner apart from a later process that got the same pid.

Source: [src/session/run-owner.ts:25](https://github.com/Tahul/zidane/blob/main/src/session/run-owner.ts#L25)

### `RunUserWait`

Interface.

Time a run spent waiting on the user (`SessionRun.userWait`).

```ts
interface RunUserWait
```

**Properties**

- `ms` (`number`): Finished waits, summed, in ms.
- `since?` (`number`): Start of the wait going on now, epoch ms. Absent when the run is not waiting.

Source: [src/session/index.ts:397](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L397)

### `SaveMetaOptions`

Interface.

Options for `SessionStore.saveMeta`.

```ts
interface SaveMetaOptions
```

**Properties**

- `metaKeys?` (`readonly string[]`): The metadata keys this writer changed. When set, the store merges only
  these keys into the metadata it already holds (an `undefined` value
  removes the key) instead of replacing the whole record, so a stale copy
  of the session can't write back keys another writer changed since.
  Omitted: the whole `metadata` record is written.

Source: [src/session/metadata-merge.ts:4](https://github.com/Tahul/zidane/blob/main/src/session/metadata-merge.ts#L4)

### `ScheduleStoreMethods`

Interface.

The schedule methods a store implements. Optional on `SessionStore`: a store without them has no schedules.

```ts
interface ScheduleStoreMethods
```

**Properties**

- `claimScheduleFire` (`(id: string, dueAt: number, next: number | null) => Promise<boolean>`): Take the fire due at `dueAt`: set `nextFireAt` to `next` when the
  schedule is enabled and its `nextFireAt` is still `dueAt`. `false`
  when another host took it, or it changed meanwhile.
- `deleteSchedule` (`(id: string) => Promise<boolean>`): Remove a schedule and its fires. `false` when it was gone.
- `listScheduleFires` (`(scheduleId: string, limit: number) => Promise<ScheduleFire[]>`): A schedule's fires, newest first.
- `listSchedules` (`() => Promise<Schedule[]>`): Every stored schedule, with its last fire, oldest first.
- `recordScheduleFire` (`(fire: ScheduleFire) => Promise<void>`): Store a fire. Keeps the newest [`FIRE_HISTORY`](https://zidane.si/docs/reference/chat-pure.md#FIRE_HISTORY) per schedule.
- `saveSchedule` (`(schedule: Schedule, options?: { keepNextFireAt?: boolean }) => Promise<void>`): Create or replace a schedule (`lastFire` is not stored: the fires are).
  `keepNextFireAt`: an existing schedule keeps its stored `nextFireAt`, so
  an edit does not undo a fire another host claimed meanwhile.

Source: [src/session/schedules.ts:16](https://github.com/Tahul/zidane/blob/main/src/session/schedules.ts#L16)

### `SendRequestRecord`

Interface.

A claimed request id.

```ts
interface SendRequestRecord
```

**Properties**

- `createdAt` (`number`): Epoch ms of the claim; the oldest records past [`SEND_REQUEST_HISTORY`](https://zidane.si/docs/reference/session.md#SEND_REQUEST_HISTORY) are dropped first.
- `fingerprint` (`string`): Hash of the send's content: the same id with other content is refused.
- `requestId` (`string`): The caller's idempotency key (`chat.send`'s `requestId`), unique per session.
- `result?` (`JsonValue`): The send's result once it settled; absent while it is in flight (or its process died first).

Source: [src/session/send-requests.ts:21](https://github.com/Tahul/zidane/blob/main/src/session/send-requests.ts#L21)

### `Session`

Interface.

A live session: the turn history and run records of one conversation, with an optional store that persists them.

```ts
interface Session
```

**Properties**

- `abortRun` (`(runId: string, stats?: { cost?: number; tokensIn: number; tokensOut: number; turns: number; turnUsage?: TurnUsage[] }) => void`): Mark a run as aborted. Optional `stats` lets the agent backfill the run's token totals when
  the abort happened *after* the loop accumulated meaningful usage —
  common when the user presses esc mid-streaming. Without it, the run
  record reads `0 in / 0 out` on reload regardless of how much was
  spent before the abort. Same shape as `completeRun`'s stats so the
  persisted `totalUsage` aggregate stays consistent across paths.
- `addArtifact` (`(input: SessionArtifactInput, origin?: { runId?: string; turnId?: string }) => Promise<SessionArtifact | undefined>`): Record an artifact this session produced. Fills `id`, timestamps and
  defaults `projectRoot` to the session's own; `origin` ties it to the
  producing run/turn for transcript anchoring. Resolves to the stored
  row, or `undefined` when there is no store or the store doesn't
  implement artifacts (never throws for that case — emission is
  best-effort by contract).
- `agentId?` (`string`, readonly): Agent ID (optional label)
- `appendTurns` (`(turns: SessionTurn[], options?: AppendTurnsOptions) => Promise<void>`): Append turns to in-memory history AND persist via store.appendTurns (if store present)
- `completeRun` (`(runId: string, stats: { cost?: number; tokensIn: number; tokensOut: number; turns: number; turnUsage?: TurnUsage[] }) => void`): Mark a run as completed
- `createdAt` (`number`, readonly): Creation time as epoch milliseconds.
- `errorRun` (`(runId: string, error: string, stats?: { cost?: number; tokensIn: number; tokensOut: number; turns: number; turnUsage?: TurnUsage[] }, detail?: RunErrorDetail) => void`): Mark a run as errored. Optional `stats` — same rationale as `abortRun.stats`.
- `generateTurnId` (`() => string | Promise<string>`): Generate a session-managed turn ID using store.generateTurnId if available, else crypto.randomUUID()
- `healInterruptedToolCalls` (`() => Promise<boolean>`): Close every tool call whose result was never persisted (the process died
  mid-tool) with a synthetic error result saying the tool may have
  partially run. Call it only once this process OWNS the session's next
  run (after the run claim, or where a single owner is guaranteed): while
  another process still runs the tool, its real result would land after
  the synthetic one and be dropped as an orphan. `loadSession` never does
  this.

  Persists through `store.mutateHistory` when available (re-derived from
  the stored turns, so turns appended by another writer survive), else a
  `store.save` guarded by a turn re-count. Best-effort: a failed persist
  keeps the in-memory heal, and never arms a bulk overwrite on the next
  `save()`. Resolves `true` when anything was closed in memory.
- `id` (`string`, readonly): Session ID
- `isEmpty` (`boolean`, readonly): True when this session has no turns yet.

  Use this as a first-prompt signal when setting up a run — e.g. writing initial
  configuration only on fresh sessions. Equivalent to `turns.length === 0`.
- `listArtifacts` (`(filter?: Pick<ArtifactFilter, 'kind' | 'since' | 'limit'>) => Promise<SessionArtifact[]>`): This session's artifacts, newest first. Empty when unsupported.
- `metadata` (`Record<string, unknown>`, readonly): Arbitrary metadata
- `onMeta` (`(listener: SessionMetaListener) => () => void`): Call `listener` after each `setMeta` write, synchronously, in subscription
  order. Returns the unsubscribe.
- `onSaved` (`(listener: SessionSavedListener) => () => void`): Call `listener` after each successful `save()`. Listeners run one after
  another in subscription order and `save()` awaits them; a rejection
  rejects `save()` and skips the later listeners. Returns the unsubscribe.
- `projectRoot?` (`string`, readonly): Project this session was created under — see `SessionData.projectRoot`.
  Set once on creation; surfaces here for read-only inspection.
- `runs` (`SessionRun[]`, readonly): All runs in this session
- `save` (`() => Promise<void>`): Persist the session to the store. When the store implements `saveMeta`,
  this writes the header, the changed metadata keys and the runs, and leaves
  turns alone, unless `setTurns` or `setRuns` replaced them. In every other
  case it overwrites the whole document through `store.save`. Throws when
  the session has no store.
- `saveMetaKeys?` (`(keys: readonly string[]) => Promise<void>`): Persist only these metadata keys (and the header and runs `saveMeta`
  carries), never the turns, even after `setTurns` replaced them: a small
  state change mid-run (a plan update) must not pay for a full rewrite.
  Falls back to `save()` on a store without `saveMeta`. Throws when the
  session has no store. Optional so hand-built sessions need not provide it.
- `setAgentId` (`(agentId: string) => void`): Re-label the session's agent profile (in memory; persisted by the
  next `save()`). Sessions remember their profile — pickers restore it
  on activation and other windows converge on it via `sessions.touch`.
- `setMeta` (`(key: string, value: unknown, options?: { touch?: boolean }) => void`): Set metadata key. `touch: false` keeps `updatedAt` (a rename or a favorite is not activity).
- `setRuns` (`(runs: SessionRun[]) => void`): Replace all runs in-memory (does not persist — use save() for that).
  Mirrors `setTurns` for the fork / restore case: callers that
  bootstrap a session from an externally-derived snapshot (e.g.
  `onForkTurn` copying parent runs into a child session) need this so
  the cloned runs land in `data.runs` before the first `save()`.
  Production agent runs continue to mutate runs via `startRun` /
  `completeRun` / `updateRun`; this is the bulk-replace escape hatch.
- `setTurns` (`(turns: SessionTurn[]) => void`): Replace all turns in-memory (does not persist — use save() for that)
- `startRun` (`(runId: string, prompt?: string, extras?: { agent?: SessionRunAgent; depth?: number; label?: string; parentCallId?: string; parentRunId?: string; resumedFromRunId?: string }) => void`): Start tracking a new run. `extras.parentRunId` + `extras.depth` are
  populated by the spawn tool when a child agent shares its parent's
  session; regular top-level `agent.run()` calls omit them.
- `status` (`'error' | 'running' | 'completed' | 'idle'`, readonly): Top-level session status
- `store?` (`SessionStore`, readonly): The backing store, when one was provided. Exposed so hosts can call
  its optional `close()` on teardown (e.g. release the SQLite handle).
- `toJSON` (`() => SessionData`): Serialize to SessionData
- `toMessages` (`(options?: ToWireMessagesOptions) => Promise<SessionMessage[]>`): Project the session's persisted turns into wire-ready messages safe to
  hand to any provider.

  The canonical answer to "I need to send these turns to Anthropic /
  OpenAI from outside `agent.run()`" (sub-agent classifier, side-channel
  summarizer, replay worker, …). Runs the same defensive pairing repair
  the agent loop applies just-in-time at wire-send time
  (`ensureToolResultPairing`), so orphan `tool_use` blocks and
  dangling `tool_result`s never reach the provider as the 400
  "tool_result must be preceded by a tool_call with the same toolCallId".

  Pass `provider` to also enforce the user-tail invariant
  (`ensureEndsWithUserMessage`) — required for opus 4.7 / o-series
  style models that reject assistant-prefill.

  Pure + idempotent. Resolves content refs only in the returned messages
  and does NOT mutate `session.turns`. **Do not write the result back via
  `setTurns` / `appendTurns`** — synthetic placeholders or resolved
  payloads will contaminate persisted history.
- `turns` (`SessionTurn[]`, readonly): Raw persisted turn history — kept pristine for fidelity (the model's
  reasoning chain stays intact across reloads).

  **DO NOT pass this directly to a provider request.** Mid-pair
  persistence (interrupted runs, partial-result writes, compaction
  boundaries) leaves orphan `tool_use` / `tool_result` blocks here that
  every provider 400s on (`tool_result must be preceded by a tool_call
  with the same toolCallId`). The agent's own loop repairs these
  just-in-time at wire-send time and never writes the repair back to
  `turns`.

  For provider requests built outside `agent.run()`, use
  `Session.toMessages` — it applies the same defensive pairing
  repair and resolves to a guaranteed-safe `SessionMessage[]`.
- `updatedAt` (`number`, readonly): Last update time as epoch milliseconds.
- `updateRun` (`(run: SessionRun, options?: UpdateRunOptions) => Promise<void>`): Persist an updated run record via store.updateRun (if store present)
- `updateStatus` (`(status: 'error' | 'running' | 'completed' | 'idle') => Promise<void>`): Update the session status in memory AND via store.updateStatus (if store present)

Source: [src/session/index.ts:784](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L784)

### `SessionArtifact`

Interface.

A recorded output of a session, such as a pull request, a branch, a commit or a file.

```ts
interface SessionArtifact
```

**Properties**

- `createdAt` (`number`): Creation time as epoch milliseconds.
- `id` (`string`): Stable id (uuid). Preserved across upserts of the same `(kind, uri)`.
- `kind` (`string`): Open kind string. Built-ins in [`ARTIFACT_KINDS`](https://zidane.si/docs/reference/session.md#ARTIFACT_KINDS).
- `label` (`string`): Human line. Always present — the rendering fallback for unknown kinds.
- `meta?` (`JsonValue`): Kind-specific payload, ≤ [`ARTIFACT_META_MAX_BYTES`](https://zidane.si/docs/reference/session.md#ARTIFACT_META_MAX_BYTES).
- `projectRoot?` (`string`): Project the artifact landed in, resolved at emission time from where
  the thing actually is (a worktree, a sibling repo), defaulting to the
  session's own `projectRoot`. Drives "everything produced from this
  repo" queries.
- `runId?` (`string`): Run that produced it. A `persist`-spawned subagent shares the parent
  session and records under its OWN run id (detection reaches it via the
  `child:tool:transform` bubble; see `presets/artifacts.ts`).
- `sessionId` (`string`): Id of the session the artifact belongs to.
- `turnId?` (`string`): Tool-result turn that produced it — the transcript anchor.
- `updatedAt` (`number`): Last update time as epoch milliseconds.
- `uri` (`string`): Canonical locator: PR URL, `git:<repo>#<branch>`, `file://<abs path>`, …

Source: [src/session/artifacts.ts:56](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L56)

### `SessionArtifactInput`

Interface.

What a producer hands to `session.addArtifact` / the metadata namespace.

```ts
interface SessionArtifactInput
```

**Properties**

- `kind` (`string`): Open kind string. See `ARTIFACT_KINDS` for the built-in ones.
- `label` (`string`): Human-readable line shown for the artifact.
- `meta?` (`JsonValue`): Kind-specific payload, at most `ARTIFACT_META_MAX_BYTES`.
- `projectRoot?` (`string`): Project the artifact landed in. Default: the session project.
- `uri` (`string`): Canonical locator of the artifact.

Source: [src/session/artifacts.ts:91](https://github.com/Tahul/zidane/blob/main/src/session/artifacts.ts#L91)

### `SessionData`

Interface.

Persisted form of a session: identity, turns, runs and metadata.

```ts
interface SessionData
```

**Properties**

- `agentId?` (`string`): Label of the agent profile that owns the session.
- `createdAt` (`number`): Creation time as epoch milliseconds.
- `id` (`string`): Session id.
- `metadata` (`Record<string, unknown>`): Free-form metadata, such as the title, model and extension state.
- `projectRoot?` (`string`): Absolute path of the project this session belongs to — typically
  the git root resolved from `cwd` at creation time, falling back to
  `cwd` itself when not in a git repo. Set ONCE on creation and never
  mutated thereafter (the session "belongs" to that project forever).

  Used by the TUI's sessions list to filter rows by current project,
  so the user only sees conversations relevant to where they are
  working — without needing one `.{prefix}/` directory per project.

  `undefined` on pre-tagging legacy sessions; the chat layer treats
  those as "untagged" and hides them unless `Settings.showAllProjects`
  is on.
- `runOwner?` (`RunOwner`): Process holding a `running` status, recorded by the store on every
  write that sets `running` and cleared with it. Store-managed: a value
  passed to `save` / `saveMeta` is ignored. Absent on markers older code
  wrote and on stores that keep none.
- `runs` (`SessionRun[]`): Records of the runs in this session.
- `status` (`'error' | 'running' | 'completed' | 'idle'`): Session state. A persisted `running` status is stale after a restart and is shown as `idle` on load.
- `turns` (`SessionTurn[]`): Turn history, oldest first.
- `updatedAt` (`number`): Last update time as epoch milliseconds.

Source: [src/session/index.ts:433](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L433)

### `SessionHistory`

Interface.

A session's turns and runs: what a history edit reads and writes (`SessionStore.mutateHistory`).

```ts
interface SessionHistory
```

**Properties**

- `runs` (`SessionRun[]`): The session's runs, in order.
- `turns` (`SessionTurn[]`): The session's turns, in order.

Source: [src/session/index.ts:425](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L425)

### `SessionMetaRow`

Interface.

Compact per-session summary returned by `SessionStore.listMeta`.
Raw facts only — presentation (title derivation, fallback strings)
stays in the chat layer (`listSessionMeta` / `deriveSessionTitle`).

```ts
interface SessionMetaRow
```

**Properties**

- `agentId?` (`string`): Header fields consumers like the daemon's `sessions.list` need to
  build full rows without a per-session `load()`. Optional (additive
  contract extension) — callers fall back to `load()` when absent.
- `artifactCount?` (`number`): Number of artifacts recorded for the session. Optional (additive):
  only stores implementing the artifact methods populate it; consumers
  treat absence as "unknown", not zero.
- `createdAt?` (`number`): Creation time as epoch milliseconds.
- `firstUserTurn?` (`SessionTurn`): First `role: 'user'` turn — the title-derivation source. Stores may
  omit it when `metadata.title` is already set (no derivation needed).
- `id` (`string`): Session id.
- `metadata` (`Record<string, unknown>`): Header metadata (may carry a stored `title`).
- `projectRoot?` (`string`): Absent for untagged (legacy) sessions.
- `rev?` (`number`): The store's change counter for this session: bumped by every write to
  it (in the write's own transaction), so it only grows, whatever
  `updatedAt` a writer stamps. Absent from stores that keep none.
- `runCount` (`number`): Number of run records.
- `runOwner?` (`RunOwner`): See `SessionData.runOwner`.
- `status?` (`'error' | 'running' | 'completed' | 'idle'`): Session state.
- `turnCount` (`number`): Number of turns.
- `updatedAt` (`number`): Last update time as epoch milliseconds.
- `userMessageCount` (`number`): Count of `role: 'user'` turns.

Source: [src/session/index.ts:479](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L479)

### `SessionRun`

Interface.

Record of one agent run inside a session.

```ts
interface SessionRun
```

**Properties**

- `agent?` (`SessionRunAgent`): The host's agent profile the run ran as (see `AgentRunOptions.runAgent`),
  so a transcript can name it per run. Absent when the host passes none.
- `cost?` (`number`): Estimated cost in USD
- `depth?` (`number`): Zero-based subagent depth. 0 = top-level run, 1 = direct child, …
  Recorded here so hosts can query/filter by level without walking the tree.
- `endedAt?` (`number`): End time as epoch milliseconds. Absent while the run is in progress.
- `error?` (`string`): Error message when the run failed.
- `errorDetail?` (`RunErrorDetail`): What `error`'s one line leaves out (the provider's own text, a limit's reset).
- `estimate?` (`RunEstimate`): How long the run is expected to take, when the host estimates runs
  (`Settings.runEstimates`). Saved with the run, so a window opened later
  shows the same remaining time. Absent on runs nobody estimated.
- `estimateFailure?` (`RunEstimateFailure`): Why the last estimate of this run failed, when it did. Absent otherwise.
- `id` (`string`): Run id.
- `label?` (`string`): Host-assigned display label (e.g. the spawn tool's `child-3`), persisted
  so label → run resolution is a stored fact instead of a positional
  guess. Positional `child-N` numbering can desynchronize from announced
  labels (concurrent spawns `startRun` out of reservation order, a
  pre-`startRun` throw advances the counter without a run, and reload
  ordering ties break lexicographically) — resolving by stored label is
  immune to all three. Absent on top-level runs and legacy sessions.
- `loopTurn?` (`number`): The loop turn of the run's last saved turn, kept by checkpoint runs only,
  in the same write as the turns: `Agent.resume` picks up from it. Turns
  a loop turn adds besides its assistant turn (a compaction's restored
  reads, a continue prompt) make a count of saved turns wrong.
- `options?` (`StoredRunOptions`): The run's own options as its `run()` had them: caps, model and effort.
  Kept by checkpoint runs only: `Agent.resume` runs with them, so a run
  resumed after a process death keeps its budget and model even when the
  settings changed meanwhile.
- `origin?` (`{ display?: string; kind: 'command' }`): How this run was initiated when not a plain user prompt. `command` marks
  a sub-run driven from a `/` command's `ctx.runAgent`; `display` is the
  short line the transcript shows instead of the full injected prompt
  (which often embeds bulky material like a diff). Presentation-only —
  the wire/model payload is unaffected. Absent on normal runs.
- `parentCallId?` (`string`): The `spawn` tool call that started this run, in the parent's turn (see
  `AgentRunOptions.parentCallId`): pairs a child with its call exactly,
  where parallel calls share a task. Absent on top-level runs and older
  sessions.
- `parentRunId?` (`string`): The run that spawned this one, when the agent is a subagent sharing its
  parent's session. Undefined on top-level `agent.run()`. Consumers can walk
  `runs` by `parentRunId` to reconstruct the subagent tree.
- `prompt` (`string`): Prompt the run started with.
- `resumable?` (`true`): Set while a run that keeps step checkpoints (a checkpoint durable
  runtime, `zidane/durable`) is in progress; the run clears it when it
  ends. A run that still carries it after a load died mid-run, and
  `Agent.resume` can continue it. Absent on every other run, including
  those of builds without durability.
- `resumedFromRunId?` (`string`): The prior run this one continues (see `AgentRunOptions.resumeRunId`).
  Set when a stopped/failed subagent run was resumed — consumers can
  chain runs by `resumedFromRunId` to render “child-4 (resumes child-2)”
  style markers. Absent on normal runs.
- `startedAt` (`number`): Start time as epoch milliseconds.
- `status` (`'error' | 'running' | 'completed' | 'aborted'`): Run state. A persisted `running` run whose process died is shown as `aborted` on load.
- `streamRetries?` (`SessionRunRetry[]`): The provider stream retries the run took (`stream:retry`), oldest first,
  up to the last 50: shows whether a failed run retried, how often, and on
  what. Absent on runs that never retried.
- `tokensIn?` (`number`): Input tokens used by the run.
- `tokensOut?` (`number`): Output tokens used by the run.
- `totalUsage?` (`TurnUsage`): Total usage across all turns
- `turns?` (`number`): Number of turns the run produced.
- `turnUsage?` (`TurnUsage[]`): Per-turn usage breakdown
- `userWait?` (`RunUserWait`): Time the run spent waiting on the user (an approval, a question), kept
  while its run is estimated: an estimate counts working time only.
- `wallMs?` (`number`): Wall time the run spent running, in ms, as of its last saved turn. Kept
  by checkpoint runs only: `Agent.resume` counts it toward
  `AgentBehavior.maxWallMs`, so a run's budget does not restart after a
  process death (the time the process was down is not counted).

Source: [src/session/index.ts:239](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L239)

### `SessionRunAgent`

Interface.

A host agent profile as a run record names it.

```ts
interface SessionRunAgent
```

**Properties**

- `id` (`string`): Profile id.
- `label` (`string`): Display name.

Source: [src/session/index.ts:417](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L417)

### `SessionRunRetry`

Interface.

One provider stream retry of a run (see `SessionRun.streamRetries`).

```ts
interface SessionRunRetry
```

**Properties**

- `attempt` (`number`): 1-based number of the failed attempt in its retry budget.
- `code?` (`string`): The first errno-style `code` on the error or its `cause` chain (`ECONNRESET`, `ConnectionClosed`).
- `delayMs` (`number`): The wait before the next attempt, in ms.
- `discardedText?` (`true`): The failed attempt streamed text that the retry dropped.
- `errorType?` (`string`): The thrown error's `name`.
- `maxAttempts` (`number`): That budget's size: `behavior.retry.maxAttempts`, or the transport budget for a connection failure.
- `message` (`string`): The failure's message, clipped.
- `statusCode?` (`number`): HTTP status, when the failure carried one.
- `turnId` (`string`): The turn whose stream failed.

Source: [src/session/index.ts:217](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L217)

### `SessionStore`

Interface.

Storage backend for sessions. Implement it to persist sessions somewhere else, or use a bundled store: memory, SQLite, remote or file map.

```ts
interface SessionStore
```

**Properties**

- `appendTurns` (`(sessionId: string, turns: SessionTurn[], options?: AppendTurnsOptions) => Promise<void>`): Append new turns to a session (incremental, avoids full re-save).
  `options` is for the store of a checkpoint durable runtime
  (`zidane/durable`); other stores ignore it.
- `claimRun?` (`(sessionId: string, options: { owner?: RunOwner; staleAt: number }) => Promise<{ claimed: boolean; previous?: 'error' | 'running' | 'completed' | 'idle' }>`): Optional: claim the session for a run in ONE atomic step — set `status`
  to `'running'` (and stamp `updatedAt`) unless the stored status already
  is `'running'` and its owner still holds it: written after
  `options.staleAt` by an owner not known dead (see `isRunOwnerDead`), or
  by an owner on this host that still runs (`isRunOwnerHolding`, up to
  `RUN_OWNER_HOLD_MS` of quiet). The claim records `options.owner`
  (default: this process) as the run owner. An unknown id is claimed
  without writing a row. `previous` is the status the claim replaced, for undoing a claim whose run never started.
  Callers without it fall back to a read-then-write check, which two
  processes can both pass. Stores with a lease epoch (SQLite's
  `sessions.run_epoch`) bump it on every claim.
- `claimScheduleFire?` (`(id: string, dueAt: number, next: number | null) => Promise<boolean>`): Optional: take a due fire, once across every host on this store.
- `claimSendRequest?` (`(sessionId: string, requestId: string, fingerprint: string) => Promise<SendRequestClaim>`): Optional: claim a client `requestId` for one of the session's sends, in
  ONE atomic insert-or-detect step (see `./send-requests.ts`). Resolves
  `{ claimed: true }` for the first claim, else the stored record (its
  `fingerprint` and, once settled, `result`). Bounded per session to
  `SEND_REQUEST_HISTORY` ids; `delete` drops a session's ids. Hosts
  without it deduplicate in process only, so a retry across a restart
  runs again.
- `close?` (`() => void`): Optional: release backend resources (e.g. close the SQLite handle).
  Hosts should call this on teardown when present; stores backed by
  memory or remote HTTP have nothing to release and may omit it.
- `delete` (`(sessionId: string) => Promise<void>`): Delete a session.
- `deleteArtifact?` (`(sessionId: string, artifactId: string) => Promise<void>`): Optional: remove one artifact. Unknown ids are a no-op.
- `deleteSchedule?` (`(id: string) => Promise<boolean>`): Optional: remove a schedule and its fires.
- `findSendRequest?` (`(sessionId: string, requestId: string) => Promise<SendRequestRecord | undefined>`): Optional: the record of a claimed `requestId`, without claiming it; `undefined` when it holds none.
- `generateSessionId?` (`() => string | Promise<string>`): Optional: generate a session ID server-side (e.g. a database-issued UUID).
- `generateTurnId?` (`() => string | Promise<string>`): Optional: generate a turn ID server-side for session-managed user turns.

  Provider-loop assistant and tool-result turns use AgentClock.randomUUID()
  instead, so durable-execution adapters can keep those IDs replay-stable.
- `getMeta?` (`(sessionId: string) => Promise<SessionMetaRow | null>`): Optional: one session's header row WITHOUT materializing turns.
  Hot-path reads (the cross-process run guard checks `status` +
  `updatedAt` on every prompt send) must not pay a multi-MB turn parse
  on long sessions. Callers fall back to `load()` when absent.
- `getTurns` (`(sessionId: string, from?: number, limit?: number) => Promise<SessionTurn[]>`): Return a slice of turns for a session.
- `list` (`(filter?: { agentId?: string; limit?: number; projectRoot?: string | null }) => Promise<string[]>`): List session IDs, optionally filtered. `projectRoot` restricts to
  sessions whose `SessionData.projectRoot` matches exactly — untagged
  (legacy) sessions are NOT returned under that filter; pass `null`
  explicitly to ask for untagged ones, or omit the field to ignore
  the axis entirely. `agentId` filters by recorded agent; the two
  conditions AND together when both are set.
- `listArtifacts?` (`(filter?: ArtifactFilter) => Promise<SessionArtifact[]>`): Optional: list artifacts, newest `updatedAt` first. `filter.sessionId`
  scopes to one session; `filter.projectRoot` follows the tri-state
  contract of `list`; omit both for every artifact in the store.
- `listMeta?` (`(filter?: { agentId?: string; limit?: number; projectRoot?: string | null }) => Promise<SessionMetaRow[]>`): Optional: list session summaries without materializing full sessions.

  The generic fallback (`listSessionMeta` in `zidane/chat`) is `list()`

  - `load()` per id — O(every turn in the store) because each `load`
    parses the full turn history just to count rows and derive a title.
    Stores that can answer from an index (SQL aggregates, a denormalized
    header) should implement this; the sqlite store turns a multi-hundred-
    ms TUI boot cost into a single query.

  Same filter contract and ordering (most recently updated first) as
  `list`.
- `listScheduleFires?` (`(scheduleId: string, limit: number) => Promise<ScheduleFire[]>`): Optional: a schedule's fires, newest first.
- `listSchedules?` (`() => Promise<Schedule[]>`): Optional: every stored schedule (`./schedules.ts`). A store without the schedule methods has no schedules.
- `load` (`(sessionId: string) => Promise<SessionData | null>`): Load a session by ID. Returns null if not found.
- `mutateHistory?` (`(sessionId: string, mutate: (history: SessionHistory) => SessionHistory | null) => Promise<'ok' | 'no-session' | 'unchanged'>`): Optional: rewrite a session's history as ONE atomic read-modify-write,
  so turns appended between the read and the write can't be erased (a
  `load()` + `save()` pair can). `mutate` gets the current turns and
  runs and returns the next ones, or `null` to leave them untouched; a
  throw inside it aborts without writing. Runs are written only when it
  returns a different `runs` array. History edits (delete / edit a turn,
  rewind) use it when present and fall back to `load()` + `save()`.
- `mutateTurns?` (`(sessionId: string, mutate: (turns: SessionTurn[]) => SessionTurn[] | null) => Promise<'ok' | 'no-session' | 'unchanged'>`, deprecated): Optional: the turns-only form of `SessionStore.mutateHistory`.
  When `mutateHistory` is absent, edits that leave the runs alone (delete
  or edit a turn) use it; a rewind falls back to `load()` + `save()`.

  **Deprecated.** Implement `mutateHistory` instead.
- `patchMeta?` (`(sessionId: string, patch: Record<string, unknown>) => Promise<void>`): Optional: merge `patch` into the stored metadata (an `undefined` value
  removes the key) without touching any other header column — `status`
  and `updatedAt` stay as they are, so the write can't undo a concurrent
  run marker. Unknown ids are a no-op.
- `recordScheduleFire?` (`(fire: ScheduleFire) => Promise<void>`): Optional: store a fire.
- `save` (`(session: SessionData) => Promise<void>`): Save a session (create or update, full document). Bulk overwrite:
  turns and runs on disk are replaced by the caller's snapshot, so a
  stale snapshot silently discards anything another writer appended.
  `Session.save()` only routes here when the turn list was explicitly
  replaced (`setTurns` / `setRuns` / load-time heal); everything else
  goes through `saveMeta`.

  Neither `save` nor `saveMeta` moves `status` into `running`:
  only `updateStatus` and `claimRun` do, and record the run
  owner. A snapshot's `running` keeps the stored status (`idle` for a
  new row) and the stored owner; a settled status is written as given.
- `saveMeta?` (`(session: SessionData, options?: SaveMetaOptions) => Promise<void>`): Optional: persist header fields only (agentId, projectRoot, status,
  metadata, timestamps) plus an UPSERT of the runs present in the
  snapshot — never deletes turns or runs. This is what metadata writes
  (title, model, todos, extension state) need.

  Scope of the guarantee: TURNS appended by a concurrent process survive.
  The header itself is still last-writer-wins — `agentId` and a settled
  `status` are written wholesale from the caller's snapshot (a snapshot's
  `running` is not, see `save`), and so is `metadata`
  unless `options.metaKeys` names the changed keys (`Session.save()`
  always does; stores merge them with [`mergeSessionMetadata`](https://zidane.si/docs/reference/session.md#mergeSessionMetadata)), and every run in the snapshot is upserted as-is (a reader
  that loaded through `loadSession` carries the in-memory stale-running
  demotion, so it can write `idle` / `aborted` over a live owner's marker
  until the owner's next write). Readers that only need one key should
  prefer a keyed patch (the host's `patchSessionMeta`) over `save()`.
  Stores that omit it fall back to `save`, keeping the historical bulk
  semantics.
- `saveSchedule?` (`(schedule: Schedule, options?: { keepNextFireAt?: boolean }) => Promise<void>`): Optional: create or replace a schedule.
- `settleSendRequest?` (`(sessionId: string, requestId: string, result: JsonValue) => Promise<void>`): Optional: store the claimed send's `result`; `null` drops the claim. Unknown ids are a no-op.
- `updateRun` (`(sessionId: string, run: SessionRun, options?: UpdateRunOptions) => Promise<void>`): Persist a run record (called at run start with `status: 'running'`, and
  after completeRun / abortRun / errorRun).

  **Upsert semantics required.** The agent's run lifecycle calls
  `session.startRun()` to register a new run in IN-MEMORY state and
  then `updateRun()`. The store doesn't see the run before the first
  `updateRun()` — `appendTurns` persists turns but not runs. An
  update-only implementation would silently drop every newly-created
  run record, leaving
  the on-disk `runs[]` missing entries that the persisted turns
  reference (which `eventsFromTurns` then mis-classifies via
  `ancestryOf` → broken depth, broken `child-N` labels, broken
  `userPrompts` history).

  Implementations MUST insert when the id doesn't exist and update
  when it does. The bundled `sqlite` / `memory` / `file-map` stores
  all follow this contract; remote stores using HTTP PUT get upsert
  for free via REST semantics.
- `updateStatus` (`(sessionId: string, status: 'error' | 'running' | 'completed' | 'idle', options?: { owner?: RunOwner; preserveUpdatedAt?: boolean }) => Promise<void>`): Update the top-level status of a session.

  `preserveUpdatedAt` (default false) writes the status WITHOUT
  touching `updatedAt` — for pure-read heals (`loadSession`'s stale
  `'running'` demotion) that must not reorder recency-sorted session
  lists. Best-effort contract: stores that don't manage `updatedAt`
  themselves (memory, file-map, remote) may ignore the option; the
  sqlite store honors it.

  `'running'` records `options.owner` (default: this process) as
  `SessionData.runOwner`; any other status clears it.
- `upsertArtifact?` (`(artifact: SessionArtifact) => Promise<SessionArtifact | null>`): Optional: record an artifact (see `./artifacts.ts`). Upsert on
  `(sessionId, kind, uri)` — an existing row keeps its `id` / `runId` /
  `turnId` / `createdAt` and refreshes `label` / `meta` / `updatedAt`
  (`mergeArtifact`). Resolves to the canonical stored row. Unknown
  session ids are a no-op returning `null` (no ghost headers; same
  posture as `updateRun`). Implementations enforce
  `ARTIFACTS_PER_SESSION_CAP` via `selectArtifactEvictions`.

  Every artifact method is optional: stores that don't implement them
  (file-map, remote, third-party) keep working — `Session.addArtifact`
  degrades to a no-op and listings come back empty.

Source: [src/session/index.ts:532](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L532)

### `StoredRunOptions`

Interface.

What a checkpoint run keeps of its options (`SessionRun.options`).

```ts
interface StoredRunOptions
```

**Properties**

- `behavior?` (`Pick<AgentBehavior, 'maxTurns' | 'maxTokens' | 'maxCostUsd' | 'maxTotalTokens' | 'maxWallMs' | 'retry' | 'resumeOnMaxTokens'>`): The run's caps (`AgentBehavior`): turn, token, cost and wall-clock ceilings, retries, max-tokens resume.
- `model?` (`string | (string | ModelTargetSpec)[]`): The run's explicit `model` (`AgentRunOptions.model`). Absent when the run used the agent's default.
- `modelOptions?` (`Record<string, boolean>`): The run's `modelOptions` (`AgentRunOptions.modelOptions`), when set.
- `thinking?` (`'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'adaptive'`): The run's `thinking` level (`AgentRunOptions.thinking`), when set.

Source: [src/session/index.ts:405](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L405)

### `ToWireMessagesOptions`

Interface.

Options for [`toWireMessages`](https://zidane.si/docs/reference/session.md#toWireMessages).

```ts
interface ToWireMessagesOptions
```

**Properties**

- `onRepair?` (`(repair: PairingRepair) => void`): Pairing-repair telemetry. Fires once per repair the underlying
  [`ensureToolResultPairing`](https://zidane.si/docs/reference/index.md#ensureToolResultPairing) pass performs. Mirrors the
  `pairing:repair` hook surface so consumers can wire structured logging
  without an agent instance.
- `provider?` (`Provider`): When set, also runs [`ensureEndsWithUserMessage`](https://zidane.si/docs/reference/index.md#ensureEndsWithUserMessage) so the result is
  safe for assistant-prefill-rejecting models (opus 4.7, o-series
  translation layers). Omit to skip — pure pairing repair only.
- `resolveContentRef?` (`ResolveContentRef`): Resolve persisted content references into provider-native payload bytes.
  The returned data is wire-only and must not be written back to turns.
- `userTailDirective?` (`string`): Override for the synthetic user-tail directive. Only consulted when
  `provider` is set. Defaults to `DEFAULT_USER_TAIL_DIRECTIVE`.

Source: [src/session/messages.ts:925](https://github.com/Tahul/zidane/blob/main/src/session/messages.ts#L925)

### `UpdateRunOptions`

Interface.

Options of `SessionStore.updateRun`.

```ts
interface UpdateRunOptions
```

**Properties**

- `pruneSteps?` (`{ runId: string }`): Drop every durable record of run `runId` (steps, stream partials,
  awakeables) in the same write as the run record: a run that can no
  longer be resumed. Honoured by the stores of checkpoint durable
  runtimes, like `AppendTurnsOptions.pruneSteps`; other stores
  ignore it.

Source: [src/session/index.ts:205](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L205)

## Types

### `PairingDropKind`

Type.

Classification of a single deletion [`dropInvalidToolPairings`](https://zidane.si/docs/reference/session.md#dropInvalidToolPairings)
performed. Surfaced via `onDrop` for telemetry.

```ts
type PairingDropKind =
  | 'duplicate-tool-call'
  | 'duplicate-tool-result'
  | 'orphan-tool-result'
  | 'empty-turn'
```

Source: [src/session/messages-pairing.ts:465](https://github.com/Tahul/zidane/blob/main/src/session/messages-pairing.ts#L465)

### `SendRequestClaim`

Type.

What `claimSendRequest` answers: the id is now this caller's, or the record that already holds it.

```ts
type SendRequestClaim =
  | { claimed: true }
  | { claimed: false; record: SendRequestRecord }
```

Source: [src/session/send-requests.ts:33](https://github.com/Tahul/zidane/blob/main/src/session/send-requests.ts#L33)

### `SessionMetaListener`

Type.

A `Session.onMeta` listener: the key and value `setMeta` just wrote.

```ts
type SessionMetaListener = (key: string, value: unknown) => void
```

Source: [src/session/index.ts:776](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L776)

### `SessionSavedListener`

Type.

A `Session.onSaved` listener, awaited by `save()`.

```ts
type SessionSavedListener = () => void | Promise<void>
```

Source: [src/session/index.ts:779](https://github.com/Tahul/zidane/blob/main/src/session/index.ts#L779)

## Re-exports

These names are documented on the entry point that owns them.

- [zidane/types](https://zidane.si/docs/reference/types.md): [`SessionContentBlock`](https://zidane.si/docs/reference/types.md#SessionContentBlock), [`SessionMessage`](https://zidane.si/docs/reference/types.md#SessionMessage), [`SessionTurn`](https://zidane.si/docs/reference/types.md#SessionTurn)
