@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/CHANGELOG.md +11 -0
- package/dist/_virtual/rolldown_runtime.cjs +25 -0
- package/dist/index.cjs +525 -699
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +36 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +522 -693
- package/dist/index.js.map +1 -1
- package/dist/shallow.cjs +403 -536
- package/dist/shallow.cjs.map +1 -0
- package/dist/shallow.d.cts +46 -0
- package/dist/shallow.d.cts.map +1 -0
- package/dist/shallow.d.ts +32 -25
- package/dist/shallow.d.ts.map +1 -1
- package/dist/shallow.js +401 -531
- package/dist/shallow.js.map +1 -1
- package/dist/store.cjs +616 -811
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.cts +138 -0
- package/dist/store.d.cts.map +1 -0
- package/dist/store.d.ts +120 -114
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +613 -807
- package/dist/store.js.map +1 -1
- package/package.json +36 -39
- package/index.cjs +0 -1
- package/index.d.cts +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/shallow.cjs +0 -1
- package/shallow.d.cts +0 -1
- package/shallow.d.ts +0 -1
- package/shallow.js +0 -1
- package/store.cjs +0 -1
- package/store.d.cts +0 -1
- package/store.d.ts +0 -1
- package/store.js +0 -1
package/dist/index.js
CHANGED
|
@@ -1,699 +1,528 @@
|
|
|
1
|
-
import { BaseCheckpointSaver,
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|