convex-durable-agents 0.2.3 → 0.2.4

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 (70) hide show
  1. package/README.md +81 -7
  2. package/dist/client/api.d.ts.map +1 -1
  3. package/dist/client/api.js +23 -4
  4. package/dist/client/api.js.map +1 -1
  5. package/dist/client/handler.d.ts +20 -0
  6. package/dist/client/handler.d.ts.map +1 -1
  7. package/dist/client/handler.js +239 -117
  8. package/dist/client/handler.js.map +1 -1
  9. package/dist/client/streamer.js +1 -1
  10. package/dist/client/streamer.js.map +1 -1
  11. package/dist/client/tools.d.ts +17 -0
  12. package/dist/client/tools.d.ts.map +1 -1
  13. package/dist/client/tools.js +16 -0
  14. package/dist/client/tools.js.map +1 -1
  15. package/dist/client/types.d.ts +75 -1
  16. package/dist/client/types.d.ts.map +1 -1
  17. package/dist/client/types.js +11 -0
  18. package/dist/client/types.js.map +1 -1
  19. package/dist/component/_generated/component.d.ts +89 -0
  20. package/dist/component/_generated/component.d.ts.map +1 -1
  21. package/dist/component/agent.d.ts.map +1 -1
  22. package/dist/component/agent.js +21 -2
  23. package/dist/component/agent.js.map +1 -1
  24. package/dist/component/schema.d.ts +70 -2
  25. package/dist/component/schema.d.ts.map +1 -1
  26. package/dist/component/schema.js +21 -0
  27. package/dist/component/schema.js.map +1 -1
  28. package/dist/component/streams.js +2 -2
  29. package/dist/component/streams.js.map +1 -1
  30. package/dist/component/threads.d.ts +92 -2
  31. package/dist/component/threads.d.ts.map +1 -1
  32. package/dist/component/threads.js +83 -2
  33. package/dist/component/threads.js.map +1 -1
  34. package/dist/component/tool_calls.d.ts +54 -3
  35. package/dist/component/tool_calls.d.ts.map +1 -1
  36. package/dist/component/tool_calls.js +345 -35
  37. package/dist/component/tool_calls.js.map +1 -1
  38. package/dist/utils/logger.d.ts.map +1 -0
  39. package/dist/utils/logger.js.map +1 -0
  40. package/dist/utils/retry.d.ts +69 -0
  41. package/dist/utils/retry.d.ts.map +1 -0
  42. package/dist/utils/retry.js +404 -0
  43. package/dist/utils/retry.js.map +1 -0
  44. package/dist/utils/streaming.d.ts +4 -0
  45. package/dist/utils/streaming.d.ts.map +1 -0
  46. package/dist/utils/streaming.js +4 -0
  47. package/dist/utils/streaming.js.map +1 -0
  48. package/package.json +1 -1
  49. package/src/client/api.ts +24 -4
  50. package/src/client/handler.ts +308 -133
  51. package/src/client/streamer.ts +1 -1
  52. package/src/client/tools.ts +43 -1
  53. package/src/client/types.ts +60 -0
  54. package/src/component/_generated/component.ts +103 -0
  55. package/src/component/agent.ts +24 -2
  56. package/src/component/schema.ts +22 -0
  57. package/src/component/streams.ts +2 -2
  58. package/src/component/threads.ts +92 -3
  59. package/src/component/tool_calls.ts +421 -44
  60. package/src/utils/retry.ts +528 -0
  61. package/src/{streaming.ts → utils/streaming.ts} +2 -3
  62. package/dist/logger.d.ts.map +0 -1
  63. package/dist/logger.js.map +0 -1
  64. package/dist/streaming.d.ts +0 -3
  65. package/dist/streaming.d.ts.map +0 -1
  66. package/dist/streaming.js +0 -4
  67. package/dist/streaming.js.map +0 -1
  68. /package/dist/{logger.d.ts → utils/logger.d.ts} +0 -0
  69. /package/dist/{logger.js → utils/logger.js} +0 -0
  70. /package/src/{logger.ts → utils/logger.ts} +0 -0
