# How Open Notebook Handles Chat Session Management and Message History Persistence

> Discover how Open Notebook manages chat sessions and message history. Learn about its dual-storage architecture using SurrealDB and SQLite for persistent chat data.

- Repository: [Luis Novo/open-notebook](https://github.com/lfnovo/open-notebook)
- Tags: deep-dive
- Published: 2026-06-22

---

**Open Notebook implements a dual-storage architecture where chat session metadata lives in SurrealDB while the actual message history persists in a SQLite-backed LangGraph checkpoint system.**

Open Notebook is an open-source knowledge management platform that separates conversation metadata from message content to optimize for both query performance and stateful AI interactions. This article examines how the repository handles chat session management and message history persistence by analyzing the actual source code implementation in `lfnovo/open-notebook`.

## Chat Session Architecture Overview

The architecture cleanly separates **session metadata** from **message content**. Session records contain only lightweight metadata such as titles, timestamps, and model overrides, while the chronological message flow lives in LangGraph's checkpointed state graph.

This design provides durable session tracking through SurrealDB while enabling fast, resumable conversation state via SQLite checkpointing.

### Session Metadata Storage in SurrealDB

The `ChatSession` class defined in [`open_notebook/domain/notebook.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/domain/notebook.py) represents the persistent session record:

```python
class ChatSession(ObjectModel):
    table_name: ClassVar[str] = "chat_session"
    nullable_fields: ClassVar[set[str]] = {"model_override"}
    title: Optional[str] = None
    model_override: Optional[str] = None

```

When creating a new session via the `POST /chat/sessions` endpoint, the system:

1. Instantiates a `ChatSession` object
2. Persists it to SurrealDB via `await session.save()`
3. Creates a `refers_to` edge linking the session to its parent notebook via `await session.relate_to_notebook(request.notebook_id)`

### Message History Persistence in LangGraph

Unlike session metadata, the actual conversation history is **not** stored in SurrealDB. Instead, it resides in the LangGraph `ThreadState`, defined in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py):

```python
class ThreadState(TypedDict):
    messages: Annotated[list, add_messages]
    notebook: Optional[Notebook]
    context: Optional[str]
    context_config: Optional[dict]
    model_override: Optional[str]

```

The graph persists this state using `SqliteSaver` connected to a dedicated checkpoint file:

```python
conn = sqlite3.connect(LANGGRAPH_CHECKPOINT_FILE, check_same_thread=False)
memory = SqliteSaver(conn)
...
graph = agent_state.compile(checkpointer=memory)

```

This configuration ensures that conversation state survives process restarts while maintaining ACID properties through SQLite.

## Creating and Managing Chat Sessions

New sessions are created through the API layer in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py). The endpoint handles the dual-write pattern: creating the SurrealDB record while initializing the LangGraph thread state.

The client interaction follows this pattern:

```typescript
import { chat_service } from '@/lib/api/chat';

await chat_service.create_session(notebookId, {
  title: 'Research Q&A',
  model_override: 'gpt-4o-mini',
});

```

Behind the scenes, this generates a unique thread ID that serves as the bridge between the SurrealDB session record and the LangGraph checkpoint.

## Retrieving Session History

When fetching a session via `GET /chat/sessions/{session_id}`, the system reconstructs the full conversation by combining both storage layers:

```python
thread_state = await asyncio.to_thread(
    chat_graph.get_state,
    config=RunnableConfig(configurable={"thread_id": full_session_id}),
)

```

The endpoint extracts the `messages` list from `thread_state.values["messages"]` and transforms it into the `ChatSessionWithMessagesResponse` model. This delivers the complete message history alongside the session metadata.

Client-side retrieval follows this pattern:

```typescript
const session = await chat_service.get_session(sessionId);
// `session.messages` now contains HumanMessage / AIMessage objects

```

## Executing Chat Turns

The `/chat/execute` endpoint manages the message flow through the LangGraph state machine. The implementation in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) follows this sequence:

1. Load the current LangGraph state for the session using `chat_graph.get_state`
2. Append the new `HumanMessage` to the state values
3. Invoke the graph's `agent` node via `chat_graph.invoke`
4. Persist the updated session timestamp to SurrealDB

The server-side handling looks like this:

```python

# 1️⃣ Load current state

current_state = await asyncio.to_thread(
    chat_graph.get_state,
    config=RunnableConfig(configurable={"thread_id": full_session_id}),
)

# 2️⃣ Prepare state values

state_values = current_state.values if current_state else {}
state_values["messages"] = state_values.get("messages", [])
state_values["messages"].append(HumanMessage(content=request.message))

# 3️⃣ Run the LangGraph node (model call)

result = chat_graph.invoke(
    input=state_values,
    config=RunnableConfig(
        configurable={"thread_id": full_session_id,
                      "model_id": model_override},
    ),
)

# 4️⃣ Persist session timestamp

await session.save()

```

The `agent` node in [`open_notebook/graphs/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/graphs/chat.py) handles the actual LLM interaction, building the system prompt, sending the full message list to the selected model, cleaning the response, and returning an `AIMessage`.

## Utility Functions and Message Counting

For list views requiring message counts without loading full histories, [`open_notebook/utils/graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/open_notebook/utils/graph_utils.py) provides optimized access to the LangGraph checkpoint data. This utility reads the SQLite checkpoint directly to return message counts efficiently, avoiding the overhead of deserializing full conversation states.

## Summary

- **Dual-storage architecture**: Session metadata lives in SurrealDB tables (`chat_session`), while message histories persist in SQLite via LangGraph checkpoints.
- **LangGraph state management**: The `ThreadState` TypedDict defines the conversation structure, with `SqliteSaver` providing durable persistence through `LANGGRAPH_CHECKPOINT_FILE`.
- **API bridging**: Endpoints in [`api/routers/chat.py`](https://github.com/lfnovo/open-notebook/blob/main/api/routers/chat.py) coordinate between SurrealDB records and LangGraph state using thread IDs as the linkage key.
- **Immutable history**: Messages are appended to the checkpoint state and never modified, ensuring complete conversation audibility.
- **Performance optimization**: The [`graph_utils.py`](https://github.com/lfnovo/open-notebook/blob/main/graph_utils.py) module provides lightweight message counting for list views without loading full state.

## Frequently Asked Questions

### Where is the chat message history actually stored in Open Notebook?

The message history is stored in a SQLite database file configured via `LANGGRAPH_CHECKPOINT_FILE`, managed by LangGraph's `SqliteSaver` class. This is separate from the SurrealDB instance that stores session metadata. The SQLite file contains serialized `ThreadState` objects that include the `messages` list.

### How does Open Notebook link chat sessions to notebooks?

When creating a session, the system creates a `refers_to` edge in SurrealDB via the `relate_to_notebook` method on the `ChatSession` class. This graph relationship connects the session record to its parent notebook while maintaining the session's independent lifecycle in the checkpoint system.

### Can chat sessions survive application restarts?

Yes. Because LangGraph uses `SqliteSaver` with a persistent SQLite connection, the conversation state survives process restarts. The `ChatSession` record in SurrealDB maintains the thread ID required to retrieve the checkpointed state when the application resumes.

### What happens when a user sends a new message?

The `/chat/execute` endpoint retrieves the current checkpoint state, appends a `HumanMessage` to the `messages` list, invokes the LangGraph agent node to generate a response, and stores the resulting `AIMessage` back into the checkpoint. The session's `updated` timestamp in SurrealDB is also refreshed to reflect activity.