@langchain/langgraph 1.4.21 → 1.5.0

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 (51) hide show
  1. package/dist/channels/base.cjs +113 -8
  2. package/dist/channels/base.cjs.map +1 -1
  3. package/dist/channels/base.d.cts +26 -4
  4. package/dist/channels/base.d.cts.map +1 -1
  5. package/dist/channels/base.d.ts +26 -4
  6. package/dist/channels/base.d.ts.map +1 -1
  7. package/dist/channels/base.js +110 -10
  8. package/dist/channels/base.js.map +1 -1
  9. package/dist/channels/index.cjs +2 -0
  10. package/dist/channels/index.d.cts +2 -2
  11. package/dist/channels/index.d.ts +2 -2
  12. package/dist/channels/index.js +2 -2
  13. package/dist/constants.cjs +14 -0
  14. package/dist/constants.cjs.map +1 -1
  15. package/dist/constants.d.cts.map +1 -1
  16. package/dist/constants.d.ts.map +1 -1
  17. package/dist/constants.js +13 -1
  18. package/dist/constants.js.map +1 -1
  19. package/dist/graph/index.cjs +1 -1
  20. package/dist/graph/index.js +1 -1
  21. package/dist/graph/messages_reducer.cjs +7 -0
  22. package/dist/graph/messages_reducer.cjs.map +1 -1
  23. package/dist/graph/messages_reducer.js +7 -1
  24. package/dist/graph/messages_reducer.js.map +1 -1
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/pregel/algo.cjs +46 -45
  28. package/dist/pregel/algo.cjs.map +1 -1
  29. package/dist/pregel/algo.d.cts.map +1 -1
  30. package/dist/pregel/algo.d.ts.map +1 -1
  31. package/dist/pregel/algo.js +47 -47
  32. package/dist/pregel/algo.js.map +1 -1
  33. package/dist/pregel/checkpoint.cjs +70 -0
  34. package/dist/pregel/checkpoint.cjs.map +1 -0
  35. package/dist/pregel/checkpoint.js +67 -0
  36. package/dist/pregel/checkpoint.js.map +1 -0
  37. package/dist/pregel/index.cjs +87 -34
  38. package/dist/pregel/index.cjs.map +1 -1
  39. package/dist/pregel/index.d.cts.map +1 -1
  40. package/dist/pregel/index.d.ts.map +1 -1
  41. package/dist/pregel/index.js +91 -38
  42. package/dist/pregel/index.js.map +1 -1
  43. package/dist/pregel/loop.cjs +325 -109
  44. package/dist/pregel/loop.cjs.map +1 -1
  45. package/dist/pregel/loop.js +330 -115
  46. package/dist/pregel/loop.js.map +1 -1
  47. package/dist/state/prebuilt/messages.cjs +1 -1
  48. package/dist/state/prebuilt/messages.js +1 -1
  49. package/dist/web.cjs +1 -1
  50. package/dist/web.js +1 -1
  51. package/package.json +5 -5
@@ -11,6 +11,9 @@ const STRUCTURED_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
11
11
  function isDeltaChannel(channel) {
12
12
  return channel != null && channel.lc_graph_name === "DeltaChannel";
13
13
  }
14
+ function deltaChannelWrites(channels, writes) {
15
+ return writes.filter(([ch]) => isDeltaChannel(channels[String(ch)]));
16
+ }
14
17
  function isBaseChannel(obj) {
15
18
  return obj != null && obj.lg_is_channel === true;
16
19
  }
@@ -93,12 +96,46 @@ function emptyChannels(channels, checkpoint) {
93
96
  *
94
97
  * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
95
98
  * preserves chronological order while remaining a valid RFC UUID (required by
96
- * Postgres `checkpoint_writes.task_id uuid` columns).
99
+ * Postgres `checkpoint_writes.task_id uuid` columns). Never `NULL_TASK_ID`:
100
+ * readers apply writes under it as the anchor checkpoint's own pending writes.
97
101
  */
98
102
  function exitDeltaTaskId(step, taskId) {
99
103
  if (!STRUCTURED_UUID.test(taskId)) throw new TypeError(`Invalid task id for exit delta: ${taskId}`);
100
104
  const parts = taskId.toLowerCase().split("-");
101
- return `${String(step).padStart(8, "0")}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;
105
+ const synthetic = `${String(step).padStart(8, "0")}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;
106
+ return synthetic === "00000000-0000-0000-0000-000000000000" ? require_constants.EXIT_DELTA_NULL_TASK_ID : synthetic;
107
+ }
108
+ /**
109
+ * Synthetic task id for exit-mode writes of a superstep after the anchor's
110
+ * own.
111
+ *
112
+ * Sorts after every real task id, in step order, so replay keeps them after
113
+ * the anchor's own superstep whether a saver orders writes by task path
114
+ * (`writesSortKey`) or by task id. Mirrors Python's
115
+ * `exit_delta_late_task_id` exactly, including the two hexadecimal step
116
+ * groups (step is split across them).
117
+ */
118
+ function exitDeltaLateTaskId(step, taskId) {
119
+ if (!STRUCTURED_UUID.test(taskId)) throw new TypeError(`Invalid task id for exit delta: ${taskId}`);
120
+ const parts = taskId.toLowerCase().split("-");
121
+ return `ffffffff-${(step >> 16).toString(16).padStart(4, "0")}-${(step & 65535).toString(16).padStart(4, "0")}-${parts[3]}-${parts[4]}`;
122
+ }
123
+ /**
124
+ * DeltaChannels a branch starting from this checkpoint must snapshot.
125
+ *
126
+ * A checkpoint's pending writes belong to the child that consumed them, and
127
+ * nothing records which child that was. A new branch snapshots every delta
128
+ * channel they touch, so its ancestor walk never replays them (port of
129
+ * Python #8548's `delta_channels_with_pending_writes`).
130
+ */
131
+ function deltaChannelsWithPendingWrites(channels, pendingWrites) {
132
+ const result = /* @__PURE__ */ new Set();
133
+ if (pendingWrites === void 0) return result;
134
+ for (const [, ch] of pendingWrites) {
135
+ const channel = channels[ch];
136
+ if (channel != null && isDeltaChannel(channel)) result.add(ch);
137
+ }
138
+ return result;
102
139
  }
103
140
  /**
104
141
  * Return the set of {@link DeltaChannel} names that should snapshot now.
@@ -113,19 +150,23 @@ function exitDeltaTaskId(step, taskId) {
113
150
  */
114
151
  function deltaChannelsToSnapshot(channels, countersSinceDeltaSnapshot, channelVersions) {
115
152
  const result = /* @__PURE__ */ new Set();
116
- const maxSupersteps = require_constants.getDeltaMaxSuperstepsSinceSnapshot();
117
153
  for (const name in channels) {
118
154
  if (!Object.prototype.hasOwnProperty.call(channels, name)) continue;
119
155
  const ch = channels[name];
120
156
  if (!isDeltaChannel(ch) || !ch.isAvailable() || channelVersions !== void 0 && channelVersions[name] === void 0) continue;
121
- const [updates, supersteps] = countersSinceDeltaSnapshot[name] ?? [0, 0];
122
- if (updates >= ch.snapshotFrequency || supersteps >= maxSupersteps) result.add(name);
157
+ if (reachedSnapshotBound(ch, countersSinceDeltaSnapshot[name])) result.add(name);
123
158
  }
124
159
  return result;
125
160
  }
161
+ /** Whether a delta channel's counters reached the bound where it snapshots. */
162
+ function reachedSnapshotBound(channel, counters) {
163
+ const [updates, supersteps] = counters ?? [0, 0];
164
+ return updates >= channel.snapshotFrequency || supersteps >= require_constants.getDeltaMaxSuperstepsSinceSnapshot();
165
+ }
126
166
  function createCheckpoint(checkpoint, channels, step, options) {
127
167
  const channelsToSnapshot = options?.channelsToSnapshot ?? /* @__PURE__ */ new Set();
128
- const { updatedChannels, getNextVersion } = options ?? {};
168
+ const { storedVersions, getNextVersion } = options ?? {};
169
+ const bumped = {};
129
170
  let values;
130
171
  let channelVersions = checkpoint.channel_versions;
131
172
  if (channels === void 0) values = checkpoint.channel_values;
@@ -136,7 +177,11 @@ function createCheckpoint(checkpoint, channels, step, options) {
136
177
  if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;
137
178
  const channel = channels[k];
138
179
  if (channelsToSnapshot.has(k)) {
139
- if (getNextVersion !== void 0 && (updatedChannels === void 0 || !updatedChannels.has(k))) channelVersions[k] = getNextVersion(channelVersions[k]);
180
+ if (getNextVersion !== void 0 && storedVersions !== void 0 && channelVersions[k] === storedVersions[k]) {
181
+ const old = channelVersions[k];
182
+ channelVersions[k] = getNextVersion(old);
183
+ bumped[k] = [old, channelVersions[k]];
184
+ }
140
185
  values[k] = new _langchain_langgraph_checkpoint.DeltaSnapshot(channel.get());
141
186
  continue;
142
187
  }
@@ -154,10 +199,65 @@ function createCheckpoint(checkpoint, channels, step, options) {
154
199
  ts: (/* @__PURE__ */ new Date()).toISOString(),
155
200
  channel_values: values,
156
201
  channel_versions: channelVersions,
157
- versions_seen: checkpoint.versions_seen
202
+ versions_seen: markBumpsSeen(checkpoint.versions_seen, bumped, options?.triggerToNodes)
158
203
  };
159
204
  }
160
205
  /**
206
+ * Advance whoever had seen a bumped channel's old version to the new one.
207
+ *
208
+ * A bump that only stores a snapshot is not a write. Left unseen, it would
209
+ * re-fire `interruptBefore` and rerun the channel's subscribers. A channel
210
+ * bumped from no version was never written, so it also goes to the
211
+ * subscribers that never ran: they have no entry, and would start on the bump.
212
+ * For each entry it advances, {@link SNAPSHOT_BUMPS} keeps the new version and
213
+ * the one the node really read, so {@link versionsSeenWithoutBumps} can put
214
+ * the read back.
215
+ */
216
+ function markBumpsSeen(versionsSeen, bumped, triggerToNodes = {}) {
217
+ if (Object.keys(bumped).length === 0) return versionsSeen;
218
+ const out = { ...versionsSeen };
219
+ for (const [k, [old]] of Object.entries(bumped)) {
220
+ if (old !== void 0) continue;
221
+ for (const node of triggerToNodes[k] ?? []) out[node] ??= {};
222
+ }
223
+ const marks = { ...versionsSeen[require_constants.SNAPSHOT_BUMPS] };
224
+ for (const node in out) {
225
+ if (!Object.prototype.hasOwnProperty.call(out, node) || node === "__snapshot_bumps__") continue;
226
+ for (const [k, [old, next]] of Object.entries(bumped)) {
227
+ if (out[node][k] !== old) continue;
228
+ const [advanced, read] = bumpKeys(node, k);
229
+ if (old !== void 0 && marks[advanced] !== old) marks[read] = old;
230
+ marks[advanced] = next;
231
+ out[node] = {
232
+ ...out[node],
233
+ [k]: next
234
+ };
235
+ }
236
+ }
237
+ if (Object.keys(marks).length > 0) out[require_constants.SNAPSHOT_BUMPS] = marks;
238
+ return out;
239
+ }
240
+ const bumpKeys = (node, channel) => [`${node}|${channel}`, `${node}:${channel}`];
241
+ /**
242
+ * `versions_seen` as the nodes read it: an entry a snapshot bump advanced goes
243
+ * back to the version the node really read, or away if it never read one.
244
+ */
245
+ function versionsSeenWithoutBumps(versionsSeen) {
246
+ const marks = versionsSeen[require_constants.SNAPSHOT_BUMPS];
247
+ if (marks === void 0 || Object.keys(marks).length === 0) return versionsSeen;
248
+ const out = {};
249
+ for (const node in versionsSeen) {
250
+ if (!Object.prototype.hasOwnProperty.call(versionsSeen, node) || node === "__snapshot_bumps__") continue;
251
+ out[node] = {};
252
+ for (const [k, v] of Object.entries(versionsSeen[node])) {
253
+ const [advanced, read] = bumpKeys(node, k);
254
+ if (marks[advanced] !== v) out[node][k] = v;
255
+ else if (read in marks) out[node][k] = marks[read];
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ /**
161
261
  * Checkpoint metadata key, `true` when every DeltaChannel write on the
162
262
  * checkpoint's branch carries a version. A delta channel with no version there
163
263
  * was never written, so it needs no walk. Branches started before this key
@@ -225,12 +325,17 @@ exports.BaseChannel = BaseChannel;
225
325
  exports.DELTA_WRITES_VERSIONED = DELTA_WRITES_VERSIONED;
226
326
  exports.channelsFromCheckpoint = channelsFromCheckpoint;
227
327
  exports.createCheckpoint = createCheckpoint;
328
+ exports.deltaChannelWrites = deltaChannelWrites;
228
329
  exports.deltaChannelsToSnapshot = deltaChannelsToSnapshot;
330
+ exports.deltaChannelsWithPendingWrites = deltaChannelsWithPendingWrites;
229
331
  exports.emptyChannels = emptyChannels;
332
+ exports.exitDeltaLateTaskId = exitDeltaLateTaskId;
230
333
  exports.exitDeltaTaskId = exitDeltaTaskId;
231
334
  exports.getOnlyChannels = getOnlyChannels;
232
335
  exports.isBaseChannel = isBaseChannel;
233
336
  exports.isDeltaChannel = isDeltaChannel;
234
337
  exports.isDeltaWritesVersioned = isDeltaWritesVersioned;
338
+ exports.reachedSnapshotBound = reachedSnapshotBound;
339
+ exports.versionsSeenWithoutBumps = versionsSeenWithoutBumps;
235
340
 
236
341
  //# sourceMappingURL=base.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.cjs","names":["EmptyChannelError","getDeltaMaxSuperstepsSinceSnapshot","DeltaSnapshot"],"sources":["../../src/channels/base.ts"],"sourcesContent":["import {\n ReadonlyCheckpoint,\n uuid6,\n Checkpoint,\n DeltaSnapshot,\n type BaseCheckpointSaver,\n type CheckpointMetadata,\n type DeltaChannelHistory,\n} from \"@langchain/langgraph-checkpoint\";\nimport type { RunnableConfig } from \"@langchain/core/runnables\";\nimport { EmptyChannelError } from \"../errors.js\";\nimport { getDeltaMaxSuperstepsSinceSnapshot } from \"../constants.js\";\n\n/** Matches Postgres `uuid` / Python `uuid.UUID` (128-bit, 8-4-4-4-12 hex). */\nconst STRUCTURED_UUID =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Structural check for a {@link DeltaChannel} without importing it (avoids an\n * import cycle: `delta.ts` imports `base.ts`).\n */\nexport function isDeltaChannel(channel: BaseChannel): boolean {\n return channel != null && channel.lc_graph_name === \"DeltaChannel\";\n}\n\nexport function isBaseChannel(obj: unknown): obj is BaseChannel {\n return obj != null && (obj as BaseChannel).lg_is_channel === true;\n}\n\n/** @internal */\nexport abstract class BaseChannel<\n ValueType = unknown,\n UpdateType = unknown,\n CheckpointType = unknown,\n> {\n ValueType: ValueType;\n\n UpdateType: UpdateType;\n\n /**\n * The name of the channel.\n */\n abstract lc_graph_name: string;\n\n /** @ignore */\n lg_is_channel = true;\n\n /**\n * Return a new identical channel, optionally initialized from a checkpoint.\n * Can be thought of as a \"restoration\" from a checkpoint which is a \"snapshot\" of the channel's state.\n *\n * @param {CheckpointType | undefined} checkpoint\n * @param {CheckpointType | undefined} initialValue\n * @returns {this}\n */\n abstract fromCheckpoint(checkpoint?: CheckpointType): this;\n\n /**\n * Update the channel's value with the given sequence of updates.\n * The order of the updates in the sequence is arbitrary.\n * This method is called by Pregel for all channels at the end of each step.\n * If there are no updates, it is called with an empty sequence.\n *\n * Raises InvalidUpdateError if the sequence of updates is invalid.\n * Returns True if the channel was updated, False otherwise.\n *\n * @throws {InvalidUpdateError} if the sequence of updates is invalid.\n * @param {Array<UpdateType>} values\n * @returns {void}\n */\n abstract update(values: UpdateType[]): boolean;\n\n /**\n * Return the current value of the channel.\n *\n * @throws {EmptyChannelError} if the channel is empty (never updated yet).\n * @returns {ValueType}\n */\n abstract get(): ValueType;\n\n /**\n * Return a string representation of the channel's current state.\n *\n * @throws {EmptyChannelError} if the channel is empty (never updated yet), or doesn't support checkpoints.\n * @returns {CheckpointType | undefined}\n */\n abstract checkpoint(): CheckpointType | undefined;\n\n /**\n * Mark the current value of the channel as consumed. By default, no-op.\n * A channel can use this method to modify its state, preventing the value\n * from being consumed again.\n *\n * Returns True if the channel was updated, False otherwise.\n */\n consume(): boolean {\n return false;\n }\n\n /**\n * Notify the channel that the Pregel run is finishing. By default, no-op.\n * A channel can use this method to modify its state, preventing finish.\n *\n * Returns True if the channel was updated, False otherwise.\n */\n finish(): boolean {\n return false;\n }\n\n /**\n * Return True if the channel is available (not empty), False otherwise.\n * Subclasses should override this method to provide a more efficient\n * implementation than calling get() and catching EmptyChannelError.\n */\n isAvailable(): boolean {\n try {\n this.get();\n return true;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (error: any) {\n if (error.name === EmptyChannelError.unminifiable_name) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Compare this channel with another channel for equality.\n * Used to determine if two channels with the same key are semantically equivalent.\n * Subclasses should override this method to provide a meaningful comparison.\n *\n * @param {BaseChannel} other - The other channel to compare with.\n * @returns {boolean} True if the channels are equal, false otherwise.\n */\n equals(other: BaseChannel): boolean {\n return this === other;\n }\n}\n\nconst IS_ONLY_BASE_CHANNEL = Symbol.for(\"LG_IS_ONLY_BASE_CHANNEL\");\nexport function getOnlyChannels(\n channels: Record<string, BaseChannel>\n): Record<string, BaseChannel> {\n // @ts-expect-error - we know it's a record of base channels\n if (channels[IS_ONLY_BASE_CHANNEL] === true) return channels;\n\n const newChannels = {} as Record<string, BaseChannel>;\n for (const k in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;\n const value = channels[k];\n if (isBaseChannel(value)) newChannels[k] = value;\n }\n\n Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true });\n return newChannels;\n}\n\nexport function emptyChannels<Cc extends Record<string, BaseChannel>>(\n channels: Cc,\n checkpoint: ReadonlyCheckpoint\n): Cc {\n const filteredChannels = getOnlyChannels(channels) as Cc;\n\n const newChannels = {} as Cc;\n for (const k in filteredChannels) {\n if (!Object.prototype.hasOwnProperty.call(filteredChannels, k)) continue;\n const channelValue = checkpoint.channel_values[k];\n newChannels[k] = filteredChannels[k].fromCheckpoint(channelValue);\n }\n Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true });\n return newChannels;\n}\n\n/**\n * Minimal structural view of a {@link DeltaChannel}, used by helpers in this\n * module that must not import the concrete class (import-cycle avoidance).\n */\ninterface DeltaChannelLike extends BaseChannel {\n snapshotFrequency: number;\n replayWrites(writes: DeltaChannelHistory[\"writes\"]): void;\n}\n\n/**\n * Synthetic task id for exit-mode DeltaChannel writes.\n *\n * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`\n * preserves chronological order while remaining a valid RFC UUID (required by\n * Postgres `checkpoint_writes.task_id uuid` columns).\n */\nexport function exitDeltaTaskId(step: number, taskId: string): string {\n if (!STRUCTURED_UUID.test(taskId)) {\n throw new TypeError(`Invalid task id for exit delta: ${taskId}`);\n }\n const parts = taskId.toLowerCase().split(\"-\");\n const stepPart = String(step).padStart(8, \"0\");\n return `${stepPart}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;\n}\n\n/**\n * Return the set of {@link DeltaChannel} names that should snapshot now.\n *\n * A channel snapshots when EITHER its accumulated update count reaches\n * `snapshotFrequency` OR the total supersteps since its last snapshot reaches\n * `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. Given `channelVersions`, a channel\n * without a version is skipped: it was never written on this branch, so it\n * has nothing to snapshot. Pass `undefined` on a branch without\n * {@link DELTA_WRITES_VERSIONED}, where such a channel can still hold writes\n * and this snapshot is what bounds its walk. Pure predicate, no mutation.\n */\nexport function deltaChannelsToSnapshot(\n channels: Record<string, BaseChannel>,\n countersSinceDeltaSnapshot: Record<string, [number, number]>,\n channelVersions: Record<string, number | string> | undefined\n): Set<string> {\n const result = new Set<string>();\n const maxSupersteps = getDeltaMaxSuperstepsSinceSnapshot();\n for (const name in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, name)) continue;\n const ch = channels[name];\n if (\n !isDeltaChannel(ch) ||\n !ch.isAvailable() ||\n (channelVersions !== undefined && channelVersions[name] === undefined)\n ) {\n continue;\n }\n const [updates, supersteps] = countersSinceDeltaSnapshot[name] ?? [0, 0];\n if (\n updates >= (ch as DeltaChannelLike).snapshotFrequency ||\n supersteps >= maxSupersteps\n ) {\n result.add(name);\n }\n }\n return result;\n}\n\nexport function createCheckpoint<ValueType>(\n checkpoint: ReadonlyCheckpoint,\n channels: Record<string, BaseChannel<ValueType>> | undefined,\n step: number,\n options?: {\n id?: string;\n channelsToSnapshot?: Set<string>;\n updatedChannels?: Set<string>;\n getNextVersion?: (current: number | string | undefined) => number | string;\n }\n): Checkpoint {\n const channelsToSnapshot = options?.channelsToSnapshot ?? new Set<string>();\n const { updatedChannels, getNextVersion } = options ?? {};\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let values: Record<string, any>;\n let channelVersions: Record<string, number | string> =\n checkpoint.channel_versions;\n if (channels === undefined) {\n values = checkpoint.channel_values;\n } else {\n values = {};\n channelVersions = { ...checkpoint.channel_versions };\n for (const k in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;\n const channel = channels[k];\n if (channelsToSnapshot.has(k)) {\n // Snapshot a DeltaChannel: store the materialized value directly. In\n // exit/deferred modes the channel may have reached its snapshot\n // threshold over several supersteps without the LAST superstep\n // writing to it, so its version wouldn't be bumped by applyWrites —\n // bump it here so the saver includes the snapshot blob.\n if (\n getNextVersion !== undefined &&\n (updatedChannels === undefined || !updatedChannels.has(k))\n ) {\n channelVersions[k] = getNextVersion(channelVersions[k]);\n }\n values[k] = new DeltaSnapshot(channel.get());\n continue;\n }\n if (isDeltaChannel(channel)) {\n // Omitted from channel_values; reconstructed from ancestor writes.\n continue;\n }\n try {\n values[k] = channel.checkpoint();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (error: any) {\n if (error.name === EmptyChannelError.unminifiable_name) {\n // no-op\n } else {\n throw error; // Rethrow unexpected errors\n }\n }\n }\n }\n\n return {\n v: 4,\n id: options?.id ?? uuid6(step),\n ts: new Date().toISOString(),\n channel_values: values,\n channel_versions: channelVersions,\n versions_seen: checkpoint.versions_seen,\n };\n}\n\n/**\n * Checkpoint metadata key, `true` when every DeltaChannel write on the\n * checkpoint's branch carries a version. A delta channel with no version there\n * was never written, so it needs no walk. Branches started before this key\n * existed never get it: an older `updateState` stored a delta channel's first\n * write without a version, and only the walk finds such a write.\n */\nexport const DELTA_WRITES_VERSIONED = \"delta_writes_versioned\";\n\nexport function isDeltaWritesVersioned(\n metadata: CheckpointMetadata | undefined\n): boolean {\n return (\n (metadata as Record<string, unknown> | undefined)?.[\n DELTA_WRITES_VERSIONED\n ] === true\n );\n}\n\n/**\n * Delta channels whose value at `checkpoint` the ancestor walk rebuilds.\n *\n * A {@link DeltaSnapshot} or a migrated plain value in `channel_values`\n * resolves directly, so only a channel with nothing stored there needs the\n * walk. With `versionedOnly`, a channel with no version was never written, so\n * it is empty without one; a walk for it would find no snapshot to stop at and\n * read every ancestor, every time the thread is loaded.\n */\nfunction deltaChannelsToReplay(\n specs: Record<string, BaseChannel>,\n checkpoint: ReadonlyCheckpoint,\n versionedOnly: boolean\n): string[] {\n const keys: string[] = [];\n for (const k in specs) {\n if (!Object.prototype.hasOwnProperty.call(specs, k)) continue;\n if (\n isDeltaChannel(specs[k]) &&\n (!versionedOnly || checkpoint.channel_versions[k] !== undefined) &&\n !Object.prototype.hasOwnProperty.call(checkpoint.channel_values, k)\n ) {\n keys.push(k);\n }\n }\n return keys;\n}\n\n/**\n * Hydrate channels from a checkpoint, reconstructing any {@link DeltaChannel}\n * whose value is absent from `channel_values` by replaying ancestor writes.\n *\n * For most channels (and for delta channels with a {@link DeltaSnapshot} or a\n * migrated plain value in `channel_values`), {@link emptyChannels} is\n * sufficient and no saver access is required. When a delta channel is absent\n * from `channel_values`, an ancestor walk via `saver.getDeltaChannelHistory`\n * finds the nearest seed and accumulates the writes between it and the\n * target. All delta channels needing replay are batched into a single saver\n * call. Pass `deltaWritesVersioned` for a checkpoint whose metadata carries\n * {@link DELTA_WRITES_VERSIONED}, so delta channels without a version are\n * left empty instead of walked.\n */\nexport async function channelsFromCheckpoint<\n Cc extends Record<string, BaseChannel>,\n>(\n specs: Cc,\n checkpoint: ReadonlyCheckpoint,\n options?: {\n saver?: BaseCheckpointSaver;\n config?: RunnableConfig;\n deltaWritesVersioned?: boolean;\n }\n): Promise<Cc> {\n const channels = emptyChannels(specs, checkpoint);\n const { saver, config } = options ?? {};\n\n const filteredSpecs = getOnlyChannels(specs);\n const deltaKeys = deltaChannelsToReplay(\n filteredSpecs,\n checkpoint,\n options?.deltaWritesVersioned ?? false\n );\n\n const written = deltaKeys.filter(\n (k) => checkpoint.channel_versions[k] !== undefined\n );\n if ((saver === undefined || config === undefined) && written.length > 0) {\n throw new Error(\n `DeltaChannel ${written.join(\", \")} has history to replay but no checkpointer or config was passed to read it`\n );\n }\n if (deltaKeys.length === 0 || saver === undefined || config === undefined) {\n return channels;\n }\n\n const histories = await saver.getDeltaChannelHistory({\n config,\n channels: deltaKeys,\n });\n for (const k of deltaKeys) {\n const history = histories[k];\n if (history === undefined) continue;\n const replayCh = filteredSpecs[k].fromCheckpoint(\n history.seed\n ) as unknown as DeltaChannelLike;\n replayCh.replayWrites(history.writes);\n (channels as Record<string, BaseChannel>)[k] = replayCh;\n }\n return channels;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"base.cjs","names":["EmptyChannelError","EXIT_DELTA_NULL_TASK_ID","getDeltaMaxSuperstepsSinceSnapshot","DeltaSnapshot","SNAPSHOT_BUMPS"],"sources":["../../src/channels/base.ts"],"sourcesContent":["import {\n ReadonlyCheckpoint,\n uuid6,\n Checkpoint,\n DeltaSnapshot,\n type BaseCheckpointSaver,\n type CheckpointMetadata,\n type DeltaChannelHistory,\n} from \"@langchain/langgraph-checkpoint\";\nimport type { RunnableConfig } from \"@langchain/core/runnables\";\nimport { EmptyChannelError } from \"../errors.js\";\nimport {\n CHECKPOINT_NAMESPACE_END,\n CHECKPOINT_NAMESPACE_SEPARATOR,\n EXIT_DELTA_NULL_TASK_ID,\n getDeltaMaxSuperstepsSinceSnapshot,\n NULL_TASK_ID,\n SNAPSHOT_BUMPS,\n} from \"../constants.js\";\n\n/** Matches Postgres `uuid` / Python `uuid.UUID` (128-bit, 8-4-4-4-12 hex). */\nconst STRUCTURED_UUID =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\n/**\n * Structural check for a {@link DeltaChannel} without importing it (avoids an\n * import cycle: `delta.ts` imports `base.ts`).\n */\nexport function isDeltaChannel(channel: BaseChannel): boolean {\n return channel != null && channel.lc_graph_name === \"DeltaChannel\";\n}\n\nexport function deltaChannelWrites<W extends readonly [PropertyKey, unknown]>(\n channels: Record<string, BaseChannel>,\n writes: ReadonlyArray<W>\n): W[] {\n return writes.filter(([ch]) => isDeltaChannel(channels[String(ch)]));\n}\n\nexport function isBaseChannel(obj: unknown): obj is BaseChannel {\n return obj != null && (obj as BaseChannel).lg_is_channel === true;\n}\n\n/** @internal */\nexport abstract class BaseChannel<\n ValueType = unknown,\n UpdateType = unknown,\n CheckpointType = unknown,\n> {\n ValueType: ValueType;\n\n UpdateType: UpdateType;\n\n /**\n * The name of the channel.\n */\n abstract lc_graph_name: string;\n\n /** @ignore */\n lg_is_channel = true;\n\n /**\n * Return a new identical channel, optionally initialized from a checkpoint.\n * Can be thought of as a \"restoration\" from a checkpoint which is a \"snapshot\" of the channel's state.\n *\n * @param {CheckpointType | undefined} checkpoint\n * @param {CheckpointType | undefined} initialValue\n * @returns {this}\n */\n abstract fromCheckpoint(checkpoint?: CheckpointType): this;\n\n /**\n * Update the channel's value with the given sequence of updates.\n * The order of the updates in the sequence is arbitrary.\n * This method is called by Pregel for all channels at the end of each step.\n * If there are no updates, it is called with an empty sequence.\n *\n * Raises InvalidUpdateError if the sequence of updates is invalid.\n * Returns True if the channel was updated, False otherwise.\n *\n * @throws {InvalidUpdateError} if the sequence of updates is invalid.\n * @param {Array<UpdateType>} values\n * @returns {void}\n */\n abstract update(values: UpdateType[]): boolean;\n\n /**\n * Return the current value of the channel.\n *\n * @throws {EmptyChannelError} if the channel is empty (never updated yet).\n * @returns {ValueType}\n */\n abstract get(): ValueType;\n\n /**\n * Return a string representation of the channel's current state.\n *\n * @throws {EmptyChannelError} if the channel is empty (never updated yet), or doesn't support checkpoints.\n * @returns {CheckpointType | undefined}\n */\n abstract checkpoint(): CheckpointType | undefined;\n\n /**\n * Mark the current value of the channel as consumed. By default, no-op.\n * A channel can use this method to modify its state, preventing the value\n * from being consumed again.\n *\n * Returns True if the channel was updated, False otherwise.\n */\n consume(): boolean {\n return false;\n }\n\n /**\n * Notify the channel that the Pregel run is finishing. By default, no-op.\n * A channel can use this method to modify its state, preventing finish.\n *\n * Returns True if the channel was updated, False otherwise.\n */\n finish(): boolean {\n return false;\n }\n\n /**\n * Return True if the channel is available (not empty), False otherwise.\n * Subclasses should override this method to provide a more efficient\n * implementation than calling get() and catching EmptyChannelError.\n */\n isAvailable(): boolean {\n try {\n this.get();\n return true;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (error: any) {\n if (error.name === EmptyChannelError.unminifiable_name) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Compare this channel with another channel for equality.\n * Used to determine if two channels with the same key are semantically equivalent.\n * Subclasses should override this method to provide a meaningful comparison.\n *\n * @param {BaseChannel} other - The other channel to compare with.\n * @returns {boolean} True if the channels are equal, false otherwise.\n */\n equals(other: BaseChannel): boolean {\n return this === other;\n }\n}\n\nconst IS_ONLY_BASE_CHANNEL = Symbol.for(\"LG_IS_ONLY_BASE_CHANNEL\");\nexport function getOnlyChannels(\n channels: Record<string, BaseChannel>\n): Record<string, BaseChannel> {\n // @ts-expect-error - we know it's a record of base channels\n if (channels[IS_ONLY_BASE_CHANNEL] === true) return channels;\n\n const newChannels = {} as Record<string, BaseChannel>;\n for (const k in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;\n const value = channels[k];\n if (isBaseChannel(value)) newChannels[k] = value;\n }\n\n Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true });\n return newChannels;\n}\n\nexport function emptyChannels<Cc extends Record<string, BaseChannel>>(\n channels: Cc,\n checkpoint: ReadonlyCheckpoint\n): Cc {\n const filteredChannels = getOnlyChannels(channels) as Cc;\n\n const newChannels = {} as Cc;\n for (const k in filteredChannels) {\n if (!Object.prototype.hasOwnProperty.call(filteredChannels, k)) continue;\n const channelValue = checkpoint.channel_values[k];\n newChannels[k] = filteredChannels[k].fromCheckpoint(channelValue);\n }\n Object.assign(newChannels, { [IS_ONLY_BASE_CHANNEL]: true });\n return newChannels;\n}\n\n/**\n * Minimal structural view of a {@link DeltaChannel}, used by helpers in this\n * module that must not import the concrete class (import-cycle avoidance).\n */\ninterface DeltaChannelLike extends BaseChannel {\n snapshotFrequency: number;\n replayWrites(writes: DeltaChannelHistory[\"writes\"]): void;\n}\n\n/**\n * Synthetic task id for exit-mode DeltaChannel writes.\n *\n * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`\n * preserves chronological order while remaining a valid RFC UUID (required by\n * Postgres `checkpoint_writes.task_id uuid` columns). Never `NULL_TASK_ID`:\n * readers apply writes under it as the anchor checkpoint's own pending writes.\n */\nexport function exitDeltaTaskId(step: number, taskId: string): string {\n if (!STRUCTURED_UUID.test(taskId)) {\n throw new TypeError(`Invalid task id for exit delta: ${taskId}`);\n }\n const parts = taskId.toLowerCase().split(\"-\");\n const stepPart = String(step).padStart(8, \"0\");\n const synthetic = `${stepPart}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;\n return synthetic === NULL_TASK_ID ? EXIT_DELTA_NULL_TASK_ID : synthetic;\n}\n\n/**\n * Synthetic task id for exit-mode writes of a superstep after the anchor's\n * own.\n *\n * Sorts after every real task id, in step order, so replay keeps them after\n * the anchor's own superstep whether a saver orders writes by task path\n * (`writesSortKey`) or by task id. Mirrors Python's\n * `exit_delta_late_task_id` exactly, including the two hexadecimal step\n * groups (step is split across them).\n */\nexport function exitDeltaLateTaskId(step: number, taskId: string): string {\n if (!STRUCTURED_UUID.test(taskId)) {\n throw new TypeError(`Invalid task id for exit delta: ${taskId}`);\n }\n const parts = taskId.toLowerCase().split(\"-\");\n const stepHi = (step >> 16).toString(16).padStart(4, \"0\");\n const stepLo = (step & 0xffff).toString(16).padStart(4, \"0\");\n return `ffffffff-${stepHi}-${stepLo}-${parts[3]}-${parts[4]}`;\n}\n\n/**\n * DeltaChannels a branch starting from this checkpoint must snapshot.\n *\n * A checkpoint's pending writes belong to the child that consumed them, and\n * nothing records which child that was. A new branch snapshots every delta\n * channel they touch, so its ancestor walk never replays them (port of\n * Python #8548's `delta_channels_with_pending_writes`).\n */\nexport function deltaChannelsWithPendingWrites(\n channels: Record<string, BaseChannel>,\n pendingWrites: ReadonlyArray<[string, string, unknown]> | undefined\n): Set<string> {\n const result = new Set<string>();\n if (pendingWrites === undefined) return result;\n for (const [, ch] of pendingWrites) {\n const channel = channels[ch];\n if (channel != null && isDeltaChannel(channel)) result.add(ch);\n }\n return result;\n}\n\n/**\n * Return the set of {@link DeltaChannel} names that should snapshot now.\n *\n * A channel snapshots when EITHER its accumulated update count reaches\n * `snapshotFrequency` OR the total supersteps since its last snapshot reaches\n * `DELTA_MAX_SUPERSTEPS_SINCE_SNAPSHOT`. Given `channelVersions`, a channel\n * without a version is skipped: it was never written on this branch, so it\n * has nothing to snapshot. Pass `undefined` on a branch without\n * {@link DELTA_WRITES_VERSIONED}, where such a channel can still hold writes\n * and this snapshot is what bounds its walk. Pure predicate, no mutation.\n */\nexport function deltaChannelsToSnapshot(\n channels: Record<string, BaseChannel>,\n countersSinceDeltaSnapshot: Record<string, [number, number]>,\n channelVersions: Record<string, number | string> | undefined\n): Set<string> {\n const result = new Set<string>();\n for (const name in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, name)) continue;\n const ch = channels[name];\n if (\n !isDeltaChannel(ch) ||\n !ch.isAvailable() ||\n (channelVersions !== undefined && channelVersions[name] === undefined)\n ) {\n continue;\n }\n if (reachedSnapshotBound(ch, countersSinceDeltaSnapshot[name])) {\n result.add(name);\n }\n }\n return result;\n}\n\n/** Whether a delta channel's counters reached the bound where it snapshots. */\nexport function reachedSnapshotBound(\n channel: BaseChannel,\n counters: [number, number] | undefined\n): boolean {\n const [updates, supersteps] = counters ?? [0, 0];\n return (\n updates >= (channel as DeltaChannelLike).snapshotFrequency ||\n supersteps >= getDeltaMaxSuperstepsSinceSnapshot()\n );\n}\n\nexport function createCheckpoint<ValueType>(\n checkpoint: ReadonlyCheckpoint,\n channels: Record<string, BaseChannel<ValueType>> | undefined,\n step: number,\n options?: {\n id?: string;\n channelsToSnapshot?: Set<string>;\n /** The channel versions of the last checkpoint the saver stored. */\n storedVersions?: Record<string, number | string>;\n getNextVersion?: (current: number | string | undefined) => number | string;\n triggerToNodes?: Record<string, string[]>;\n }\n): Checkpoint {\n const channelsToSnapshot = options?.channelsToSnapshot ?? new Set<string>();\n const { storedVersions, getNextVersion } = options ?? {};\n const bumped: Record<string, [number | string | undefined, number | string]> =\n {};\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let values: Record<string, any>;\n let channelVersions: Record<string, number | string> =\n checkpoint.channel_versions;\n if (channels === undefined) {\n values = checkpoint.channel_values;\n } else {\n values = {};\n channelVersions = { ...checkpoint.channel_versions };\n for (const k in channels) {\n if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;\n const channel = channels[k];\n if (channelsToSnapshot.has(k)) {\n // `put` only stores a blob for a channel whose version moved, so\n // snapshotting a channel that didn't move since the last stored\n // checkpoint needs a bump: exit mode reaching the cadence on a\n // superstep that skipped the channel, and a fork's first checkpoint.\n if (\n getNextVersion !== undefined &&\n storedVersions !== undefined &&\n channelVersions[k] === storedVersions[k]\n ) {\n const old = channelVersions[k];\n channelVersions[k] = getNextVersion(old);\n bumped[k] = [old, channelVersions[k]];\n }\n values[k] = new DeltaSnapshot(channel.get());\n continue;\n }\n if (isDeltaChannel(channel)) {\n // Omitted from channel_values; reconstructed from ancestor writes.\n continue;\n }\n try {\n values[k] = channel.checkpoint();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n } catch (error: any) {\n if (error.name === EmptyChannelError.unminifiable_name) {\n // no-op\n } else {\n throw error; // Rethrow unexpected errors\n }\n }\n }\n }\n\n return {\n v: 4,\n id: options?.id ?? uuid6(step),\n ts: new Date().toISOString(),\n channel_values: values,\n channel_versions: channelVersions,\n versions_seen: markBumpsSeen(\n checkpoint.versions_seen,\n bumped,\n options?.triggerToNodes\n ),\n };\n}\n\n/**\n * Advance whoever had seen a bumped channel's old version to the new one.\n *\n * A bump that only stores a snapshot is not a write. Left unseen, it would\n * re-fire `interruptBefore` and rerun the channel's subscribers. A channel\n * bumped from no version was never written, so it also goes to the\n * subscribers that never ran: they have no entry, and would start on the bump.\n * For each entry it advances, {@link SNAPSHOT_BUMPS} keeps the new version and\n * the one the node really read, so {@link versionsSeenWithoutBumps} can put\n * the read back.\n */\nfunction markBumpsSeen(\n versionsSeen: ReadonlyCheckpoint[\"versions_seen\"],\n bumped: Record<string, [number | string | undefined, number | string]>,\n triggerToNodes: Record<string, string[]> = {}\n): Checkpoint[\"versions_seen\"] {\n if (Object.keys(bumped).length === 0) return versionsSeen;\n const out = { ...versionsSeen };\n for (const [k, [old]] of Object.entries(bumped)) {\n if (old !== undefined) continue;\n for (const node of triggerToNodes[k] ?? []) out[node] ??= {};\n }\n const marks = { ...versionsSeen[SNAPSHOT_BUMPS] };\n for (const node in out) {\n if (\n !Object.prototype.hasOwnProperty.call(out, node) ||\n node === SNAPSHOT_BUMPS\n ) {\n continue;\n }\n for (const [k, [old, next]] of Object.entries(bumped)) {\n if (out[node][k] !== old) continue;\n const [advanced, read] = bumpKeys(node, k);\n // If an earlier bump set `old`, the real read is already recorded.\n if (old !== undefined && marks[advanced] !== old) marks[read] = old;\n marks[advanced] = next;\n out[node] = { ...out[node], [k]: next };\n }\n }\n if (Object.keys(marks).length > 0) out[SNAPSHOT_BUMPS] = marks;\n return out;\n}\n\n// Node names can't contain either separator, so the keys can't collide.\nconst bumpKeys = (node: string, channel: string): [string, string] => [\n `${node}${CHECKPOINT_NAMESPACE_SEPARATOR}${channel}`,\n `${node}${CHECKPOINT_NAMESPACE_END}${channel}`,\n];\n\n/**\n * `versions_seen` as the nodes read it: an entry a snapshot bump advanced goes\n * back to the version the node really read, or away if it never read one.\n */\nexport function versionsSeenWithoutBumps(\n versionsSeen: ReadonlyCheckpoint[\"versions_seen\"]\n): ReadonlyCheckpoint[\"versions_seen\"] {\n const marks = versionsSeen[SNAPSHOT_BUMPS];\n if (marks === undefined || Object.keys(marks).length === 0) {\n return versionsSeen;\n }\n const out: Checkpoint[\"versions_seen\"] = {};\n for (const node in versionsSeen) {\n if (\n !Object.prototype.hasOwnProperty.call(versionsSeen, node) ||\n node === SNAPSHOT_BUMPS\n ) {\n continue;\n }\n out[node] = {};\n for (const [k, v] of Object.entries(versionsSeen[node])) {\n const [advanced, read] = bumpKeys(node, k);\n if (marks[advanced] !== v) out[node][k] = v;\n else if (read in marks) out[node][k] = marks[read];\n }\n }\n return out;\n}\n\n/**\n * Checkpoint metadata key, `true` when every DeltaChannel write on the\n * checkpoint's branch carries a version. A delta channel with no version there\n * was never written, so it needs no walk. Branches started before this key\n * existed never get it: an older `updateState` stored a delta channel's first\n * write without a version, and only the walk finds such a write.\n */\nexport const DELTA_WRITES_VERSIONED = \"delta_writes_versioned\";\n\nexport function isDeltaWritesVersioned(\n metadata: CheckpointMetadata | undefined\n): boolean {\n return (\n (metadata as Record<string, unknown> | undefined)?.[\n DELTA_WRITES_VERSIONED\n ] === true\n );\n}\n\n/**\n * Delta channels whose value at `checkpoint` the ancestor walk rebuilds.\n *\n * A {@link DeltaSnapshot} or a migrated plain value in `channel_values`\n * resolves directly, so only a channel with nothing stored there needs the\n * walk. With `versionedOnly`, a channel with no version was never written, so\n * it is empty without one; a walk for it would find no snapshot to stop at and\n * read every ancestor, every time the thread is loaded.\n */\nfunction deltaChannelsToReplay(\n specs: Record<string, BaseChannel>,\n checkpoint: ReadonlyCheckpoint,\n versionedOnly: boolean\n): string[] {\n const keys: string[] = [];\n for (const k in specs) {\n if (!Object.prototype.hasOwnProperty.call(specs, k)) continue;\n if (\n isDeltaChannel(specs[k]) &&\n (!versionedOnly || checkpoint.channel_versions[k] !== undefined) &&\n !Object.prototype.hasOwnProperty.call(checkpoint.channel_values, k)\n ) {\n keys.push(k);\n }\n }\n return keys;\n}\n\n/**\n * Hydrate channels from a checkpoint, reconstructing any {@link DeltaChannel}\n * whose value is absent from `channel_values` by replaying ancestor writes.\n *\n * For most channels (and for delta channels with a {@link DeltaSnapshot} or a\n * migrated plain value in `channel_values`), {@link emptyChannels} is\n * sufficient and no saver access is required. When a delta channel is absent\n * from `channel_values`, an ancestor walk via `saver.getDeltaChannelHistory`\n * finds the nearest seed and accumulates the writes between it and the\n * target. All delta channels needing replay are batched into a single saver\n * call. Pass `deltaWritesVersioned` for a checkpoint whose metadata carries\n * {@link DELTA_WRITES_VERSIONED}, so delta channels without a version are\n * left empty instead of walked.\n */\nexport async function channelsFromCheckpoint<\n Cc extends Record<string, BaseChannel>,\n>(\n specs: Cc,\n checkpoint: ReadonlyCheckpoint,\n options?: {\n saver?: BaseCheckpointSaver;\n config?: RunnableConfig;\n deltaWritesVersioned?: boolean;\n }\n): Promise<Cc> {\n const channels = emptyChannels(specs, checkpoint);\n const { saver, config } = options ?? {};\n\n const filteredSpecs = getOnlyChannels(specs);\n const deltaKeys = deltaChannelsToReplay(\n filteredSpecs,\n checkpoint,\n options?.deltaWritesVersioned ?? false\n );\n\n const written = deltaKeys.filter(\n (k) => checkpoint.channel_versions[k] !== undefined\n );\n if ((saver === undefined || config === undefined) && written.length > 0) {\n throw new Error(\n `DeltaChannel ${written.join(\", \")} has history to replay but no checkpointer or config was passed to read it`\n );\n }\n if (deltaKeys.length === 0 || saver === undefined || config === undefined) {\n return channels;\n }\n\n const histories = await saver.getDeltaChannelHistory({\n config,\n channels: deltaKeys,\n });\n for (const k of deltaKeys) {\n const history = histories[k];\n if (history === undefined) continue;\n const replayCh = filteredSpecs[k].fromCheckpoint(\n history.seed\n ) as unknown as DeltaChannelLike;\n replayCh.replayWrites(history.writes);\n (channels as Record<string, BaseChannel>)[k] = replayCh;\n }\n return channels;\n}\n"],"mappings":"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"}
@@ -91,9 +91,30 @@ declare function emptyChannels<Cc extends Record<string, BaseChannel>>(channels:
91
91
  *
92
92
  * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
93
93
  * preserves chronological order while remaining a valid RFC UUID (required by
94
- * Postgres `checkpoint_writes.task_id uuid` columns).
94
+ * Postgres `checkpoint_writes.task_id uuid` columns). Never `NULL_TASK_ID`:
95
+ * readers apply writes under it as the anchor checkpoint's own pending writes.
95
96
  */
96
97
  declare function exitDeltaTaskId(step: number, taskId: string): string;
98
+ /**
99
+ * Synthetic task id for exit-mode writes of a superstep after the anchor's
100
+ * own.
101
+ *
102
+ * Sorts after every real task id, in step order, so replay keeps them after
103
+ * the anchor's own superstep whether a saver orders writes by task path
104
+ * (`writesSortKey`) or by task id. Mirrors Python's
105
+ * `exit_delta_late_task_id` exactly, including the two hexadecimal step
106
+ * groups (step is split across them).
107
+ */
108
+ declare function exitDeltaLateTaskId(step: number, taskId: string): string;
109
+ /**
110
+ * DeltaChannels a branch starting from this checkpoint must snapshot.
111
+ *
112
+ * A checkpoint's pending writes belong to the child that consumed them, and
113
+ * nothing records which child that was. A new branch snapshots every delta
114
+ * channel they touch, so its ancestor walk never replays them (port of
115
+ * Python #8548's `delta_channels_with_pending_writes`).
116
+ */
117
+ declare function deltaChannelsWithPendingWrites(channels: Record<string, BaseChannel>, pendingWrites: ReadonlyArray<[string, string, unknown]> | undefined): Set<string>;
97
118
  /**
98
119
  * Return the set of {@link DeltaChannel} names that should snapshot now.
99
120
  *
@@ -108,9 +129,10 @@ declare function exitDeltaTaskId(step: number, taskId: string): string;
108
129
  declare function deltaChannelsToSnapshot(channels: Record<string, BaseChannel>, countersSinceDeltaSnapshot: Record<string, [number, number]>, channelVersions: Record<string, number | string> | undefined): Set<string>;
109
130
  declare function createCheckpoint<ValueType>(checkpoint: ReadonlyCheckpoint, channels: Record<string, BaseChannel<ValueType>> | undefined, step: number, options?: {
110
131
  id?: string;
111
- channelsToSnapshot?: Set<string>;
112
- updatedChannels?: Set<string>;
132
+ channelsToSnapshot?: Set<string>; /** The channel versions of the last checkpoint the saver stored. */
133
+ storedVersions?: Record<string, number | string>;
113
134
  getNextVersion?: (current: number | string | undefined) => number | string;
135
+ triggerToNodes?: Record<string, string[]>;
114
136
  }): Checkpoint;
115
137
  /**
116
138
  * Hydrate channels from a checkpoint, reconstructing any {@link DeltaChannel}
@@ -132,5 +154,5 @@ declare function channelsFromCheckpoint<Cc extends Record<string, BaseChannel>>(
132
154
  deltaWritesVersioned?: boolean;
133
155
  }): Promise<Cc>;
134
156
  //#endregion
135
- export { BaseChannel, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, emptyChannels, exitDeltaTaskId, isDeltaChannel };
157
+ export { BaseChannel, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, deltaChannelsWithPendingWrites, emptyChannels, exitDeltaLateTaskId, exitDeltaTaskId, isDeltaChannel };
136
158
  //# sourceMappingURL=base.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.cts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AAqBA;;iBAAgB,cAAA,CAAe,OAAA,EAAS,WAAA;;uBASlB,WAAA;EAKpB,SAAA,EAAW,SAAA;EAEX,UAAA,EAAY,UAAA;EAFD;;;EAAA,SAOF,aAAA;EAoCO;EAjChB,aAAA;EA0Fc;;;;;;;;EAAA,SAhFL,cAAA,CAAe,UAAA,GAAa,cAAA;EAlBzB;;;;;;;;;;;;;EAAA,SAiCH,MAAA,CAAO,MAAA,EAAQ,UAAA;EAmCxB;;;;;;EAAA,SA3BS,GAAA,CAAA,GAAO,SAAA;EAgFF;;;;;;EAAA,SAxEL,UAAA,CAAA,GAAc,cAAA;EA2EtB;;;;;;;EAlED,OAAA,CAAA;EAiEY;;;;;AA8Bd;EArFE,MAAA,CAAA;;;;AAyGF;;EAhGE,WAAA,CAAA;EAiGyB;;;;;;;;EA5EzB,MAAA,CAAO,KAAA,EAAO,WAAA;AAAA;AAAA,iBAuBA,aAAA,YAAyB,MAAA,SAAe,WAAA,EAAA,CACtD,QAAA,EAAU,EAAA,EACV,UAAA,EAAY,kBAAA,GACX,EAAA;;;;;;AA6EH;;iBAhDgB,eAAA,CAAgB,IAAA,UAAc,MAAA;;;;;;;;;;;;iBAoB9B,uBAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,0BAAA,EAA4B,MAAA,4BAC5B,eAAA,EAAiB,MAAA,wCAChB,GAAA;AAAA,iBAwBa,gBAAA,WAAA,CACd,UAAA,EAAY,kBAAA,EACZ,QAAA,EAAU,MAAA,SAAe,WAAA,CAAY,SAAA,gBACrC,IAAA,UACA,OAAA;EACE,EAAA;EACA,kBAAA,GAAqB,GAAA;EACrB,eAAA,GAAkB,GAAA;EAClB,cAAA,IAAkB,OAAA;AAAA,IAEnB,UAAA;;AAsHH;;;;;;;;;;;;;iBAAsB,sBAAA,YACT,MAAA,SAAe,WAAA,EAAA,CAE1B,KAAA,EAAO,EAAA,EACP,UAAA,EAAY,kBAAA,EACZ,OAAA;EACE,KAAA,GAAQ,mBAAA;EACR,MAAA,GAAS,cAAA;EACT,oBAAA;AAAA,IAED,OAAA,CAAQ,EAAA"}
1
+ {"version":3,"file":"base.d.cts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AA4BA;;iBAAgB,cAAA,CAAe,OAAA,EAAS,WAAA;AAgBxC;AAAA,uBAAsB,WAAA;EAKpB,SAAA,EAAW,SAAA;EAEX,UAAA,EAAY,UAAA;EAAA;;;EAAA,SAKH,aAAA;EA4Cc;EAzCvB,aAAA;EA0FyB;;;;;;;;EAAA,SAhFhB,cAAA,CAAe,UAAA,GAAa,cAAA;EAb5B;;;;;;;;;;;;;EAAA,SA4BA,MAAA,CAAO,MAAA,EAAQ,UAAA;EA4CxB;;;;;;EAAA,SApCS,GAAA,CAAA,GAAO,SAAA;EAgFW;;;;;;EAAA,SAxElB,UAAA,CAAA,GAAc,cAAA;EA2EpB;;;;;;;EAlEH,OAAA,CAAA;EAiEA;;;;AA+BF;;EAtFE,MAAA,CAAA;EAsF8B;;AAoBhC;;;EAjGE,WAAA,CAAA;EAiG8D;AAkBhE;;;;;;;EA9FE,MAAA,CAAO,KAAA,EAAO,WAAA;AAAA;AAAA,iBAuBA,aAAA,YAAyB,MAAA,SAAe,WAAA,EAAA,CACtD,QAAA,EAAU,EAAA,EACV,UAAA,EAAY,kBAAA,GACX,EAAA;;;;;;;AA4FH;;iBA9DgB,eAAA,CAAgB,IAAA,UAAc,MAAA;;;;;;;;;;;iBAoB9B,mBAAA,CAAoB,IAAA,UAAc,MAAA;;;;;;;AA6ElD;;iBA3DgB,8BAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,aAAA,EAAe,aAAA,0CACd,GAAA;;;;;;;;;;;;iBAqBa,uBAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,0BAAA,EAA4B,MAAA,4BAC5B,eAAA,EAAiB,MAAA,wCAChB,GAAA;AAAA,iBA+Ba,gBAAA,WAAA,CACd,UAAA,EAAY,kBAAA,EACZ,QAAA,EAAU,MAAA,SAAe,WAAA,CAAY,SAAA,gBACrC,IAAA,UACA,OAAA;EACE,EAAA;EACA,kBAAA,GAAqB,GAAA,UAHvB;EAKE,cAAA,GAAiB,MAAA;EACjB,cAAA,IAAkB,OAAA;EAClB,cAAA,GAAiB,MAAA;AAAA,IAElB,UAAA;;;;;;;;;;;;;;;iBA4MmB,sBAAA,YACT,MAAA,SAAe,WAAA,EAAA,CAE1B,KAAA,EAAO,EAAA,EACP,UAAA,EAAY,kBAAA,EACZ,OAAA;EACE,KAAA,GAAQ,mBAAA;EACR,MAAA,GAAS,cAAA;EACT,oBAAA;AAAA,IAED,OAAA,CAAQ,EAAA"}
@@ -91,9 +91,30 @@ declare function emptyChannels<Cc extends Record<string, BaseChannel>>(channels:
91
91
  *
92
92
  * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
93
93
  * preserves chronological order while remaining a valid RFC UUID (required by
94
- * Postgres `checkpoint_writes.task_id uuid` columns).
94
+ * Postgres `checkpoint_writes.task_id uuid` columns). Never `NULL_TASK_ID`:
95
+ * readers apply writes under it as the anchor checkpoint's own pending writes.
95
96
  */
96
97
  declare function exitDeltaTaskId(step: number, taskId: string): string;
98
+ /**
99
+ * Synthetic task id for exit-mode writes of a superstep after the anchor's
100
+ * own.
101
+ *
102
+ * Sorts after every real task id, in step order, so replay keeps them after
103
+ * the anchor's own superstep whether a saver orders writes by task path
104
+ * (`writesSortKey`) or by task id. Mirrors Python's
105
+ * `exit_delta_late_task_id` exactly, including the two hexadecimal step
106
+ * groups (step is split across them).
107
+ */
108
+ declare function exitDeltaLateTaskId(step: number, taskId: string): string;
109
+ /**
110
+ * DeltaChannels a branch starting from this checkpoint must snapshot.
111
+ *
112
+ * A checkpoint's pending writes belong to the child that consumed them, and
113
+ * nothing records which child that was. A new branch snapshots every delta
114
+ * channel they touch, so its ancestor walk never replays them (port of
115
+ * Python #8548's `delta_channels_with_pending_writes`).
116
+ */
117
+ declare function deltaChannelsWithPendingWrites(channels: Record<string, BaseChannel>, pendingWrites: ReadonlyArray<[string, string, unknown]> | undefined): Set<string>;
97
118
  /**
98
119
  * Return the set of {@link DeltaChannel} names that should snapshot now.
99
120
  *
@@ -108,9 +129,10 @@ declare function exitDeltaTaskId(step: number, taskId: string): string;
108
129
  declare function deltaChannelsToSnapshot(channels: Record<string, BaseChannel>, countersSinceDeltaSnapshot: Record<string, [number, number]>, channelVersions: Record<string, number | string> | undefined): Set<string>;
109
130
  declare function createCheckpoint<ValueType>(checkpoint: ReadonlyCheckpoint, channels: Record<string, BaseChannel<ValueType>> | undefined, step: number, options?: {
110
131
  id?: string;
111
- channelsToSnapshot?: Set<string>;
112
- updatedChannels?: Set<string>;
132
+ channelsToSnapshot?: Set<string>; /** The channel versions of the last checkpoint the saver stored. */
133
+ storedVersions?: Record<string, number | string>;
113
134
  getNextVersion?: (current: number | string | undefined) => number | string;
135
+ triggerToNodes?: Record<string, string[]>;
114
136
  }): Checkpoint;
