Skip to content

Partial Graph State Missing Due to Mixed thread_id Formats in checkpoint_writes #6623

@lfge-hash

Description

@lfge-hash

Checked other resources

  • This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • 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

from dataclasses import dataclass
from typing import Literal, Optional, TypedDict

from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.graph import StateGraph, START, END
from langgraph.types import Command, interrupt


@dataclass
class Context:
    """Context schema defined by the developer."""
    user_id: str
    db_connection: str

class ApprovalState(TypedDict):
    action_details: str
    status: Optional[Literal["pending", "approved", "rejected"]]


def approval_node(state: ApprovalState) -> Command[Literal["proceed", "cancel"]]:
    # Expose details so the caller can render them in a UI
    decision = interrupt({
        "question": "Approve this action?",
        "details": state["action_details"],
    })

    # Route to the appropriate node after resume
    return Command(goto="proceed" if decision else "cancel")


def proceed_node(state: ApprovalState):
    return {"status": "approved"}


def cancel_node(state: ApprovalState):
    return {"status": "rejected"}


builder = StateGraph(ApprovalState)
builder.add_node("approval", approval_node)
builder.add_node("proceed", proceed_node)
builder.add_node("cancel", cancel_node)
builder.add_edge(START, "approval")
builder.add_edge("proceed", END)
builder.add_edge("cancel", END)

# Use a more durable checkpointer in production
# checkpointer = MemorySaver()
checkpointer_cm = PostgresSaver.from_conn_string(
        "postgresql://xxx:xxx@localhost:5432/postgres"
    )
with checkpointer_cm as checkpointer:
    graph = builder.compile(checkpointer=checkpointer)

    config = {"configurable": {"thread_id": "approval-123"}}
    initial = graph.invoke(
        {"action_details": "Transfer $500", "status": "pending"},
        config=config,
    )
    print(initial["__interrupt__"])  # -> [Interrupt(value={'question': ..., 'details': ...})]

    result = graph.get_state(config)

    # Resume with the decision; True routes to proceed, False to cancel
    resumed = graph.invoke(Command(resume=True), config=config)

    print(resumed["status"])  # -> "approved"

    result = graph.get_state(config)

    resumed = graph.invoke(Command(resume=True), config=config)

    print(resumed["status"])  # -> "approved"

    result = graph.get_state(config)

    print("test")

Error Message and Stack Trace (if applicable)

thread_id          | checkpoint_ns |            checkpoint_id             |               task_id                | idx |      channel       |  type   |
                                                                            blob
      task_path
----------------------------+---------------+--------------------------------------+--------------------------------------+-----+--------------------+---------+----
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------
 approval-123               |               | 1f0e0728-95db-6158-bfff-aae7f05d1fa1 | c9699148-6d9d-4d35-3417-efc0e235fda8 |   0 | action_details     | msgpack | \xa

_pregel_pull, __start__
 approval-123               |               | 1f0e0728-95db-6158-bfff-aae7f05d1fa1 | c9699148-6d9d-4d35-3417-efc0e235fda8 |   1 | status             | msgpack | \xa

_pregel_pull, __start__
 approval-123               |               | 1f0e0728-95db-6158-bfff-aae7f05d1fa1 | c9699148-6d9d-4d35-3417-efc0e235fda8 |   2 | branch:to:approval | null    | \x

_pregel_pull, __start__
 approval-123               |               | 1f0e0728-95e2-66a2-8000-885ac3f329f9 | d01e943c-7a45-f535-002f-4e2bb0d1ba12 |  -3 | __interrupt__      | msgpack | \x9
465727275707482a576616c756582a87175657374696f6eb4417070726f7665207468697320616374696f6e3fa764657461696c73ad5472616e736665722024353030a26964d920353539643733346265316
_pregel_pull, approval
 \x617070726f76616c2d313233 |               | 1f0e0728-95e2-66a2-8000-885ac3f329f9 | d01e943c-7a45-f535-002f-4e2bb0d1ba12 |  -4 | __resume__         | msgpack | \x9

_pregel_pull, approval

Description

When using PostgreSQL as the checkpointer storage in LangGraph, after an interruption occurs, the checkpoint_writes table contains two forms of thread_id: one stored as a plain string and the other stored in hexadecimal-encoded form. Although they represent the same logical thread_id, when retrieving the graph state using the string-form thread_id, some of the state is missing because part of it is stored under the hexadecimal-encoded form.

System Info

System Information

OS: Windows
OS Version: 10.0.26100
Python Version: 3.11.14 | packaged by Anaconda, Inc. | (main, Oct 21 2025, 18:30:03) [MSC v.1929 64 bit (AMD64)]

Package Information

langchain_core: 1.1.2
langchain: 1.1.3
langsmith: 0.4.58
langchain_mcp_adapters: 0.1.14
langchain_openai: 1.1.1
langgraph_sdk: 0.2.14

Optional packages not installed

langserve

Other Dependencies

httpx: 0.28.1
jsonpatch: 1.33
langgraph: 1.0.4
mcp: 1.11.0
openai: 2.9.0
opentelemetry-api: 1.39.0
opentelemetry-exporter-otlp-proto-http: 1.39.0
opentelemetry-sdk: 1.39.0
orjson: 3.11.5
packaging: 24.2
pydantic: 2.11.7
pyyaml: 6.0.2
requests: 2.32.5
requests-toolbelt: 1.0.0
rich: 14.2.0
tenacity: 9.1.2
tiktoken: 0.12.0
typing-extensions: 4.15.0
uuid-utils: 0.12.0
zstandard: 0.25.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpendingawaiting review/confirmation by maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions