# Durability

Journal every turn to a store, reopen a session by ID after a restart, and retry a prompt by request ID without repeating its side effects.

## Overview

A durable session keeps each turn's prompt, model steps, tool receipts, and subagent tree in a store. A native Rust runtime journals to `sessions.jsonl` in the nanocode home (`~/.nanocode` unless `NANOCODE_HOME` is set). nanocode-wasm keeps no state between instances and writes every session through a `SqlHost`.

A prompt sent with a request ID is idempotent. If the process stops during the turn, the same request ID and prompt recover the turn. After the turn finishes, they return the recorded result instead of a new model request.

#### Rust

```rust
use nanocode::PromptRequest;

// Reopen a saved session and send a prompt that is safe to repeat
let (agent, _events) = runtime.open_session(&session_id).await?;
let request =
    PromptRequest::new("Draft a five-step release checklist.")
        .request_id("checklist-1");
let result = agent.prompt(request).await?.result().await?;
```

#### TypeScript

```ts
// Reopen the session from the same database, then submit a prompt that
// is safe to repeat
await runtime.open(
  { ...config, scope: 'owner', session_id: savedSessionId },
  { dispatch },
)
await runtime.submit('owner', {
  metadata: {},
  prompt: 'Draft a release checklist.',
  request_id: 'checklist-1',
})
```

## Walkthrough

The steps below create a session on the first run and reopen it on the next run. The Rust steps follow `crates/nanocode/examples/durable_session.rs`. The TypeScript steps use the `module`, `config`, and `dispatch` values from [Getting Started](/docs/introduction/getting-started).

### Open a Store

In Rust, `JsonlStore::open_async` opens or creates a JSONL journal at a path, and `session_store` wraps it in the `SessionStore` that the runtime uses. In TypeScript, `SqlHost.transaction` runs one SDK operation's statements in one atomic transaction and returns each statement's rows as arrays.

#### Rust

```rust title="src/main.rs"
use nanocode::{
    Codex, JsonlStore, Model, Nanocode, OpenAi, PromptRequest, Tools,
};

type Result<T> =
    std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;

#[tokio::main]
async fn main() -> Result<()> {
    // The journal lives in the current directory instead of the
    // nanocode home
    let store = nanocode::session_store(
        JsonlStore::open_async("sessions.jsonl").await?,
    )?;
```

#### TypeScript

```ts title="sql.ts"
import { DatabaseSync, type SQLInputValue } from 'node:sqlite'
import type { SqlHost, SqlValue } from 'nanocode-wasm'

export const db = new DatabaseSync('agent.sqlite')

export const sql: SqlHost = {
  transaction(_kind, _operation, statements) {
    // 1. Take the write lock for the whole operation
    db.exec('BEGIN IMMEDIATE')
    try {
      // 2. Run each statement and return its rows as arrays
      const rows = statements.map(([query, ...values]) => {
        const statement = db.prepare(query)
        statement.setReturnArrays(true)
        return statement.all(
          ...(values as SQLInputValue[]),
        ) as unknown as SqlValue[][]
      })
      db.exec('COMMIT')
      return rows
    } catch (error) {
      // 3. Leave the database as it was before the operation
      db.exec('ROLLBACK')
      throw error
    }
  },
}
```

### Build the Runtime

In Rust, `.durability(store)` selects one store for every root agent and its subagents. Keep a clone of the store to read sessions back. In TypeScript, `instantiate` takes the SQL host, and the SDK creates its `states`, `owners`, `records`, `turns`, and `events` tables on first use.

#### Rust

```rust title="src/main.rs"
// The runtime registers the Codex and Claude checkpoint formats on the
// store
let runtime = Nanocode::builder()
    .openai(OpenAi::default())
    .tools(Tools::builder().without_defaults().build()?)
    .without_cua()
    .runner_management(false)
    .durability(store.clone())
    .for_backend::<Codex>()
    .model(Model::Luna)
    .build_async()
    .await?;
```

#### TypeScript

```ts title="host.ts"
import { AgentRuntime, sessionUuid } from 'nanocode-wasm'
import { sql } from './sql.js'

// One instance over the database that holds every session
const runtime = await AgentRuntime.instantiate(module, sql, {
  published: () => {},
})
```

### Create or Reopen

In Rust, `open_session(id)` reopens a session. If two processes open the same session, the later one fences the earlier one and resumes the pending turns. In TypeScript, open with the same `scope` and `session_id`, and the next turn replays the stored history.

#### Rust

```rust title="src/main.rs"
    // Reopen the session named on the command line, or start a new one
    let (agent, _events) = match std::env::args().nth(1) {
        Some(id) => runtime.open_session(&id).await?,
        None => runtime.spawn().build().await?,
    };
    eprintln!("session {}", agent.session_id());
```

#### TypeScript

```ts title="host.ts"
// Reopen the session ID from the command line, or start a new one
const sessionId = process.argv[2] ?? sessionUuid()
await runtime.open(
  { ...config, scope: 'owner', session_id: sessionId },
  { dispatch },
)
console.error('session', sessionId)
```

### Send the Prompt

Send the prompt with a stable request ID. In TypeScript, `submit` returns `existing` for a request ID and prompt that the session already holds, and `conflict` for the same request ID with a different prompt.

#### Rust

```rust title="src/main.rs"
// A repeated request ID returns the recorded result
let request =
    PromptRequest::new("Draft a five-step release checklist.")
        .request_id("checklist-1");
println!(
    "{}",
    agent.prompt(request).await?.result().await?.final_message()
);
```

#### TypeScript

```ts title="host.ts"
// 1. "accepted" on the first run, "existing" on the next
const { submitted } = await runtime.submit('owner', {
  metadata: {},
  prompt: 'Draft a five-step release checklist.',
  request_id: 'checklist-1',
})

// 2. Drive the turn until nothing is pending, as in Getting Started
const next = (await runtime.pending('owner'))[0]
if (next) await runtime.drive(next.report.request_id)
```

### Read the Session

In Rust, `agent.flush()` waits for every pending write, and `store.load(id)` reads the saved session with its turns. In TypeScript, read sessions through the runtime, such as with `page` and `turn`, since the SDK owns the table layout.

#### Rust

```rust title="src/main.rs"
    // Wait for the journal, then count the saved turns
    agent.flush().await?;
    let saved = store.load(agent.session_id()).await?;
    eprintln!("{} turns persisted", saved.turns.len());

    runtime.shutdown().await?;
    Ok(())
}
```

#### TypeScript

```ts title="host.ts"
// The turn report and the logged events survive the restart
console.log((await runtime.turn('owner', 'checklist-1'))?.report)
const { events } = await runtime.page('owner', 0, 100)
await runtime.dispose()
```

Run the program once, then pass the printed session ID. The second run returns the result recorded for `checklist-1` instead of prompting the model again.

```bash
# 1. Create the session and run checklist-1 (or: node host.ts)
cargo run
# 2. Reopen it, and checklist-1 returns its recorded result (or: node host.ts SESSION_ID)
cargo run -- SESSION_ID
```

## Recipes

### Replay Rules

A resumed turn replays the journal instead of repeating work. The SDK applies these rules under both consumers.

* **Receipts:** A tool call with a committed receipt replays its recorded output.
* **Interrupted calls:** A call that started without a receipt reaches the model as an unknown outcome and never runs again.
* **Pending turns:** A reopened session resumes pending turns in accepted order before the first new prompt.
* **Blocking:** In Rust, a pending turn blocks a different prompt until it is resumed or cancelled.

```rust
// A committed lookup receipt replays, so the tool does not run again
let (agent, _events) = runtime.open_session(&id).await?;
```

### Resume Pending Turns

In Rust, `store.load(id)` lists each turn with its status, and `decode_durable_prompt` decodes a pending turn's stored input to run it under its original request ID. In TypeScript, `pending` lists unfinished turns in admission order, and `drive` runs each one.

#### Rust