115
137
  /**
116
138
  * Hydrate channels from a checkpoint, reconstructing any {@link DeltaChannel}
@@ -132,5 +154,5 @@ declare function channelsFromCheckpoint<Cc extends Record<string, BaseChannel>>(
132
154
  deltaWritesVersioned?: boolean;
133
155
  }): Promise<Cc>;
134
156
  //#endregion
135
- export { BaseChannel, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, emptyChannels, exitDeltaTaskId, isDeltaChannel };
157
+ export { BaseChannel, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, deltaChannelsWithPendingWrites, emptyChannels, exitDeltaLateTaskId, exitDeltaTaskId, isDeltaChannel };
136
158
  //# sourceMappingURL=base.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.d.ts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AAqBA;;iBAAgB,cAAA,CAAe,OAAA,EAAS,WAAA;;uBASlB,WAAA;EAKpB,SAAA,EAAW,SAAA;EAEX,UAAA,EAAY,UAAA;EAFD;;;EAAA,SAOF,aAAA;EAoCO;EAjChB,aAAA;EA0Fc;;;;;;;;EAAA,SAhFL,cAAA,CAAe,UAAA,GAAa,cAAA;EAlBzB;;;;;;;;;;;;;EAAA,SAiCH,MAAA,CAAO,MAAA,EAAQ,UAAA;EAmCxB;;;;;;EAAA,SA3BS,GAAA,CAAA,GAAO,SAAA;EAgFF;;;;;;EAAA,SAxEL,UAAA,CAAA,GAAc,cAAA;EA2EtB;;;;;;;EAlED,OAAA,CAAA;EAiEY;;;;;AA8Bd;EArFE,MAAA,CAAA;;;;AAyGF;;EAhGE,WAAA,CAAA;EAiGyB;;;;;;;;EA5EzB,MAAA,CAAO,KAAA,EAAO,WAAA;AAAA;AAAA,iBAuBA,aAAA,YAAyB,MAAA,SAAe,WAAA,EAAA,CACtD,QAAA,EAAU,EAAA,EACV,UAAA,EAAY,kBAAA,GACX,EAAA;;;;;;AA6EH;;iBAhDgB,eAAA,CAAgB,IAAA,UAAc,MAAA;;;;;;;;;;;;iBAoB9B,uBAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,0BAAA,EAA4B,MAAA,4BAC5B,eAAA,EAAiB,MAAA,wCAChB,GAAA;AAAA,iBAwBa,gBAAA,WAAA,CACd,UAAA,EAAY,kBAAA,EACZ,QAAA,EAAU,MAAA,SAAe,WAAA,CAAY,SAAA,gBACrC,IAAA,UACA,OAAA;EACE,EAAA;EACA,kBAAA,GAAqB,GAAA;EACrB,eAAA,GAAkB,GAAA;EAClB,cAAA,IAAkB,OAAA;AAAA,IAEnB,UAAA;;AAsHH;;;;;;;;;;;;;iBAAsB,sBAAA,YACT,MAAA,SAAe,WAAA,EAAA,CAE1B,KAAA,EAAO,EAAA,EACP,UAAA,EAAY,kBAAA,EACZ,OAAA;EACE,KAAA,GAAQ,mBAAA;EACR,MAAA,GAAS,cAAA;EACT,oBAAA;AAAA,IAED,OAAA,CAAQ,EAAA"}
1
+ {"version":3,"file":"base.d.ts","names":[],"sources":["../../src/channels/base.ts"],"mappings":";;;;;;AA4BA;;iBAAgB,cAAA,CAAe,OAAA,EAAS,WAAA;AAgBxC;AAAA,uBAAsB,WAAA;EAKpB,SAAA,EAAW,SAAA;EAEX,UAAA,EAAY,UAAA;EAAA;;;EAAA,SAKH,aAAA;EA4Cc;EAzCvB,aAAA;EA0FyB;;;;;;;;EAAA,SAhFhB,cAAA,CAAe,UAAA,GAAa,cAAA;EAb5B;;;;;;;;;;;;;EAAA,SA4BA,MAAA,CAAO,MAAA,EAAQ,UAAA;EA4CxB;;;;;;EAAA,SApCS,GAAA,CAAA,GAAO,SAAA;EAgFW;;;;;;EAAA,SAxElB,UAAA,CAAA,GAAc,cAAA;EA2EpB;;;;;;;EAlEH,OAAA,CAAA;EAiEA;;;;AA+BF;;EAtFE,MAAA,CAAA;EAsF8B;;AAoBhC;;;EAjGE,WAAA,CAAA;EAiG8D;AAkBhE;;;;;;;EA9FE,MAAA,CAAO,KAAA,EAAO,WAAA;AAAA;AAAA,iBAuBA,aAAA,YAAyB,MAAA,SAAe,WAAA,EAAA,CACtD,QAAA,EAAU,EAAA,EACV,UAAA,EAAY,kBAAA,GACX,EAAA;;;;;;;AA4FH;;iBA9DgB,eAAA,CAAgB,IAAA,UAAc,MAAA;;;;;;;;;;;iBAoB9B,mBAAA,CAAoB,IAAA,UAAc,MAAA;;;;;;;AA6ElD;;iBA3DgB,8BAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,aAAA,EAAe,aAAA,0CACd,GAAA;;;;;;;;;;;;iBAqBa,uBAAA,CACd,QAAA,EAAU,MAAA,SAAe,WAAA,GACzB,0BAAA,EAA4B,MAAA,4BAC5B,eAAA,EAAiB,MAAA,wCAChB,GAAA;AAAA,iBA+Ba,gBAAA,WAAA,CACd,UAAA,EAAY,kBAAA,EACZ,QAAA,EAAU,MAAA,SAAe,WAAA,CAAY,SAAA,gBACrC,IAAA,UACA,OAAA;EACE,EAAA;EACA,kBAAA,GAAqB,GAAA,UAHvB;EAKE,cAAA,GAAiB,MAAA;EACjB,cAAA,IAAkB,OAAA;EAClB,cAAA,GAAiB,MAAA;AAAA,IAElB,UAAA;;;;;;;;;;;;;;;iBA4MmB,sBAAA,YACT,MAAA,SAAe,WAAA,EAAA,CAE1B,KAAA,EAAO,EAAA,EACP,UAAA,EAAY,kBAAA,EACZ,OAAA;EACE,KAAA,GAAQ,mBAAA;EACR,MAAA,GAAS,cAAA;EACT,oBAAA;AAAA,IAED,OAAA,CAAQ,EAAA"}
@@ -1,4 +1,4 @@
1
- import { getDeltaMaxSuperstepsSinceSnapshot } from "../constants.js";
1
+ import { EXIT_DELTA_NULL_TASK_ID, SNAPSHOT_BUMPS, getDeltaMaxSuperstepsSinceSnapshot } from "../constants.js";
2
2
  import { EmptyChannelError } from "../errors.js";
3
3
  import { DeltaSnapshot, uuid6 } from "@langchain/langgraph-checkpoint";
4
4
  //#region src/channels/base.ts
@@ -11,6 +11,9 @@ const STRUCTURED_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-
11
11
  function isDeltaChannel(channel) {
12
12
  return channel != null && channel.lc_graph_name === "DeltaChannel";
13
13
  }
14
+ function deltaChannelWrites(channels, writes) {
15
+ return writes.filter(([ch]) => isDeltaChannel(channels[String(ch)]));
16
+ }
14
17
  function isBaseChannel(obj) {
15
18
  return obj != null && obj.lg_is_channel === true;
16
19
  }
@@ -93,12 +96,46 @@ function emptyChannels(channels, checkpoint) {
93
96
  *
94
97
  * Embeds the superstep in the first UUID group so `ORDER BY task_id, idx`
95
98
  * preserves chronological order while remaining a valid RFC UUID (required by
96
- * Postgres `checkpoint_writes.task_id uuid` columns).
99
+ * Postgres `checkpoint_writes.task_id uuid` columns). Never `NULL_TASK_ID`:
100
+ * readers apply writes under it as the anchor checkpoint's own pending writes.
97
101
  */
