langgraph-checkpoint-postgres 3.1.0a1__tar.gz → 3.1.0a3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/PKG-INFO +2 -2
  2. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/__init__.py +32 -16
  3. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/aio.py +32 -14
  4. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/base.py +91 -86
  5. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/pyproject.toml +2 -2
  6. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/uv.lock +2 -2
  7. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/.gitignore +0 -0
  8. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/LICENSE +0 -0
  9. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/Makefile +0 -0
  10. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/README.md +0 -0
  11. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  12. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/_internal.py +0 -0
  13. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/py.typed +0 -0
  14. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/checkpoint/postgres/shallow.py +0 -0
  15. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/__init__.py +0 -0
  16. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/aio.py +0 -0
  17. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/base.py +0 -0
  18. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/langgraph/store/postgres/py.typed +0 -0
  19. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/__init__.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/compose-postgres.yml +0 -0
  21. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/conftest.py +0 -0
  22. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/embed_test_utils.py +0 -0
  23. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_async.py +0 -0
  24. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_async_store.py +0 -0
  25. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_store.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.0a1 → langgraph_checkpoint_postgres-3.1.0a3}/tests/test_sync.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 3.1.0a1
3
+ Version: 3.1.0a3
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres
6
6
  Project-URL: Twitter, https://x.com/langchain_oss
@@ -9,7 +9,7 @@ Project-URL: Reddit, https://www.reddit.com/r/LangChain/
9
9
  License-Expression: MIT
10
10
  License-File: LICENSE
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a1
12
+ Requires-Dist: langgraph-checkpoint<5.0.0,>=4.1.0a3
13
13
  Requires-Dist: orjson>=3.11.5
14
14
  Requires-Dist: psycopg-pool>=3.2.0
15
15
  Requires-Dist: psycopg>=3.2.0
@@ -19,6 +19,7 @@ from langgraph.checkpoint.base import (
19
19
  get_serializable_checkpoint_metadata,
20
20
  )
21
21
  from langgraph.checkpoint.serde.base import SerializerProtocol
22
+ from langgraph.checkpoint.serde.types import _DeltaSnapshot
22
23
  from psycopg import Capabilities, Connection, Cursor, Pipeline
23
24
  from psycopg.rows import DictRow, dict_row
24
25
  from psycopg.types.json import Jsonb
@@ -26,9 +27,11 @@ from psycopg_pool import ConnectionPool
26
27
 
27
28
  from langgraph.checkpoint.postgres import _internal
28
29
  from langgraph.checkpoint.postgres.base import (
29
- SELECT_DELTA_COMBINED_SQL,
30
+ SELECT_DELTA_STAGE1_SQL,
31
+ SELECT_DELTA_STAGE2_SQL,
30
32
  BasePostgresSaver,
31
- _DeltaCombinedRow,
33
+ _DeltaStage1Row,
34
+ _DeltaStage2Row,
32
35
  )
33
36
  from langgraph.checkpoint.postgres.shallow import ShallowPostgresSaver
34
37
 
@@ -308,7 +311,12 @@ class PostgresSaver(BasePostgresSaver):
308
311
  # others are stored in blobs table
309
312
  blob_values = {}
310
313
  for k, v in checkpoint["channel_values"].items():
311
- if v is None or isinstance(v, (str, int, float, bool)):
314
+ if v is DELTA_SENTINEL:
315
+ copy["channel_values"].pop(k)
316
+ elif isinstance(v, _DeltaSnapshot):
317
+ blob_values[k] = copy["channel_values"].pop(k)
318
+ copy["channel_values"][k] = True
319
+ elif v is None or isinstance(v, (str, int, float, bool)):
312
320
  pass
313
321
  else:
314
322
  blob_values[k] = copy["channel_values"].pop(k)
@@ -441,41 +449,49 @@ class PostgresSaver(BasePostgresSaver):
441
449
  ) -> _ChannelWritesHistory:
442
450
  """Fast-path override of `BaseCheckpointSaver._get_channel_writes_history`.
443
451
 
444
- One combined UNION ALL query (`SELECT_DELTA_COMBINED_SQL`) fetches rows
445
- from `checkpoints`, `checkpoint_writes`, and `checkpoint_blobs` in a
446
- single roundtrip; the ancestor walk runs in Python.
452
+ Two-stage query: stage 1 scans checkpoint metadata to walk the parent
453
+ chain and locate the nearest snapshot; stage 2 fetches only the
454
+ chain-limited writes and single seed blob.
447
455
  """
448
456
  thread_id = config["configurable"]["thread_id"]
449
457
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
450
458
  checkpoint_id = get_checkpoint_id(config)
451
459
  if checkpoint_id is None:
452
- # Caller didn't specify a target — resolve to the latest
453
- # checkpoint on the thread. `get_tuple` without `checkpoint_id`
454
- # returns the newest; its config carries the resolved id.
455
460
  target = self.get_tuple(config)
456
461
  if target is None:
457
462
  return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
458
463
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
464
+
459
465
  with self._cursor() as cur:
460
466
  cur.execute(
461
- SELECT_DELTA_COMBINED_SQL,
467
+ SELECT_DELTA_STAGE1_SQL,
468
+ (channel, channel, thread_id, checkpoint_ns),
469
+ )
470
+ stage1_rows = cur.fetchall()
471
+ chain_cids, seed_version = self._walk_stage1(
472
+ cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
473
+ )
474
+ seed_versions = [seed_version] if seed_version else []
475
+ with self._cursor() as cur:
476
+ cur.execute(
477
+ SELECT_DELTA_STAGE2_SQL,
462
478
  (
463
- channel,
464
- thread_id,
465
- checkpoint_ns,
466
479
  thread_id,
467
480
  checkpoint_ns,
468
481
  channel,
482
+ chain_cids,
469
483
  thread_id,
470
484
  checkpoint_ns,
471
485
  channel,
486
+ seed_versions,
472
487
  ),
473
488
  )
474
- rows = cur.fetchall()
489
+ stage2_rows = cur.fetchall()
475
490
  return self._build_delta_channel_writes_history(
476
491
  channel=channel,
477
- target_id=checkpoint_id,
478
- rows=cast("list[_DeltaCombinedRow]", rows),
492
+ chain_cids=chain_cids,
493
+ seed_version=seed_version,
494
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
479
495
  )
480
496
 
481
497
  def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -19,6 +19,7 @@ from langgraph.checkpoint.base import (
19
19
  get_serializable_checkpoint_metadata,
20
20
  )
21
21
  from langgraph.checkpoint.serde.base import SerializerProtocol
22
+ from langgraph.checkpoint.serde.types import _DeltaSnapshot
22
23
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
23
24
  from psycopg.rows import DictRow, dict_row
24
25
  from psycopg.types.json import Jsonb
@@ -26,9 +27,11 @@ from psycopg_pool import AsyncConnectionPool
26
27
 
27
28
  from langgraph.checkpoint.postgres import _ainternal
28
29
  from langgraph.checkpoint.postgres.base import (
29
- SELECT_DELTA_COMBINED_SQL,
30
+ SELECT_DELTA_STAGE1_SQL,
31
+ SELECT_DELTA_STAGE2_SQL,
30
32
  BasePostgresSaver,
31
- _DeltaCombinedRow,
33
+ _DeltaStage1Row,
34
+ _DeltaStage2Row,
32
35
  )
33
36
  from langgraph.checkpoint.postgres.shallow import AsyncShallowPostgresSaver
34
37
 
@@ -267,7 +270,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
267
270
  # others are stored in blobs table
268
271
  blob_values = {}
269
272
  for k, v in checkpoint["channel_values"].items():
270
- if v is None or isinstance(v, (str, int, float, bool)):
273
+ if v is DELTA_SENTINEL:
274
+ copy["channel_values"].pop(k)
275
+ elif isinstance(v, _DeltaSnapshot):
276
+ blob_values[k] = copy["channel_values"].pop(k)
277
+ copy["channel_values"][k] = True
278
+ elif v is None or isinstance(v, (str, int, float, bool)):
271
279
  pass
272
280
  else:
273
281
  blob_values[k] = copy["channel_values"].pop(k)
@@ -402,10 +410,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
402
410
  ) -> _ChannelWritesHistory:
403
411
  """Fast-path override of `BaseCheckpointSaver._aget_channel_writes_history`.
404
412
 
405
- One combined UNION ALL query (`SELECT_DELTA_COMBINED_SQL`) fetches rows
406
- from `checkpoints`, `checkpoint_writes`, and `checkpoint_blobs` in a
407
- single roundtrip; rows are assembled by the shared pure helper on
408
- `BasePostgresSaver`.
413
+ Two-stage query: stage 1 scans checkpoint metadata to walk the parent
414
+ chain and locate the nearest snapshot; stage 2 fetches only the
415
+ chain-limited writes and single seed blob.
409
416
  """
410
417
  thread_id = config["configurable"]["thread_id"]
411
418
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
@@ -415,26 +422,37 @@ class AsyncPostgresSaver(BasePostgresSaver):
415
422
  if target is None:
416
423
  return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
417
424
  checkpoint_id = target.config["configurable"]["checkpoint_id"]
425
+
418
426
  async with self._cursor() as cur:
419
427
  await cur.execute(
420
- SELECT_DELTA_COMBINED_SQL,
428
+ SELECT_DELTA_STAGE1_SQL,
429
+ (channel, channel, thread_id, checkpoint_ns),
430
+ )
431
+ stage1_rows = await cur.fetchall()
432
+ chain_cids, seed_version = self._walk_stage1(
433
+ cast("list[_DeltaStage1Row]", stage1_rows), checkpoint_id
434
+ )
435
+ seed_versions = [seed_version] if seed_version else []
436
+ async with self._cursor() as cur:
437
+ await cur.execute(
438
+ SELECT_DELTA_STAGE2_SQL,
421
439
  (
422
- channel,
423
- thread_id,
424
- checkpoint_ns,
425
440
  thread_id,
426
441
  checkpoint_ns,
427
442
  channel,
443
+ chain_cids,
428
444
  thread_id,
429
445
  checkpoint_ns,
430
446
  channel,
447
+ seed_versions,
431
448
  ),
432
449
  )
433
- rows = await cur.fetchall()
450
+ stage2_rows = await cur.fetchall()
434
451
  return self._build_delta_channel_writes_history(
435
452
  channel=channel,
436
- target_id=checkpoint_id,
437
- rows=cast("list[_DeltaCombinedRow]", rows),
453
+ chain_cids=chain_cids,
454
+ seed_version=seed_version,
455
+ stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
438
456
  )
439
457
 
440
458
  async def _load_checkpoint_tuple(self, value: DictRow) -> CheckpointTuple:
@@ -156,62 +156,62 @@ INSERT_CHECKPOINT_WRITES_SQL = """
156
156
  """
157
157
 
158
158
 
159
- class _DeltaCombinedRow(TypedDict, total=False):
160
- """One row from `SELECT_DELTA_COMBINED_SQL` (a UNION ALL of three tables).
159
+ class _DeltaStage2Row(TypedDict, total=False):
160
+ """One row from `SELECT_DELTA_STAGE2_SQL` (a UNION ALL of writes and blobs)."""
161
161
 
162
- Every row carries `_kind` ("p" / "w" / "b") plus whichever columns are
163
- relevant for that kind; irrelevant columns are NULL and typed as `None`.
164
- """
165
-
166
- _kind: str # always present: "p", "w", or "b"
167
- # checkpoint row ("p")
168
- checkpoint_id: str | None
169
- parent_checkpoint_id: str | None
170
- ver: str | None
171
- # write / blob rows ("w", "b")
162
+ _kind: str # "w" or "b"
163
+ checkpoint_id: str | None # "w" rows only
172
164
  type: str | None
173
165
  blob: bytes | None
174
- # write row only ("w")
175
- task_id: str | None
176
- idx: int | None
177
- # blob row only ("b")
178
- version: str | None
166
+ task_id: str | None # "w" rows only
167
+ idx: int | None # "w" rows only
168
+ version: str | None # "b" rows only
179
169
 
180
170
 
181
- # DeltaChannel reconstruction: one UNION ALL query fetches checkpoints,
182
- # writes, and blobs for `channel` in one roundtrip; the ancestor walk runs
183
- # in Python in `_build_delta_channel_writes_history`.
171
+ # Two-stage DeltaChannel reconstruction. Stage 1 scans checkpoint
172
+ # metadata (no blob bytes) to walk the parent chain and locate the
173
+ # nearest snapshot marker. Stage 2 fetches only the chain-limited
174
+ # writes and the single seed snapshot blob.
184
175
  #
185
- # Parameter order: (channel, thread_id, checkpoint_ns,
186
- # thread_id, checkpoint_ns, channel,
187
- # thread_id, checkpoint_ns, channel)
188
- SELECT_DELTA_COMBINED_SQL = """
189
- SELECT 'p'::text AS _kind,
190
- checkpoint_id,
176
+ # Parameter order:
177
+ # stage1: (channel, channel, thread_id, checkpoint_ns)
178
+ # stage2: (thread_id, checkpoint_ns, channel, chain_cids[],
179
+ # thread_id, checkpoint_ns, channel, seed_versions[])
180
+
181
+ SELECT_DELTA_STAGE1_SQL = """
182
+ SELECT checkpoint_id,
191
183
  parent_checkpoint_id,
192
184
  checkpoint -> 'channel_versions' ->> %s AS ver,
