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,250 @@
1
+ /**
2
+ * Pluggable cold-tier interface for thread archives.
3
+ *
4
+ * Zeitlich's thread manager is a Redis-backed hot tier optimized for
5
+ * the duration of a workflow run. A `ColdThreadStore` provides the
6
+ * durable archive: each thread is serialized to a single
7
+ * {@link ThreadSnapshot} blob (messages + persisted state slice +
8
+ * dedup-id ledger) at session-exit time, and restored at session-entry
9
+ * time when the workflow is resumed or forked.
10
+ *
11
+ * The contract is intentionally minimal — one read, one write, one
12
+ * delete keyed by `(threadKey, threadId)`. Any storage backend that
13
+ * can satisfy these three calls (S3, R2, GCS, Postgres, the local
14
+ * filesystem, etc.) can plug into the same tiered manager.
15
+ *
16
+ * Concurrency assumption: zeitlich assumes a single active session
17
+ * per `(threadKey, threadId)` at a time, so cold writes are
18
+ * last-writer-wins; no compare-and-swap is required.
19
+ */
20
+
21
+ import { gzipSync, gunzipSync } from "node:zlib";
22
+ import type { PersistedThreadState } from "../state/types";
23
+ import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
24
+
25
+ /**
26
+ * Serialized form of a thread that can be written to and read from a
27
+ * {@link ColdThreadStore}.
28
+ *
29
+ * `messages` is the list of raw-serialized message strings exactly as
30
+ * they were stored in the Redis list — keeping the cold tier opaque
31
+ * to the adapter-specific message envelope. `state` is the
32
+ * {@link PersistedThreadState} that the session writes via
33
+ * `saveThreadState` on every exit path, or `null` if none has been
34
+ * written yet. `dedupIds` lets the tiered manager re-prime the
35
+ * idempotent-append dedup markers when restoring, so a rewind retry
36
+ * after a continue cannot accidentally re-append a message.
37
+ */
38
+ export interface ThreadSnapshot {
39
+ v: 1;
40
+ messages: string[];
41
+ state: PersistedThreadState | null;
42
+ dedupIds: string[];
43
+ }
44
+
45
+ /** Pluggable cold archive for thread snapshots. */
46
+ export interface ColdThreadStore {
47
+ /**
48
+ * Read the latest snapshot for `(threadKey, threadId)`, or return
49
+ * `null` if no snapshot has ever been written.
50
+ */
51
+ read(
52
+ threadKey: string,
53
+ threadId: string
54
+ ): Promise<ThreadSnapshot | null>;
55
+ /**
56
+ * Persist `snapshot` as the latest archive for `(threadKey,
57
+ * threadId)`. Overwrites any prior snapshot in place.
58
+ */
59
+ write(
60
+ threadKey: string,
61
+ threadId: string,
62
+ snapshot: ThreadSnapshot
63
+ ): Promise<void>;
64
+ /**
65
+ * Permanently remove the archive for `(threadKey, threadId)`.
66
+ * No-op if no snapshot exists.
67
+ */
68
+ delete(threadKey: string, threadId: string): Promise<void>;
69
+ }
70
+
71
+ /**
72
+ * Compact, duck-typed shape of an S3 client. Zeitlich only needs the
73
+ * `send(...)` method; declaring this locally avoids forcing
74
+ * `@aws-sdk/client-s3` to be installed when the consumer is using a
75
+ * different cold-store backend.
76
+ */
77
+ export interface S3LikeClient {
78
+ send<TInput, TOutput>(
79
+ command: { input: TInput } & object
80
+ ): Promise<TOutput>;
81
+ }
82
+
83
+ /** Configuration for the built-in S3 cold store. */
84
+ export interface S3ColdStoreConfig {
85
+ /** An `@aws-sdk/client-s3` `S3Client` (or duck-typed equivalent). */
86
+ s3: S3LikeClient;
87
+ /** S3 bucket that holds the archive. */
88
+ bucket: string;
89
+ /**
90
+ * Optional key prefix applied to every object. The final key layout
91
+ * is `${prefix}/${threadKey}/${threadId}.json[.gz]` with leading
92
+ * slashes stripped. Defaults to `"threads"`.
93
+ */
94
+ prefix?: string;
95
+ /**
96
+ * Gzip the JSON payload before uploading and assume gzip on read.
97
+ * Defaults to `true` — message lists are highly compressible.
98
+ */
99
+ gzip?: boolean;
100
+ /**
101
+ * Optional `Content-Type` override. Defaults to
102
+ * `application/json` (or `application/gzip` when `gzip: true`).
103
+ */
104
+ contentType?: string;
105
+ }
106
+
107
+ function joinKey(parts: string[]): string {
108
+ return parts
109
+ .map((p) => p.replace(/^\/+|\/+$/g, ""))
110
+ .filter((p) => p.length > 0)
111
+ .join("/");
112
+ }
113
+
114
+ function buildKey(
115
+ prefix: string | undefined,
116
+ threadKey: string,
117
+ threadId: string,
118
+ gzip: boolean
119
+ ): string {
120
+ const ext = gzip ? "json.gz" : "json";
121
+ return joinKey([
122
+ prefix ?? "threads",
123
+ threadKey,
124
+ `${threadId}.${ext}`,
125
+ ]);
126
+ }
127
+
128
+ async function streamToBuffer(
129
+ body: unknown
130
+ ): Promise<Buffer> {
131
+ if (body == null) return Buffer.alloc(0);
132
+ if (body instanceof Uint8Array) return Buffer.from(body);
133
+ if (typeof (body as { transformToByteArray?: () => Promise<Uint8Array> })
134
+ .transformToByteArray === "function") {
135
+ const bytes = await (
136
+ body as { transformToByteArray: () => Promise<Uint8Array> }
137
+ ).transformToByteArray();
138
+ return Buffer.from(bytes);
139
+ }
140
+ if (typeof (body as { arrayBuffer?: () => Promise<ArrayBuffer> })
141
+ .arrayBuffer === "function") {
142
+ const ab = await (
143
+ body as { arrayBuffer: () => Promise<ArrayBuffer> }
144
+ ).arrayBuffer();
145
+ return Buffer.from(ab);
146
+ }
147
+ // Node.js Readable stream fallback
148
+ const chunks: Buffer[] = [];
149
+ for await (const chunk of body as AsyncIterable<Buffer | Uint8Array>) {
150
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
151
+ }
152
+ return Buffer.concat(chunks);
153
+ }
154
+
155
+ /**
156
+ * Build an S3-backed {@link ColdThreadStore}.
157
+ *
158
+ * One object per thread at
159
+ * `${prefix}/${threadKey}/${threadId}.json[.gz]`, JSON-encoded and
160
+ * gzip-compressed by default. The consumer owns the `S3Client`
161
+ * instance, so credentials, region, and endpoint configuration live
162
+ * outside zeitlich.
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { S3Client } from "@aws-sdk/client-s3";
167
+ * import { createS3ColdStore } from "zeitlich";
168
+ *
169
+ * const coldStore = createS3ColdStore({
170
+ * s3: new S3Client({ region: "us-east-1" }),
171
+ * bucket: "my-threads",
172
+ * prefix: "prod/threads",
173
+ * });
174
+ * ```
175
+ */
176
+ export function createS3ColdStore(
177
+ config: S3ColdStoreConfig
178
+ ): ColdThreadStore {
179
+ const { s3, bucket, prefix, gzip = true } = config;
180
+ const contentType =
181
+ config.contentType ?? (gzip ? "application/gzip" : "application/json");
182
+
183
+ return {
184
+ async read(
185
+ threadKey: string,
186
+ threadId: string
187
+ ): Promise<ThreadSnapshot | null> {
188
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
189
+
190
+ try {
191
+ const resp = (await s3.send(
192
+ new GetObjectCommand({ Bucket: bucket, Key })
193
+ )) as { Body?: unknown };
194
+ const buf = await streamToBuffer(resp.Body);
195
+ const json = gzip
196
+ ? gunzipSync(buf).toString("utf8")
197
+ : buf.toString("utf8");
198
+ return JSON.parse(json) as ThreadSnapshot;
199
+ } catch (err) {
200
+ if (isNotFound(err)) return null;
201
+ throw err;
202
+ }
203
+ },
204
+
205
+ async write(
206
+ threadKey: string,
207
+ threadId: string,
208
+ snapshot: ThreadSnapshot
209
+ ): Promise<void> {
210
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
211
+ const json = JSON.stringify(snapshot);
212
+ const body = gzip ? gzipSync(Buffer.from(json, "utf8")) : json;
213
+
214
+ await s3.send(
215
+ new PutObjectCommand({
216
+ Bucket: bucket,
217
+ Key,
218
+ Body: body,
219
+ ContentType: contentType,
220
+ })
221
+ );
222
+ },
223
+
224
+ async delete(
225
+ threadKey: string,
226
+ threadId: string
227
+ ): Promise<void> {
228
+ const Key = buildKey(prefix, threadKey, threadId, gzip);
229
+
230
+ await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key }));
231
+ },
232
+ };
233
+ }
234
+
235
+ function isNotFound(err: unknown): boolean {
236
+ if (typeof err !== "object" || err === null) return false;
237
+ const e = err as {
238
+ name?: string;
239
+ Code?: string;
240
+ code?: string;
241
+ $metadata?: { httpStatusCode?: number };
242
+ };
243
+ return (
244
+ e.name === "NoSuchKey" ||
245
+ e.Code === "NoSuchKey" ||
246
+ e.code === "NoSuchKey" ||
247
+ e.name === "NotFound" ||
248
+ e.$metadata?.httpStatusCode === 404
249
+ );
250
+ }
@@ -4,6 +4,8 @@ export {
4
4
  THREAD_TTL_SECONDS,
5
5
  getThreadListKey,
6
6
  getThreadMetaKey,
7
+ getThreadStateKey,
8
+ getThreadDedupKey,
7
9
  } from "./keys";