98
102
  function exitDeltaTaskId(step, taskId) {
99
103
  if (!STRUCTURED_UUID.test(taskId)) throw new TypeError(`Invalid task id for exit delta: ${taskId}`);
100
104
  const parts = taskId.toLowerCase().split("-");
101
- return `${String(step).padStart(8, "0")}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;
105
+ const synthetic = `${String(step).padStart(8, "0")}-${parts[1]}-${parts[2]}-${parts[3]}-${parts[4]}`;
106
+ return synthetic === "00000000-0000-0000-0000-000000000000" ? EXIT_DELTA_NULL_TASK_ID : synthetic;
107
+ }
108
+ /**
109
+ * Synthetic task id for exit-mode writes of a superstep after the anchor's
110
+ * own.
111
+ *
112
+ * Sorts after every real task id, in step order, so replay keeps them after
113
+ * the anchor's own superstep whether a saver orders writes by task path
114
+ * (`writesSortKey`) or by task id. Mirrors Python's
115
+ * `exit_delta_late_task_id` exactly, including the two hexadecimal step
116
+ * groups (step is split across them).
117
+ */
118
+ function exitDeltaLateTaskId(step, taskId) {
119
+ if (!STRUCTURED_UUID.test(taskId)) throw new TypeError(`Invalid task id for exit delta: ${taskId}`);
120
+ const parts = taskId.toLowerCase().split("-");
121
+ return `ffffffff-${(step >> 16).toString(16).padStart(4, "0")}-${(step & 65535).toString(16).padStart(4, "0")}-${parts[3]}-${parts[4]}`;
122
+ }
123
+ /**
124
+ * DeltaChannels a branch starting from this checkpoint must snapshot.
125
+ *
126
+ * A checkpoint's pending writes belong to the child that consumed them, and
127
+ * nothing records which child that was. A new branch snapshots every delta
128
+ * channel they touch, so its ancestor walk never replays them (port of
129
+ * Python #8548's `delta_channels_with_pending_writes`).
130
+ */
131
+ function deltaChannelsWithPendingWrites(channels, pendingWrites) {
132
+ const result = /* @__PURE__ */ new Set();
133
+ if (pendingWrites === void 0) return result;
134
+ for (const [, ch] of pendingWrites) {
135
+ const channel = channels[ch];
136
+ if (channel != null && isDeltaChannel(channel)) result.add(ch);
137
+ }
138
+ return result;
102
139
  }
103
140
  /**
104
141
  * Return the set of {@link DeltaChannel} names that should snapshot now.
@@ -113,19 +150,23 @@ function exitDeltaTaskId(step, taskId) {
113
150
  */
114
151
  function deltaChannelsToSnapshot(channels, countersSinceDeltaSnapshot, channelVersions) {
115
152
  const result = /* @__PURE__ */ new Set();
116
- const maxSupersteps = getDeltaMaxSuperstepsSinceSnapshot();
117
153
  for (const name in channels) {
118
154
  if (!Object.prototype.hasOwnProperty.call(channels, name)) continue;
119
155
  const ch = channels[name];
120
156
  if (!isDeltaChannel(ch) || !ch.isAvailable() || channelVersions !== void 0 && channelVersions[name] === void 0) continue;
121
- const [updates, supersteps] = countersSinceDeltaSnapshot[name] ?? [0, 0];
122
- if (updates >= ch.snapshotFrequency || supersteps >= maxSupersteps) result.add(name);
157
+ if (reachedSnapshotBound(ch, countersSinceDeltaSnapshot[name])) result.add(name);
123
158
  }
124
159
  return result;
125
160
  }
161
+ /** Whether a delta channel's counters reached the bound where it snapshots. */
162
+ function reachedSnapshotBound(channel, counters) {
163
+ const [updates, supersteps] = counters ?? [0, 0];
164
+ return updates >= channel.snapshotFrequency || supersteps >= getDeltaMaxSuperstepsSinceSnapshot();
165
+ }
126
166
  function createCheckpoint(checkpoint, channels, step, options) {
127
167
  const channelsToSnapshot = options?.channelsToSnapshot ?? /* @__PURE__ */ new Set();
128
- const { updatedChannels, getNextVersion } = options ?? {};
168
+ const { storedVersions, getNextVersion } = options ?? {};
169
+ const bumped = {};
129
170
  let values;
130
171
  let channelVersions = checkpoint.channel_versions;
131
172
  if (channels === void 0) values = checkpoint.channel_values;
@@ -136,7 +177,11 @@ function createCheckpoint(checkpoint, channels, step, options) {
136
177
  if (!Object.prototype.hasOwnProperty.call(channels, k)) continue;
137
178
  const channel = channels[k];
138
179
  if (channelsToSnapshot.has(k)) {
139
- if (getNextVersion !== void 0 && (updatedChannels === void 0 || !updatedChannels.has(k))) channelVersions[k] = getNextVersion(channelVersions[k]);
180
+ if (getNextVersion !== void 0 && storedVersions !== void 0 && channelVersions[k] === storedVersions[k]) {
181
+ const old = channelVersions[k];
182
+ channelVersions[k] = getNextVersion(old);
183
+ bumped[k] = [old, channelVersions[k]];
184
+ }
140
185
  values[k] = new DeltaSnapshot(channel.get());
141
186
  continue;
142
187
  }
@@ -154,10 +199,65 @@ function createCheckpoint(checkpoint, channels, step, options) {
154
199
  ts: (/* @__PURE__ */ new Date()).toISOString(),
155
200
  channel_values: values,
156
201
  channel_versions: channelVersions,
157
- versions_seen: checkpoint.versions_seen
202
+ versions_seen: markBumpsSeen(checkpoint.versions_seen, bumped, options?.triggerToNodes)
158
203
  };
159
204
  }
160
205
  /**
206
+ * Advance whoever had seen a bumped channel's old version to the new one.
207
+ *
208
+ * A bump that only stores a snapshot is not a write. Left unseen, it would
209
+ * re-fire `interruptBefore` and rerun the channel's subscribers. A channel
210
+ * bumped from no version was never written, so it also goes to the
211
+ * subscribers that never ran: they have no entry, and would start on the bump.
212
+ * For each entry it advances, {@link SNAPSHOT_BUMPS} keeps the new version and
213
+ * the one the node really read, so {@link versionsSeenWithoutBumps} can put
214
+ * the read back.
215
+ */
216
+ function markBumpsSeen(versionsSeen, bumped, triggerToNodes = {}) {
217
+ if (Object.keys(bumped).length === 0) return versionsSeen;
218
+ const out = { ...versionsSeen };
219
+ for (const [k, [old]] of Object.entries(bumped)) {
220
+ if (old !== void 0) continue;
221
+ for (const node of triggerToNodes[k] ?? []) out[node] ??= {};
222
+ }
223
+ const marks = { ...versionsSeen[SNAPSHOT_BUMPS] };
224
+ for (const node in out) {
225
+ if (!Object.prototype.hasOwnProperty.call(out, node) || node === "__snapshot_bumps__") continue;
226
+ for (const [k, [old, next]] of Object.entries(bumped)) {
227
+ if (out[node][k] !== old) continue;
228
+ const [advanced, read] = bumpKeys(node, k);
229
+ if (old !== void 0 && marks[advanced] !== old) marks[read] = old;
230
+ marks[advanced] = next;
231
+ out[node] = {
232
+ ...out[node],
233
+ [k]: next
234
+ };
235
+ }
236
+ }
237
+ if (Object.keys(marks).length > 0) out[SNAPSHOT_BUMPS] = marks;
238
+ return out;
239
+ }
240
+ const bumpKeys = (node, channel) => [`${node}|${channel}`, `${node}:${channel}`];
241
+ /**
242
+ * `versions_seen` as the nodes read it: an entry a snapshot bump advanced goes
243
+ * back to the version the node really read, or away if it never read one.
244
+ */
245
+ function versionsSeenWithoutBumps(versionsSeen) {
246
+ const marks = versionsSeen[SNAPSHOT_BUMPS];
247
+ if (marks === void 0 || Object.keys(marks).length === 0) return versionsSeen;
248
+ const out = {};
249
+ for (const node in versionsSeen) {
250
+ if (!Object.prototype.hasOwnProperty.call(versionsSeen, node) || node === "__snapshot_bumps__") continue;
251
+ out[node] = {};
252
+ for (const [k, v] of Object.entries(versionsSeen[node])) {
253
+ const [advanced, read] = bumpKeys(node, k);
254
+ if (marks[advanced] !== v) out[node][k] = v;
255
+ else if (read in marks) out[node][k] = marks[read];
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ /**
161
261
  * Checkpoint metadata key, `true` when every DeltaChannel write on the
162
262
  * checkpoint's branch carries a version. A delta channel with no version there
163
263
  * was never written, so it needs no walk. Branches started before this key
@@ -221,6 +321,6 @@ async function channelsFromCheckpoint(specs, checkpoint, options) {
221
321
  return channels;
222
322
  }
223
323
  //#endregion
224
- export { BaseChannel, DELTA_WRITES_VERSIONED, channelsFromCheckpoint, createCheckpoint, deltaChannelsToSnapshot, emptyChannels, exitDeltaTaskId, getOnlyChannels, isBaseChannel, isDeltaChannel, isDeltaWritesVersioned };
324
+ export { BaseChannel, DELTA_WRITES_VERSIONED, channelsFromCheckpoint, createCheckpoint, deltaChannelWrites, deltaChannelsToSnapshot, deltaChannelsWithPendingWrites, emptyChannels, exitDeltaLateTaskId, exitDeltaTaskId, getOnlyChannels, isBaseChannel, isDeltaChannel, isDeltaWritesVersioned, reachedSnapshotBound, versionsSeenWithoutBumps };
225
325
 
226
326
  //# sourceMappingURL=base.js.map