193
- NULL::text AS type,
194
- NULL::bytea AS blob,
195
- NULL::text AS task_id,
196
- NULL::int AS idx,
197
- NULL::text AS version
185
+ (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS has_snapshot
198
186
  FROM checkpoints
199
187
  WHERE thread_id = %s AND checkpoint_ns = %s
200
- UNION ALL
201
- SELECT 'w',
202
- checkpoint_id, NULL, NULL,
203
- type, blob, task_id, idx, NULL
188
+ """
189
+
190
+ SELECT_DELTA_STAGE2_SQL = """
191
+ SELECT 'w'::text AS _kind,
192
+ checkpoint_id,
193
+ type, blob, task_id, idx, NULL::text AS version
204
194
  FROM checkpoint_writes
205
195
  WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
196
+ AND checkpoint_id = ANY(%s)
206
197
  UNION ALL
207
- SELECT 'b',
208
- NULL, NULL, NULL,
198
+ SELECT 'b', NULL,
209
199
  type, blob, NULL, NULL, version
210
200
  FROM checkpoint_blobs
211
201
  WHERE thread_id = %s AND checkpoint_ns = %s AND channel = %s
202
+ AND version = ANY(%s)
212
203
  """
213
204
 
214
205
 
206
+ class _DeltaStage1Row(TypedDict):
207
+ """One row from `SELECT_DELTA_STAGE1_SQL`."""
208
+
209
+ checkpoint_id: str
210
+ parent_checkpoint_id: str | None
211
+ ver: str | None
212
+ has_snapshot: bool
213
+
214
+
215
215
  class BasePostgresSaver(BaseCheckpointSaver[str]):
216
216
  SELECT_SQL = SELECT_SQL
217
217
  SELECT_PENDING_SENDS_SQL = SELECT_PENDING_SENDS_SQL
@@ -254,38 +254,59 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
254
254
  if t.decode() != "empty"
255
255
  }
256
256
 
257
+ @staticmethod
258
+ def _walk_stage1(
259
+ stage1_rows: Sequence[_DeltaStage1Row],
260
+ target_id: str,
261
+ ) -> tuple[list[str], str | None]:
262
+ """Walk the parent chain from stage 1 metadata rows.
263
+
264
+ Returns (chain_cids, seed_version):
265
+ chain_cids: ancestor checkpoint IDs from target's parent down to
266
+ the seed (or root), in newest-first order.
267
+ seed_version: the channel blob version at the nearest ancestor
268
+ with has_snapshot=True, or None if pure delta.
269
+ """
270
+ parent_of: dict[str, str | None] = {}
271
+ ver_of: dict[str, str | None] = {}
272
+ snapshot_of: dict[str, bool] = {}
273
+ for r in stage1_rows:
274
+ cid = r["checkpoint_id"]
275
+ parent_of[cid] = r["parent_checkpoint_id"]
276
+ ver_of[cid] = r["ver"]
277
+ snapshot_of[cid] = r["has_snapshot"]
278
+
279
+ chain_cids: list[str] = []
280
+ seed_version: str | None = None
281
+ cur_cid: str | None = parent_of.get(target_id)
282
+ while cur_cid is not None:
283
+ chain_cids.append(cur_cid)
284
+ if snapshot_of.get(cur_cid, False):
285
+ seed_version = ver_of.get(cur_cid)
286
+ break
287
+ cur_cid = parent_of.get(cur_cid)
288
+ return chain_cids, seed_version
289
+
257
290
  def _build_delta_channel_writes_history(
258
291
  self,
259
292
  *,
260
293
  channel: str,
261
- target_id: str,
262
- rows: Sequence[_DeltaCombinedRow],
294
+ chain_cids: list[str],
295
+ seed_version: str | None,
296
+ stage2_rows: Sequence[_DeltaStage2Row],
263
297
  ) -> _ChannelWritesHistory:
264
- """Reconstruct one delta channel's history from the combined UNION ALL rows.
265
-
266
- Pure data transform shared by sync (`PostgresSaver`) and async
267
- (`AsyncPostgresSaver`); both paths run `SELECT_DELTA_COMBINED_SQL`
268
- and feed the tagged rows here.
298
+ """Reconstruct delta channel history from two-stage query results.
269
299
 
270
- Walk is newest → oldest from the target's parent. A non-sentinel
271
- blob in `checkpoint_blobs` (a pre-delta snapshot) terminates the
272
- walk and is returned as the seed so replay starts from it.
273
-
274
- Writes stored at `target_id` itself are pending writes for the next
275
- step and are excluded — the walk begins at the target's parent.
300
+ chain_cids are in newest-first order (target's parent first).
301
+ stage2_rows contain only writes for chain_cids and the single
302
+ seed blob at seed_version.
276
303
  """
277
- parent_of: dict[str, str | None] = {}
278
- ver_of: dict[str, str | None] = {}
279
304
  writes_by_cid: dict[str, list[tuple[str, bytes, str, int]]] = {}
280
- blob_by_ver: dict[str, tuple[str, bytes]] = {}
305
+ seed_blob: tuple[str, bytes] | None = None
281
306
 
282
- for r in rows:
307
+ for r in stage2_rows:
283
308
  kind = r["_kind"]
284
- if kind == "p":
285
- cid = cast(str, r["checkpoint_id"])
286
- parent_of[cid] = r["parent_checkpoint_id"]
287
- ver_of[cid] = r["ver"]
288
- elif kind == "w":
309
+ if kind == "w":
289
310
  cid = cast(str, r["checkpoint_id"])
290
311
  writes_by_cid.setdefault(cid, []).append(
291
312
  cast(
@@ -294,42 +315,26 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
294
315
  )
295
316
  )
296
317
  else: # kind == "b"
297
- blob_by_ver[cast(str, r["version"])] = cast(
298
- "tuple[str, bytes]", (r["type"], r["blob"])
299
- )
318
+ seed_blob = cast("tuple[str, bytes]", (r["type"], r["blob"]))
300
319
 
301
- # newest write first per ancestor (task_id DESC, idx DESC)
302
320
  for ws in writes_by_cid.values():
303
321
  ws.sort(key=lambda w: (w[2], w[3]), reverse=True)
304
322
 
305
- ancestors: list[str] = []
306
- cur_cid: str | None = parent_of.get(target_id)
307
- while cur_cid is not None:
308
- ancestors.append(cur_cid)
309
- cur_cid = parent_of.get(cur_cid)
310
- if not ancestors:
323
+ if not chain_cids:
311
324
  return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=[])