8
10
 
9
11
  export type {
@@ -12,3 +14,33 @@ export type {
12
14
  ProviderThreadManager,
13
15
  ThreadManagerHooks,
14
16
  } from "./types";
17
+
18
+ // Cold-tier (S3-style) thread archive
19
+ export { createS3ColdStore } from "./cold-store";
20
+ export type {
21
+ ColdThreadStore,
22
+ ThreadSnapshot,
23
+ S3LikeClient,
24
+ S3ColdStoreConfig,
25
+ } from "./cold-store";
26
+
27
+ // Tiered (Redis hot + pluggable cold) thread manager
28
+ export { createTieredThreadManager } from "./tiered";
29
+ export type {
30
+ TieredThreadManager,
31
+ TieredThreadManagerConfig,
32
+ FlushOptions,
33
+ } from "./tiered";
34
+
35
+ // Low-level snapshot helpers (advanced — for custom cold stores or
36
+ // admin tooling that mirrors zeitlich's hot↔cold transitions).
37
+ export {
38
+ encodeSnapshot,
39
+ applySnapshot,
40
+ clearHotTier,
41
+ } from "./snapshot";
42
+ export type {
43
+ EncodeSnapshotConfig,
44
+ ApplySnapshotConfig,
45
+ ClearHotTierConfig,
46
+ } from "./snapshot";
@@ -92,3 +92,23 @@ export function getThreadStateKey(
92
92
  ): string {
93
93
  return `${threadKey}:state:thread:${threadId}`;
94
94
  }
95
+
96
+ /**
97
+ * Build the Redis key that guards an idempotent append against a
98
+ * duplicate write of the message (or message batch) identified by
99
+ * `dedupId`. Zeitlich's thread manager writes one of these per
100
+ * single-message append (and one per batch for multi-message appends),
101
+ * keyed by the message id returned by the configured `idOf`.
102
+ *
103
+ * Note: the key layout intentionally does **not** include the
104
+ * `threadKey` prefix — the dedup namespace is shared across thread
105
+ * keys for a given `threadId`, mirroring the original internal
106
+ * implementation.
107
+ *
108
+ * @param threadId - Thread id as provided to the thread manager.
109
+ * @param dedupId - Joined message ids (single message id for the
110
+ * common single-append case).
111
+ */
112
+ export function getThreadDedupKey(threadId: string, dedupId: string): string {
113
+ return `dedup:${dedupId}:thread:${threadId}`;
114
+ }
@@ -5,6 +5,7 @@ import {
5
5
  getThreadListKey,
6
6
  getThreadMetaKey,
7
7
  getThreadStateKey,
8
+ getThreadDedupKey,
8
9
  } from "./keys";
9
10
 
10
11
  /**
@@ -28,10 +29,6 @@ redis.call('SET', KEYS[1], '1', 'EX', tonumber(ARGV[1]))
28
29
  return 1
29
30
  `;
30
31
 
31
- function getDedupKey(threadId: string, id: string): string {
32
- return `dedup:${id}:thread:${threadId}`;
33
- }
34
-
35
32
  /**
36
33
  * Creates a generic thread manager for handling conversation state in Redis.
37
34
  * Framework-agnostic — works with any serializable message type.
@@ -46,10 +43,12 @@ export function createThreadManager<T>(
46
43
  serialize = (m: T): string => JSON.stringify(m),
47
44
  deserialize = (raw: string): T => JSON.parse(raw) as T,
48
45
  idOf,
46
+ ttlSeconds = THREAD_TTL_SECONDS,
49
47
  } = config;
50
48
  const redisKey = getThreadListKey(key, threadId);
51
49
  const metaKey = getThreadMetaKey(key, threadId);
52
50
  const stateKey = getThreadStateKey(key, threadId);
51
+ const dedupKey = (id: string): string => getThreadDedupKey(threadId, id);
53
52
 
54
53
  async function assertThreadExists(): Promise<void> {
55
54
  const exists = await redis.exists(metaKey);
@@ -61,7 +60,7 @@ export function createThreadManager<T>(
61
60
  return {
62
61
  async initialize(): Promise<void> {
63
62
  await redis.del(redisKey);
64
- await redis.set(metaKey, "1", "EX", THREAD_TTL_SECONDS);
63
+ await redis.set(metaKey, "1", "EX", ttlSeconds);
65
64
  },
66
65
 
67
66
  async load(): Promise<T[]> {
@@ -76,18 +75,17 @@ export function createThreadManager<T>(
76
75
 
77
76
  if (idOf) {
78
77
  const dedupId = messages.map(idOf).join(":");
79
- const dedupKey = getDedupKey(threadId, dedupId);
80
78
  await redis.eval(
81
79
  APPEND_IDEMPOTENT_SCRIPT,
82
80
  2,
83
- dedupKey,
81
+ dedupKey(dedupId),
84
82
  redisKey,
85
- String(THREAD_TTL_SECONDS),
83
+ String(ttlSeconds),
86
84
  ...messages.map(serialize)
87
85
  );
88
86
  } else {
89
87
  await redis.rpush(redisKey, ...messages.map(serialize));
90
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
88
+ await redis.expire(redisKey, ttlSeconds);
91
89
  }
92
90
  },
93
91
 
@@ -103,11 +101,11 @@ export function createThreadManager<T>(
103
101
  if (data.length > 0) {
104
102
  const newKey = getThreadListKey(key, newThreadId);
105
103
  await redis.rpush(newKey, ...data);
106
- await redis.expire(newKey, THREAD_TTL_SECONDS);
104
+ await redis.expire(newKey, ttlSeconds);
107
105
  }
108
106
  if (stateRaw != null) {
109
107
  const newStateKey = getThreadStateKey(key, newThreadId);
110
- await redis.set(newStateKey, stateRaw, "EX", THREAD_TTL_SECONDS);
108
+ await redis.set(newStateKey, stateRaw, "EX", ttlSeconds);
111
109
  }
112
110
  return forked;
113
111
  },
@@ -125,15 +123,13 @@ export function createThreadManager<T>(
125
123
  .filter((id): id is string => typeof id === "string");
126
124
  await redis.del(redisKey);
127
125
  if (existingIds.length > 0) {
128
- await redis.del(
129
- ...existingIds.map((id) => getDedupKey(threadId, id))
130
- );
126
+ await redis.del(...existingIds.map(dedupKey));
131
127
  }
132
128
  if (messages.length > 0) {
133
129
  await redis.rpush(redisKey, ...messages.map(serialize));
134
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
130
+ await redis.expire(redisKey, ttlSeconds);
135
131
  }
136
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
132
+ await redis.expire(metaKey, ttlSeconds);
137
133
  },
138
134
 
139
135
  async delete(): Promise<void> {
@@ -148,12 +144,7 @@ export function createThreadManager<T>(
148
144
 
149
145
  async saveState(state: PersistedThreadState): Promise<void> {
150
146
  await assertThreadExists();
151
- await redis.set(
152
- stateKey,
153
- JSON.stringify(state),
154
- "EX",
155
- THREAD_TTL_SECONDS
156
- );
147
+ await redis.set(stateKey, JSON.stringify(state), "EX", ttlSeconds);
157
148
  },
158
149
 
159
150
  async deleteState(): Promise<void> {
@@ -185,19 +176,17 @@ export function createThreadManager<T>(
185
176
  if (idx === -1) return;
186
177
  if (idx === 0) {
187
178
  await redis.del(redisKey);
188
- await redis.expire(metaKey, THREAD_TTL_SECONDS);
179
+ await redis.expire(metaKey, ttlSeconds);
189
180
  } else {
190
181
  await redis.ltrim(redisKey, 0, idx - 1);
191
- await redis.expire(redisKey, THREAD_TTL_SECONDS);
182
+ await redis.expire(redisKey, ttlSeconds);
192
183
  }
193
184
  // Clear dedup markers for the removed messages so that a rewind
194
185
  // retry which reuses the same ids (e.g. the same assistantId) can
195
186
  // re-append without the idempotent-append Lua script treating it
196
187
  // as a duplicate.
197
188
  if (removedIds.length > 0) {
198
- await redis.del(
199
- ...removedIds.map((id) => getDedupKey(threadId, id))
200
- );
189
+ await redis.del(...removedIds.map(dedupKey));
201
190
  }
202
191
  },
203
192
  };
@@ -56,5 +56,7 @@ export function createThreadOpsProxy(
56
56
  truncateThread: acts[p("truncateThread")],
57
57
  loadThreadState: acts[p("loadThreadState")],
58
58
  saveThreadState: acts[p("saveThreadState")],
59
+ hydrateThread: acts[p("hydrateThread")],
60
+ flushThread: acts[p("flushThread")],
59
61
  } as ActivityInterfaceFor<ThreadOps>;
60
62
  }