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