```rust
use nanocode::PromptRequest;
use nanocode::durability::TurnStatus;

// Replay each interrupted turn under its own ID before new work
let pending: Vec<_> = store
    .load(&id)
    .await?
    .turns
    .into_iter()
    .filter(|turn| turn.status == TurnStatus::Pending)
    .collect();
let (agent, _events) = runtime.open_session(&id).await?;
for turn in pending {
    let prompt = agent.decode_durable_prompt(turn.input)?;
    agent
        .prompt(PromptRequest::new(prompt).request_id(turn.id))
        .await?
        .result()
        .await?;
}
```

#### TypeScript

```ts
// Drive every turn that the earlier instance left unfinished
for (const turn of await runtime.pending('owner'))
  await runtime.drive(turn.report.request_id)
```

In Rust, `PromptRequest::cancel_on_admission()` records a pending turn as cancelled without a model request. In TypeScript, `cancel` does the same for a queued turn.

### Save a Checkpoint

Checkpoints are Rust only. A `SessionCheckpoint` holds a session's model, thinking level, and state. `to_json` and `SessionCheckpoint::from_json` move it between processes, and `runtime.resume` rebuilds the agent from it.

```rust
use nanocode::SessionCheckpoint;

// 1. Save the agent's checkpoint as JSON
let json = agent.checkpoint().await?.to_json()?;

// 2. Later, rebuild the agent from the saved checkpoint
let (agent, _events) =
    runtime.resume(SessionCheckpoint::from_json(&json)?).await?;
```

A finished turn also returns its checkpoint through `TurnResult::checkpoint`, as an `Option`.

### Pick a Store

The store list is Rust only. nanocode-wasm always stores sessions through its `SqlHost`. Every Rust store implements the `StateStore` contract. Payloads are content-addressed with BLAKE3 and chunked, so a save writes only the chunks that changed.

| Store           | Crate                     | Use                                                                  |
| --------------- | ------------------------- | -------------------------------------------------------------------- |
| `JsonlStore`    | `nanocode`                | One append-only journal with a rebuildable SQLite index              |
| `SqliteStore`   | `nanocode-store-sqlite`   | A local SQLite database                                              |
| `PostgresStore` | `nanocode-store-postgres` | Shared servers                                                       |
| `SqlStore`      | `nanocode::durability`    | Any SQLite host, such as Durable Objects, D1, libSQL, or sqlite-wasm |
| `KeyValueStore` | `nanocode::durability`    | Any key-value store with linearizable compare-and-swap               |
| `MemoryStore`   | `nanocode::durability`    | Tests and short-lived sessions, with no persistence                  |

```rust
use nanocode::durability::MemoryStore;

// An in-memory store loses every session when the process exits
let runtime = Nanocode::builder()
    .openai(OpenAi::default())
    .durability(nanocode::session_store(MemoryStore::new()?)?)
    .build_async()
    .await?;
```

### Bring a Database

Both consumers run the SDK's SQL over a host that the application supplies. The host runs the statements with text parameters in one atomic transaction and returns each statement's rows. The SDK owns every table and statement, so a host writes no schema or queries.

#### Rust

```rust
use nanocode::durability::{
    Sql, SqlRow, SqlStatement, SqlStore, StoreError, StoreFuture,
};

struct Database;

impl Sql for Database {
    // Run every statement in one transaction and return each
    // statement's rows
    fn transaction<'a>(
        &'a self,
        _operation: &'static str,
        statements: &'a [SqlStatement<'a>],
    ) -> StoreFuture<'a, Result<Vec<Vec<SqlRow>>, StoreError>> {
        Box::pin(async move {
            todo!("run {} statements atomically", statements.len())
        })
    }
}

let store = nanocode::session_store(SqlStore::new(Database))?;
```

`KeyValueStore` runs over a `KeyValue` host with `get`, `put_values`, `compare_and_swap`, and an optional `list_keys`. Compare-and-swap must be linearizable per key, so Cloudflare KV cannot back it.

#### TypeScript

* **`kind`:** `store` for the SDK's `SqlStore` (checkpoints and records) or `log` for its `SessionLog` (turns and events).
* **`operation`:** The SDK's name for the operation. Use it for metrics or tracing.
* **`statements`:** An array of `[sql, ...params]` tuples. The SDK binds only text parameters.
* **Return value:** One `SqlValue[][]` per statement. Every cell must be text or `null`, and any other value throws.

```ts
// The whole storage contract, as nanocode-wasm declares it
interface SqlHost {
  readonly value_limit?: number
  transaction(
    kind: SqlKind,
    operation: string,
    statements: Statement[],
  ): SqlValue[][][] | Promise<SqlValue[][][]>
}
```

### Use the Browser

This recipe is TypeScript only. In a page, use sqlite-wasm. An in-memory database lasts as long as the page, and the `exec` options return rows as arrays.

```ts title="sql.ts"
import sqlite3InitModule from '@sqlite.org/sqlite-wasm'
import type { SqlHost, SqlValue } from 'nanocode-wasm'

const sqlite3 = await sqlite3InitModule()
const database = new sqlite3.oo1.DB(':memory:', 'c')

// Runs each SDK transaction against the in-memory database, all or
// nothing
export const sql: SqlHost = {
  transaction(_kind, _operation, statements) {
    database.exec('BEGIN IMMEDIATE')
    try {
      const rows = statements.map(
        ([query, ...bind]) =>
          database.exec({
            bind: bind as never,
            returnValue: 'resultRows',
            rowMode: 'array',
            sql: query,
          }) as SqlValue[][],
      )
      database.exec('COMMIT')
      return rows
    } catch (error) {
      database.exec('ROLLBACK')
      throw error
    }
  },
}
```

### Use Durable Objects

A Durable Object's SQLite storage satisfies `SqlHost` synchronously. `transactionSync` makes the statements atomic, and `raw()` returns rows as arrays. Rust hosts reach the same storage through `SqlStore`.

```ts title="sql.ts"
import type { SqlHost, SqlValue } from 'nanocode-wasm'

// Runs inside the Durable Object, where ctx is its DurableObjectState
export function sql(ctx: DurableObjectState): SqlHost {
  return {
    transaction(_kind, _operation, statements) {
      return ctx.storage.transactionSync(() =>
        statements.map(
          ([query, ...values]) =>
            [
              ...ctx.storage.sql.exec(query, ...values).raw(),
            ] as SqlValue[][],
        ),
      )
    },
  }
}
```

> [!NOTE]
> nanocode-wasm states that `transactionSync` satisfies the contract, but the package has no Cloudflare code. This snippet is not from its sources. Check the storage limits that Cloudflare documents before you set `value_limit`.

### Limit Value Size

`value_limit` is TypeScript only. It is the largest string, BLOB, or row that the database stores, in bytes, and `instantiate` sends it to the module before any other command.

```ts
// The database stores values of up to 2 MB
const sql: SqlHost = { transaction, value_limit: 2_000_000 }
```

With a limit set, the SDK keeps each write within it:

* **Events:** An event above the limit is logged as an `omitted` marker with its `source`, `event_type`, and `bytes`.
* **Results:** A turn whose result is above the limit fails.
* **Prompts:** `submit` returns `{ submitted: 'too_large', bytes, limit }` for a prompt that does not fit.
* **Imports:** `import` writes its turns in batches within the limit.

### Count Transactions

`runtime.metrics` is TypeScript only. It counts the transactions of each kind, which shows how much storage traffic a turn makes.

```ts
// Store and log transactions since the instance started
const { log_calls, store_calls } = runtime.metrics
```

## More

- [Tools](/docs/guides/tools): How tool receipts replay after a restart.

- [Cloudflare Workers](/docs/guides/workers): Run sessions in Durable Objects.

- [nanocode on Cloudflare](/cloudflare/): Reset a Durable Object mid-turn in a live demo.

- [nanocode on Vercel](/vercel): Kill a Function mid-turn in a live demo.

- [session_store](/docs/api/rust/session_store): Reference for wrapping a store for the runtime.

- [Agent](/docs/api/rust/Agent): Reference for flush, checkpoint, and decode_durable_prompt.

- [Nanocode](/docs/api/rust/Nanocode): Reference for durability, open_session, and resume.

- [SqlHost](/docs/api/wasm/SqlHost): Reference for the TypeScript storage contract.

- [page](/docs/api/wasm/page): Read logged events from the database.