@@ -33,6 +33,17 @@ export type PaginationResult<T> = {
33
33
 
34
34
  export type ThreadStatus = "streaming" | "awaiting_tool_results" | "completed" | "failed" | "stopped";
35
35
 
36
+ export type RetryState = {
37
+ scope: "stream";
38
+ attempt: number;
39
+ maxAttempts: number;
40
+ nextRetryAt: number;
41
+ error: string;
42
+ kind?: string;
43
+ retryable: boolean;
44
+ requiresExplicitHandling: boolean;
45
+ };
46
+
36
47
  export type ThreadDoc = {
37
48
  _id: string;
38
49
  _creationTime: number;
@@ -40,6 +51,7 @@ export type ThreadDoc = {
40
51
  stopSignal: boolean;
41
52
  streamId?: string | null;
42
53
  streamFnHandle: string;
54
+ retryState?: RetryState;
43
55
  };
44
56
 
45
57
  const vThreadStatus = v.union(
@@ -59,6 +71,19 @@ export const _vClientThreadDoc = v.object({
59
71
  streamFnHandle: v.string(),
60
72
  workpoolEnqueueAction: v.optional(v.string()),
61
73
  toolExecutionWorkpoolEnqueueAction: v.optional(v.string()),
74
+ retryState: v.optional(
75
+ v.object({
76
+ scope: v.literal("stream"),
77
+ attempt: v.number(),
78
+ maxAttempts: v.number(),
79
+ nextRetryAt: v.number(),
80
+ error: v.string(),
81
+ kind: v.optional(v.string()),
82
+ retryable: v.boolean(),
83
+ requiresExplicitHandling: v.boolean(),
84
+ retryFnId: v.optional(v.string()),
85
+ }),
86
+ ),
62
87
  });
63
88
 
64
89
  export type MessageDoc = UIMessage<any> & {
@@ -77,12 +102,47 @@ export function messageDocToUIMessage(message: MessageDoc): UIMessage {
77
102
  };
78
103
  }
79
104
 
105
+ export type RetryBackoffConfig =
106
+ | {
107
+ strategy?: "fixed";
108
+ delayMs: number;
109
+ jitter?: boolean;
110
+ }
111
+ | {
112
+ strategy: "exponential";
113
+ initialDelayMs: number;
114
+ multiplier?: number;
115
+ maxDelayMs?: number;
116
+ jitter?: boolean;
117
+ };
118
+
119
+ export type SyncToolRetryOptions = {
120
+ /**
121
+ * Opt-in to retry for this sync tool.
122
+ */
123
+ enabled: true;
124
+ /**
125
+ * Maximum execution attempts including the initial attempt.
126
+ */
127
+ maxAttempts?: number;
128
+ /**
129
+ * Retry backoff policy.
130
+ */
131
+ backoff?: RetryBackoffConfig;
132
+ /**
133
+ * Optional function to classify whether an error is retryable.
134
+ * Receives { threadId, toolCallId, toolName, args, error, attempt, maxAttempts }.
135
+ */
136
+ shouldRetryError?: FunctionReference<"action", "internal" | "public">;
137
+ };
138
+
80
139
  // Sync durable tool definition - handler returns the result directly
81
140
  export type SyncTool<INPUT = unknown, OUTPUT = unknown> = {
82
141
  type: "sync";
83
142
  description: string;
84
143
  parameters: unknown; // JSON Schema
85
144
  handler: FunctionReference<"action", "internal" | "public">;
145
+ retry?: SyncToolRetryOptions;
86
146
  _inputType?: INPUT;
87
147
  _outputType?: OUTPUT;
88
148
  };
@@ -163,6 +163,13 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
163
163
  >;
164
164
  };
165
165
  threads: {
166
+ clearRetryState: FunctionReference<
167
+ "mutation",
168
+ "internal",
169
+ { threadId: string },
170
+ null,
171
+ Name
172
+ >;
166
173
  clearStreamId: FunctionReference<
167
174
  "mutation",
168
175
  "internal",
@@ -182,6 +189,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
182
189
  {
183
190
  _creationTime: number;
184
191
  _id: string;
192
+ retryState?: {
193
+ attempt: number;
194
+ error: string;
195
+ kind?: string;
196
+ maxAttempts: number;
197
+ nextRetryAt: number;
198
+ requiresExplicitHandling: boolean;
199
+ retryFnId?: string;
200
+ retryable: boolean;
201
+ scope: "stream";
202
+ };
185
203
  status:
186
204
  | "streaming"
187
205
  | "awaiting_tool_results"
@@ -220,6 +238,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
220
238
  {
221
239
  _creationTime: number;
222
240
  _id: string;
241
+ retryState?: {
242
+ attempt: number;
243
+ error: string;
244
+ kind?: string;
245
+ maxAttempts: number;
246
+ nextRetryAt: number;
247
+ requiresExplicitHandling: boolean;
248
+ retryFnId?: string;
249
+ retryable: boolean;
250
+ scope: "stream";
251
+ };
223
252
  status:
224
253
  | "streaming"
225
254
  | "awaiting_tool_results"
@@ -241,6 +270,17 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
241
270
  Array<{
242
271
  _creationTime: number;
243
272
  _id: string;
273
+ retryState?: {
274
+ attempt: number;
275
+ error: string;
276
+ kind?: string;
277
+ maxAttempts: number;
278
+ nextRetryAt: number;
279
+ requiresExplicitHandling: boolean;
280
+ retryFnId?: string;
281
+ retryable: boolean;
282
+ scope: "stream";
283
+ };
244
284
  status:
245
285
  | "streaming"
246
286
  | "awaiting_tool_results"
@@ -276,6 +316,23 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
276
316
  null,
277
317
  Name
278
318
  >;
319
+ scheduleRetry: FunctionReference<
320
+ "mutation",
321
+ "internal",
322
+ {
323
+ attempt: number;
324
+ error: string;
325
+ kind?: string;
326
+ maxAttempts: number;
327
+ nextRetryAt: number;
328
+ requiresExplicitHandling: boolean;
329
+ retryable: boolean;
330
+ scope: "stream";
331
+ threadId: string;
332
+ },
333
+ null,
334
+ Name
335
+ >;
279
336
  setStatus: FunctionReference<
280
337
  "mutation",
281
338
  "internal",
@@ -321,7 +378,9 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
321
378
  {
322
379
  args: any;
323
380
  callback?: string;
381
+ handler?: string;
324
382
  msgId: string;
383
+ retry?: any;
325
384
  saveDelta: boolean;
326
385
  threadId: string;
327
386
  toolCallId: string;
@@ -331,9 +390,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
331
390
  _creationTime: number;
332
391
  _id: string;
333
392
  args: any;
393
+ callbackAttempt?: number;
394
+ callbackLastError?: string;
334
395
  error?: string;
396
+ executionAttempt?: number;
397
+ executionLastError?: string;
398
+ executionMaxAttempts?: number;
399
+ executionRetryPolicy?: any;
400
+ handler?: string;
335
401
  msgId: string;
402
+ nextRetryAt?: number;
336
403
  result?: any;
404
+ status: "pending" | "completed" | "failed";
337
405
  threadId: string;
338
406
  toolCallId: string;
339
407
  toolName: string;
@@ -348,9 +416,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
348
416
  _creationTime: number;
349
417
  _id: string;
350
418
  args: any;
419
+ callbackAttempt?: number;
420
+ callbackLastError?: string;
351
421
  error?: string;
422
+ executionAttempt?: number;
423
+ executionLastError?: string;
424
+ executionMaxAttempts?: number;
425
+ executionRetryPolicy?: any;
426
+ handler?: string;
352
427
  msgId: string;
428
+ nextRetryAt?: number;
353
429
  result?: any;
430
+ status: "pending" | "completed" | "failed";
354
431
  threadId: string;
355
432
  toolCallId: string;
356
433
  toolName: string;
@@ -365,9 +442,18 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
365
442
  _creationTime: number;
366
443
  _id: string;
367
444
  args: any;
445
+ callbackAttempt?: number;
446
+ callbackLastError?: string;
368
447
  error?: string;
448
+ executionAttempt?: number;
449
+ executionLastError?: string;
450
+ executionMaxAttempts?: number;
451
+ executionRetryPolicy?: any;
452
+ handler?: string;
369
453
  msgId: string;
454
+ nextRetryAt?: number;
370
455
  result?: any;
456
+ status: "pending" | "completed" | "failed";
371
457
  threadId: string;
372
458
  toolCallId: string;
373
459
  toolName: string;
@@ -382,15 +468,31 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
382
468
  _creationTime: number;
383
469
  _id: string;
384
470
  args: any;
471
+ callbackAttempt?: number;
472
+ callbackLastError?: string;
385
473
  error?: string;
474
+ executionAttempt?: number;
475
+ executionLastError?: string;
476
+ executionMaxAttempts?: number;
477
+ executionRetryPolicy?: any;
478
+ handler?: string;
386
479
  msgId: string;
480
+ nextRetryAt?: number;
387
481
  result?: any;
482
+ status: "pending" | "completed" | "failed";
388
483
  threadId: string;
389
484
  toolCallId: string;
390
485
  toolName: string;
391
486
  }>,
392
487
  Name
393
488
  >;
489
+ resumePendingSyncToolExecutions: FunctionReference<
490
+ "mutation",
491
+ "internal",
492
+ { limit?: number },
493
+ number,
494
+ Name
495
+ >;
394
496
  scheduleAsyncToolCall: FunctionReference<
395
497
  "mutation",
396
498
  "internal",
@@ -413,6 +515,7 @@ export type ComponentApi<Name extends string | undefined = string | undefined> =
413
515
  args: any;
414
516
  handler: string;
415
517
  msgId: string;
518
+ retry?: any;
416
519
  saveDelta: boolean;
417
520
  threadId: string;
418
521
  toolCallId: string;
@@ -1,7 +1,7 @@
1
1
  import type { FunctionHandle, FunctionReference, GenericDataModel, GenericMutationCtx } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import { Logger } from "../logger.js";
4
- import { STREAM_LIVENESS_THRESHOLD_MS } from "../streaming.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { STREAM_LIVENESS_THRESHOLD_MS } from "../utils/streaming.js";
5
5
  import { api, internal } from "./_generated/api.js";
6
6
  import { action, mutation } from "./_generated/server.js";
7
7
  import { cancelStream, isAlive } from "./streams.js";
@@ -69,6 +69,7 @@ export const continueStream = mutation({
69
69
  status: "stopped",
70
70
  activeStream: null,
71
71
  continue: false,
72
+ retryState: undefined,
72
73
  });
73
74
  if (thread.onStatusChangeHandle && previousStatus !== "stopped") {
74
75
  await ctx.runMutation(thread.onStatusChangeHandle as FunctionHandle<"mutation">, {
@@ -93,6 +94,27 @@ export const continueStream = mutation({
93
94
  return null;
94
95
  }
95
96
 
97
+ if (thread.retryState?.scope === "stream") {
98
+ const now = Date.now();
99
+ if (thread.retryState.nextRetryAt > now) {
100
+ const delayMs = Math.max(0, thread.retryState.nextRetryAt - now);
101
+ logger.debug(
102
+ `Retry pending for thread=${args.threadId}; nextRetryAt=${thread.retryState.nextRetryAt}, now=${now}`,
103
+ );
104
+ const retryFnId = await ctx.scheduler.runAfter(delayMs, api.agent.continueStream, {
105
+ threadId: args.threadId,
106
+ });
107
+ await ctx.db.patch(thread._id, {
108
+ retryState: {
109
+ ...thread.retryState,
110
+ retryFnId,
111
+ },
112
+ });
113
+ return null;
114
+ }
115
+ logger.debug(`Retry window reached for thread=${args.threadId}; continuing with persisted retryState`);
116
+ }
117
+
96
118
  // Check for pending tool calls
97
119
  const pendingToolCalls = await ctx.runQuery(api.tool_calls.listPending, {
98
120
  threadId: args.threadId,
@@ -10,6 +10,18 @@ export const vThreadStatus = v.union(
10
10
  v.literal("stopped"),
11
11
  );
12
12
 
13
+ export const vRetryState = v.object({
14
+ scope: v.literal("stream"),
15
+ attempt: v.number(),
16
+ maxAttempts: v.number(),
17
+ nextRetryAt: v.number(),
18
+ error: v.string(),
19
+ kind: v.optional(v.string()),
20
+ retryable: v.boolean(),
21
+ requiresExplicitHandling: v.boolean(),
22
+ retryFnId: v.optional(v.id("_scheduled_functions")),
23
+ });
24
+
13
25
  // AI SDK message content - supports both string and array of parts
14
26
  export const vMessageContent = v.union(
15
27
  v.string(),
@@ -70,6 +82,8 @@ const schema = defineSchema({
70
82
  onStatusChangeHandle: v.optional(v.string()),
71
83
  // Monotonically increasing sequence number for streams of this thread
72
84
  seq: v.number(),
85
+ // Pending retry metadata for stream retries.
86
+ retryState: v.optional(vRetryState),
73
87
  }).index("by_status", ["status"]),
74
88
 
75
89
  // AI SDK compatible message storage
@@ -95,6 +109,13 @@ const schema = defineSchema({
95
109
  callback: v.optional(v.string()),
96
110
  callbackAttempt: v.optional(v.number()),
97
111
  callbackLastError: v.optional(v.string()),
112
+ executionAttempt: v.optional(v.number()),
113
+ executionMaxAttempts: v.optional(v.number()),
114
+ executionLastError: v.optional(v.string()),
115
+ executionRetryPolicy: v.optional(v.any()),
116
+ nextRetryAt: v.optional(v.number()),
117
+ executionRetryFnId: v.optional(v.id("_scheduled_functions")),
118
+ handler: v.optional(v.string()),
98
119
  timeoutMs: v.optional(v.union(v.number(), v.null())),
99
120
  expiresAt: v.optional(v.union(v.number(), v.null())),
100
121
  timeoutFnId: v.optional(v.id("_scheduled_functions")),
@@ -105,6 +126,7 @@ const schema = defineSchema({
105
126
  saveDelta: v.optional(v.boolean()),
106
127
  })
107
128
  .index("by_thread", ["threadId"])
129
+ .index("by_status_only", ["status"])
108
130
  .index("by_status", ["threadId", "status"])
109
131
  .index("by_thread_tool_call_id", ["threadId", "toolCallId"])
110
132
  .index("by_tool_call_id", ["toolCallId"]),
@@ -1,7 +1,7 @@
1
1
  import type { UIMessageChunk } from "ai";
2
2
  import { type Infer, v } from "convex/values";
3
- import { Logger } from "../logger.js";
4
- import { STREAM_LIVENESS_THRESHOLD_MS } from "../streaming.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { STREAM_LIVENESS_THRESHOLD_MS } from "../utils/streaming.js";
5
5
  import { api, internal } from "./_generated/api";
6
6
  import type { Doc, Id } from "./_generated/dataModel";
7
7
  import { internalMutation, type MutationCtx, mutation, query } from "./_generated/server";
@@ -1,9 +1,10 @@
1
1
  import type { FunctionHandle } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import { Logger } from "../logger.js";
3
+ import { Logger } from "../utils/logger.js";
4
+ import { api } from "./_generated/api.js";
4
5
  import type { Doc, Id } from "./_generated/dataModel.js";
5
- import { internalQuery, mutation, query } from "./_generated/server.js";
6
- import { vThreadStatus } from "./schema.js";
6
+ import { internalQuery, type MutationCtx, mutation, query } from "./_generated/server.js";
7
+ import { vRetryState, vThreadStatus } from "./schema.js";
7
8
 
8
9
  const logger = new Logger("threads");
9
10
  const FINALIZER_MISMATCH_ALERT_WINDOW_MS = 5 * 60 * 1000;
@@ -64,6 +65,7 @@ export type ThreadDoc = {
64
65
  streamFnHandle: string;
65
66
  workpoolEnqueueAction?: string;
66
67
  toolExecutionWorkpoolEnqueueAction?: string;
68
+ retryState?: Doc<"threads">["retryState"];
67
69
  };
68
70
 
69
71
  function publicThread(thread: Doc<"threads">): ThreadDoc {
@@ -76,6 +78,7 @@ function publicThread(thread: Doc<"threads">): ThreadDoc {
76
78
  streamFnHandle: thread.streamFnHandle,
77
79
  workpoolEnqueueAction: thread.workpoolEnqueueAction,
78
80
  toolExecutionWorkpoolEnqueueAction: thread.toolExecutionWorkpoolEnqueueAction,
81
+ retryState: thread.retryState,
79
82
  };
80
83
  }
81
84
 
@@ -89,6 +92,7 @@ export const vThreadDoc = v.object({
89
92
  streamFnHandle: v.string(),
90
93
  workpoolEnqueueAction: v.optional(v.string()),
91
94
  toolExecutionWorkpoolEnqueueAction: v.optional(v.string()),
95
+ retryState: v.optional(vRetryState),
92
96
  });
93
97
 
94
98
  export const vThreadDocWithStreamFnHandle = v.object({
@@ -104,8 +108,19 @@ export const vThreadDocWithStreamFnHandle = v.object({
104
108
  activeStream: v.optional(v.union(v.id("streams"), v.null())),
105
109
  continue: v.optional(v.boolean()),
106
110
  seq: v.number(),
111
+ retryState: v.optional(vRetryState),
107
112
  });
108
113
 
114
+ async function cancelRetryFnIfPending(ctx: MutationCtx, thread: Doc<"threads">): Promise<void> {
115
+ if (!thread.retryState?.retryFnId) {
116
+ return;
117
+ }
118
+ const retryFn = await ctx.db.system.get(thread.retryState.retryFnId);
119
+ if (retryFn?.state.kind === "pending") {
120
+ await ctx.scheduler.cancel(thread.retryState.retryFnId);
121
+ }
122
+ }
123
+
109
124
  export const create = mutation({
110
125
  args: {
111
126
  streamFnHandle: v.string(),
@@ -149,6 +164,74 @@ export const getWithStreamFnHandle = internalQuery({
149
164
  },
150
165
  });
151
166
 
167
+ export const scheduleRetry = mutation({
168
+ args: {
169
+ threadId: v.id("threads"),
170
+ scope: v.literal("stream"),
171
+ attempt: v.number(),
172
+ maxAttempts: v.number(),
173
+ nextRetryAt: v.number(),
174
+ error: v.string(),
175
+ kind: v.optional(v.string()),
176
+ retryable: v.boolean(),
177
+ requiresExplicitHandling: v.boolean(),
178
+ },
179
+ returns: v.null(),
180
+ handler: async (ctx, args) => {
181
+ const thread = await ctx.db.get(args.threadId);
182
+ if (!thread) {
183
+ throw new Error(`Thread ${args.threadId} not found`);
184
+ }
185
+
186
+ await cancelRetryFnIfPending(ctx, thread);
187
+
188
+ if (thread.stopSignal) {
189
+ await ctx.db.patch(args.threadId, {
190
+ retryState: undefined,
191
+ });
192
+ return null;
193
+ }
194
+
195
+ const delayMs = Math.max(0, args.nextRetryAt - Date.now());
196
+ const retryFnId = await ctx.scheduler.runAfter(delayMs, api.agent.continueStream, {
197
+ threadId: args.threadId,
198
+ });
199
+
200
+ await ctx.db.patch(args.threadId, {
201
+ retryState: {
202
+ scope: args.scope,
203
+ attempt: args.attempt,
204
+ maxAttempts: args.maxAttempts,
205
+ nextRetryAt: args.nextRetryAt,
206
+ error: args.error,
207
+ kind: args.kind,
208
+ retryable: args.retryable,
209
+ requiresExplicitHandling: args.requiresExplicitHandling,
210
+ retryFnId,
211
+ },
212
+ });
213
+ return null;
214
+ },
215
+ });
216
+
217
+ export const clearRetryState = mutation({
218
+ args: {
219
+ threadId: v.id("threads"),
220
+ },
221
+ returns: v.null(),
222
+ handler: async (ctx, args) => {
223
+ const thread = await ctx.db.get(args.threadId);
224
+ if (!thread) {
225
+ throw new Error(`Thread ${args.threadId} not found`);
226
+ }
227
+ await cancelRetryFnIfPending(ctx, thread);
228
+ await ctx.db.patch(args.threadId, {
229
+ retryState: undefined,
230
+ });
231
+ return null;
232
+ },
233
+ });
234
+
152
235
  export const resume = mutation({
153
236
  args: {
154
237
  threadId: v.id("threads"),
@@ -344,6 +427,11 @@ export const setStopSignal = mutation({
344
427
  if (!thread) {
345
428
  throw new Error(`Thread ${args.threadId} not found`);
346
429
  }
430
+ if (args.stopSignal) {
431
+ await cancelRetryFnIfPending(ctx, thread);
432
+ await ctx.db.patch(args.threadId, { stopSignal: args.stopSignal, retryState: undefined });
433
+ return null;
434
+ }
347
435
  await ctx.db.patch(args.threadId, { stopSignal: args.stopSignal });
348
436
  return null;
349
437
  },
@@ -383,6 +471,7 @@ export const remove = mutation({
383
471
  if (!thread) {
384
472
  throw new Error(`Thread ${args.threadId} not found`);
385
473
  }
474
+ await cancelRetryFnIfPending(ctx, thread);
386
475
  // Delete all messages for this thread
387
476
  const messages = await ctx.db
388
477
  .query("messages")