DocsGuidesPersistence

Persistence

Thread persistence keeps conversations alive across page refreshes, browser restarts, and server deployments. This guide covers configuring checkpointers on the Python side and wiring up thread management in your Angular components with streamResource().

How it works

LangGraph checkpoints agent state at every super-step. Each checkpoint is keyed by a thread ID. streamResource() connects to these checkpoints automatically, so your users resume exactly where they left off — even if your server restarted between sessions.

Python: Checkpointer Setup

Every LangGraph agent needs a checkpointer to persist state between invocations. The checkpointer you choose depends on your environment.

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, END, MessagesState, StateGraph
from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-5-mini")
 
def call_model(state: MessagesState) -> dict:
    return {"messages": [llm.invoke(state["messages"])]}
 
builder = StateGraph(MessagesState)
builder.add_node("model", call_model)
builder.add_edge(START, "model")
builder.add_edge("model", END)
 
# MemorySaver stores checkpoints in-process memory
# Fast for development — lost when the process restarts
graph = builder.compile(checkpointer=MemorySaver())
Production checkpointers

MemorySaver is for development only — all state vanishes when the process exits. For anything users depend on, use PostgresSaver. SqliteSaver is a middle ground for prototypes and single-server deployments where you need persistence without a database.

Python: Thread IDs in Graph Invocation

The thread ID is how LangGraph associates a conversation with its checkpoint history. Pass it in the configurable dict every time you invoke the graph:

# First message creates the thread
result = graph.invoke(
    {"messages": [{"role": "user", "content": "What is LangGraph?"}]},
    config={"configurable": {"thread_id": "user_123"}}
)
 
# Second message continues the same conversation
result = graph.invoke(
    {"messages": [{"role": "user", "content": "How does it handle state?"}]},
    config={"configurable": {"thread_id": "user_123"}}
)
# The agent sees both messages — the full history is restored from the checkpoint
Thread ID strategy

Use stable, user-scoped identifiers for thread IDs. A common pattern is f"{user_id}_{session_id}" — this prevents cross-user data leaks and lets one user have multiple conversations.

Angular: Basic Thread Persistence

Save the thread ID to localStorage so conversations survive page refreshes. streamResource() handles thread creation and restoration automatically.

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { signal } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
 
@Component({
  selector: 'app-chat',
  templateUrl: './chat.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChatComponent {
  chat = streamResource<{ messages: BaseMessage[] }>({
    assistantId: 'chat_agent',
    // Restore thread from localStorage on mount
    threadId: signal(localStorage.getItem('threadId')),
    // Persist thread ID whenever a new thread is created
    onThreadId: (id) => localStorage.setItem('threadId', id),
  });
 
  send(text: string) {
    this.chat.submit({ messages: [{ role: 'user', content: text }] });
  }
}

Angular: Thread-List Component

A real chat application needs a sidebar showing all conversations. Here is a full thread-list component that manages multiple threads alongside your chat resource.

import { ChangeDetectionStrategy, Component, signal, computed } from '@angular/core';
import { streamResource } from '@cacheplane/stream-resource';
 
interface Thread {
  id: string;
  title: string;
  updatedAt: Date;
}
 
@Component({
  selector: 'app-thread-list',
  templateUrl: './thread-list.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ThreadListComponent {
  threads = signal<Thread[]>(this.loadThreads());
  activeThreadId = signal<string | null>(null);
 
  chat = streamResource<{ messages: BaseMessage[] }>({
    assistantId: 'chat_agent',
    threadId: this.activeThreadId,
    onThreadId: (id) => {
      this.activeThreadId.set(id);
      this.addThread(id, 'New conversation');
    },
  });
 
  activeThread = computed(() =>
    this.threads().find((t) => t.id === this.activeThreadId())
  );
 
  selectThread(id: string) {
    this.activeThreadId.set(id);
  }
 
  newConversation() {
    this.chat.switchThread(null);
    // A new thread ID is assigned on the next submit
  }
 
  private addThread(id: string, title: string) {
    this.threads.update((list) => [
      { id, title, updatedAt: new Date() },
      ...list.filter((t) => t.id !== id),
    ]);
    this.saveThreads();
  }
 
  private loadThreads(): Thread[] {
    return JSON.parse(localStorage.getItem('threads') ?? '[]');
  }
 
  private saveThreads() {
    localStorage.setItem('threads', JSON.stringify(this.threads()));
  }
}

Reactive Thread Switching

When you pass a Signal as threadId, streamResource() reacts to every change. Set the signal and the conversation switches automatically — no imperative calls needed.

activeThreadId = signal<string | null>(null);
 
chat = streamResource<{ messages: BaseMessage[] }>({
  assistantId: 'chat_agent',
  threadId: this.activeThreadId,  // Signal — switches reactively
  onThreadId: (id) => this.activeThreadId.set(id),
});
 
// Clicking a thread in the sidebar triggers a reactive switch
selectThread(id: string) {
  this.activeThreadId.set(id);
  // streamResource detects the signal change, fetches the thread's
  // checkpoint from the server, and updates all derived signals
}
Thread loading state

Use the isThreadLoading() signal to show a skeleton UI while streamResource() fetches checkpoint state from the server. This avoids a flash of empty content when switching threads.

Manual Thread Switching

Use switchThread() for imperative thread changes. This is useful when you want to explicitly control when the switch happens — for example, after an animation completes or a modal closes.

// Start a fresh conversation (null = new thread on next submit)
newConversation() {
  this.chat.switchThread(null);
}
 
// Jump to a specific thread
loadConversation(threadId: string) {
  this.chat.switchThread(threadId);
}
 
// Fork a conversation — create a new thread from current state
forkConversation() {
  this.chat.switchThread(null);
  this.chat.submit({
    messages: this.chat.messages(),
  });
}

Checkpoint Recovery

When a connection drops mid-stream, joinStream() reconnects to an in-progress run without restarting the agent. This prevents duplicate work and lost tokens.

// Rejoin a running stream after a network interruption
await chat.joinStream(runId, lastEventId);
// Picks up from the last event — no duplicate agent execution
Automatic recovery

In most cases streamResource() handles reconnection internally. Use joinStream() directly only when you need explicit control — for example, when restoring a run ID from a URL parameter after a full page reload.

Thread Lifecycle

1
Component mounts

streamResource() reads the threadId signal. If it contains a value, the existing thread's checkpoint is fetched from the server.

2
User sends first message

If threadId is null, streamResource() creates a new thread via the LangGraph API and fires onThreadId with the new ID.

3
Agent streams response

Each super-step is checkpointed server-side. The messages() signal updates in real time as events arrive.

4
User switches threads

Setting the threadId signal (or calling switchThread()) loads the target thread's latest checkpoint. All signals update to reflect the restored state.

5
Connection drops

joinStream() reconnects to the in-progress run. The agent does not restart — streaming resumes from the last received event.

What's Next