zeitlich 0.2.44 → 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 (99) hide show
  1. package/README.md +78 -10
  2. package/dist/{activities-CPIB2v2C.d.ts → activities-Bm4TLTid.d.ts} +24 -4
  3. package/dist/{activities-DnmNOnq4.d.cts → activities-CyeiqK_f.d.cts} +24 -4
  4. package/dist/adapters/sandbox/daytona/index.d.cts +2 -2
  5. package/dist/adapters/sandbox/daytona/index.d.ts +2 -2
  6. package/dist/adapters/sandbox/daytona/workflow.d.cts +1 -1
  7. package/dist/adapters/sandbox/daytona/workflow.d.ts +1 -1
  8. package/dist/adapters/sandbox/e2b/index.d.cts +1 -1
  9. package/dist/adapters/sandbox/e2b/index.d.ts +1 -1
  10. package/dist/adapters/thread/anthropic/index.cjs +171 -65
  11. package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
  12. package/dist/adapters/thread/anthropic/index.d.cts +19 -4
  13. package/dist/adapters/thread/anthropic/index.d.ts +19 -4
  14. package/dist/adapters/thread/anthropic/index.js +171 -65
  15. package/dist/adapters/thread/anthropic/index.js.map +1 -1
  16. package/dist/adapters/thread/anthropic/workflow.cjs +3 -1
  17. package/dist/adapters/thread/anthropic/workflow.cjs.map +1 -1
  18. package/dist/adapters/thread/anthropic/workflow.d.cts +4 -4
  19. package/dist/adapters/thread/anthropic/workflow.d.ts +4 -4
  20. package/dist/adapters/thread/anthropic/workflow.js +3 -1
  21. package/dist/adapters/thread/anthropic/workflow.js.map +1 -1
  22. package/dist/adapters/thread/google-genai/index.cjs +171 -69
  23. package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
  24. package/dist/adapters/thread/google-genai/index.d.cts +5 -4
  25. package/dist/adapters/thread/google-genai/index.d.ts +5 -4
  26. package/dist/adapters/thread/google-genai/index.js +171 -69
  27. package/dist/adapters/thread/google-genai/index.js.map +1 -1
  28. package/dist/adapters/thread/google-genai/workflow.cjs +3 -1
  29. package/dist/adapters/thread/google-genai/workflow.cjs.map +1 -1
  30. package/dist/adapters/thread/google-genai/workflow.d.cts +5 -4
  31. package/dist/adapters/thread/google-genai/workflow.d.ts +5 -4
  32. package/dist/adapters/thread/google-genai/workflow.js +3 -1
  33. package/dist/adapters/thread/google-genai/workflow.js.map +1 -1
  34. package/dist/adapters/thread/langchain/index.cjs +181 -77
  35. package/dist/adapters/thread/langchain/index.cjs.map +1 -1
  36. package/dist/adapters/thread/langchain/index.d.cts +18 -4
  37. package/dist/adapters/thread/langchain/index.d.ts +18 -4
  38. package/dist/adapters/thread/langchain/index.js +182 -74
  39. package/dist/adapters/thread/langchain/index.js.map +1 -1
  40. package/dist/adapters/thread/langchain/workflow.cjs +3 -1
  41. package/dist/adapters/thread/langchain/workflow.cjs.map +1 -1
  42. package/dist/adapters/thread/langchain/workflow.d.cts +4 -4
  43. package/dist/adapters/thread/langchain/workflow.d.ts +4 -4
  44. package/dist/adapters/thread/langchain/workflow.js +3 -1
  45. package/dist/adapters/thread/langchain/workflow.js.map +1 -1
  46. package/dist/cold-store-BC5L5Z8A.d.cts +117 -0
  47. package/dist/cold-store-CFHwemBJ.d.ts +117 -0
  48. package/dist/index.cjs +252 -53
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.cts +138 -8
  51. package/dist/index.d.ts +138 -8
  52. package/dist/index.js +247 -54
  53. package/dist/index.js.map +1 -1
  54. package/dist/{proxy-DTnc5rqT.d.cts → proxy-BxFyd6cg.d.cts} +1 -1
  55. package/dist/{proxy-B7Xi1znZ.d.ts → proxy-Cskmj4Yx.d.ts} +1 -1
  56. package/dist/{thread-manager-BlX2TwRN.d.cts → thread-manager-9tezUcLW.d.cts} +9 -3
  57. package/dist/{thread-manager-BAv340mi.d.ts → thread-manager-B-zy3xrs.d.ts} +9 -3
  58. package/dist/{thread-manager-D2xorI-J.d.ts → thread-manager-D33SUmZa.d.cts} +10 -4
  59. package/dist/{thread-manager-BWv6ZXI3.d.cts → thread-manager-DduoSkvJ.d.ts} +10 -4
  60. package/dist/{types-C90VoEpt.d.cts → types-CjY93AWZ.d.cts} +1 -1
  61. package/dist/{types-4Wmk-wRq.d.cts → types-CnuN9T6t.d.cts} +23 -1
  62. package/dist/{types-DKsCdAtQ.d.ts → types-CwN6_tAL.d.ts} +23 -1
  63. package/dist/{types-Clhqautb.d.ts → types-L5bvbF-n.d.ts} +17 -1
  64. package/dist/{types-DpFD8ofR.d.ts → types-gVa5XCWD.d.ts} +1 -1
  65. package/dist/{types-DRJt1TMi.d.cts → types-oxt8GN97.d.cts} +17 -1
  66. package/dist/{workflow-D32TRMr-.d.ts → workflow-B1TOcHbt.d.ts} +33 -2
  67. package/dist/{workflow-XVt0ww8K.d.cts → workflow-DIaIV7L2.d.cts} +33 -2
  68. package/dist/workflow.cjs +29 -19
  69. package/dist/workflow.cjs.map +1 -1
  70. package/dist/workflow.d.cts +2 -2
  71. package/dist/workflow.d.ts +2 -2
  72. package/dist/workflow.js +29 -19
  73. package/dist/workflow.js.map +1 -1
  74. package/package.json +6 -1
  75. package/src/adapters/thread/anthropic/activities.ts +72 -36
  76. package/src/adapters/thread/anthropic/thread-manager.ts +9 -1
  77. package/src/adapters/thread/google-genai/activities.ts +64 -40
  78. package/src/adapters/thread/google-genai/thread-manager.ts +9 -1
  79. package/src/adapters/thread/langchain/activities.ts +63 -36
  80. package/src/adapters/thread/langchain/thread-manager.ts +9 -1
  81. package/src/index.ts +20 -1
  82. package/src/lib/session/session-edge-cases.integration.test.ts +12 -0
  83. package/src/lib/session/session.integration.test.ts +138 -0
  84. package/src/lib/session/session.ts +47 -22
  85. package/src/lib/session/types.ts +22 -0
  86. package/src/lib/thread/cold-store.test.ts +193 -0
  87. package/src/lib/thread/cold-store.ts +250 -0
  88. package/src/lib/thread/index.ts +32 -0
  89. package/src/lib/thread/keys.ts +20 -0
  90. package/src/lib/thread/manager.ts +16 -27
  91. package/src/lib/thread/proxy.ts +2 -0
  92. package/src/lib/thread/snapshot.test.ts +443 -0
  93. package/src/lib/thread/snapshot.ts +163 -0
  94. package/src/lib/thread/test-utils.ts +228 -0
  95. package/src/lib/thread/tiered.test.ts +281 -0
  96. package/src/lib/thread/tiered.ts +135 -0
  97. package/src/lib/thread/types.ts +16 -0
  98. package/src/lib/.env +0 -1
  99. package/src/tools/bash/.env +0 -1
@@ -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,13 +5,14 @@ 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
 
11
13
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
14
 
13
15
  var z14__default = /*#__PURE__*/_interopDefault(z14);
14
- var crypto__default = /*#__PURE__*/_interopDefault(crypto);
15
16
 
16
17
  // src/lib/session/session.ts
17
18
 
@@ -1039,7 +1040,9 @@ async function createSession(config) {
1039
1040
  appendAgentMessage,
1040
1041
  forkThread,
1041
1042
  loadThreadState,
1042
- saveThreadState
1043
+ saveThreadState,
1044
+ hydrateThread,
1045
+ flushThread
1043
1046
  } = threadOps;
1044
1047
  const plugins = [];
1045
1048
  let destroySubagentSandboxes;
@@ -1099,10 +1102,7 @@ async function createSession(config) {
1099
1102
  stateManager.run();
1100
1103
  }
1101
1104
  );
1102
- const lifecycle = resolveSessionLifecycle(
1103
- sandboxInit,
1104
- sandboxShutdown
1105
- );
1105
+ const lifecycle = resolveSessionLifecycle(sandboxInit, sandboxShutdown);
1106
1106
  const sandboxMode = lifecycle.mode;
1107
1107
  const resolvedShutdown = lifecycle.shutdown;
1108
1108
  let sandboxId;
@@ -1177,20 +1177,6 @@ async function createSession(config) {
1177
1177
  ...baseSnapshot && { baseSnapshot }
1178
1178
  });
1179
1179
  }
1180
- if (hooks.onSessionStart) {
1181
- await hooks.onSessionStart({
1182
- threadId,
1183
- agentName,
1184
- metadata
1185
- });
1186
- }
1187
- workflow.log.info("session started", {
1188
- agentName,
1189
- threadId,
1190
- threadMode,
1191
- maxTurns,
1192
- ...sandboxId && { sandboxId }
1193
- });
1194
1180
  const sessionStartMs = Date.now();
1195
1181
  const systemPrompt = stateManager.getSystemPrompt();
