Checkpointing and Message History Persistence in Open Notebook Chat Sessions

Open Notebook persists every chat session using LangGraph checkpointing stored in a local SQLite database, enabling message history to survive process restarts through thread-isolated state management.

The lfnovo/open-notebook repository implements durable chat history by combining LangGraph's checkpointing system with SQLite persistence. This architecture ensures that every message, context override, and model response is automatically preserved across API restarts and container redeployments.

SQLite Checkpoint Configuration

The checkpoint file location is defined in open_notebook/config.py, which points to a persistent SQLite database at data/sqlite-db/checkpoints.sqlite. This file stores the serialized state of all active and historical chat sessions, making it the single source of truth for conversation continuity.

LangGraph State Graph Setup

The chat workflow is declared in open_notebook/graphs/chat.py, where a LangGraph state graph manages the conversation flow. Lines 88-92 instantiate a SqliteSaver with a connection to the checkpoint file:


# Conceptual excerpt from open_notebook/graphs/chat.py

memory = SqliteSaver.from_conn_string(Config.LANGGRAPH_CHECKPOINT_FILE)

The graph (agent_state) is compiled with checkpointer=memory, instructing LangGraph to read and write session state from that SQLite saver. This binding ensures that every state transition is automatically persisted to disk.

Session Isolation via Thread IDs

When a request hits the /chat endpoints, the session identifier is passed as the LangGraph thread_id. In api/routers/chat.py (lines 119-124), this key isolates each conversation's state inside the same SQLite file, preventing cross-contamination between different chat sessions. This mechanism allows multiple conversations to share one database while maintaining strict separation of their message histories.

Message Persistence Flow

Each time the model is invoked via call_model_with_messages in graphs/chat.py, the returned AIMessage is stored back into the messages field of the state dictionary. Because the SqliteSaver persists this state after every graph step, the full message list survives process restarts and can be reconstructed on subsequent API calls. The persistence happens automatically within the LangGraph framework without requiring explicit database writes in the business logic.

Querying Message History

To retrieve conversation metadata without loading entire message payloads, open_notebook/utils/graph_utils.py provides get_session_message_count(). This helper fetches the current messages list from the checkpoint via a thread-safe call to graph.get_state and returns its length. The API uses this in api/routers/chat.py (lines 112-114) to report message_count on session resources efficiently.

Implementation Examples

The following examples demonstrate how to interact with persisted chat sessions using the Open Notebook API.

Creating a New Session

import httpx

# Create a session for notebook "nb-123"

resp = httpx.post(
    "http://localhost:5055/chat/sessions",
    json={"notebook_id": "nb-123", "title": "Research Q&A"},
)
session = resp.json()
print(session["id"])      # → e.g. "chat_session:abcd1234"

The session ID is automatically prefixed with chat_session: because the router uses that format when persisting the state.

Sending Messages

msg = {
    "session_id": session["id"],
    "message": "What are the key findings from the latest PDF?",
    "context": {},                # optional source/note context

}
resp = httpx.post("http://localhost:5055/chat/execute", json=msg)
print(resp.json()["messages"][-1]["content"])

Behind the scenes, graphs/chat.py builds the prompt, provisions the model, and stores the new AIMessage in the checkpoint.

Retrieving Full Message History

resp = httpx.get(f"http://localhost:5055/chat/sessions/{session['id']}")
history = resp.json()["messages"]
for m in history:
    print(f"{m['type']}: {m['content']}")

The router calls graph.get_state (via utils/graph_utils.get_session_message_count) to pull the persisted messages list from the SQLite checkpoint.

Getting Message Counts

resp = httpx.get("http://localhost:5055/chat/sessions", params={"notebook_id": "nb-123"})
for s in resp.json():
    print(f"Session {s['id']}{s['message_count']} messages")

The count is calculated without loading the entire message payload, using the helper in open_notebook/utils/graph_utils.py.

Key Implementation Files

Summary

  • SQLite-backed persistence: All chat states are stored in data/sqlite-db/checkpoints.sqlite as defined in open_notebook/config.py.
  • LangGraph integration: The SqliteSaver checkpoint is bound to the state graph in open_notebook/graphs/chat.py (lines 88-92), ensuring automatic state serialization.
  • Session isolation: Each chat session uses a unique thread_id passed via api/routers/chat.py (lines 119-124) to isolate conversations within the shared database.
  • Automatic message storage: Model outputs are persisted through the call_model_with_messages flow without manual database operations.
  • Efficient metadata queries: The get_session_message_count() utility in open_notebook/utils/graph_utils.py provides thread-safe access to conversation length without loading full histories.

Frequently Asked Questions

Where are Open Notebook chat sessions stored?

Chat sessions are persisted in a SQLite database located at data/sqlite-db/checkpoints.sqlite, as configured in open_notebook/config.py. This file contains the LangGraph checkpoints for all conversation states, ensuring durability across process restarts.

How does Open Notebook isolate different chat sessions?

The system uses LangGraph's thread_id parameter to isolate sessions. In api/routers/chat.py (lines 119-124), each session's unique identifier is passed as the thread_id, which keys the state in the SQLite checkpoint file. This allows multiple conversations to coexist in one database without state leakage.

Can message history survive a container restart?

Yes. Because checkpoints are written to a persistent SQLite file on disk rather than held in memory, the full message history survives Docker container restarts, API process restarts, and host machine reboots. The SqliteSaver ensures state is committed to disk after each graph step.

How does the API retrieve message counts without loading full histories?

The API uses get_session_message_count() from open_notebook/utils/graph_utils.py, which calls graph.get_state with the session's thread_id and returns only the length of the messages list. This approach, utilized in api/routers/chat.py (lines 112-114), avoids the overhead of deserializing entire conversation histories when only metadata is needed.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →