Checkpoints are not generating new Checkpoint Ids #183

Closed
opened 2026-02-15 17:16:54 -05:00 by yindo · 1 comment
Owner

Originally created by @RussellCanfield on GitHub (Mar 2, 2025).

Checked other resources

  • This is a bug, not a usage question. For questions, please use GitHub Discussions.
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

import { Annotation, MemorySaver } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
import { END, START, StateGraph } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";
import type { RunnableConfig } from "@langchain/core/runnables";

const model = new ChatOpenAI({ model: "gpt-4o" });

const searchTool = tool(async ({}: { query: string }) => {
  // This is a placeholder for the actual implementation
  return "Cold, with a low of 13 ℃";
}, {
  name: "search",
  description:
    "Use to surf the web, fetch current information, check the weather, and retrieve other information.",
  schema: z.object({
    query: z.string().describe("The query to use in your search."),
  }),
});

await searchTool.invoke({ query: "What's the weather like?" });

const tools = [searchTool];
const boundModel = model.bindTools(tools);

const toolNode = new ToolNode(tools);

const GraphState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
  }),
});

const routeMessage = (state: typeof GraphState.State) => {
  const { messages } = state;
  const lastMessage = messages[messages.length - 1] as AIMessage;
  // If no tools are called, we can finish (respond to the user)
  if (!lastMessage.tool_calls?.length) {
    return END;
  }
  // Otherwise if there is, we continue and call the tools
  return "tools";
};

const callModel = async (state: typeof GraphState.State) => {
    const { messages } = state;
    const response = await boundModel.invoke(messages);
    // Always append timestamp to force state change
    return { messages: [...messages, response, new AIMessage(Date.now().toString())] };
  };
  

const workflow = new StateGraph(GraphState)
  .addNode("agent", callModel)
  .addNode("tools", toolNode)
  .addEdge(START, "agent")
  .addConditionalEdges("agent", routeMessage)
  .addEdge("tools", "agent");

  const config = { configurable: { thread_id: "conversation-2" } };;
  const mem = new MemorySaver();
const graph = workflow.compile({
    checkpointer: mem
});


let inputs = { messages: [{ role: "user", content: "Hi I'm Yu, nice to meet you." }] };
for await (
    //@ts-expect-error
  const event of await graph.streamEvents(inputs, {
    streamMode: "values",
    version: "v2",
    ...config
  })
) {
  console.log(event);
}

let state = await graph.getState(config);
console.log('Checkpoint first: ', state);

inputs = { messages: [{ role: "user", content: "yo" }] };
for await (
    //@ts-expect-error
  const event of await graph.streamEvents(inputs, {
    streamMode: "values",
    version: "v2",
    ...config
  })
) {
  console.log(event);
}

state = await graph.getState(config);

console.log(state);
console.log('Checkpoint second: ', state);

for await (const item of mem.list(config)) {
    console.log(item);
}
console.warn('blah')

Error Message and Stack Trace (if applicable)


Description

Using the LangGraph typescript library, I am following the example here:
https://langchain-ai.github.io/langgraphjs/concepts/persistence/?h=checkpoint#replay

I would expect that every invocation of streamEvents, since its using the same threadId, it should generate a unique checkpoint reflected by a unique checkpoint Id. But I get the same Id no matter what:

1eff77f3-155f-6980-8001-6126c0a663ea

This must be hard coded somewhere because it is always this Id in both my production app and my test app - which is the code I pasted above.

If you run the example, "Checkpoint first" and "Checkpoint second" should have unique values for the checkpoint ID but they always have "1eff77f3-155f-6980-8001-6126c0a663ea"

Is this expected? I wrote a custom checkpointer to verify, and the put method is always passed the UUID I mentioned above. It appears to have sufficient state mutation with the messages array changing

System Info

MacOS ARM64 M4 Max Pro
Node 23.9.0