1196
1182
  const rehydrateFromSlice = (slice) => {
@@ -1200,10 +1186,12 @@ async function createSession(config) {
1200
1186
  });
1201
1187
  };
1202
1188
  if (threadMode === "fork" && sourceThreadId) {
1189
+ await hydrateThread(sourceThreadId, threadKey);
1203
1190
  await forkThread(sourceThreadId, threadId, threadKey);
1204
1191
  const forkedSlice = await loadThreadState(threadId, threadKey);
1205
1192
  if (forkedSlice) rehydrateFromSlice(forkedSlice);
1206
1193
  } else if (threadMode === "continue") {
1194
+ await hydrateThread(threadId, threadKey);
1207
1195
  const continuedSlice = await loadThreadState(threadId, threadKey);
1208
1196
  if (continuedSlice) rehydrateFromSlice(continuedSlice);
1209
1197
  } else {
@@ -1263,6 +1251,20 @@ async function createSession(config) {
1263
1251
  );
1264
1252
  let exitReason = "completed";
1265
1253
  let finalMessage = null;
1254
+ if (hooks.onSessionStart) {
1255
+ await hooks.onSessionStart({
1256
+ threadId,
1257
+ agentName,
1258
+ metadata
1259
+ });
1260
+ }
1261
+ workflow.log.info("session started", {
1262
+ agentName,
1263
+ threadId,
1264
+ threadMode,
1265
+ maxTurns,
1266
+ ...sandboxId && { sandboxId }
1267
+ });
1266
1268
  try {
1267
1269
  let assistantId;
1268
1270
  while (stateManager.isRunning() && !stateManager.isTerminal() && stateManager.getTurns() < maxTurns) {
@@ -1371,6 +1373,15 @@ async function createSession(config) {
1371
1373
  error: persistError instanceof Error ? persistError.message : String(persistError)
1372
1374
  });
1373
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
+ }
1374
1385
  await callSessionEnd(exitReason, stateManager.getTurns());
1375
1386
  if (sandboxOwned && sandboxId && sandboxOps) {
1376
1387
  switch (resolvedShutdown) {
@@ -1454,6 +1465,9 @@ function getThreadMetaKey(threadKey, threadId) {
1454
1465
  function getThreadStateKey(threadKey, threadId) {
1455
1466
  return `${threadKey}:state:thread:${threadId}`;
1456
1467
  }
1468
+ function getThreadDedupKey(threadId, dedupId) {
1469
+ return `dedup:${dedupId}:thread:${threadId}`;
1470
+ }
1457
1471
 
1458
1472
  // src/lib/types.ts
1459
1473
  function isTerminalStatus(status) {
@@ -1660,7 +1674,7 @@ function createAgentStateManager({
1660
1674
  };
1661
1675
  }
1662
1676
 
1663
- // ../2_monorepo/node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/stringify.js
1677
+ // node_modules/uuid/dist/esm/stringify.js
1664
1678
  var byteToHex = [];
1665
1679
  for (let i = 0; i < 256; ++i) {
1666
1680
  byteToHex.push((i + 256).toString(16).slice(1));
@@ -1672,26 +1686,30 @@ var rnds8Pool = new Uint8Array(256);
1672
1686
  var poolPtr = rnds8Pool.length;
1673
1687
  function rng() {
1674
1688
  if (poolPtr > rnds8Pool.length - 16) {
1675
- crypto__default.default.randomFillSync(rnds8Pool);
1689
+ crypto.randomFillSync(rnds8Pool);
1676
1690
  poolPtr = 0;
1677
1691
  }
1678
1692
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
1679
1693
  }
1680
- var native_default = {
1681
- randomUUID: crypto__default.default.randomUUID
1682
- };
1694
+ var native_default = { randomUUID: crypto.randomUUID };
1683
1695
 
1684
- // ../2_monorepo/node_modules/.pnpm/uuid@10.0.0/node_modules/uuid/dist/esm-node/v4.js
1696
+ // node_modules/uuid/dist/esm/v4.js
1685
1697
  function v4(options, buf, offset) {
1686
1698
  if (native_default.randomUUID && !buf && !options) {
1687
1699
  return native_default.randomUUID();
1688
1700
  }
1689
1701
  options = options || {};
1690
- const rnds = options.random || (options.rng || rng)();
1702
+ const rnds = options.random ?? options.rng?.() ?? rng();
1703
+ if (rnds.length < 16) {
1704
+ throw new Error("Random bytes length must be >= 16");
1705
+ }
1691
1706
  rnds[6] = rnds[6] & 15 | 64;
1692
1707
  rnds[8] = rnds[8] & 63 | 128;
1693
1708
  if (buf) {
1694
1709
  offset = offset || 0;
1710
+ if (offset < 0 || offset + 16 > buf.length) {
1711
+ throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);
1712
+ }
1695
1713
  for (let i = 0; i < 16; ++i) {
1696
1714
  buf[offset + i] = rnds[i];
1697
1715
  }
@@ -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;