312
325
 
313
- collected: list[PendingWrite] = [] # newest first; reversed at the end
314
- for cid in ancestors:
315
- # Collect writes first — they encode the transition FROM this
316
- # ancestor's state to its child's and must be included even if
317
- # this ancestor is also the seed checkpoint.
326
+ collected: list[PendingWrite] = []
327
+ for cid in chain_cids:
318
328
  for type_tag, write_blob, task_id, _idx in writes_by_cid.get(cid, []):
319
329
  val = self.serde.loads_typed((type_tag, write_blob))
320
330
  collected.append((task_id, channel, val))
321
- # Then check seed terminator.
322
- ver = ver_of.get(cid)
323
- if ver is not None:
324
- seed_blob = blob_by_ver.get(ver)
325
- if seed_blob is not None and seed_blob[0] != "empty":
326
- blob_value = self.serde.loads_typed(seed_blob)
327
- if blob_value is not DELTA_SENTINEL:
328
- collected.reverse()
329
- return _ChannelWritesHistory(seed=blob_value, writes=collected)
330
-
331
- collected.reverse() # oldest → newest
332
- return _ChannelWritesHistory(seed=DELTA_SENTINEL, writes=collected)
331
+
332
+ seed: Any = DELTA_SENTINEL
333
+ if seed_blob is not None and seed_blob[0] != "empty":
334
+ seed = self.serde.loads_typed(seed_blob)
335
+
336
+ collected.reverse()
337
+ return _ChannelWritesHistory(seed=seed, writes=collected)
333
338
 
334
339
  def _dump_blobs(
335
340
  self,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "langgraph-checkpoint-postgres"
7
- version = "3.1.0a1"
7
+ version = "3.1.0a3"
8
8
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
9
9
  authors = []
10
10
  requires-python = ">=3.10"
@@ -12,7 +12,7 @@ readme = "README.md"
12
12
  license = "MIT"
13
13
  license-files = ['LICENSE']
14
14
  dependencies = [
15
- "langgraph-checkpoint>=4.1.0a1,<5.0.0",
15
+ "langgraph-checkpoint>=4.1.0a3,<5.0.0",
16
16
  "orjson>=3.11.5",
17
17
  "psycopg>=3.2.0",
18
18
  "psycopg-pool>=3.2.0",
@@ -259,7 +259,7 @@ wheels = [
259
259
 
260
260
  [[package]]
261
261
  name = "langgraph-checkpoint"
262
- version = "4.1.0a1"
262
+ version = "4.1.0a3"
263
263
  source = { editable = "../checkpoint" }
264
264
  dependencies = [
265
265
  { name = "langchain-core" },
@@ -307,7 +307,7 @@ test = [
307
307
 
308
308
  [[package]]
309
309
  name = "langgraph-checkpoint-postgres"
310
- version = "3.1.0a1"
310
+ version = "3.1.0a3"
311
311
  source = { editable = "." }
312
312
  dependencies = [
313
313
  { name = "langgraph-checkpoint" },