Originally created by @RussellCanfield on GitHub (Mar 2, 2025). ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use GitHub Discussions. - [x] I added a clear and detailed title that summarizes the issue. - [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example). - [x] I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue. ### Example Code ```python import { Annotation, MemorySaver } from "@langchain/langgraph"; import { BaseMessage } from "@langchain/core/messages"; import { tool } from "@langchain/core/tools"; import { z } from "zod"; import { ToolNode } from "@langchain/langgraph/prebuilt"; import { ChatOpenAI } from "@langchain/openai"; import { END, START, StateGraph } from "@langchain/langgraph"; import { AIMessage } from "@langchain/core/messages"; import type { RunnableConfig } from "@langchain/core/runnables"; const model = new ChatOpenAI({ model: "gpt-4o" }); const searchTool = tool(async ({}: { query: string }) => { // This is a placeholder for the actual implementation return "Cold, with a low of 13 ℃"; }, { name: "search", description: "Use to surf the web, fetch current information, check the weather, and retrieve other information.", schema: z.object({ query: z.string().describe("The query to use in your search."), }), }); await searchTool.invoke({ query: "What's the weather like?" }); const tools = [searchTool]; const boundModel = model.bindTools(tools); const toolNode = new ToolNode(tools); const GraphState = Annotation.Root({ messages: Annotation<BaseMessage[]>({ reducer: (x, y) => x.concat(y), }), }); const routeMessage = (state: typeof GraphState.State) => { const { messages } = state; const lastMessage = messages[messages.length - 1] as AIMessage; // If no tools are called, we can finish (respond to the user) if (!lastMessage.tool_calls?.length) { return END; } // Otherwise if there is, we continue and call the tools return "tools"; }; const callModel = async (state: typeof GraphState.State) => { const { messages } = state; const response = await boundModel.invoke(messages); // Always append timestamp to force state change return { messages: [...messages, response, new AIMessage(Date.now().toString())] }; }; const workflow = new StateGraph(GraphState) .addNode("agent", callModel) .addNode("tools", toolNode) .addEdge(START, "agent") .addConditionalEdges("agent", routeMessage) .addEdge("tools", "agent"); const config = { configurable: { thread_id: "conversation-2" } };; const mem = new MemorySaver(); const graph = workflow.compile({ checkpointer: mem }); let inputs = { messages: [{ role: "user", content: "Hi I'm Yu, nice to meet you." }] }; for await ( //@ts-expect-error const event of await graph.streamEvents(inputs, { streamMode: "values", version: "v2", ...config }) ) { console.log(event); } let state = await graph.getState(config); console.log('Checkpoint first: ', state); inputs = { messages: [{ role: "user", content: "yo" }] }; for await ( //@ts-expect-error const event of await graph.streamEvents(inputs, { streamMode: "values", version: "v2", ...config }) ) { console.log(event); } state = await graph.getState(config); console.log(state); console.log('Checkpoint second: ', state); for await (const item of mem.list(config)) { console.log(item); } console.warn('blah') ``` ### Error Message and Stack Trace (if applicable) ```shell ``` ### Description Using the LangGraph typescript library, I am following the example here: [https://langchain-ai.github.io/langgraphjs/concepts/persistence/?h=checkpoint#replay](https://langchain-ai.github.io/langgraphjs/concepts/persistence/?h=checkpoint#replay) I would expect that every invocation of streamEvents, since its using the same threadId, it should generate a unique checkpoint reflected by a unique checkpoint Id. But I get the same Id no matter what: 1eff77f3-155f-6980-8001-6126c0a663ea This must be hard coded somewhere because it is always this Id in both my production app and my test app - which is the code I pasted above. If you run the example, "Checkpoint first" and "Checkpoint second" should have unique values for the checkpoint ID but they always have "1eff77f3-155f-6980-8001-6126c0a663ea" Is this expected? I wrote a custom checkpointer to verify, and the put method is always passed the UUID I mentioned above. It appears to have sufficient state mutation with the messages array changing ### System Info MacOS ARM64 M4 Max Pro Node 23.9.0
yindo added the invalid label 2026-02-15 17:16:54 -05:00
yindo closed this issue 2026-02-15 17:16:54 -05:00
Author
Owner

@benjamincburns commented on GitHub (Mar 4, 2025):

We use UUIDv6. This gives us orderable checkpoint keys, which makes it easy to sort the checkpoints by ID to get the latest for a given thread. Because of this, UUIDv6 tends to generate consecutive IDs that are very similar.

I modified your code slightly so that it only prints the checkpoint ID from the fetched state object:

...
let state = await graph.getState(config);
console.log('Checkpoint first: ', state.config?.configurable?.checkpoint_id);
...
let state = await graph.getState(config);
console.log('Checkpoint second: ', state.config?.configurable?.checkpoint_id);

When I run this, I see two distinct checkpoint IDs, as designed:

$ tsx src/checkpoint_id_test.ts
Checkpoint first:  1eff9379-1f51-6410-8001-9415ab5bd5ad
Checkpoint second:  1eff9379-2798-6650-8004-e3ebbe3b0723

When I run this multiple times, I see different IDs:

Checkpoint first:  1eff9386-bab2-6dc0-8001-de964baffc33
Checkpoint second:  1eff9386-c2c1-6d90-8004-af9665870645
...
Checkpoint first:  1eff9388-1696-6eb0-8001-710c2dc6f695
Checkpoint second:  1eff9388-1fad-6940-8004-d8531e5c90cb

If you're seeing identical IDs in a different location, please let me know more specifically where you're looking to find that ID.

@benjamincburns commented on GitHub (Mar 4, 2025): We use [UUIDv6](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-6). This gives us orderable checkpoint keys, which makes it easy to sort the checkpoints by ID to get the latest for a given thread. Because of this, UUIDv6 tends to generate consecutive IDs that are very similar. I modified your code slightly so that it only prints the checkpoint ID from the fetched state object: ```ts ... let state = await graph.getState(config); console.log('Checkpoint first: ', state.config?.configurable?.checkpoint_id); ... let state = await graph.getState(config); console.log('Checkpoint second: ', state.config?.configurable?.checkpoint_id); ``` When I run this, I see two distinct checkpoint IDs, as designed: ```bash $ tsx src/checkpoint_id_test.ts Checkpoint first: 1eff9379-1f51-6410-8001-9415ab5bd5ad Checkpoint second: 1eff9379-2798-6650-8004-e3ebbe3b0723 ``` When I run this multiple times, I see different IDs: ``` Checkpoint first: 1eff9386-bab2-6dc0-8001-de964baffc33 Checkpoint second: 1eff9386-c2c1-6d90-8004-af9665870645 ... Checkpoint first: 1eff9388-1696-6eb0-8001-710c2dc6f695 Checkpoint second: 1eff9388-1fad-6940-8004-d8531e5c90cb ``` If you're seeing identical IDs in a different location, please let me know more specifically where you're looking to find that ID.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#183