How the Open Notebook Chat Graph Maintains Message History and Notebook Context Across Sessions

The Open Notebook chat system uses LangGraph with a SQLite checkpoint (SqliteSaver) to persist thread-isolated state, storing messages and notebook references keyed by the session's SurrealDB record ID.

The lfnovo/open-notebook repository implements a sophisticated conversational AI layer that preserves full dialogue history and notebook context across disconnected HTTP requests. By leveraging LangGraph's stateful graph architecture and SQLite-based persistence, the system ensures that every chat session maintains continuity without requiring in-memory state management.

Thread-Isolated State Architecture

The foundation of cross-session persistence lies in the ThreadState schema defined in open_notebook/graphs/chat.py. This TypedDict structure encapsulates everything required to reconstruct a conversation's context:

class ThreadState(TypedDict):
    messages: Annotated[list, add_messages]   # Stored chat history

    notebook: Optional[Notebook]              # Linked notebook object

    context: Optional[str]                    # Optional raw context string

    context_config: Optional[dict]            # Context-generation configuration

    model_override: Optional[str]             # Per-session model selection

Each chat session receives a unique thread ID derived from the SurrealDB record ID of the chat_session object (e.g., chat_session:abc123). This identifier serves as the primary key for state retrieval from the checkpoint store.

SQLite Checkpoint Persistence

The chat graph is compiled with a SqliteSaver backend that writes state to a local SQLite file specified by LANGGRAPH_CHECKPOINT_FILE. When the API endpoint /chat/execute in api/routers/chat.py receives a request, it reconstructs the conversation state by querying this checkpoint:


# Retrieve persisted state for this specific thread

current_state = await asyncio.to_thread(
    chat_graph.get_state,
    config=RunnableConfig(configurable={"thread_id": full_session_id}),
)
state_values = current_state.values if current_state else {}
state_values["messages"] = state_values.get("messages", [])
state_values["notebook"] = notebook
state_values["context"] = request.context

This mechanism ensures that even if the server restarts between requests, the full message history (as LangChain HumanMessage and AIMessage objects) remains available in the SQLite database.

Notebook Context Injection

Beyond message history, the system persists notebook references across sessions. When a chat session is created, the notebook relationship is recorded in SurrealDB via session.relate_to_notebook. During each /chat/execute call, the API loads the notebook object and injects it into the state:

state_values["notebook"] = notebook

Because the entire ThreadState dictionary is checkpointed after each graph invocation, the notebook reference survives indefinitely. The system prompt template ([Chat/system]) can embed this notebook metadata, allowing the LLM to reference specific research materials throughout the conversation.

Message Processing and State Updates

The graph consists of a single node named "agent" that executes call_model_with_messages. This function receives the current ThreadState, constructs a system prompt with notebook context, and invokes the LLM:

ai_message = model.invoke(payload)  # LLM generates response

cleaned_message = ai_message.model_copy(
    update={"content": cleaned_content}
)
return {"messages": cleaned_message}  # State mutation

When graph.invoke completes, LangGraph automatically writes the updated state—including the new AIMessage—back to the SQLite checkpoint. Subsequent requests for the same thread_id retrieve this augmented message list, creating a seamless conversational experience.

Querying History Without Mutation

The utility function in open_notebook/utils/graph_utils.py enables read-only inspection of message counts without modifying state:

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

This helper supports the /chat/sessions endpoint, which reports message_count for each session by reading directly from the checkpoint store.

Summary

  • LangGraph with SQLite checkpointing (SqliteSaver) provides durable persistence for chat state across server restarts and HTTP requests.
  • ThreadState in open_notebook/graphs/chat.py encapsulates messages, notebook references, and configuration in a thread-isolated structure.
  • Thread IDs derived from SurrealDB chat_session record IDs ensure each conversation maintains separate state.
  • Automatic checkpointing occurs after every graph invocation, persisting new AI messages and maintaining full conversation history.
  • Notebook context survives across sessions because the notebook object is stored within the checkpointed state and reinjected on each request.

Frequently Asked Questions

How does the chat graph handle server restarts without losing conversation history?

The system uses a SQLite checkpoint file (LANGGRAPH_CHECKPOINT_FILE) managed by LangGraph's SqliteSaver. Since state persists to disk rather than memory, restarting the server preserves all thread states. When the /chat/execute endpoint receives a request, it loads the existing state from this SQLite database using the session's thread_id, ensuring conversations resume exactly where they left off.

Can multiple chat sessions access the same notebook simultaneously?

Yes. Each chat session maintains its own thread-isolated state with a unique thread_id, but multiple sessions can reference the same notebook object. The notebook is injected into each session's ThreadState independently, allowing different conversations to explore the same research materials while maintaining separate message histories and context configurations.

What happens if a chat session references a notebook that gets deleted?

The checkpoint store maintains a snapshot of the notebook object within the ThreadState at the time of each graph invocation. However, the system typically loads the notebook fresh from SurrealDB on each request in api/routers/chat.py. If the notebook is deleted, the next request would fail to load the object and likely raise an error during state preparation, though previously checkpointed states would retain the last known notebook reference.

How does the system count messages without loading the entire conversation?

The get_message_count utility in open_notebook/utils/graph_utils.py uses graph.get_state to retrieve only the state metadata for a specific thread_id, then checks the length of the messages list within state_values. This operation reads from the SQLite checkpoint without invoking the full graph or mutating state, providing an efficient way to report message counts for the /chat/sessions listing endpoint.

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 →