How Open Notebook Uses SqliteSaver to Persist Chat Message History
The Open Notebook chat service persists conversation history by wiring LangGraph's SqliteSaver as a checkpointer when compiling state graphs, storing each session's messages in a local SQLite file isolated by thread ID while wrapping synchronous operations for async FastAPI compatibility.
Open Notebook is an open-source knowledge management system that implements conversational AI using LangGraph state machines. The chat service uses SqliteSaver to automatically checkpoint every message exchange to a local SQLite database, ensuring durable storage without external database dependencies. This implementation manages both generic chat flows and source-aware conversations through the same persistence mechanism.
Configuring the SQLite Checkpoint File
The checkpoint database location is defined centrally in open_notebook/config.py. The configuration specifies data/sqlite-db/checkpoints.sqlite as the storage path through the LANGGRAPH_CHECKPOINT_FILE constant.
# open_notebook/config.py
LANGGRAPH_CHECKPOINT_FILE = "data/sqlite-db/checkpoints.sqlite"
The application ensures this directory exists at startup, creating a dedicated SQLite file for all LangGraph checkpoints across the application.
Initializing SqliteSaver in LangGraph Graphs
Both the standard chat graph (open_notebook/graphs/chat.py) and the source-aware variant (open_notebook/graphs/source_chat.py) initialize the checkpoint system identically. They establish a raw SQLite connection and wrap it with SqliteSaver.
# 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)
The check_same_thread=False parameter is required because the connection may be accessed across different threads when wrapped in async executors later.
Compiling the State Graph with SqliteSaver
After defining the graph structure using StateGraph, the saver is attached during compilation via the checkpointer parameter. This wires the SQLite persistence into every state transition.
# open_notebook/graphs/chat.py
from langgraph.graph import StateGraph, START, END
from open_notebook.graphs.state import ThreadState
agent_state = StateGraph(ThreadState)
agent_state.add_node("agent", call_model_with_messages)
agent_state.add_edge(START, "agent")
agent_state.add_edge("agent", END)
graph = agent_state.compile(checkpointer=memory)
Once compiled, the graph object automatically writes checkpoint data—including the messages list—to SQLite after each node execution.
Session Isolation via Thread IDs
Open Notebook isolates conversations by mapping each chat session to a unique thread ID. The session ID follows the format chat_session:<uuid>, which is passed through RunnableConfig when interacting with the graph.
config = RunnableConfig(configurable={"thread_id": full_session_id})
SqliteSaver uses this thread_id as the primary key for checkpoint rows, ensuring that distinct chat sessions never collide in the database. Even when the same graph instance processes multiple conversations, the thread ID maintains separate state histories.
Retrieving Stored Message History
The FastAPI endpoint GET /chat/sessions/{session_id} reads historical messages by querying the checkpoint store. Since SqliteSaver operates synchronously, the call is wrapped in asyncio.to_thread to prevent blocking the async event loop.
# api/routers/chat.py
thread_state = await asyncio.to_thread(
chat_graph.get_state,
config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
messages = []
if thread_state and thread_state.values and "messages" in thread_state.values:
for msg in thread_state.values["messages"]:
messages.append(
ChatMessage(
id=getattr(msg, "id", ""),
type=msg.type,
content=msg.content,
)
)
The returned thread_state.values["messages"] contains the full list of HumanMessage and AIMessage objects stored from previous interactions.
Persisting New Messages Through the Graph
When processing new user input via POST /chat/execute, the service appends the incoming message to the existing state and invokes the graph. The SqliteSaver automatically persists the updated message list, including the AI's response.
# api/routers/chat.py
# 1. Retrieve current state
current_state = await asyncio.to_thread(
chat_graph.get_state,
config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
# 2. Prepare mutable state with new message
state_vals = current_state.values if current_state else {}
state_vals.setdefault("messages", []).append(HumanMessage(content=request.message))
# 3. Invoke graph (automatically checkpoints to SQLite)
result = await asyncio.to_thread(
chat_graph.invoke,
input=state_vals,
config=RunnableConfig(
configurable={"thread_id": full_session_id, "model_id": model_override}
),
)
The invoke call executes the call_model_with_messages node, generates an AIMessage, and writes the complete updated history to checkpoints.sqlite through the configured SqliteSaver.
Handling Async Operations with Synchronous SqliteSaver
SqliteSaver is intentionally synchronous, while Open Notebook uses FastAPI's async architecture. The codebase bridges this gap by executing all checkpoint operations—get_state, invoke, and update—inside asyncio.to_thread or ThreadPoolExecutor pools.
This design pattern guarantees that:
- The FastAPI server remains fully asynchronous and non-blocking
- SQLite operations execute safely on background threads
- No complex async database drivers are required for checkpoint persistence
The utility function get_session_message_count in open_notebook/utils/graph_utils.py follows the same pattern, wrapping synchronous checkpoint reads for async consumption.
Summary
- SqliteSaver persists chat history to
data/sqlite-db/checkpoints.sqliteas defined inopen_notebook/config.py - Both
chat.pyandsource_chat.pyinitialize the saver by wrapping asqlite3connection withcheck_same_thread=False - Graphs compile with
checkpointer=memoryto enable automatic state checkpointing - Thread IDs (
chat_session:<uuid>) isolate sessions in the database viaRunnableConfig - The API wraps synchronous
get_stateandinvokecalls inasyncio.to_threadfor FastAPI compatibility - Message history flows through
thread_state.values["messages"]containing LangChain message objects
Frequently Asked Questions
Where is the chat history physically stored in Open Notebook?
The chat history is stored in a local SQLite file located at data/sqlite-db/checkpoints.sqlite, as configured by the LANGGRAPH_CHECKPOINT_FILE constant in open_notebook/config.py. This file contains serialized checkpoints for all LangGraph state transitions, including the message lists for every chat session.
How does Open Notebook prevent different chat sessions from overwriting each other?
Each chat session is assigned a unique thread ID in the format chat_session:<uuid>, which is passed to the graph via RunnableConfig(configurable={"thread_id": full_session_id}). SqliteSaver uses this thread ID as the primary key for checkpoint rows, ensuring complete isolation between sessions in the same SQLite database.
Why does the Open Notebook codebase use asyncio.to_thread with SqliteSaver operations?
SqliteSaver provides only synchronous APIs for reading and writing checkpoints, while Open Notebook runs on FastAPI's asynchronous framework. The developers wrap every SqliteSaver operation—such as get_state and invoke—in asyncio.to_thread to execute blocking database calls on background threads without stalling the async event loop.
Can I replace SqliteSaver with a different checkpoint backend in Open Notebook?
While the current implementation hardcodes SqliteSaver in both open_notebook/graphs/chat.py and open_notebook/graphs/source_chat.py, LangGraph supports alternative checkpointers like PostgresSaver. Replacing the backend would require modifying the connection initialization and imports in those graph definition files while maintaining the same checkpointer interface passed to graph.compile().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →