@langchain/langgraph-checkpoint-redis 0.0.2 → 1.0.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.
package/dist/index.cjs CHANGED
@@ -1,703 +1,529 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RedisSaver = void 0;
4
- const langgraph_checkpoint_1 = require("@langchain/langgraph-checkpoint");
5
- const redis_1 = require("redis");
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ const __langchain_langgraph_checkpoint = require_rolldown_runtime.__toESM(require("@langchain/langgraph-checkpoint"));
3
+ const redis = require_rolldown_runtime.__toESM(require("redis"));
4
+
5
+ //#region src/index.ts
6
6
  const SCHEMAS = [
7
- {
8
- index: "checkpoints",
9
- prefix: "checkpoint:",
10
- schema: {
11
- "$.thread_id": { type: "TAG", AS: "thread_id" },
12
- "$.checkpoint_ns": { type: "TAG", AS: "checkpoint_ns" },
13
- "$.checkpoint_id": { type: "TAG", AS: "checkpoint_id" },
14
- "$.parent_checkpoint_id": { type: "TAG", AS: "parent_checkpoint_id" },
15
- "$.checkpoint_ts": { type: "NUMERIC", AS: "checkpoint_ts" },
16
- "$.has_writes": { type: "TAG", AS: "has_writes" },
17
- "$.source": { type: "TAG", AS: "source" },
18
- "$.step": { type: "NUMERIC", AS: "step" },
19
- },
20
- },
21
- {
22
- index: "checkpoint_blobs",
23
- prefix: "checkpoint_blob:",
24
- schema: {
25
- "$.thread_id": { type: "TAG", AS: "thread_id" },
26
- "$.checkpoint_ns": { type: "TAG", AS: "checkpoint_ns" },
27
- "$.checkpoint_id": { type: "TAG", AS: "checkpoint_id" },
28
- "$.channel": { type: "TAG", AS: "channel" },
29
- "$.version": { type: "TAG", AS: "version" },
30
- "$.type": { type: "TAG", AS: "type" },
31
- },
32
- },
33
- {
34
- index: "checkpoint_writes",
35
- prefix: "checkpoint_write:",
36
- schema: {
37
- "$.thread_id": { type: "TAG", AS: "thread_id" },
38
- "$.checkpoint_ns": { type: "TAG", AS: "checkpoint_ns" },
39
- "$.checkpoint_id": { type: "TAG", AS: "checkpoint_id" },
40
- "$.task_id": { type: "TAG", AS: "task_id" },
41
- "$.idx": { type: "NUMERIC", AS: "idx" },
42
- "$.channel": { type: "TAG", AS: "channel" },
43
- "$.type": { type: "TAG", AS: "type" },
44
- },
45
- },
7
+ {
8
+ index: "checkpoints",
9
+ prefix: "checkpoint:",
10
+ schema: {
11
+ "$.thread_id": {
12
+ type: "TAG",
13
+ AS: "thread_id"
14
+ },
15
+ "$.checkpoint_ns": {
16
+ type: "TAG",
17
+ AS: "checkpoint_ns"
18
+ },
19
+ "$.checkpoint_id": {
20
+ type: "TAG",
21
+ AS: "checkpoint_id"
22
+ },
23
+ "$.parent_checkpoint_id": {
24
+ type: "TAG",
25
+ AS: "parent_checkpoint_id"
26
+ },
27
+ "$.checkpoint_ts": {
28
+ type: "NUMERIC",
29
+ AS: "checkpoint_ts"
30
+ },
31
+ "$.has_writes": {
32
+ type: "TAG",
33
+ AS: "has_writes"
34
+ },
35
+ "$.source": {
36
+ type: "TAG",
37
+ AS: "source"
38
+ },
39
+ "$.step": {
40
+ type: "NUMERIC",
41
+ AS: "step"
42
+ }
43
+ }
44
+ },
45
+ {
46
+ index: "checkpoint_blobs",
47
+ prefix: "checkpoint_blob:",
48
+ schema: {
49
+ "$.thread_id": {
50
+ type: "TAG",
51
+ AS: "thread_id"
52
+ },
53
+ "$.checkpoint_ns": {
54
+ type: "TAG",
55
+ AS: "checkpoint_ns"
56
+ },
57
+ "$.checkpoint_id": {
58
+ type: "TAG",
59
+ AS: "checkpoint_id"
60
+ },
61
+ "$.channel": {
62
+ type: "TAG",
63
+ AS: "channel"
64
+ },
65
+ "$.version": {
66
+ type: "TAG",
67
+ AS: "version"
68
+ },
69
+ "$.type": {
70
+ type: "TAG",
71
+ AS: "type"
72
+ }
73
+ }
74
+ },
75
+ {
76
+ index: "checkpoint_writes",
77
+ prefix: "checkpoint_write:",
78
+ schema: {
79
+ "$.thread_id": {
80
+ type: "TAG",
81
+ AS: "thread_id"
82
+ },
83
+ "$.checkpoint_ns": {
84
+ type: "TAG",
85
+ AS: "checkpoint_ns"
86
+ },
87
+ "$.checkpoint_id": {
88
+ type: "TAG",
89
+ AS: "checkpoint_id"
90
+ },
91
+ "$.task_id": {
92
+ type: "TAG",
93
+ AS: "task_id"
94
+ },
95
+ "$.idx": {
96
+ type: "NUMERIC",
97
+ AS: "idx"
98
+ },
99
+ "$.channel": {
100
+ type: "TAG",
101
+ AS: "channel"
102
+ },
103
+ "$.type": {
104
+ type: "TAG",
105
+ AS: "type"
106
+ }
107
+ }
108
+ }
46
109
  ];
47
- class RedisSaver extends langgraph_checkpoint_1.BaseCheckpointSaver {
48
- client;
49
- ttlConfig;
50
- constructor(client, ttlConfig) {
51
- super();
52
- this.client = client;
53
- this.ttlConfig = ttlConfig;
54
- }
55
- static async fromUrl(url, ttlConfig) {
56
- const client = (0, redis_1.createClient)({ url });
57
- await client.connect();
58
- const saver = new RedisSaver(client, ttlConfig);
59
- await saver.ensureIndexes();
60
- return saver;
61
- }
62
- static async fromCluster(rootNodes, ttlConfig) {
63
- const client = (0, redis_1.createCluster)({ rootNodes });
64
- await client.connect();
65
- const saver = new RedisSaver(client, ttlConfig);
66
- await saver.ensureIndexes();
67
- return saver;
68
- }
69
- async get(config) {
70
- const tuple = await this.getTuple(config);
71
- return tuple?.checkpoint;
72
- }
73
- async getTuple(config) {
74
- const threadId = config.configurable?.thread_id;
75
- const checkpointNs = config.configurable?.checkpoint_ns ?? "";
76
- const checkpointId = config.configurable?.checkpoint_id;
77
- if (!threadId) {
78
- return undefined;
79
- }
80
- let key;
81
- let jsonDoc;
82
- if (checkpointId) {
83
- // Get specific checkpoint
84
- key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
85
- jsonDoc = (await this.client.json.get(key));
86
- }
87
- else {
88
- // Get latest checkpoint - need to search
89
- const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
90
- // Use keys for simplicity - scan would be better for large datasets
91
- const keys = await this.client.keys(pattern);
92
- if (keys.length === 0) {
93
- return undefined;
94
- }
95
- // Sort by key to get latest
96
- keys.sort();
97
- key = keys[keys.length - 1];
98
- jsonDoc = (await this.client.json.get(key));
99
- }
100
- if (!jsonDoc) {
101
- return undefined;
102
- }
103
- // Refresh TTL if configured
104
- if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {
105
- await this.applyTTL(key);
106
- }
107
- // Load checkpoint with pending writes
108
- const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
109
- return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
110
- }
111
- async put(config, checkpoint, metadata, newVersions) {
112
- await this.ensureIndexes();
113
- const threadId = config.configurable?.thread_id;
114
- const checkpointNs = config.configurable?.checkpoint_ns ?? "";
115
- const parentCheckpointId = config.configurable?.checkpoint_id;
116
- if (!threadId) {
117
- throw new Error("thread_id is required");
118
- }
119
- const checkpointId = checkpoint.id || (0, langgraph_checkpoint_1.uuid6)(0);
120
- const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
121
- // Copy checkpoint and filter channel_values to only include changed channels
122
- const storedCheckpoint = (0, langgraph_checkpoint_1.copyCheckpoint)(checkpoint);
123
- // If newVersions is provided and has keys, only store those channels that changed
124
- // If newVersions is empty {}, store no channel values
125
- // If newVersions is not provided (undefined), keep all channel_values as-is
126
- if (storedCheckpoint.channel_values && newVersions !== undefined) {
127
- if (Object.keys(newVersions).length === 0) {
128
- // Empty newVersions means no channels changed - store empty channel_values
129
- storedCheckpoint.channel_values = {};
130
- }
131
- else {
132
- // Only store the channels that are in newVersions
133
- const filteredChannelValues = {};
134
- for (const channel of Object.keys(newVersions)) {
135
- if (channel in storedCheckpoint.channel_values) {
136
- filteredChannelValues[channel] =
137
- storedCheckpoint.channel_values[channel];
138
- }
139
- }
140
- storedCheckpoint.channel_values = filteredChannelValues;
141
- }
142
- }
143
- // If newVersions is undefined, keep all channel_values as-is (for backward compatibility)
144
- // Structure matching Python implementation
145
- const jsonDoc = {
146
- thread_id: threadId,
147
- // Store empty namespace as "__empty__" for RediSearch compatibility
148
- checkpoint_ns: checkpointNs === "" ? "__empty__" : checkpointNs,
149
- checkpoint_id: checkpointId,
150
- parent_checkpoint_id: parentCheckpointId || null,
151
- checkpoint: storedCheckpoint,
152
- metadata: metadata,
153
- checkpoint_ts: Date.now(),
154
- has_writes: "false",
155
- };
156
- // Store metadata fields at top-level for searching
157
- this.addSearchableMetadataFields(jsonDoc, metadata);
158
- // Use Redis JSON commands
159
- await this.client.json.set(key, "$", jsonDoc);
160
- // Apply TTL if configured
161
- if (this.ttlConfig?.defaultTTL) {
162
- await this.applyTTL(key);
163
- }
164
- return {
165
- configurable: {
166
- thread_id: threadId,
167
- checkpoint_ns: checkpointNs,
168
- checkpoint_id: checkpointId,
169
- },
170
- };
171
- }
172
- async *list(config, options) {
173
- await this.ensureIndexes();
174
- // If filter is provided (even if empty), use search functionality
175
- if (options?.filter !== undefined) {
176
- // Check if we have null values in the filter which RediSearch can't handle
177
- const hasNullFilter = Object.values(options.filter).some((v) => v === null);
178
- // Build search query
179
- const queryParts = [];
180
- // Add thread_id constraint if provided
181
- if (config?.configurable?.thread_id) {
182
- const threadId = config.configurable.thread_id.replace(/[-.@]/g, "\\$&");
183
- queryParts.push(`(@thread_id:{${threadId}})`);
184
- }
185
- // Add checkpoint_ns constraint if provided
186
- if (config?.configurable?.checkpoint_ns !== undefined) {
187
- const checkpointNs = config.configurable.checkpoint_ns;
188
- if (checkpointNs === "") {
189
- // Empty string needs special handling in RediSearch
190
- // We'll store it as "__empty__" in the index
191
- queryParts.push(`(@checkpoint_ns:{__empty__})`);
192
- }
193
- else {
194
- const escapedNs = checkpointNs.replace(/[-.@]/g, "\\$&");
195
- queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);
196
- }
197
- }
198
- // Skip metadata filters in search query when 'before' parameter is used
199
- // We'll apply them after the before filtering to get correct results
200
- if (!options?.before && options?.filter) {
201
- // Add metadata filters (but skip null values)
202
- for (const [key, value] of Object.entries(options.filter)) {
203
- if (value === undefined) {
204
- // Skip undefined filters
205
- }
206
- else if (value === null) {
207
- // Skip null values for RediSearch query, will handle in post-processing
208
- }
209
- else if (typeof value === "string") {
210
- // Don't escape, just wrap in braces for exact match
211
- queryParts.push(`(@${key}:{${value}})`);
212
- }
213
- else if (typeof value === "number") {
214
- queryParts.push(`(@${key}:[${value} ${value}])`);
215
- }
216
- else if (typeof value === "object" &&
217
- Object.keys(value).length === 0) {
218
- // Skip empty objects
219
- }
220
- }
221
- }
222
- if (queryParts.length === 0) {
223
- queryParts.push("*");
224
- }
225
- const query = queryParts.join(" ");
226
- const limit = options?.limit ?? 10;
227
- try {
228
- // Fetch more results than the limit to handle post-filtering for 'before'
229
- // When no thread_id is specified but 'before' is used, we need to fetch all results
230
- const fetchLimit = options?.before && !config?.configurable?.thread_id
231
- ? 1000 // Fetch many results for global search with 'before' filtering
232
- : options?.before
233
- ? limit * 10
234
- : limit;
235
- const results = await this.client.ft.search("checkpoints", query, {
236
- LIMIT: { from: 0, size: fetchLimit },
237
- SORTBY: { BY: "checkpoint_ts", DIRECTION: "DESC" },
238
- });
239
- let documents = results.documents;
240
- let yieldedCount = 0;
241
- for (const doc of documents) {
242
- if (yieldedCount >= limit)
243
- break;
244
- // Handle 'before' parameter - filter based on checkpoint_id comparison
245
- // UUID6 IDs are time-sortable, so string comparison works for ordering
246
- if (options?.before?.configurable?.checkpoint_id) {
247
- const currentCheckpointId = doc.value.checkpoint_id;
248
- const beforeCheckpointId = options.before.configurable.checkpoint_id;
249
- // Skip checkpoints that are not before the specified checkpoint
250
- if (currentCheckpointId >= beforeCheckpointId) {
251
- continue;
252
- }
253
- }
254
- const jsonDoc = doc.value;
255
- // Apply metadata filters manually (either for null filters or when before parameter was used)
256
- let matches = true;
257
- if ((hasNullFilter || options?.before) && options?.filter) {
258
- for (const [filterKey, filterValue] of Object.entries(options.filter)) {
259
- if (filterValue === null) {
260
- // Check if the field exists and is null in metadata
261
- // This should only match explicit null, not missing fields
262
- const metadataValue = jsonDoc.metadata?.[filterKey];
263
- if (metadataValue !== null) {
264
- matches = false;
265
- break;
266
- }
267
- }
268
- else if (filterValue !== undefined) {
269
- // Check other metadata values
270
- const metadataValue = jsonDoc.metadata?.[filterKey];
271
- // For objects, do deep equality check with deterministic key ordering
272
- if (typeof filterValue === "object" && filterValue !== null) {
273
- if (deterministicStringify(metadataValue) !==
274
- deterministicStringify(filterValue)) {
275
- matches = false;
276
- break;
277
- }
278
- }
279
- else if (metadataValue !== filterValue) {
280
- matches = false;
281
- break;
282
- }
283
- }
284
- }
285
- if (!matches)
286
- continue;
287
- }
288
- // Load checkpoint with pending writes and migrate sends
289
- const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
290
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
291
- yieldedCount++;
292
- }
293
- // Search succeeded, return without falling through
294
- return;
295
- }
296
- catch (error) {
297
- if (error.message?.includes("no such index")) {
298
- // Index doesn't exist yet, fall through to regular listing
299
- }
300
- else {
301
- throw error;
302
- }
303
- }
304
- // If search failed due to missing index, fall through to regular listing
305
- if (config?.configurable?.thread_id) {
306
- // Fall back to regular listing with manual filtering when thread_id is specified
307
- const threadId = config.configurable.thread_id;
308
- const checkpointNs = config.configurable.checkpoint_ns ?? "";
309
- const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
310
- // Use scan for better performance and cluster compatibility
311
- // Use keys for simplicity - scan would be better for large datasets
312
- const keys = await this.client.keys(pattern);
313
- keys.sort().reverse();
314
- let filteredKeys = keys;
315
- // Handle 'before' parameter
316
- if (options?.before?.configurable?.checkpoint_id) {
317
- const beforeThreadId = options.before.configurable.thread_id || threadId;
318
- const beforeCheckpointNs = options.before.configurable.checkpoint_ns ?? checkpointNs;
319
- const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;
320
- const beforeIndex = keys.indexOf(beforeKey);
321
- if (beforeIndex > 0) {
322
- // Return all items that come after the found index (i.e., before in time)
323
- filteredKeys = keys.slice(beforeIndex + 1);
324
- }
325
- else if (beforeIndex === 0) {
326
- // Nothing before the first item (most recent)
327
- filteredKeys = [];
328
- }
329
- // If not found, return all
330
- }
331
- const limit = options?.limit ?? 10;
332
- const limitedKeys = filteredKeys.slice(0, limit);
333
- for (const key of limitedKeys) {
334
- const jsonDoc = (await this.client.json.get(key));
335
- if (jsonDoc) {
336
- // Check if metadata matches filter
337
- let matches = true;
338
- for (const [filterKey, filterValue] of Object.entries(options.filter)) {
339
- const metadataValue = jsonDoc.metadata?.[filterKey];
340
- if (filterValue === null) {
341
- if (metadataValue !== null) {
342
- matches = false;
343
- break;
344
- }
345
- }
346
- else if (metadataValue !== filterValue) {
347
- matches = false;
348
- break;
349
- }
350
- }
351
- if (!matches)
352
- continue;
353
- // Load checkpoint with pending writes and migrate sends
354
- const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
355
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
356
- }
357
- }
358
- }
359
- else {
360
- // Fall back to global search when thread_id is undefined
361
- // This is needed for validation tests that search globally with 'before' parameter
362
- const globalPattern = config?.configurable?.checkpoint_ns !== undefined
363
- ? `checkpoint:*:${config.configurable.checkpoint_ns === ""
364
- ? "__empty__"
365
- : config.configurable.checkpoint_ns}:*`
366
- : "checkpoint:*";
367
- const allKeys = await this.client.keys(globalPattern);
368
- const allDocuments = [];
369
- // Load all matching documents
370
- for (const key of allKeys) {
371
- const jsonDoc = (await this.client.json.get(key));
372
- if (jsonDoc) {
373
- allDocuments.push({ key, doc: jsonDoc });
374
- }
375
- }
376
- // Sort by timestamp (descending) to match the search behavior
377
- allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);
378
- let yieldedCount = 0;
379
- const limit = options?.limit ?? 10;
380
- for (const { doc: jsonDoc } of allDocuments) {
381
- if (yieldedCount >= limit)
382
- break;
383
- // Handle 'before' parameter - filter based on checkpoint_id comparison
384
- if (options?.before?.configurable?.checkpoint_id) {
385
- const currentCheckpointId = jsonDoc.checkpoint_id;
386
- const beforeCheckpointId = options.before.configurable.checkpoint_id;
387
- // Skip checkpoints that are not before the specified checkpoint
388
- if (currentCheckpointId >= beforeCheckpointId) {
389
- continue;
390
- }
391
- }
392
- // Apply metadata filters manually
393
- let matches = true;
394
- if (options?.filter) {
395
- for (const [filterKey, filterValue] of Object.entries(options.filter)) {
396
- if (filterValue === null) {
397
- // Check if the field exists and is null in metadata
398
- const metadataValue = jsonDoc.metadata?.[filterKey];
399
- if (metadataValue !== null) {
400
- matches = false;
401
- break;
402
- }
403
- }
404
- else if (filterValue !== undefined) {
405
- // Check other metadata values
406
- const metadataValue = jsonDoc.metadata?.[filterKey];
407
- // For objects, do deep equality check with deterministic key ordering
408
- if (typeof filterValue === "object" && filterValue !== null) {
409
- if (deterministicStringify(metadataValue) !==
410
- deterministicStringify(filterValue)) {
411
- matches = false;
412
- break;
413
- }
414
- }
415
- else if (metadataValue !== filterValue) {
416
- matches = false;
417
- break;
418
- }
419
- }
420
- }
421
- if (!matches)
422
- continue;
423
- }
424
- // Load checkpoint with pending writes and migrate sends
425
- const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
426
- yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
427
- yieldedCount++;
428
- }
429
- }
430
- return;
431
- }
432
- // Regular listing without filter - use search with empty filter instead
433
- // This ensures consistent behavior between filter={} and filter=undefined
434
- const searchOptions = {
435
- ...options,
436
- filter: {},
437
- };
438
- // Delegate to the search path
439
- yield* this.list(config, searchOptions);
440
- return;
441
- }
442
- async putWrites(config, writes, taskId) {
443
- await this.ensureIndexes();
444
- const threadId = config.configurable?.thread_id;
445
- const checkpointNs = config.configurable?.checkpoint_ns ?? "";
446
- const checkpointId = config.configurable?.checkpoint_id;
447
- if (!threadId || !checkpointId) {
448
- throw new Error("thread_id and checkpoint_id are required");
449
- }
450
- // Collect write keys for sorted set tracking
451
- const writeKeys = [];
452
- // Use high-resolution timestamp to ensure unique ordering across putWrites calls
453
- const baseTimestamp = performance.now() * 1000; // Microsecond precision
454
- // Store each write as a separate indexed JSON document
455
- for (let idx = 0; idx < writes.length; idx++) {
456
- const [channel, value] = writes[idx];
457
- const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;
458
- writeKeys.push(writeKey);
459
- const writeDoc = {
460
- thread_id: threadId,
461
- checkpoint_ns: checkpointNs,
462
- checkpoint_id: checkpointId,
463
- task_id: taskId,
464
- idx: idx,
465
- channel: channel,
466
- type: typeof value === "object" ? "json" : "string",
467
- value: value,
468
- timestamp: baseTimestamp,
469
- global_idx: baseTimestamp + idx, // Add microseconds for sub-millisecond ordering
470
- };
471
- await this.client.json.set(writeKey, "$", writeDoc);
472
- }
473
- // Register write keys in sorted set for efficient retrieval
474
- if (writeKeys.length > 0) {
475
- const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
476
- // Use timestamp + idx for scoring to maintain correct order
477
- const zaddArgs = {};
478
- writeKeys.forEach((key, idx) => {
479
- zaddArgs[key] = baseTimestamp + idx;
480
- });
481
- await this.client.zAdd(zsetKey, Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key })));
482
- // Apply TTL to write keys and zset if configured
483
- if (this.ttlConfig?.defaultTTL) {
484
- // Apply TTL to write keys and zset
485
- await this.applyTTL(...writeKeys, zsetKey);
486
- }
487
- }
488
- // Update checkpoint to indicate it has writes
489
- const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
490
- const checkpointExists = await this.client.exists(checkpointKey);
491
- if (checkpointExists) {
492
- // Get the current document and update it
493
- const currentDoc = (await this.client.json.get(checkpointKey));
494
- if (currentDoc) {
495
- currentDoc.has_writes = "true";
496
- await this.client.json.set(checkpointKey, "$", currentDoc);
497
- }
498
- }
499
- }
500
- async deleteThread(threadId) {
501
- // Delete checkpoints
502
- const checkpointPattern = `checkpoint:${threadId}:*`;
503
- // Use scan for better performance and cluster compatibility
504
- // Use keys for simplicity - scan would be better for large datasets
505
- const checkpointKeys = await this.client.keys(checkpointPattern);
506
- if (checkpointKeys.length > 0) {
507
- await this.client.del(checkpointKeys);
508
- }
509
- // Delete writes
510
- const writesPattern = `writes:${threadId}:*`;
511
- // Use scan for better performance and cluster compatibility
512
- // Use keys for simplicity - scan would be better for large datasets
513
- const writesKeys = await this.client.keys(writesPattern);
514
- if (writesKeys.length > 0) {
515
- await this.client.del(writesKeys);
516
- }
517
- }
518
- async end() {
519
- await this.client.quit();
520
- }
521
- // Helper method to load channel blobs (simplified - no blob support for now)
522
- // private async loadChannelBlobs(
523
- // checkpoint: Checkpoint & { channel_blobs?: any }
524
- // ): Promise<Checkpoint> {
525
- // // Since we're not using blobs anymore, just return the checkpoint as-is
526
- // return checkpoint;
527
- // }
528
- // Helper method to load pending writes
529
- async loadPendingWrites(threadId, checkpointNs, checkpointId) {
530
- // Search for all write documents for this checkpoint
531
- const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;
532
- const writeKeys = await this.client.keys(pattern);
533
- if (writeKeys.length === 0) {
534
- return undefined;
535
- }
536
- const writeDocuments = [];
537
- for (const writeKey of writeKeys) {
538
- const writeDoc = (await this.client.json.get(writeKey));
539
- if (writeDoc) {
540
- writeDocuments.push(writeDoc);
541
- }
542
- }
543
- // Sort by global_idx (which represents insertion order across all putWrites calls)
544
- // This matches how SQLite would naturally order by insertion time + idx
545
- writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));
546
- const pendingWrites = [];
547
- for (const writeDoc of writeDocuments) {
548
- pendingWrites.push([writeDoc.task_id, writeDoc.channel, writeDoc.value]);
549
- }
550
- return pendingWrites;
551
- }
552
- // Helper method to load checkpoint with pending writes
553
- async loadCheckpointWithWrites(jsonDoc) {
554
- // Load checkpoint directly from JSON
555
- const checkpoint = { ...jsonDoc.checkpoint };
556
- // Migrate pending sends ONLY for OLD checkpoint versions (v < 4) with parents
557
- // Modern checkpoints (v >= 4) should NEVER have pending sends migrated
558
- if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {
559
- // Convert back from "__empty__" to empty string for migration
560
- const actualNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
561
- await this.migratePendingSends(checkpoint, jsonDoc.thread_id, actualNs, jsonDoc.parent_checkpoint_id);
562
- }
563
- // Load this checkpoint's own pending writes (but don't migrate them)
564
- let pendingWrites;
565
- if (jsonDoc.has_writes === "true") {
566
- // Convert back from "__empty__" to empty string for key lookup
567
- const actualNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
568
- pendingWrites = await this.loadPendingWrites(jsonDoc.thread_id, actualNs, jsonDoc.checkpoint_id);
569
- }
570
- return { checkpoint, pendingWrites };
571
- }
572
- // Migrate pending sends from parent checkpoint (matches SQLite implementation)
573
- async migratePendingSends(checkpoint, threadId, checkpointNs, parentCheckpointId) {
574
- // Load pending writes from parent checkpoint that have TASKS channel
575
- const parentWrites = await this.loadPendingWrites(threadId, checkpointNs, parentCheckpointId);
576
- if (!parentWrites || parentWrites.length === 0) {
577
- return;
578
- }
579
- // Filter for TASKS channel writes only
580
- const taskWrites = parentWrites.filter(([, channel]) => channel === langgraph_checkpoint_1.TASKS);
581
- if (taskWrites.length === 0) {
582
- return;
583
- }
584
- // Collect all task values in order
585
- const allTasks = [];
586
- for (const [, , value] of taskWrites) {
587
- allTasks.push(value);
588
- }
589
- // Add pending sends to checkpoint
590
- checkpoint.channel_values ??= {};
591
- checkpoint.channel_values[langgraph_checkpoint_1.TASKS] = allTasks;
592
- // Add to versions (matches SQLite logic)
593
- checkpoint.channel_versions[langgraph_checkpoint_1.TASKS] =
594
- Object.keys(checkpoint.channel_versions).length > 0
595
- ? (0, langgraph_checkpoint_1.maxChannelVersion)(...Object.values(checkpoint.channel_versions))
596
- : 1;
597
- }
598
- // Helper method to create checkpoint tuple from json document
599
- createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
600
- // Convert back from "__empty__" to empty string
601
- const checkpointNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
602
- return {
603
- config: {
604
- configurable: {
605
- thread_id: jsonDoc.thread_id,
606
- checkpoint_ns: checkpointNs,
607
- checkpoint_id: jsonDoc.checkpoint_id,
608
- },
609
- },
610
- checkpoint,
611
- metadata: jsonDoc.metadata,
612
- parentConfig: jsonDoc.parent_checkpoint_id
613
- ? {
614
- configurable: {
615
- thread_id: jsonDoc.thread_id,
616
- checkpoint_ns: checkpointNs,
617
- checkpoint_id: jsonDoc.parent_checkpoint_id,
618
- },
619
- }
620
- : undefined,
621
- pendingWrites,
622
- };
623
- }
624
- // Helper method to add searchable metadata fields
625
- addSearchableMetadataFields(jsonDoc, metadata) {
626
- if (!metadata)
627
- return;
628
- // Add common searchable fields at top level
629
- if ("source" in metadata) {
630
- jsonDoc.source = metadata.source;
631
- }
632
- if ("step" in metadata) {
633
- jsonDoc.step = metadata.step;
634
- }
635
- if ("writes" in metadata) {
636
- // Writes field needs to be JSON stringified for TAG search
637
- jsonDoc.writes =
638
- typeof metadata.writes === "object"
639
- ? JSON.stringify(metadata.writes)
640
- : metadata.writes;
641
- }
642
- if ("score" in metadata) {
643
- jsonDoc.score = metadata.score;
644
- }
645
- }
646
- // Helper method to apply TTL to keys
647
- async applyTTL(...keys) {
648
- if (!this.ttlConfig?.defaultTTL)
649
- return;
650
- const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
651
- const results = await Promise.allSettled(keys.map((key) => this.client.expire(key, ttlSeconds)));
652
- // Log any failures but don't throw - TTL is best effort
653
- for (let i = 0; i < results.length; i++) {
654
- if (results[i].status === "rejected") {
655
- console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
656
- }
657
- }
658
- }
659
- async ensureIndexes() {
660
- for (const schema of SCHEMAS) {
661
- try {
662
- // Try to create the index
663
- await this.client.ft.create(schema.index, schema.schema, {
664
- ON: "JSON",
665
- PREFIX: schema.prefix,
666
- });
667
- }
668
- catch (error) {
669
- // Ignore if index already exists
670
- if (!error.message?.includes("Index already exists")) {
671
- console.error(`Failed to create index ${schema.index}:`, error.message);
672
- }
673
- }
674
- }
675
- }
676
- }
677
- exports.RedisSaver = RedisSaver;
678
- // Helper function for deterministic object comparison
110
+ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseCheckpointSaver {
111
+ client;
112
+ ttlConfig;
113
+ constructor(client, ttlConfig) {
114
+ super();
115
+ this.client = client;
116
+ this.ttlConfig = ttlConfig;
117
+ }
118
+ static async fromUrl(url, ttlConfig) {
119
+ const client = (0, redis.createClient)({ url });
120
+ await client.connect();
121
+ const saver = new RedisSaver(client, ttlConfig);
122
+ await saver.ensureIndexes();
123
+ return saver;
124
+ }
125
+ static async fromCluster(rootNodes, ttlConfig) {
126
+ const client = (0, redis.createCluster)({ rootNodes });
127
+ await client.connect();
128
+ const saver = new RedisSaver(client, ttlConfig);
129
+ await saver.ensureIndexes();
130
+ return saver;
131
+ }
132
+ async get(config) {
133
+ const tuple = await this.getTuple(config);
134
+ return tuple?.checkpoint;
135
+ }
136
+ async getTuple(config) {
137
+ const threadId = config.configurable?.thread_id;
138
+ const checkpointNs = config.configurable?.checkpoint_ns ?? "";
139
+ const checkpointId = config.configurable?.checkpoint_id;
140
+ if (!threadId) return void 0;
141
+ let key;
142
+ let jsonDoc;
143
+ if (checkpointId) {
144
+ key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
145
+ jsonDoc = await this.client.json.get(key);
146
+ } else {
147
+ const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
148
+ const keys = await this.client.keys(pattern);
149
+ if (keys.length === 0) return void 0;
150
+ keys.sort();
151
+ key = keys[keys.length - 1];
152
+ jsonDoc = await this.client.json.get(key);
153
+ }
154
+ if (!jsonDoc) return void 0;
155
+ if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
156
+ const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
157
+ return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
158
+ }
159
+ async put(config, checkpoint, metadata, newVersions) {
160
+ await this.ensureIndexes();
161
+ const threadId = config.configurable?.thread_id;
162
+ const checkpointNs = config.configurable?.checkpoint_ns ?? "";
163
+ const parentCheckpointId = config.configurable?.checkpoint_id;
164
+ if (!threadId) throw new Error("thread_id is required");
165
+ const checkpointId = checkpoint.id || (0, __langchain_langgraph_checkpoint.uuid6)(0);
166
+ const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
167
+ const storedCheckpoint = (0, __langchain_langgraph_checkpoint.copyCheckpoint)(checkpoint);
168
+ if (storedCheckpoint.channel_values && newVersions !== void 0) if (Object.keys(newVersions).length === 0) storedCheckpoint.channel_values = {};
169
+ else {
170
+ const filteredChannelValues = {};
171
+ for (const channel of Object.keys(newVersions)) if (channel in storedCheckpoint.channel_values) filteredChannelValues[channel] = storedCheckpoint.channel_values[channel];
172
+ storedCheckpoint.channel_values = filteredChannelValues;
173
+ }
174
+ const jsonDoc = {
175
+ thread_id: threadId,
176
+ checkpoint_ns: checkpointNs === "" ? "__empty__" : checkpointNs,
177
+ checkpoint_id: checkpointId,
178
+ parent_checkpoint_id: parentCheckpointId || null,
179
+ checkpoint: storedCheckpoint,
180
+ metadata,
181
+ checkpoint_ts: Date.now(),
182
+ has_writes: "false"
183
+ };
184
+ this.addSearchableMetadataFields(jsonDoc, metadata);
185
+ await this.client.json.set(key, "$", jsonDoc);
186
+ if (this.ttlConfig?.defaultTTL) await this.applyTTL(key);
187
+ return { configurable: {
188
+ thread_id: threadId,
189
+ checkpoint_ns: checkpointNs,
190
+ checkpoint_id: checkpointId
191
+ } };
192
+ }
193
+ async *list(config, options) {
194
+ await this.ensureIndexes();
195
+ if (options?.filter !== void 0) {
196
+ const hasNullFilter = Object.values(options.filter).some((v) => v === null);
197
+ const queryParts = [];
198
+ if (config?.configurable?.thread_id) {
199
+ const threadId = config.configurable.thread_id.replace(/[-.@]/g, "\\$&");
200
+ queryParts.push(`(@thread_id:{${threadId}})`);
201
+ }
202
+ if (config?.configurable?.checkpoint_ns !== void 0) {
203
+ const checkpointNs = config.configurable.checkpoint_ns;
204
+ if (checkpointNs === "") queryParts.push(`(@checkpoint_ns:{__empty__})`);
205
+ else {
206
+ const escapedNs = checkpointNs.replace(/[-.@]/g, "\\$&");
207
+ queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);
208
+ }
209
+ }
210
+ if (!options?.before && options?.filter) {
211
+ for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") queryParts.push(`(@${key}:{${value}})`);
212
+ else if (typeof value === "number") queryParts.push(`(@${key}:[${value} ${value}])`);
213
+ else if (typeof value === "object" && Object.keys(value).length === 0) {}
214
+ }
215
+ if (queryParts.length === 0) queryParts.push("*");
216
+ const query = queryParts.join(" ");
217
+ const limit = options?.limit ?? 10;
218
+ try {
219
+ const fetchLimit = options?.before && !config?.configurable?.thread_id ? 1e3 : options?.before ? limit * 10 : limit;
220
+ const results = await this.client.ft.search("checkpoints", query, {
221
+ LIMIT: {
222
+ from: 0,
223
+ size: fetchLimit
224
+ },
225
+ SORTBY: {
226
+ BY: "checkpoint_ts",
227
+ DIRECTION: "DESC"
228
+ }
229
+ });
230
+ let documents = results.documents;
231
+ let yieldedCount = 0;
232
+ for (const doc of documents) {
233
+ if (yieldedCount >= limit) break;
234
+ if (options?.before?.configurable?.checkpoint_id) {
235
+ const currentCheckpointId = doc.value.checkpoint_id;
236
+ const beforeCheckpointId = options.before.configurable.checkpoint_id;
237
+ if (currentCheckpointId >= beforeCheckpointId) continue;
238
+ }
239
+ const jsonDoc = doc.value;
240
+ let matches = true;
241
+ if ((hasNullFilter || options?.before) && options?.filter) {
242
+ for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
243
+ const metadataValue = jsonDoc.metadata?.[filterKey];
244
+ if (metadataValue !== null) {
245
+ matches = false;
246
+ break;
247
+ }
248
+ } else if (filterValue !== void 0) {
249
+ const metadataValue = jsonDoc.metadata?.[filterKey];
250
+ if (typeof filterValue === "object" && filterValue !== null) {
251
+ if (deterministicStringify(metadataValue) !== deterministicStringify(filterValue)) {
252
+ matches = false;
253
+ break;
254
+ }
255
+ } else if (metadataValue !== filterValue) {
256
+ matches = false;
257
+ break;
258
+ }
259
+ }
260
+ if (!matches) continue;
261
+ }
262
+ const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
263
+ yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
264
+ yieldedCount++;
265
+ }
266
+ return;
267
+ } catch (error) {
268
+ if (error.message?.includes("no such index")) {} else throw error;
269
+ }
270
+ if (config?.configurable?.thread_id) {
271
+ const threadId = config.configurable.thread_id;
272
+ const checkpointNs = config.configurable.checkpoint_ns ?? "";
273
+ const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
274
+ const keys = await this.client.keys(pattern);
275
+ keys.sort().reverse();
276
+ let filteredKeys = keys;
277
+ if (options?.before?.configurable?.checkpoint_id) {
278
+ const beforeThreadId = options.before.configurable.thread_id || threadId;
279
+ const beforeCheckpointNs = options.before.configurable.checkpoint_ns ?? checkpointNs;
280
+ const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;
281
+ const beforeIndex = keys.indexOf(beforeKey);
282
+ if (beforeIndex > 0) filteredKeys = keys.slice(beforeIndex + 1);
283
+ else if (beforeIndex === 0) filteredKeys = [];
284
+ }
285
+ const limit$1 = options?.limit ?? 10;
286
+ const limitedKeys = filteredKeys.slice(0, limit$1);
287
+ for (const key of limitedKeys) {
288
+ const jsonDoc = await this.client.json.get(key);
289
+ if (jsonDoc) {
290
+ let matches = true;
291
+ for (const [filterKey, filterValue] of Object.entries(options.filter)) {
292
+ const metadataValue = jsonDoc.metadata?.[filterKey];
293
+ if (filterValue === null) {
294
+ if (metadataValue !== null) {
295
+ matches = false;
296
+ break;
297
+ }
298
+ } else if (metadataValue !== filterValue) {
299
+ matches = false;
300
+ break;
301
+ }
302
+ }
303
+ if (!matches) continue;
304
+ const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
305
+ yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
306
+ }
307
+ }
308
+ } else {
309
+ const globalPattern = config?.configurable?.checkpoint_ns !== void 0 ? `checkpoint:*:${config.configurable.checkpoint_ns === "" ? "__empty__" : config.configurable.checkpoint_ns}:*` : "checkpoint:*";
310
+ const allKeys = await this.client.keys(globalPattern);
311
+ const allDocuments = [];
312
+ for (const key of allKeys) {
313
+ const jsonDoc = await this.client.json.get(key);
314
+ if (jsonDoc) allDocuments.push({
315
+ key,
316
+ doc: jsonDoc
317
+ });
318
+ }
319
+ allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);
320
+ let yieldedCount = 0;
321
+ const limit$1 = options?.limit ?? 10;
322
+ for (const { doc: jsonDoc } of allDocuments) {
323
+ if (yieldedCount >= limit$1) break;
324
+ if (options?.before?.configurable?.checkpoint_id) {
325
+ const currentCheckpointId = jsonDoc.checkpoint_id;
326
+ const beforeCheckpointId = options.before.configurable.checkpoint_id;
327
+ if (currentCheckpointId >= beforeCheckpointId) continue;
328
+ }
329
+ let matches = true;
330
+ if (options?.filter) {
331
+ for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
332
+ const metadataValue = jsonDoc.metadata?.[filterKey];
333
+ if (metadataValue !== null) {
334
+ matches = false;
335
+ break;
336
+ }
337
+ } else if (filterValue !== void 0) {
338
+ const metadataValue = jsonDoc.metadata?.[filterKey];
339
+ if (typeof filterValue === "object" && filterValue !== null) {
340
+ if (deterministicStringify(metadataValue) !== deterministicStringify(filterValue)) {
341
+ matches = false;
342
+ break;
343
+ }
344
+ } else if (metadataValue !== filterValue) {
345
+ matches = false;
346
+ break;
347
+ }
348
+ }
349
+ if (!matches) continue;
350
+ }
351
+ const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
352
+ yield this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
353
+ yieldedCount++;
354
+ }
355
+ }
356
+ return;
357
+ }
358
+ const searchOptions = {
359
+ ...options,
360
+ filter: {}
361
+ };
362
+ yield* this.list(config, searchOptions);
363
+ }
364
+ async putWrites(config, writes, taskId) {
365
+ await this.ensureIndexes();
366
+ const threadId = config.configurable?.thread_id;
367
+ const checkpointNs = config.configurable?.checkpoint_ns ?? "";
368
+ const checkpointId = config.configurable?.checkpoint_id;
369
+ if (!threadId || !checkpointId) throw new Error("thread_id and checkpoint_id are required");
370
+ const writeKeys = [];
371
+ const baseTimestamp = performance.now() * 1e3;
372
+ for (let idx = 0; idx < writes.length; idx++) {
373
+ const [channel, value] = writes[idx];
374
+ const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;
375
+ writeKeys.push(writeKey);
376
+ const writeDoc = {
377
+ thread_id: threadId,
378
+ checkpoint_ns: checkpointNs,
379
+ checkpoint_id: checkpointId,
380
+ task_id: taskId,
381
+ idx,
382
+ channel,
383
+ type: typeof value === "object" ? "json" : "string",
384
+ value,
385
+ timestamp: baseTimestamp,
386
+ global_idx: baseTimestamp + idx
387
+ };
388
+ await this.client.json.set(writeKey, "$", writeDoc);
389
+ }
390
+ if (writeKeys.length > 0) {
391
+ const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
392
+ const zaddArgs = {};
393
+ writeKeys.forEach((key, idx) => {
394
+ zaddArgs[key] = baseTimestamp + idx;
395
+ });
396
+ await this.client.zAdd(zsetKey, Object.entries(zaddArgs).map(([key, score]) => ({
397
+ score,
398
+ value: key
399
+ })));
400
+ if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
401
+ }
402
+ const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
403
+ const checkpointExists = await this.client.exists(checkpointKey);
404
+ if (checkpointExists) {
405
+ const currentDoc = await this.client.json.get(checkpointKey);
406
+ if (currentDoc) {
407
+ currentDoc.has_writes = "true";
408
+ await this.client.json.set(checkpointKey, "$", currentDoc);
409
+ }
410
+ }
411
+ }
412
+ async deleteThread(threadId) {
413
+ const checkpointPattern = `checkpoint:${threadId}:*`;
414
+ const checkpointKeys = await this.client.keys(checkpointPattern);
415
+ if (checkpointKeys.length > 0) await this.client.del(checkpointKeys);
416
+ const writesPattern = `writes:${threadId}:*`;
417
+ const writesKeys = await this.client.keys(writesPattern);
418
+ if (writesKeys.length > 0) await this.client.del(writesKeys);
419
+ }
420
+ async end() {
421
+ await this.client.quit();
422
+ }
423
+ async loadPendingWrites(threadId, checkpointNs, checkpointId) {
424
+ const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;
425
+ const writeKeys = await this.client.keys(pattern);
426
+ if (writeKeys.length === 0) return void 0;
427
+ const writeDocuments = [];
428
+ for (const writeKey of writeKeys) {
429
+ const writeDoc = await this.client.json.get(writeKey);
430
+ if (writeDoc) writeDocuments.push(writeDoc);
431
+ }
432
+ writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));
433
+ const pendingWrites = [];
434
+ for (const writeDoc of writeDocuments) pendingWrites.push([
435
+ writeDoc.task_id,
436
+ writeDoc.channel,
437
+ writeDoc.value
438
+ ]);
439
+ return pendingWrites;
440
+ }
441
+ async loadCheckpointWithWrites(jsonDoc) {
442
+ const checkpoint = { ...jsonDoc.checkpoint };
443
+ if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {
444
+ const actualNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
445
+ await this.migratePendingSends(checkpoint, jsonDoc.thread_id, actualNs, jsonDoc.parent_checkpoint_id);
446
+ }
447
+ let pendingWrites;
448
+ if (jsonDoc.has_writes === "true") {
449
+ const actualNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
450
+ pendingWrites = await this.loadPendingWrites(jsonDoc.thread_id, actualNs, jsonDoc.checkpoint_id);
451
+ }
452
+ return {
453
+ checkpoint,
454
+ pendingWrites
455
+ };
456
+ }
457
+ async migratePendingSends(checkpoint, threadId, checkpointNs, parentCheckpointId) {
458
+ const parentWrites = await this.loadPendingWrites(threadId, checkpointNs, parentCheckpointId);
459
+ if (!parentWrites || parentWrites.length === 0) return;
460
+ const taskWrites = parentWrites.filter(([, channel]) => channel === __langchain_langgraph_checkpoint.TASKS);
461
+ if (taskWrites.length === 0) return;
462
+ const allTasks = [];
463
+ for (const [, , value] of taskWrites) allTasks.push(value);
464
+ checkpoint.channel_values ??= {};
465
+ checkpoint.channel_values[__langchain_langgraph_checkpoint.TASKS] = allTasks;
466
+ checkpoint.channel_versions[__langchain_langgraph_checkpoint.TASKS] = Object.keys(checkpoint.channel_versions).length > 0 ? (0, __langchain_langgraph_checkpoint.maxChannelVersion)(...Object.values(checkpoint.channel_versions)) : 1;
467
+ }
468
+ createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
469
+ const checkpointNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
470
+ return {
471
+ config: { configurable: {
472
+ thread_id: jsonDoc.thread_id,
473
+ checkpoint_ns: checkpointNs,
474
+ checkpoint_id: jsonDoc.checkpoint_id
475
+ } },
476
+ checkpoint,
477
+ metadata: jsonDoc.metadata,
478
+ parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
479
+ thread_id: jsonDoc.thread_id,
480
+ checkpoint_ns: checkpointNs,
481
+ checkpoint_id: jsonDoc.parent_checkpoint_id
482
+ } } : void 0,
483
+ pendingWrites
484
+ };
485
+ }
486
+ addSearchableMetadataFields(jsonDoc, metadata) {
487
+ if (!metadata) return;
488
+ if ("source" in metadata) jsonDoc.source = metadata.source;
489
+ if ("step" in metadata) jsonDoc.step = metadata.step;
490
+ if ("writes" in metadata) jsonDoc.writes = typeof metadata.writes === "object" ? JSON.stringify(metadata.writes) : metadata.writes;
491
+ if ("score" in metadata) jsonDoc.score = metadata.score;
492
+ }
493
+ async applyTTL(...keys) {
494
+ if (!this.ttlConfig?.defaultTTL) return;
495
+ const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
496
+ const results = await Promise.allSettled(keys.map((key) => this.client.expire(key, ttlSeconds)));
497
+ for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
498
+ }
499
+ async ensureIndexes() {
500
+ for (const schema of SCHEMAS) try {
501
+ await this.client.ft.create(schema.index, schema.schema, {
502
+ ON: "JSON",
503
+ PREFIX: schema.prefix
504
+ });
505
+ } catch (error) {
506
+ if (!error.message?.includes("Index already exists")) console.error(`Failed to create index ${schema.index}:`, error.message);
507
+ }
508
+ }
509
+ };
679
510
  function deterministicStringify(obj) {
680
- if (obj === null || typeof obj !== "object") {
681
- return JSON.stringify(obj);
682
- }
683
- if (Array.isArray(obj)) {
684
- return JSON.stringify(obj.map((item) => deterministicStringify(item)));
685
- }
686
- const sortedObj = {};
687
- const sortedKeys = Object.keys(obj).sort();
688
- for (const key of sortedKeys) {
689
- sortedObj[key] = obj[key];
690
- }
691
- return JSON.stringify(sortedObj, (_, value) => {
692
- if (value !== null && typeof value === "object" && !Array.isArray(value)) {
693
- const sorted = {};
694
- const keys = Object.keys(value).sort();
695
- for (const k of keys) {
696
- sorted[k] = value[k];
697
- }
698
- return sorted;
699
- }
700
- return value;
701
- });
511
+ if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
512
+ if (Array.isArray(obj)) return JSON.stringify(obj.map((item) => deterministicStringify(item)));
513
+ const sortedObj = {};
514
+ const sortedKeys = Object.keys(obj).sort();
515
+ for (const key of sortedKeys) sortedObj[key] = obj[key];
516
+ return JSON.stringify(sortedObj, (_, value) => {
517
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
518
+ const sorted = {};
519
+ const keys = Object.keys(value).sort();
520
+ for (const k of keys) sorted[k] = value[k];
521
+ return sorted;
522
+ }
523
+ return value;
524
+ });
702
525
  }
703
- //# sourceMappingURL=index.js.map
526
+
527
+ //#endregion
528
+ exports.RedisSaver = RedisSaver;
529
+ //# sourceMappingURL=index.cjs.map