Unit 10.02: Reproducing a failure from a checkpoint
A bug you can only observe in production is a bug you debug by guessing. The recorded inputs are what turn it into something you can run.
Replay on a fresh thread with the same inputs
The checkpoint holds the inputs a run started with. Replaying means invoking the same graph on a new thread with those exact values, changing nothing else.
The code runs a graph, reads its recorded state, and replays it.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
class State(TypedDict):
value: int
trail: list
def step_a(state: State):
return {"value": state["value"] + 1, "trail": state["trail"] + ["a"]}
def step_b(state: State):
return {"value": state["value"] * 10, "trail": state["trail"] + ["b"]}
g = StateGraph(State)
g.add_node("a", step_a)
g.add_node("b", step_b)
g.add_edge(START, "a")
g.add_edge("a", "b")
g.add_edge("b", END)
app = g.compile(checkpointer=InMemorySaver())
original = {"configurable": {"thread_id": "prod-run"}}
app.invoke({"value": 4, "trail": []}, original)
failed_state = app.get_state(original).values
print("production run ended as:", failed_state)
# Replay it on a fresh thread with the exact inputs, changing nothing else.
replay = {"configurable": {"thread_id": "replay-1"}}
print("replayed :", app.invoke({"value": 4, "trail": []}, replay))
# Reproducing from the recorded inputs is the whole reason to store them. A bug
# you can only observe in production is a bug you debug by guessing.
The replay produces the same result, which confirms the failure is deterministic given those inputs - a genuinely useful thing to establish early. If it had not reproduced, that is equally informative: the failure depends on something outside the recorded state, which narrows the search to external calls, time, or randomness.
Using a fresh thread_id matters. Replaying onto the original thread overwrites the evidence you are trying to investigate.
The mistake this prevents
The mistake is changing something while reproducing - a smaller input, a different model, an extra log line inside the node. You then learn whether the modified thing fails, which is a different question. Reproduce exactly first, then start changing one thing at a time.
Takeaway
Replay from the recorded inputs on a fresh thread, changing nothing. Whether it reproduces is itself the first useful finding: deterministic means it is in your code, non-deterministic means it is outside the state.
