# How SqliteSaver Checkpoints and Persists Chat Message History in Open-Notebook

> Discover how Open-Notebook leverages SqliteSaver to checkpoint and persist chat message history in SQLite, ensuring reliable conversation threads without manual database management.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: how-to-guide
- Published: 2026-06-24

---

**Open-Notebook uses LangGraph's `SqliteSaver` to automatically serialize chat message history to a SQLite database file, enabling persistent conversation threads across API requests without manual database management.**

Open-Notebook leverages LangGraph's checkpointing system to maintain persistent chat histories across user sessions. The implementation utilizes `SqliteSaver` from the `langgraph.checkpoint.sqlite` module to store thread states—including the complete message list—in a local SQLite database. This approach ensures that every state transition in the chat workflow is automatically persisted and can be retrieved using a unique thread identifier.

## Configuration and Database Location

The SQLite checkpoint file path is defined centrally in the configuration module. This ensures all graph instances reference the same persistent storage location.

In [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py), the checkpoint file is configured to live within the data folder:

```python

# open_notebook/config.py

LANGGRAPH_CHECKPOINT_FILE = f"{DATA_FOLDER}/sqlite-db/checkpoints.sqlite"

```

The application creates this directory at startup using `os.makedirs`, guaranteeing that `./data/sqlite-db/checkpoints.sqlite` exists before any graph operations begin. This single SQLite file holds all thread states for the entire application.

## Instantiating the SqliteSaver

Each chat graph that requires persistence constructs a `SqliteSaver` instance using a shared SQLite connection. The connection is opened with `check_same_thread=False` to allow compatibility with LangGraph's internal threading model.

In [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py), the saver is instantiated as follows:

```python

# open_notebook/graphs/chat.py

import sqlite3
from langgraph.checkpoint.sqlite import SqliteSaver
from open_notebook.config import LANGGRAPH_CHECKPOINT_FILE

conn = sqlite3.connect(LANGGRAPH_CHECKPOINT_FILE, check_same_thread=False)
memory = SqliteSaver(conn)  # ← checkpointer instance

```

The same pattern appears in [`open_notebook/graphs/source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/source_chat.py), ensuring consistent persistence behavior across different chat workflows. This connection object becomes the bridge between the graph's state and the SQLite database.

## Compiling the Graph with Checkpointing

Persistence is enabled by passing the `SqliteSaver` instance to the graph's `compile` method. This wires the graph so that **every state transition triggers an automatic database write**.

From [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py):

```python

# open_notebook/graphs/chat.py

from langgraph.graph import StateGraph, START, END

agent_state = StateGraph(ThreadState)

# ... nodes and edges defined here ...

graph = agent_state.compile(checkpointer=memory)  # ← persistence enabled

```

Once compiled with the checkpointer, the graph automatically calls `SqliteSaver.save_state` (internal to LangGraph) after each node execution. This serializes the entire state object—including the `messages` list—into the SQLite database under the corresponding thread identifier.

## Automatic State Serialization

When a chat node processes a message, it returns a dictionary containing the updated state. LangGraph's runtime merges this into the current state and immediately persists it to SQLite.

Inside the chat node (`call_model_with_messages`), updating the message history looks like this:

```python

# Inside open_notebook/graphs/chat.py node function

state_values["messages"].append(HumanMessage(content=request.message))

# Returning the updated dict triggers the checkpoint save

return {"messages": state_values["messages"]}

```

The `SqliteSaver` captures the complete `ThreadState`, ensuring that the full conversation history—including all previous Human and AI messages—is preserved after each interaction.

## Retrieving Persisted Chat History

API endpoints retrieve saved history by calling `graph.get_state`. Because `SqliteSaver` provides only a **synchronous** `get_state` method, Open-Notebook wraps these calls using `asyncio.to_thread` to prevent blocking the FastAPI event loop.

In [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py), the retrieval pattern is:

```python

# api/routers/chat.py

import asyncio
from langchain_core.runnables import RunnableConfig

thread_state = await asyncio.to_thread(
    chat_graph.get_state,
    config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
messages = thread_state.values["messages"]  # Complete chat history

```

A similar utility pattern appears in [`open_notebook/utils/graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py) for counting stored messages:

```python

# open_notebook/utils/graph_utils.py

thread_state = await asyncio.to_thread(
    graph.get_state,
    config=RunnableConfig(configurable={"thread_id": session_id}),
)
message_count = len(thread_state.values["messages"])

```

## Thread Isolation and Session Management

The key to multi-session persistence is the **`thread_id`** parameter passed within the `RunnableConfig`. This identifier scopes all checkpoints to specific chat sessions, allowing multiple concurrent conversations to maintain independent histories within the same SQLite file.

When a new chat session starts, a unique `thread_id` is generated. All subsequent calls to `get_state` or graph invocations using this ID access the exact same checkpointed state, effectively restoring the conversation context from the database.

## Summary

Open-Notebook implements persistent chat history through LangGraph's checkpointing mechanism:

- **Configuration**: The SQLite file path is defined in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py) as `LANGGRAPH_CHECKPOINT_FILE`
- **Instantiation**: `SqliteSaver` is created with a `sqlite3` connection in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) and [`source_chat.py`](https://github.com/lfnovo/open-notebook/blob/main/source_chat.py)
- **Compilation**: The graph is compiled with `checkpointer=memory` to enable automatic state serialization
- **Persistence**: Each node return triggers `SqliteSaver.save_state`, storing the complete message list under the thread ID
- **Retrieval**: API endpoints use `asyncio.to_thread` with `graph.get_state` to fetch history asynchronously
- **Isolation**: The `thread_id` parameter ensures separate conversation histories coexist in the same database file

Because the saver operates at the graph layer, no additional manual database code is required to maintain chat history.

## Frequently Asked Questions

### Where is the chat history physically stored in Open-Notebook?

The chat history is stored in a SQLite database file located at `./data/sqlite-db/checkpoints.sqlite` relative to the application root. This path is defined in [`open_notebook/config.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/config.py) and created automatically at startup. All thread states and message histories persist to this single file.

### How does Open-Notebook handle the synchronous SqliteSaver in an async FastAPI application?

Open-Notebook uses `asyncio.to_thread` to execute the synchronous `get_state` method of `SqliteSaver` in a separate thread pool. This pattern appears in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) and [`open_notebook/utils/graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py), preventing the SQLite operations from blocking the main event loop while maintaining async compatibility.

### What triggers the actual database write when saving chat messages?

The database write is triggered automatically by LangGraph's runtime when a node returns an updated state dictionary. In [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py), when the `call_model_with_messages` node returns `{"messages": updated_list}`, the compiled graph (which was initialized with `checkpointer=memory`) automatically invokes the internal `save_state` method to serialize the thread state to SQLite.

### Can multiple chat sessions use the same SqliteSaver instance?

Yes, multiple chat sessions share the same `SqliteSaver` instance and SQLite file. Session isolation is maintained through the `thread_id` parameter in the `RunnableConfig`. Each unique `thread_id` scopes its own checkpoint history within the database, allowing concurrent sessions to operate independently without data collision.