zeitlich 0.2.45 → 0.2.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +78 -10
  2. package/dist/{activities-CrN-ghLo.d.ts → activities-Bm4TLTid.d.ts} +22 -2
  3. package/dist/{activities-Coafq5zr.d.cts → activities-CyeiqK_f.d.cts} +22 -2
  4. package/dist/adapters/thread/anthropic/index.cjs +171 -65
  5. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  6. package/dist/adapters/thread/anthropic/index.d.cts +19 -4
  7. package/dist/adapters/thread/anthropic/index.d.ts +19 -4
  8. package/dist/adapters/thread/anthropic/index.js +171 -65
  9. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  10. package/dist/adapters/thread/anthropic/workflow.cjs +3 -1
  11. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
  13. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
  14. package/dist/adapters/thread/anthropic/workflow.js +3 -1
  15. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  16. package/dist/adapters/thread/google-genai/index.cjs +171 -69
  17. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  18. package/dist/adapters/thread/google-genai/index.d.cts +5 -4
  19. package/dist/adapters/thread/google-genai/index.d.ts +5 -4
  20. package/dist/adapters/thread/google-genai/index.js +171 -69
  21. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/workflow.cjs +3 -1
  23. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -4
  25. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -4
  26. package/dist/adapters/thread/google-genai/workflow.js +3 -1
  27. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  28. package/dist/adapters/thread/langchain/index.cjs +170 -66
  29. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  30. package/dist/adapters/thread/langchain/index.d.cts +18 -4
  31. package/dist/adapters/thread/langchain/index.d.ts +18 -4
  32. package/dist/adapters/thread/langchain/index.js +170 -66
  33. package/dist/adapters/thread/langchain/index.js.map +1 -1
  34. package/dist/adapters/thread/langchain/workflow.cjs +3 -1
  35. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  36. package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
  37. package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
  38. package/dist/adapters/thread/langchain/workflow.js +3 -1
  39. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  40. package/dist/cold-store-BC5L5Z8A.d.cts +117 -0
  41. package/dist/cold-store-CFHwemBJ.d.ts +117 -0
  42. package/dist/index.cjs +226 -27
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +138 -8
  45. package/dist/index.d.ts +138 -8
  46. package/dist/index.js +220 -28
  47. package/dist/index.js.map +1 -1
  48. package/dist/{proxy-Bf7uI-Hw.d.cts → proxy-BxFyd6cg.d.cts} +1 -1
  49. package/dist/{proxy-COqA95FW.d.ts → proxy-Cskmj4Yx.d.ts} +1 -1
  50. package/dist/{thread-manager-BsLO3Fgc.d.cts → thread-manager-9tezUcLW.d.cts} +8 -2
  51. package/dist/{thread-manager-Bi1XlbpJ.d.ts → thread-manager-B-zy3xrs.d.ts} +8 -2
  52. package/dist/{thread-manager-wRVVBFgj.d.cts → thread-manager-D33SUmZa.d.cts} +8 -2
  53. package/dist/{thread-manager-BhkOyQ1I.d.ts → thread-manager-DduoSkvJ.d.ts} +8 -2
  54. package/dist/{types-CdALEF3z.d.cts → types-CnuN9T6t.d.cts} +22 -0
  55. package/dist/{types-ChAy_jSP.d.ts → types-CwN6_tAL.d.ts} +22 -0
  56. package/dist/{types-BkX4HLzi.d.ts → types-L5bvbF-n.d.ts} +17 -1
  57. package/dist/{types-C66-BVBr.d.cts → types-oxt8GN97.d.cts} +17 -1
  58. package/dist/{workflow-BwT5EybR.d.ts → workflow-B1TOcHbt.d.ts} +33 -2
  59. package/dist/{workflow-DMmiaw6w.d.cts → workflow-DIaIV7L2.d.cts} +33 -2
  60. package/dist/workflow.cjs +14 -1
  61. package/dist/workflow.cjs.map +1 -1
  62. package/dist/workflow.d.cts +2 -2
  63. package/dist/workflow.d.ts +2 -2
  64. package/dist/workflow.js +14 -1
  65. package/dist/workflow.js.map +1 -1
  66. package/package.json +6 -1
  67. package/src/adapters/thread/anthropic/activities.ts +72 -36
  68. package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
  69. package/src/adapters/thread/google-genai/activities.ts +64 -40
  70. package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
  71. package/src/adapters/thread/langchain/activities.ts +63 -36
  72. package/src/adapters/thread/langchain/thread-manager.ts +9 -1
  73. package/src/index.ts +20 -1
  74. package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
  75. package/src/lib/session/session.integration.test.ts +138 -0
  76. package/src/lib/session/session.ts +29 -0
  77. package/src/lib/session/types.ts +22 -0
  78. package/src/lib/thread/cold-store.test.ts +193 -0
  79. package/src/lib/thread/cold-store.ts +250 -0
  80. package/src/lib/thread/index.ts +32 -0
  81. package/src/lib/thread/keys.ts +20 -0
  82. package/src/lib/thread/manager.ts +16 -27
  83. package/src/lib/thread/proxy.ts +2 -0
  84. package/src/lib/thread/snapshot.test.ts +443 -0
  85. package/src/lib/thread/snapshot.ts +163 -0
  86. package/src/lib/thread/test-utils.ts +228 -0
  87. package/src/lib/thread/tiered.test.ts +281 -0
  88. package/src/lib/thread/tiered.ts +135 -0
  89. package/src/lib/thread/types.ts +16 -0
@@ -0,0 +1,117 @@
1
+ import { P as PersistedThreadState } from './types-CwN6_tAL.js';
2
+
3
+ /**
4
+ * Pluggable cold-tier interface for thread archives.
5
+ *
6
+ * Zeitlich's thread manager is a Redis-backed hot tier optimized for
7
+ * the duration of a workflow run. A `ColdThreadStore` provides the
8
+ * durable archive: each thread is serialized to a single
9
+ * {@link ThreadSnapshot} blob (messages + persisted state slice +
10
+ * dedup-id ledger) at session-exit time, and restored at session-entry
11
+ * time when the workflow is resumed or forked.
12
+ *
13
+ * The contract is intentionally minimal — one read, one write, one
14
+ * delete keyed by `(threadKey, threadId)`. Any storage backend that
15
+ * can satisfy these three calls (S3, R2, GCS, Postgres, the local
16
+ * filesystem, etc.) can plug into the same tiered manager.
17
+ *
18
+ * Concurrency assumption: zeitlich assumes a single active session
19
+ * per `(threadKey, threadId)` at a time, so cold writes are
20
+ * last-writer-wins; no compare-and-swap is required.
21
+ */
22
+
23
+ /**
24
+ * Serialized form of a thread that can be written to and read from a
25
+ * {@link ColdThreadStore}.
26
+ *
27
+ * `messages` is the list of raw-serialized message strings exactly as
28
+ * they were stored in the Redis list — keeping the cold tier opaque
29
+ * to the adapter-specific message envelope. `state` is the
30
+ * {@link PersistedThreadState} that the session writes via
31
+ * `saveThreadState` on every exit path, or `null` if none has been
32
+ * written yet. `dedupIds` lets the tiered manager re-prime the
33
+ * idempotent-append dedup markers when restoring, so a rewind retry
34
+ * after a continue cannot accidentally re-append a message.
35
+ */
36
+ interface ThreadSnapshot {
37
+ v: 1;
38
+ messages: string[];
39
+ state: PersistedThreadState | null;
40
+ dedupIds: string[];
41
+ }
42
+ /** Pluggable cold archive for thread snapshots. */
43
+ interface ColdThreadStore {
44
+ /**
45
+ * Read the latest snapshot for `(threadKey, threadId)`, or return
46
+ * `null` if no snapshot has ever been written.
47
+ */
48
+ read(threadKey: string, threadId: string): Promise<ThreadSnapshot | null>;
49
+ /**
50
+ * Persist `snapshot` as the latest archive for `(threadKey,
51
+ * threadId)`. Overwrites any prior snapshot in place.
52
+ */
53
+ write(threadKey: string, threadId: string, snapshot: ThreadSnapshot): Promise<void>;
54
+ /**
55
+ * Permanently remove the archive for `(threadKey, threadId)`.
56
+ * No-op if no snapshot exists.
57
+ */
58
+ delete(threadKey: string, threadId: string): Promise<void>;
59
+ }
60
+ /**
61
+ * Compact, duck-typed shape of an S3 client. Zeitlich only needs the
62
+ * `send(...)` method; declaring this locally avoids forcing
63
+ * `@aws-sdk/client-s3` to be installed when the consumer is using a
64
+ * different cold-store backend.
65
+ */
66
+ interface S3LikeClient {
67
+ send<TInput, TOutput>(command: {
68
+ input: TInput;
69
+ } & object): Promise<TOutput>;
70
+ }
71
+ /** Configuration for the built-in S3 cold store. */
72
+ interface S3ColdStoreConfig {
73
+ /** An `@aws-sdk/client-s3` `S3Client` (or duck-typed equivalent). */
74
+ s3: S3LikeClient;
75
+ /** S3 bucket that holds the archive. */
76
+ bucket: string;
77
+ /**
78
+ * Optional key prefix applied to every object. The final key layout
79
+ * is `${prefix}/${threadKey}/${threadId}.json[.gz]` with leading
80
+ * slashes stripped. Defaults to `"threads"`.
81
+ */
82
+ prefix?: string;
83
+ /**
84
+ * Gzip the JSON payload before uploading and assume gzip on read.
85
+ * Defaults to `true` — message lists are highly compressible.
86
+ */
87
+ gzip?: boolean;
88
+ /**
89
+ * Optional `Content-Type` override. Defaults to
90
+ * `application/json` (or `application/gzip` when `gzip: true`).
91
+ */
92
+ contentType?: string;
93
+ }
94
+ /**
95
+ * Build an S3-backed {@link ColdThreadStore}.
96
+ *
97
+ * One object per thread at
98
+ * `${prefix}/${threadKey}/${threadId}.json[.gz]`, JSON-encoded and
99
+ * gzip-compressed by default. The consumer owns the `S3Client`
100
+ * instance, so credentials, region, and endpoint configuration live
101
+ * outside zeitlich.
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * import { S3Client } from "@aws-sdk/client-s3";
106
+ * import { createS3ColdStore } from "zeitlich";
107
+ *
108
+ * const coldStore = createS3ColdStore({
109
+ * s3: new S3Client({ region: "us-east-1" }),
110
+ * bucket: "my-threads",
111
+ * prefix: "prod/threads",
112
+ * });
113
+ * ```
114
+ */
115
+ declare function createS3ColdStore(config: S3ColdStoreConfig): ColdThreadStore;
116
+
117
+ export { type ColdThreadStore as C, type S3ColdStoreConfig as S, type ThreadSnapshot as T, type S3LikeClient as a, createS3ColdStore as c };
package/dist/index.cjs CHANGED
@@ -5,6 +5,8 @@ var z14 = require('zod');
5
5
  var crypto = require('crypto');
6
6
  var common = require('@temporalio/common');
7
7
  var path = require('path');
8
+ var zlib = require('zlib');
9
+ var clientS3 = require('@aws-sdk/client-s3');
8
10
  var activity = require('@temporalio/activity');
9
11
  var fs = require('fs');
10
12
 
@@ -1038,7 +1040,9 @@ async function createSession(config) {
1038
1040
  appendAgentMessage,
1039
1041
  forkThread,
1040
1042
  loadThreadState,
1041
- saveThreadState
1043
+ saveThreadState,
1044
+ hydrateThread,
1045
+ flushThread
1042
1046
  } = threadOps;
1043
1047
  const plugins = [];
1044
1048
  let destroySubagentSandboxes;
@@ -1182,10 +1186,12 @@ async function createSession(config) {
1182
1186
  });
1183
1187
  };
1184
1188
  if (threadMode === "fork" && sourceThreadId) {
1189
+ await hydrateThread(sourceThreadId, threadKey);
1185
1190
  await forkThread(sourceThreadId, threadId, threadKey);
1186
1191
  const forkedSlice = await loadThreadState(threadId, threadKey);
1187
1192
  if (forkedSlice) rehydrateFromSlice(forkedSlice);
1188
1193
  } else if (threadMode === "continue") {
1194
+ await hydrateThread(threadId, threadKey);
1189
1195
  const continuedSlice = await loadThreadState(threadId, threadKey);
1190
1196
  if (continuedSlice) rehydrateFromSlice(continuedSlice);
1191
1197
  } else {
@@ -1367,6 +1373,15 @@ async function createSession(config) {
1367
1373
  error: persistError instanceof Error ? persistError.message : String(persistError)
1368
1374
  });
1369
1375
  }
1376
+ try {
1377
+ await flushThread(threadId, threadKey);
1378
+ } catch (flushError) {
1379
+ workflow.log.warn("failed to flush thread to cold tier", {
1380
+ agentName,
1381
+ threadId,
1382
+ error: flushError instanceof Error ? flushError.message : String(flushError)
1383
+ });
1384
+ }
1370
1385
  await callSessionEnd(exitReason, stateManager.getTurns());
1371
1386
  if (sandboxOwned && sandboxId && sandboxOps) {
1372
1387
  switch (resolvedShutdown) {
@@ -1450,6 +1465,9 @@ function getThreadMetaKey(threadKey, threadId) {
1450
1465
  function getThreadStateKey(threadKey, threadId) {
1451
1466
  return `${threadKey}:state:thread:${threadId}`;
1452
1467
  }
1468
+ function getThreadDedupKey(threadId, dedupId) {
1469
+ return `dedup:${dedupId}:thread:${threadId}`;
1470
+ }
1453
1471
 
1454
1472
  // src/lib/types.ts
1455
1473
  function isTerminalStatus(status) {
@@ -2594,9 +2612,6 @@ redis.call('EXPIRE', KEYS[2], tonumber(ARGV[1]))
2594
2612
  redis.call('SET', KEYS[1], '1', 'EX', tonumber(ARGV[1]))
2595
2613
  return 1
2596
2614
  `;
2597
- function getDedupKey(threadId, id) {
2598
- return `dedup:${id}:thread:${threadId}`;
2599
- }
2600
2615
  function createThreadManager(config) {
2601
2616
  const {
2602
2617
  redis,
@@ -2604,11 +2619,13 @@ function createThreadManager(config) {
2604
2619
  key = "messages",
2605
2620
  serialize = (m) => JSON.stringify(m),
2606
2621
  deserialize = (raw) => JSON.parse(raw),
2607
- idOf
2622
+ idOf,
2623
+ ttlSeconds = THREAD_TTL_SECONDS
2608
2624
  } = config;
2609
2625
  const redisKey = getThreadListKey(key, threadId);
2610
2626
  const metaKey = getThreadMetaKey(key, threadId);
2611
2627
  const stateKey = getThreadStateKey(key, threadId);
2628
+ const dedupKey = (id) => getThreadDedupKey(threadId, id);
2612
2629
  async function assertThreadExists() {
2613
2630
  const exists = await redis.exists(metaKey);
2614
2631
  if (!exists) {
@@ -2618,7 +2635,7 @@ function createThreadManager(config) {
2618
2635
  return {
2619
2636
  async initialize() {
2620
2637
  await redis.del(redisKey);
2621
- await redis.set(metaKey, "1", "EX", THREAD_TTL_SECONDS);
2638
+ await redis.set(metaKey, "1", "EX", ttlSeconds);
2622
2639
  },
2623
2640
  async load() {
2624
2641
  await assertThreadExists();
@@ -2630,18 +2647,17 @@ function createThreadManager(config) {
2630
2647
  await assertThreadExists();
2631
2648
  if (idOf) {
2632
2649
  const dedupId = messages.map(idOf).join(":");
2633
- const dedupKey = getDedupKey(threadId, dedupId);
2634
2650
  await redis.eval(
2635
2651
  APPEND_IDEMPOTENT_SCRIPT,
2636
2652
  2,
2637
- dedupKey,
2653
+ dedupKey(dedupId),
2638
2654
  redisKey,
2639
- String(THREAD_TTL_SECONDS),
2655
+ String(ttlSeconds),
2640
2656
  ...messages.map(serialize)
2641
2657
  );
2642
2658
  } else {
2643
2659
  await redis.rpush(redisKey, ...messages.map(serialize));
2644
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2660
+ await redis.expire(redisKey, ttlSeconds);
2645
2661
  }
2646
2662
  },
2647
2663
  async fork(newThreadId) {
@@ -2656,11 +2672,11 @@ function createThreadManager(config) {
2656
2672
  if (data.length > 0) {
2657
2673
  const newKey = getThreadListKey(key, newThreadId);
2658
2674
  await redis.rpush(newKey, ...data);
2659
- await redis.expire(newKey, THREAD_TTL_SECONDS);
2675
+ await redis.expire(newKey, ttlSeconds);
2660
2676
  }
2661
2677
  if (stateRaw != null) {
2662
2678
  const newStateKey = getThreadStateKey(key, newThreadId);
2663
- await redis.set(newStateKey, stateRaw, "EX", THREAD_TTL_SECONDS);
2679
+ await redis.set(newStateKey, stateRaw, "EX", ttlSeconds);
2664
2680
  }
2665
2681
  return forked;
2666
2682
  },
@@ -2675,15 +2691,13 @@ function createThreadManager(config) {
2675
2691
  const existingIds = existing.map((raw) => idOf(deserialize(raw))).filter((id) => typeof id === "string");
2676
2692
  await redis.del(redisKey);
2677
2693
  if (existingIds.length > 0) {
2678
- await redis.del(
2679
- ...existingIds.map((id) => getDedupKey(threadId, id))
2680
- );
2694
+ await redis.del(...existingIds.map(dedupKey));
2681
2695
  }
2682
2696
  if (messages.length > 0) {
2683
2697
  await redis.rpush(redisKey, ...messages.map(serialize));
2684
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2698
+ await redis.expire(redisKey, ttlSeconds);
2685
2699
  }
2686
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
2700
+ await redis.expire(metaKey, ttlSeconds);
2687
2701
  },
2688
2702
  async delete() {
2689
2703
  await redis.del(redisKey, metaKey, stateKey);
@@ -2695,12 +2709,7 @@ function createThreadManager(config) {
2695
2709
  },
2696
2710
  async saveState(state) {
2697
2711
  await assertThreadExists();
2698
- await redis.set(
2699
- stateKey,
2700
- JSON.stringify(state),
2701
- "EX",
2702
- THREAD_TTL_SECONDS
2703
- );
2712
+ await redis.set(stateKey, JSON.stringify(state), "EX", ttlSeconds);
2704
2713
  },
2705
2714
  async deleteState() {
2706
2715
  await redis.del(stateKey);
@@ -2729,19 +2738,202 @@ function createThreadManager(config) {
2729
2738
  if (idx === -1) return;
2730
2739
  if (idx === 0) {
2731
2740
  await redis.del(redisKey);
2732
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
2741
+ await redis.expire(metaKey, ttlSeconds);
2733
2742
  } else {
2734
2743
  await redis.ltrim(redisKey, 0, idx - 1);
2735
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
2744
+ await redis.expire(redisKey, ttlSeconds);
2736
2745
  }
2737
2746
  if (removedIds.length > 0) {
2738
- await redis.del(
2739
- ...removedIds.map((id) => getDedupKey(threadId, id))
2747
+ await redis.del(...removedIds.map(dedupKey));
2748
+ }
2749
+ }
2750
+ };
2751
+ }
2752
+ function joinKey(parts) {
2753
+ return parts.map((p) => p.replace(/^\/+|\/+$/g, "")).filter((p) => p.length > 0).join("/");
2754
+ }
2755
+ function buildKey(prefix, threadKey, threadId, gzip) {
2756
+ const ext = gzip ? "json.gz" : "json";
2757
+ return joinKey([
2758
+ prefix ?? "threads",
2759
+ threadKey,
2760
+ `${threadId}.${ext}`
2761
+ ]);
2762
+ }
2763
+ async function streamToBuffer(body) {
2764
+ if (body == null) return Buffer.alloc(0);
2765
+ if (body instanceof Uint8Array) return Buffer.from(body);
2766
+ if (typeof body.transformToByteArray === "function") {
2767
+ const bytes = await body.transformToByteArray();
2768
+ return Buffer.from(bytes);
2769
+ }
2770
+ if (typeof body.arrayBuffer === "function") {
2771
+ const ab = await body.arrayBuffer();
2772
+ return Buffer.from(ab);
2773
+ }
2774
+ const chunks = [];
2775
+ for await (const chunk of body) {
2776
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2777
+ }
2778
+ return Buffer.concat(chunks);
2779
+ }
2780
+ function createS3ColdStore(config) {
2781
+ const { s3, bucket, prefix, gzip = true } = config;
2782
+ const contentType = config.contentType ?? (gzip ? "application/gzip" : "application/json");
2783
+ return {
2784
+ async read(threadKey, threadId) {
2785
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2786
+ try {
2787
+ const resp = await s3.send(
2788
+ new clientS3.GetObjectCommand({ Bucket: bucket, Key })
2740
2789
  );
2790
+ const buf = await streamToBuffer(resp.Body);
2791
+ const json = gzip ? zlib.gunzipSync(buf).toString("utf8") : buf.toString("utf8");
2792
+ return JSON.parse(json);
2793
+ } catch (err) {
2794
+ if (isNotFound(err)) return null;
2795
+ throw err;
2741
2796
  }
2797
+ },
2798
+ async write(threadKey, threadId, snapshot) {
2799
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2800
+ const json = JSON.stringify(snapshot);
2801
+ const body = gzip ? zlib.gzipSync(Buffer.from(json, "utf8")) : json;
2802
+ await s3.send(
2803
+ new clientS3.PutObjectCommand({
2804
+ Bucket: bucket,
2805
+ Key,
2806
+ Body: body,
2807
+ ContentType: contentType
2808
+ })
2809
+ );
2810
+ },
2811
+ async delete(threadKey, threadId) {
2812
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
2813
+ await s3.send(new clientS3.DeleteObjectCommand({ Bucket: bucket, Key }));
2742
2814
  }
2743
2815
  };
2744
2816
  }
2817
+ function isNotFound(err) {
2818
+ if (typeof err !== "object" || err === null) return false;
2819
+ const e = err;
2820
+ return e.name === "NoSuchKey" || e.Code === "NoSuchKey" || e.code === "NoSuchKey" || e.name === "NotFound" || e.$metadata?.httpStatusCode === 404;
2821
+ }
2822
+
2823
+ // src/lib/thread/snapshot.ts
2824
+ async function encodeSnapshot(config) {
2825
+ const { redis, threadKey, threadId, idOf } = config;
2826
+ const metaKey = getThreadMetaKey(threadKey, threadId);
2827
+ if (await redis.exists(metaKey) === 0) {
2828
+ return null;
2829
+ }
2830
+ const listKey = getThreadListKey(threadKey, threadId);
2831
+ const stateKey = getThreadStateKey(threadKey, threadId);
2832
+ const messages = await redis.lrange(listKey, 0, -1);
2833
+ const stateRaw = await redis.get(stateKey);
2834
+ const state = stateRaw == null ? null : JSON.parse(stateRaw);
2835
+ const dedupIds = idOf ? messages.map(idOf) : [];
2836
+ return { v: 1, messages, state, dedupIds };
2837
+ }
2838
+ async function applySnapshot(config) {
2839
+ const {
2840
+ redis,
2841
+ threadKey,
2842
+ threadId,
2843
+ snapshot,
2844
+ ttlSeconds = THREAD_TTL_SECONDS
2845
+ } = config;
2846
+ const metaKey = getThreadMetaKey(threadKey, threadId);
2847
+ if (await redis.exists(metaKey) === 1) {
2848
+ return;
2849
+ }
2850
+ const listKey = getThreadListKey(threadKey, threadId);
2851
+ const stateKey = getThreadStateKey(threadKey, threadId);
2852
+ await redis.del(listKey, stateKey);
2853
+ const pipeline = redis.pipeline();
2854
+ if (snapshot.messages.length > 0) {
2855
+ pipeline.rpush(listKey, ...snapshot.messages);
2856
+ pipeline.expire(listKey, ttlSeconds);
2857
+ }
2858
+ if (snapshot.state != null) {
2859
+ pipeline.set(stateKey, JSON.stringify(snapshot.state), "EX", ttlSeconds);
2860
+ }
2861
+ for (const id of snapshot.dedupIds) {
2862
+ pipeline.set(getThreadDedupKey(threadId, id), "1", "EX", ttlSeconds);
2863
+ }
2864
+ const results = await pipeline.exec();
2865
+ if (results) {
2866
+ const firstErr = results.find(([err]) => err)?.[0] ?? null;
2867
+ if (firstErr) {
2868
+ await redis.del(
2869
+ listKey,
2870
+ stateKey,
2871
+ ...snapshot.dedupIds.map((id) => getThreadDedupKey(threadId, id))
2872
+ ).catch(() => void 0);
2873
+ throw firstErr;
2874
+ }
2875
+ }
2876
+ await redis.set(metaKey, "1", "EX", ttlSeconds);
2877
+ }
2878
+ async function clearHotTier(config) {
2879
+ const { redis, threadKey, threadId, dedupIds = [] } = config;
2880
+ const keys = [
2881
+ getThreadListKey(threadKey, threadId),
2882
+ getThreadMetaKey(threadKey, threadId),
2883
+ getThreadStateKey(threadKey, threadId),
2884
+ ...dedupIds.map((id) => getThreadDedupKey(threadId, id))
2885
+ ];
2886
+ await redis.del(...keys);
2887
+ }
2888
+
2889
+ // src/lib/thread/tiered.ts
2890
+ function createTieredThreadManager(config) {
2891
+ const {
2892
+ redis,
2893
+ threadId,
2894
+ key = "messages",
2895
+ coldStore,
2896
+ idOf,
2897
+ deserialize = (raw) => JSON.parse(raw),
2898
+ ttlSeconds = THREAD_TTL_SECONDS
2899
+ } = config;
2900
+ const base = createThreadManager(config);
2901
+ const rawIdOf = idOf ? (raw) => idOf(deserialize(raw)) : void 0;
2902
+ return Object.assign(base, {
2903
+ async hydrate() {
2904
+ if (!coldStore) return;
2905
+ const snapshot = await coldStore.read(key, threadId);
2906
+ if (!snapshot) return;
2907
+ await applySnapshot({
2908
+ redis,
2909
+ threadKey: key,
2910
+ threadId,
2911
+ snapshot,
2912
+ ttlSeconds
2913
+ });
2914
+ },
2915
+ async flush(opts) {
2916
+ if (!coldStore) return;
2917
+ const snapshot = await encodeSnapshot({
2918
+ redis,
2919
+ threadKey: key,
2920
+ threadId,
2921
+ ...rawIdOf ? { idOf: rawIdOf } : {}
2922
+ });
2923
+ if (!snapshot) return;
2924
+ await coldStore.write(key, threadId, snapshot);
2925
+ const deleteHot = opts?.deleteHot ?? true;
2926
+ if (deleteHot) {
2927
+ await clearHotTier({
2928
+ redis,
2929
+ threadKey: key,
2930
+ threadId,
2931
+ dedupIds: snapshot.dedupIds
2932
+ });
2933
+ }
2934
+ }
2935
+ });
2936
+ }
2745
2937
  function getActivityContext() {
2746
2938
  try {
2747
2939
  const ctx = activity.Context.current();
@@ -3685,10 +3877,12 @@ exports.SandboxNotFoundError = SandboxNotFoundError;
3685
3877
  exports.SandboxNotSupportedError = SandboxNotSupportedError;
3686
3878
  exports.THREAD_TTL_SECONDS = THREAD_TTL_SECONDS;
3687
3879
  exports.VirtualFileSystem = VirtualFileSystem;
3880
+ exports.applySnapshot = applySnapshot;
3688
3881
  exports.applyVirtualTreeMutations = applyVirtualTreeMutations;
3689
3882
  exports.askUserQuestionTool = askUserQuestionTool;
3690
3883
  exports.bashHandler = bashHandler;
3691
3884
  exports.bashTool = bashTool;
3885
+ exports.clearHotTier = clearHotTier;
3692
3886
  exports.composeHooks = composeHooks;
3693
3887
  exports.createAgentStateManager = createAgentStateManager;
3694
3888
  exports.createAskUserQuestionHandler = createAskUserQuestionHandler;
@@ -3697,12 +3891,14 @@ exports.createObservabilityHooks = createObservabilityHooks;
3697
3891
  exports.createReadSkillHandler = createReadSkillHandler;
3698
3892
  exports.createReadSkillTool = createReadSkillTool;
3699
3893
  exports.createRunAgentActivity = createRunAgentActivity;
3894
+ exports.createS3ColdStore = createS3ColdStore;
3700
3895
  exports.createSession = createSession;
3701
3896
  exports.createTaskCreateHandler = createTaskCreateHandler;
3702
3897
  exports.createTaskGetHandler = createTaskGetHandler;
3703
3898
  exports.createTaskListHandler = createTaskListHandler;
3704
3899
  exports.createTaskUpdateHandler = createTaskUpdateHandler;
3705
3900
  exports.createThreadManager = createThreadManager;
3901
+ exports.createTieredThreadManager = createTieredThreadManager;
3706
3902
  exports.createToolRouter = createToolRouter;
3707
3903
  exports.createVirtualFsActivities = createVirtualFsActivities;
3708
3904
  exports.defineSubagent = defineSubagent;
@@ -3711,12 +3907,15 @@ exports.defineTool = defineTool;
3711
3907
  exports.defineWorkflow = defineWorkflow;
3712
3908
  exports.editHandler = editHandler;
3713
3909
  exports.editTool = editTool;
3910
+ exports.encodeSnapshot = encodeSnapshot;
3714
3911
  exports.filesWithMimeType = filesWithMimeType;
3715
3912
  exports.formatVirtualFileTree = formatVirtualFileTree;
3716
3913
  exports.getActivityContext = getActivityContext;
3717
3914
  exports.getShortId = getShortId;
3915
+ exports.getThreadDedupKey = getThreadDedupKey;
3718
3916
  exports.getThreadListKey = getThreadListKey;
3719
3917
  exports.getThreadMetaKey = getThreadMetaKey;
3918
+ exports.getThreadStateKey = getThreadStateKey;
3720
3919
  exports.globHandler = globHandler;
3721
3920
  exports.globTool = globTool;
3722
3921
  exports.grepTool = grepTool;