Skip to content

fix(langgraph-checkpoint-postgres): mint unique channel versions to prevent sibling-branch blob loss - #2584

Open
Nitsan Cohen (NitsanCohen770) wants to merge 5 commits into
langchain-ai:mainfrom
NitsanCohen770:fix/postgres-saver-unique-versions
Open

fix(langgraph-checkpoint-postgres): mint unique channel versions to prevent sibling-branch blob loss#2584
Nitsan Cohen (NitsanCohen770) wants to merge 5 commits into
langchain-ai:mainfrom
NitsanCohen770:fix/postgres-saver-unique-versions

Conversation

@NitsanCohen770

Copy link
Copy Markdown

Problem

PostgresSaver inherits the base integer getNextVersion (1, 2, 3, …). Channel values are stored in checkpoint_blobs keyed by (thread_id, checkpoint_ns, channel, version) and inserted with ON CONFLICT DO NOTHING:

PRIMARY KEY (thread_id, checkpoint_ns, channel, version)
INSERT INTO ... ON CONFLICT (thread_id, checkpoint_ns, channel, version) DO NOTHING

When a thread is branched with a time-travel fork (updateState with a checkpoint_id, i.e. two versions built from the same base checkpoint), both branches run the same number of super-steps and therefore advance a channel to the same integer version. The two branches write different blobs to the same primary key, so the second branch's write is silently dropped, and any later read of that branch loads the first branch's value.

Net effect: a version forked from the same base as a sibling inherits the sibling's channel data (files, messages, …) on a re-read. It only surfaces on a read in a later super-step, so single-turn tests don't catch it.

The Python PostgresSaver doesn't have this bug because it mints unique, ordered version strings; the JS port kept the base integer scheme.

Fix

Override getNextVersion in PostgresSaver to return globally-unique, lexically-ordered version strings of the form <zero-padded counter>.<uuid>:

  • the zero-padded counter prefix preserves ordering (versions are compared as strings via localeCompare, so equal-width padding keeps lexical order == numeric order);
  • the uuid suffix makes two sibling branches at the same logical counter write distinct checkpoint_blobs keys, so neither write is dropped.

getNextVersion is the single documented seam for custom version schemes, the version column is already TEXT, and reads join checkpoint.channel_versions against checkpoint_blobs.version — so no other method and no migration is needed.

Test

Adds a unit test for getNextVersion (start value, increment from numeric and prior-string versions, per-call uniqueness, and lexical == numeric ordering).

Verification

Reproduced end-to-end against a real Postgres with a deep agent that forks one thread into two branches: on the stock PostgresSaver the second branch's re-read inherits the first branch's file; with this change each branch keeps its own. The getNextVersion properties above were verified directly against the edited source.

Draft: I wasn't able to run the full monorepo CI locally; relying on CI here for tsc / lint / integration tests.

…ling-branch blob loss

PostgresSaver inherits the base integer getNextVersion (1, 2, 3, ...). Channel
values are stored in checkpoint_blobs keyed by (thread_id, checkpoint_ns,
channel, version) and inserted with ON CONFLICT DO NOTHING. When a thread is
branched via a time-travel fork (updateState with a checkpoint_id), two branches
off the same base advance a channel to the SAME integer version, so the second
branch's blob write is silently dropped and later reads of that branch return
the first branch's value.

Override getNextVersion in PostgresSaver to mint globally-unique,
lexically-ordered version strings of the form `<zero-padded counter>.<uuid>`,
mirroring the unique-version scheme the Python saver uses. The counter prefix
preserves ordering (versions are compared as strings via localeCompare); the
uuid suffix makes sibling branches write distinct checkpoint_blobs keys, so
neither write is dropped. The version column is already TEXT, so no migration is
required.

Adds a unit test for getNextVersion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 0342daf

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

… internally)

Per review feedback: the output of getNextVersion is always a string, so
`BaseCheckpointSaver<string>` is the accurate type parameter. The wider
`string | number` input is kept on the method parameter to accept legacy
integer versions read from existing checkpoints.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@langchain/langgraph-checkpoint

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint@2584

@langchain/langgraph-checkpoint-mongodb

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-mongodb@2584

@langchain/langgraph-checkpoint-postgres

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-postgres@2584

@langchain/langgraph-checkpoint-redis

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-redis@2584

@langchain/langgraph-checkpoint-sqlite

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-sqlite@2584

@langchain/langgraph-checkpoint-validation

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-checkpoint-validation@2584

create-langgraph

npm i https://pkg.pr.new/langchain-ai/langgraphjs/create-langgraph@2584

@langchain/langgraph-api

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-api@2584

@langchain/langgraph-cli

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-cli@2584

@langchain/langgraph

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph@2584

@langchain/langgraph-cua

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-cua@2584

@langchain/langgraph-supervisor

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-supervisor@2584

@langchain/langgraph-swarm

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-swarm@2584

@langchain/langgraph-ui

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-ui@2584

@langchain/langgraph-sdk

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/langgraph-sdk@2584

@langchain/angular

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/angular@2584

@langchain/react

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/react@2584

@langchain/svelte

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/svelte@2584

@langchain/vue

npm i https://pkg.pr.new/langchain-ai/langgraphjs/@langchain/vue@2584

commit: 0342daf

@NitsanCohen770 Nitsan Cohen (NitsanCohen770) changed the title fix(checkpoint-postgres): mint unique channel versions to prevent sibling-branch blob loss fix(langgraph-checkpoint-postgres): mint unique channel versions to prevent sibling-branch blob loss Jul 2, 2026
Declaring the class as `BaseCheckpointSaver<string>` made getNextVersion
return `string`, so PostgresSaver was no longer assignable to
`BaseCheckpointSaver<number>` — which every graph `compile({ checkpointer })`
call site expects (this broke langgraph-core's postgres integration tests
with `Type 'string' is not assignable to type 'number'`).

Revert to the default (`number`) generic and keep the unique, lexically
ordered version string at runtime, casting the return. PostgresSaver stays
a drop-in `BaseCheckpointSaver<number>`; channel versions are
`string | number` on the wire, so the stored string is a valid version.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NitsanCohen770
Nitsan Cohen (NitsanCohen770) marked this pull request as ready for review July 2, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant