@vellumai/assistant 0.10.3-dev.202606292136.ce8bf33 → 0.10.3-dev.202606292229.153e9b2

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +22 -22
  3. package/src/__tests__/job-handler-registry-guard.test.ts +104 -0
  4. package/src/__tests__/plugin-import-boundary-guard.test.ts +18 -0
  5. package/src/__tests__/schedule-retry.test.ts +51 -51
  6. package/src/__tests__/schedule-routes-workflow-validation.test.ts +45 -41
  7. package/src/__tests__/schedule-routes.test.ts +192 -182
  8. package/src/__tests__/schedule-store.test.ts +172 -170
  9. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  10. package/src/__tests__/scheduler-recurrence.test.ts +12 -12
  11. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -10
  12. package/src/__tests__/scheduler-wake.test.ts +6 -6
  13. package/src/__tests__/task-scheduler.test.ts +22 -22
  14. package/src/background-wake/wake-intent-hooks.test.ts +3 -3
  15. package/src/daemon/external-plugins-bootstrap.ts +22 -0
  16. package/src/daemon/lifecycle.ts +1 -1
  17. package/src/daemon/shutdown-handlers.ts +148 -147
  18. package/src/jobs/register-job-handlers.ts +24 -167
  19. package/src/memory/memory-retrospective-job.ts +5 -5
  20. package/src/memory/memory-retrospective-state.ts +7 -7
  21. package/src/notifications/conversation-pairing.ts +23 -14
  22. package/src/persistence/conversation-bootstrap.ts +24 -15
  23. package/src/persistence/conversation-crud.ts +62 -59
  24. package/src/plugins/defaults/index.ts +28 -0
  25. package/src/plugins/defaults/memory/job-handlers.ts +203 -0
  26. package/src/plugins/job-handler-registry.ts +94 -0
  27. package/src/plugins/types.ts +26 -0
  28. package/src/runtime/background-job-runner.ts +9 -7
  29. package/src/runtime/routes/conversation-cli-routes.ts +5 -1
  30. package/src/runtime/routes/conversation-management-routes.ts +6 -6
  31. package/src/runtime/routes/conversations-import-routes.ts +5 -1
  32. package/src/runtime/routes/defer-routes.ts +3 -3
  33. package/src/runtime/routes/home-feed-routes.ts +9 -4
  34. package/src/runtime/routes/schedule-routes.ts +31 -28
  35. package/src/runtime/routes/wipe-conversation-routes.ts +1 -1
  36. package/src/runtime/services/analyze-conversation.ts +18 -9
  37. package/src/schedule/retry-policy.ts +8 -8
  38. package/src/schedule/schedule-recovery.ts +8 -5
  39. package/src/schedule/schedule-store.ts +124 -109
  40. package/src/schedule/scheduler.ts +64 -52
  41. package/src/subagent/manager.ts +1 -1
  42. package/src/tasks/task-runner.ts +1 -1
  43. package/src/tools/schedule/create.ts +2 -2
  44. package/src/tools/schedule/delete.ts +1 -1
  45. package/src/tools/schedule/update.ts +1 -1
  46. package/src/util/sqlite-retry.ts +8 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606292136.ce8bf33",
3
+ "version": "0.10.3-dev.202606292229.153e9b2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -69,8 +69,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
69
69
  getRawDb().run("DELETE FROM conversations");
70
70
  });
71
71
 
72
- test("deleting a conversation with a scheduleJobId removes the schedule", () => {
73
- const schedule = createSchedule({
72
+ test("deleting a conversation with a scheduleJobId removes the schedule", async () => {
73
+ const schedule = await createSchedule({
74
74
  name: "Daily standup",
75
75
  expression: "0 9 * * 1-5",
76
76
  message: "Time for standup!",
@@ -83,7 +83,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
83
83
 
84
84
  expect(getSchedule(schedule.id)).not.toBeNull();
85
85
 
86
- deleteRoute.handler({
86
+ await deleteRoute.handler({
87
87
  pathParams: { id: conv.id },
88
88
  body: {},
89
89
  headers: {},
@@ -93,8 +93,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
93
93
  expect(getConversation(conv.id)).toBeNull();
94
94
  });
95
95
 
96
- test("deleting a conversation without a scheduleJobId does not affect schedules", () => {
97
- const schedule = createSchedule({
96
+ test("deleting a conversation without a scheduleJobId does not affect schedules", async () => {
97
+ const schedule = await createSchedule({
98
98
  name: "Unrelated schedule",
99
99
  expression: "0 12 * * *",
100
100
  message: "Noon check",
@@ -102,7 +102,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
102
102
 
103
103
  const conv = createConversation("no-schedule-conv");
104
104
 
105
- deleteRoute.handler({
105
+ await deleteRoute.handler({
106
106
  pathParams: { id: conv.id },
107
107
  body: {},
108
108
  headers: {},
@@ -112,8 +112,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
112
112
  expect(getConversation(conv.id)).toBeNull();
113
113
  });
114
114
 
115
- test("deleting a conversation with a schedule also removes its cron_runs", () => {
116
- const schedule = createSchedule({
115
+ test("deleting a conversation with a schedule also removes its cron_runs", async () => {
116
+ const schedule = await createSchedule({
117
117
  name: "Recurring job",
118
118
  expression: "0 9 * * *",
119
119
  message: "Daily task",
@@ -137,7 +137,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
137
137
  .get();
138
138
  expect(runBefore).not.toBeNull();
139
139
 
140
- deleteRoute.handler({
140
+ await deleteRoute.handler({
141
141
  pathParams: { id: conv.id },
142
142
  body: {},
143
143
  headers: {},
@@ -150,8 +150,8 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
150
150
  expect(runAfter).toBeNull();
151
151
  });
152
152
 
153
- test("deleting one of multiple conversations sharing a schedule preserves the schedule", () => {
154
- const schedule = createSchedule({
153
+ test("deleting one of multiple conversations sharing a schedule preserves the schedule", async () => {
154
+ const schedule = await createSchedule({
155
155
  name: "Recurring daily",
156
156
  expression: "0 9 * * *",
157
157
  message: "Daily task",
@@ -166,7 +166,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
166
166
  scheduleJobId: schedule.id,
167
167
  });
168
168
 
169
- deleteRoute.handler({
169
+ await deleteRoute.handler({
170
170
  pathParams: { id: conv1.id },
171
171
  body: {},
172
172
  headers: {},
@@ -175,13 +175,13 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
175
175
  expect(getSchedule(schedule.id)).not.toBeNull();
176
176
  });
177
177
 
178
- test("deleting one scheduled conversation does not affect other schedules", () => {
179
- const scheduleA = createSchedule({
178
+ test("deleting one scheduled conversation does not affect other schedules", async () => {
179
+ const scheduleA = await createSchedule({
180
180
  name: "Schedule A",
181
181
  expression: "0 9 * * *",
182
182
  message: "Task A",
183
183
  });
184
- const scheduleB = createSchedule({
184
+ const scheduleB = await createSchedule({
185
185
  name: "Schedule B",
186
186
  expression: "0 17 * * *",
187
187
  message: "Task B",
@@ -196,7 +196,7 @@ describe("DELETE /conversations/:id — schedule cleanup", () => {
196
196
  scheduleJobId: scheduleB.id,
197
197
  });
198
198
 
199
- deleteRoute.handler({
199
+ await deleteRoute.handler({
200
200
  pathParams: { id: convA.id },
201
201
  body: {},
202
202
  headers: {},
@@ -222,8 +222,8 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
222
222
  getRawDb().run("DELETE FROM conversations");
223
223
  });
224
224
 
225
- test("wiping a conversation with a scheduleJobId removes the schedule", () => {
226
- const schedule = createSchedule({
225
+ test("wiping a conversation with a scheduleJobId removes the schedule", async () => {
226
+ const schedule = await createSchedule({
227
227
  name: "Wipe-test schedule",
228
228
  expression: "0 9 * * 1-5",
229
229
  message: "Time for standup!",
@@ -236,7 +236,7 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
236
236
 
237
237
  expect(getSchedule(schedule.id)).not.toBeNull();
238
238
 
239
- wipeRoute.handler({
239
+ await wipeRoute.handler({
240
240
  pathParams: { id: conv.id },
241
241
  body: {},
242
242
  headers: {},
@@ -245,8 +245,8 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
245
245
  expect(getSchedule(schedule.id)).toBeNull();
246
246
  });
247
247
 
248
- test("wiping a conversation without a scheduleJobId does not affect schedules", () => {
249
- const schedule = createSchedule({
248
+ test("wiping a conversation without a scheduleJobId does not affect schedules", async () => {
249
+ const schedule = await createSchedule({
250
250
  name: "Unrelated schedule",
251
251
  expression: "0 12 * * *",
252
252
  message: "Noon check",
@@ -254,7 +254,7 @@ describe("POST /conversations/:id/wipe — schedule cleanup", () => {
254
254
 
255
255
  const conv = createConversation("no-schedule-wipe");
256
256
 
257
- wipeRoute.handler({
257
+ await wipeRoute.handler({
258
258
  pathParams: { id: conv.id },
259
259
  body: {},
260
260
  headers: {},
@@ -0,0 +1,104 @@
1
+ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
+
3
+ import { registerMemoryJobHandlers } from "../jobs/register-job-handlers.js";
4
+ import * as jobsWorker from "../persistence/jobs-worker.js";
5
+ import { registerDefaultPluginJobHandlers } from "../plugins/defaults/index.js";
6
+ import {
7
+ clearJobHandlerRegistry,
8
+ getRegisteredJobHandlers,
9
+ registerPluginJobHandlers,
10
+ } from "../plugins/job-handler-registry.js";
11
+
12
+ /**
13
+ * The exact job types the `default-memory` plugin contributes via its
14
+ * `jobHandlers` field. Locks the contribution against an accidental add/drop —
15
+ * the job-handler analog of the injector order guard.
16
+ */
17
+ const MEMORY_JOB_TYPES = [
18
+ "embed_segment",
19
+ "embed_summary",
20
+ "backfill",
21
+ "rebuild_index",
22
+ "delete_qdrant_vectors",
23
+ "embed_media",
24
+ "embed_attachment",
25
+ "embed_graph_node",
26
+ "embed_pkb_file",
27
+ "graph_trigger_embed",
28
+ "graph_extract",
29
+ "graph_decay",
30
+ "graph_consolidate",
31
+ "graph_pattern_scan",
32
+ "graph_narrative_refine",
33
+ "graph_bootstrap",
34
+ "embed_concept_page",
35
+ "memory_v2_sweep",
36
+ "memory_v2_consolidate",
37
+ "memory_v2_migrate",
38
+ "memory_v2_reembed",
39
+ "memory_v2_activation_recompute",
40
+ "memory_v3_maintain",
41
+ "memory_retrospective",
42
+ ].sort();
43
+
44
+ /**
45
+ * Job types wired directly by `registerMemoryJobHandlers` for domains that are
46
+ * not plugins (persistence cleanup, conversations, media, home, runtime).
47
+ */
48
+ const NON_PLUGIN_JOB_TYPES = [
49
+ "prune_old_conversations",
50
+ "prune_old_llm_request_logs",
51
+ "prune_old_trace_events",
52
+ "build_conversation_summary",
53
+ "media_processing",
54
+ "conversation_analyze",
55
+ "generate_conversation_starters",
56
+ ].sort();
57
+
58
+ describe("job-handler registry — memory plugin contribution", () => {
59
+ beforeEach(() => {
60
+ clearJobHandlerRegistry();
61
+ });
62
+ afterEach(() => {
63
+ clearJobHandlerRegistry();
64
+ });
65
+
66
+ it("default-memory contributes exactly the memory job types", () => {
67
+ registerDefaultPluginJobHandlers();
68
+ const types = getRegisteredJobHandlers()
69
+ .map((e) => e.type)
70
+ .sort();
71
+ expect(types).toEqual(MEMORY_JOB_TYPES);
72
+ // Globally unique types (the registry would have thrown on a dupe).
73
+ expect(new Set(types).size).toBe(types.length);
74
+ });
75
+
76
+ it("registerMemoryJobHandlers wires the full job-type set into the worker", () => {
77
+ const captured: string[] = [];
78
+ const spy = spyOn(jobsWorker, "registerJobHandler").mockImplementation(
79
+ (type: string) => {
80
+ captured.push(type);
81
+ },
82
+ );
83
+ try {
84
+ registerMemoryJobHandlers();
85
+ } finally {
86
+ spy.mockRestore();
87
+ }
88
+ const expected = [...MEMORY_JOB_TYPES, ...NON_PLUGIN_JOB_TYPES].sort();
89
+ expect(captured.slice().sort()).toEqual(expected);
90
+ // Each type registered exactly once — no plugin/non-plugin overlap, no drop.
91
+ expect(new Set(captured).size).toBe(captured.length);
92
+ });
93
+
94
+ it("rejects a duplicate job-handler type across plugins", () => {
95
+ registerPluginJobHandlers("plugin-a", [
96
+ { type: "graph_extract", handler: () => undefined },
97
+ ]);
98
+ expect(() =>
99
+ registerPluginJobHandlers("plugin-b", [
100
+ { type: "graph_extract", handler: () => undefined },
101
+ ]),
102
+ ).toThrow(/already registered by plugin "plugin-a"/);
103
+ });
104
+ });
@@ -125,16 +125,34 @@ const BASELINE: Record<string, readonly string[]> = {
125
125
  "../../../../util/strip-comment-lines.js",
126
126
  "../../../../util/truncate.js",
127
127
  "../../../config/loader.js",
128
+ "../../../config/types.js",
128
129
  "../../../context/strip-injections.js",
129
130
  "../../../daemon/pkb-context-tracker.js",
130
131
  "../../../daemon/pkb-reminder-builder.js",
131
132
  "../../../daemon/trust-context.js",
133
+ "../../../memory/graph/bootstrap.js",
134
+ "../../../memory/graph/consolidation.js",
132
135
  "../../../memory/graph/conversation-graph-memory.js",
136
+ "../../../memory/graph/decay.js",
137
+ "../../../memory/graph/extraction-job.js",
138
+ "../../../memory/graph/graph-search.js",
139
+ "../../../memory/graph/narrative.js",
140
+ "../../../memory/graph/pattern-scan.js",
141
+ "../../../memory/job-handlers/backfill.js",
142
+ "../../../memory/job-handlers/embedding.js",
143
+ "../../../memory/job-handlers/index-maintenance.js",
144
+ "../../../memory/jobs/embed-concept-page.js",
145
+ "../../../memory/jobs/embed-pkb-file.js",
146
+ "../../../memory/memory-retrospective-job.js",
133
147
  "../../../memory/pkb/autoinject.js",
134
148
  "../../../memory/pkb/context.js",
135
149
  "../../../memory/pkb/pkb-search.js",
136
150
  "../../../memory/pkb/types.js",
151
+ "../../../memory/v2/backfill-jobs.js",
152
+ "../../../memory/v2/consolidation-job.js",
137
153
  "../../../memory/v2/static-context.js",
154
+ "../../../memory/v2/sweep-job.js",
155
+ "../../../persistence/jobs-store.js",
138
156
  "../../../types.js",
139
157
  "../../../util/logger.js",
140
158
  "../../../util/platform.js",
@@ -107,8 +107,8 @@ describe("schedule retry store", () => {
107
107
  db.run("DELETE FROM cron_jobs");
108
108
  });
109
109
 
110
- test("scheduleRetry reverts one-shot from firing to active with future nextRunAt", () => {
111
- const schedule = createSchedule({
110
+ test("scheduleRetry reverts one-shot from firing to active with future nextRunAt", async () => {
111
+ const schedule = await createSchedule({
112
112
  name: "One-shot retry",
113
113
  expression: null,
114
114
  nextRunAt: Date.now() - 1000,
@@ -122,15 +122,15 @@ describe("schedule retry store", () => {
122
122
  ]);
123
123
 
124
124
  const futureTime = Date.now() + 120_000;
125
- scheduleRetry(schedule.id, futureTime);
125
+ await scheduleRetry(schedule.id, futureTime);
126
126
 
127
127
  const updated = getSchedule(schedule.id)!;
128
128
  expect(updated.status).toBe("active");
129
129
  expect(updated.nextRunAt).toBe(futureTime);
130
130
  });
131
131
 
132
- test("scheduleRetry sets nextRunAt for recurring schedule without changing status", () => {
133
- const schedule = createSchedule({
132
+ test("scheduleRetry sets nextRunAt for recurring schedule without changing status", async () => {
133
+ const schedule = await createSchedule({
134
134
  name: "Recurring retry",
135
135
  cronExpression: "0 * * * *",
136
136
  message: "test",
@@ -139,7 +139,7 @@ describe("schedule retry store", () => {
139
139
 
140
140
  const originalStatus = getSchedule(schedule.id)!.status;
141
141
  const futureTime = Date.now() + 120_000;
142
- scheduleRetry(schedule.id, futureTime);
142
+ await scheduleRetry(schedule.id, futureTime);
143
143
 
144
144
  const updated = getSchedule(schedule.id)!;
145
145
  expect(updated.nextRunAt).toBe(futureTime);
@@ -147,8 +147,8 @@ describe("schedule retry store", () => {
147
147
  expect(updated.status).toBe(originalStatus);
148
148
  });
149
149
 
150
- test("resetRetryCount resets retryCount to 0 after error", () => {
151
- const schedule = createSchedule({
150
+ test("resetRetryCount resets retryCount to 0 after error", async () => {
151
+ const schedule = await createSchedule({
152
152
  name: "Reset test",
153
153
  cronExpression: "0 * * * *",
154
154
  message: "test",
@@ -156,20 +156,20 @@ describe("schedule retry store", () => {
156
156
  });
157
157
 
158
158
  // Simulate an error run which increments retryCount
159
- const runId = createScheduleRun(schedule.id, "test-conv");
160
- completeScheduleRun(runId, { status: "error", error: "boom" });
159
+ const runId = await createScheduleRun(schedule.id, "test-conv");
160
+ await completeScheduleRun(runId, { status: "error", error: "boom" });
161
161
 
162
162
  const afterError = getSchedule(schedule.id)!;
163
163
  expect(afterError.retryCount).toBe(1);
164
164
 
165
- resetRetryCount(schedule.id);
165
+ await resetRetryCount(schedule.id);
166
166
 
167
167
  const afterReset = getSchedule(schedule.id)!;
168
168
  expect(afterReset.retryCount).toBe(0);
169
169
  });
170
170
 
171
- test("createSchedule with custom maxRetries and retryBackoffMs", () => {
172
- const schedule = createSchedule({
171
+ test("createSchedule with custom maxRetries and retryBackoffMs", async () => {
172
+ const schedule = await createSchedule({
173
173
  name: "Custom retry config",
174
174
  cronExpression: "0 * * * *",
175
175
  message: "test",
@@ -183,8 +183,8 @@ describe("schedule retry store", () => {
183
183
  expect(retrieved.retryBackoffMs).toBe(30000);
184
184
  });
185
185
 
186
- test("createSchedule with defaults has maxRetries=3 and retryBackoffMs=60000", () => {
187
- const schedule = createSchedule({
186
+ test("createSchedule with defaults has maxRetries=3 and retryBackoffMs=60000", async () => {
187
+ const schedule = await createSchedule({
188
188
  name: "Default retry config",
189
189
  cronExpression: "0 * * * *",
190
190
  message: "test",
@@ -213,7 +213,7 @@ describe("scheduler retry integration", () => {
213
213
  });
214
214
 
215
215
  test("execute mode failure retries with backoff", async () => {
216
- const schedule = createSchedule({
216
+ const schedule = await createSchedule({
217
217
  name: "Failing hourly",
218
218
  cronExpression: "0 * * * *",
219
219
  message: "do work",
@@ -249,7 +249,7 @@ describe("scheduler retry integration", () => {
249
249
  });
250
250
 
251
251
  test("execute mode retry success resets retryCount", async () => {
252
- const schedule = createSchedule({
252
+ const schedule = await createSchedule({
253
253
  name: "Flaky hourly",
254
254
  cronExpression: "0 * * * *",
255
255
  message: "do work",
@@ -283,7 +283,7 @@ describe("scheduler retry integration", () => {
283
283
  });
284
284
 
285
285
  test("execute mode exhaustion resets retryCount and resumes cadence", async () => {
286
- const schedule = createSchedule({
286
+ const schedule = await createSchedule({
287
287
  name: "Exhaust hourly",
288
288
  cronExpression: "0 * * * *",
289
289
  message: "do work",
@@ -320,7 +320,7 @@ describe("scheduler retry integration", () => {
320
320
  });
321
321
 
322
322
  test("one-shot failure retries with backoff", async () => {
323
- const schedule = createSchedule({
323
+ const schedule = await createSchedule({
324
324
  name: "One-shot failing",
325
325
  expression: null,
326
326
  nextRunAt: Date.now() - 1000,
@@ -347,7 +347,7 @@ describe("scheduler retry integration", () => {
347
347
  });
348
348
 
349
349
  test("one-shot exhaustion permanently cancels", async () => {
350
- const schedule = createSchedule({
350
+ const schedule = await createSchedule({
351
351
  name: "One-shot exhaust",
352
352
  expression: null,
353
353
  nextRunAt: Date.now() - 1000,
@@ -379,7 +379,7 @@ describe("scheduler retry integration", () => {
379
379
  });
380
380
 
381
381
  test("notify one-shot failure creates schedule run", async () => {
382
- const schedule = createSchedule({
382
+ const schedule = await createSchedule({
383
383
  name: "Notify one-shot",
384
384
  expression: null,
385
385
  nextRunAt: Date.now() - 1000,
@@ -406,7 +406,7 @@ describe("scheduler retry integration", () => {
406
406
  });
407
407
 
408
408
  test("recurring schedule retry blocks normal cadence", async () => {
409
- const schedule = createSchedule({
409
+ const schedule = await createSchedule({
410
410
  name: "Cadence blocking",
411
411
  cronExpression: "0 * * * *",
412
412
  message: "do work",
@@ -444,8 +444,8 @@ describe("crash recovery", () => {
444
444
  db.run("DELETE FROM cron_jobs");
445
445
  });
446
446
 
447
- test("recovers stale firing one-shot", () => {
448
- const schedule = createSchedule({
447
+ test("recovers stale firing one-shot", async () => {
448
+ const schedule = await createSchedule({
449
449
  name: "Stale one-shot",
450
450
  expression: null,
451
451
  nextRunAt: Date.now() - 60_000,
@@ -460,7 +460,7 @@ describe("crash recovery", () => {
460
460
  [Date.now() - 60_000, schedule.id],
461
461
  );
462
462
 
463
- const recovered = recoverStaleSchedules();
463
+ const recovered = await recoverStaleSchedules();
464
464
  expect(recovered).toBe(1);
465
465
 
466
466
  // A schedule run should be created with error
@@ -476,8 +476,8 @@ describe("crash recovery", () => {
476
476
  expect(updated.nextRunAt).toBeGreaterThan(Date.now() - 5000);
477
477
  });
478
478
 
479
- test("recovers stale running cron_run", () => {
480
- const schedule = createSchedule({
479
+ test("recovers stale running cron_run", async () => {
480
+ const schedule = await createSchedule({
481
481
  name: "Stale run",
482
482
  cronExpression: "0 * * * *",
483
483
  message: "test",
@@ -485,10 +485,10 @@ describe("crash recovery", () => {
485
485
  });
486
486
 
487
487
  // Create a "running" schedule run to simulate crash
488
- const runId = createScheduleRun(schedule.id, "stale-conv");
488
+ const runId = await createScheduleRun(schedule.id, "stale-conv");
489
489
  // The run is now in "running" status by default
490
490
 
491
- const recovered = recoverStaleSchedules();
491
+ const recovered = await recoverStaleSchedules();
492
492
  expect(recovered).toBe(1);
493
493
 
494
494
  const runs = getScheduleRuns(schedule.id);
@@ -498,8 +498,8 @@ describe("crash recovery", () => {
498
498
  expect(recoveredRun!.error).toContain("recovered on restart");
499
499
  });
500
500
 
501
- test("respects maxRetries on recovery - cancels exhausted one-shot", () => {
502
- const schedule = createSchedule({
501
+ test("respects maxRetries on recovery - cancels exhausted one-shot", async () => {
502
+ const schedule = await createSchedule({
503
503
  name: "Exhausted one-shot",
504
504
  expression: null,
505
505
  nextRunAt: Date.now() - 60_000,
@@ -514,15 +514,15 @@ describe("crash recovery", () => {
514
514
  [3, Date.now() - 60_000, schedule.id],
515
515
  );
516
516
 
517
- recoverStaleSchedules();
517
+ await recoverStaleSchedules();
518
518
 
519
519
  const updated = getSchedule(schedule.id)!;
520
520
  expect(updated.status).toBe("cancelled");
521
521
  expect(updated.enabled).toBe(false);
522
522
  });
523
523
 
524
- test("idempotent - second call returns 0", () => {
525
- const schedule = createSchedule({
524
+ test("idempotent - second call returns 0", async () => {
525
+ const schedule = await createSchedule({
526
526
  name: "Idempotent test",
527
527
  expression: null,
528
528
  nextRunAt: Date.now() - 60_000,
@@ -535,15 +535,15 @@ describe("crash recovery", () => {
535
535
  [Date.now() - 60_000, schedule.id],
536
536
  );
537
537
 
538
- const first = recoverStaleSchedules();
538
+ const first = await recoverStaleSchedules();
539
539
  expect(first).toBe(1);
540
540
 
541
- const second = recoverStaleSchedules();
541
+ const second = await recoverStaleSchedules();
542
542
  expect(second).toBe(0);
543
543
  });
544
544
 
545
- test("age threshold filters recent in-flight jobs", () => {
546
- const schedule = createSchedule({
545
+ test("age threshold filters recent in-flight jobs", async () => {
546
+ const schedule = await createSchedule({
547
547
  name: "Recent firing",
548
548
  expression: null,
549
549
  nextRunAt: Date.now() - 1000,
@@ -585,7 +585,7 @@ describe("scheduler-recovery equivalence", () => {
585
585
 
586
586
  test("same retry state for identical recurring job inputs", async () => {
587
587
  // Create two identical recurring schedules
588
- const scheduleA = createSchedule({
588
+ const scheduleA = await createSchedule({
589
589
  name: "Equiv A",
590
590
  cronExpression: "0 * * * *",
591
591
  message: "test",
@@ -594,7 +594,7 @@ describe("scheduler-recovery equivalence", () => {
594
594
  retryBackoffMs: 60000,
595
595
  });
596
596
 
597
- const scheduleB = createSchedule({
597
+ const scheduleB = await createSchedule({
598
598
  name: "Equiv B",
599
599
  cronExpression: "0 * * * *",
600
600
  message: "test",
@@ -620,8 +620,8 @@ describe("scheduler-recovery equivalence", () => {
620
620
  scheduleB.id,
621
621
  ]);
622
622
  // Create a run and complete it with error (same as what scheduler does)
623
- const runId = createScheduleRun(scheduleB.id, "recovery-conv");
624
- completeScheduleRun(runId, {
623
+ const runId = await createScheduleRun(scheduleB.id, "recovery-conv");
624
+ await completeScheduleRun(runId, {
625
625
  status: "error",
626
626
  error: "Process terminated during execution (recovered on restart)",
627
627
  });
@@ -631,7 +631,7 @@ describe("scheduler-recovery equivalence", () => {
631
631
  const noopLogger = new Proxy({} as Record<string, unknown>, {
632
632
  get: () => () => {},
633
633
  });
634
- applyRetryDecision({
634
+ await applyRetryDecision({
635
635
  job: jobB,
636
636
  isOneShot: false,
637
637
  errorMsg: "fail",
@@ -659,7 +659,7 @@ describe("scheduler-recovery equivalence", () => {
659
659
 
660
660
  test("same exhaust state for identical one-shot inputs", async () => {
661
661
  // Create two identical one-shot schedules at max retries
662
- const scheduleA = createSchedule({
662
+ const scheduleA = await createSchedule({
663
663
  name: "Exhaust A",
664
664
  expression: null,
665
665
  nextRunAt: Date.now() - 1000,
@@ -669,7 +669,7 @@ describe("scheduler-recovery equivalence", () => {
669
669
  retryBackoffMs: 1000,
670
670
  });
671
671
 
672
- const scheduleB = createSchedule({
672
+ const scheduleB = await createSchedule({
673
673
  name: "Exhaust B",
674
674
  expression: null,
675
675
  nextRunAt: Date.now() - 1000,
@@ -703,15 +703,15 @@ describe("scheduler-recovery equivalence", () => {
703
703
  "UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
704
704
  [Date.now() - 60_000, scheduleB.id],
705
705
  );
706
- const runB1 = createScheduleRun(scheduleB.id, "recovery-conv-1");
707
- completeScheduleRun(runB1, { status: "error", error: "fail" });
706
+ const runB1 = await createScheduleRun(scheduleB.id, "recovery-conv-1");
707
+ await completeScheduleRun(runB1, { status: "error", error: "fail" });
708
708
  // decideRetry + apply for first failure
709
709
  const jobB1 = getSchedule(scheduleB.id)!;
710
710
  const decision1 = decideRetry(jobB1);
711
711
  const noopLogger = new Proxy({} as Record<string, unknown>, {
712
712
  get: () => () => {},
713
713
  });
714
- applyRetryDecision({
714
+ await applyRetryDecision({
715
715
  job: jobB1,
716
716
  isOneShot: true,
717
717
  errorMsg: "fail",
@@ -734,11 +734,11 @@ describe("scheduler-recovery equivalence", () => {
734
734
  "UPDATE cron_jobs SET status = 'firing', last_run_at = ? WHERE id = ?",
735
735
  [Date.now() - 60_000, scheduleB.id],
736
736
  );
737
- const runB2 = createScheduleRun(scheduleB.id, "recovery-conv-2");
738
- completeScheduleRun(runB2, { status: "error", error: "fail" });
737
+ const runB2 = await createScheduleRun(scheduleB.id, "recovery-conv-2");
738
+ await completeScheduleRun(runB2, { status: "error", error: "fail" });
739
739
  const jobB2 = getSchedule(scheduleB.id)!;
740
740
  const decision2 = decideRetry(jobB2);
741
- applyRetryDecision({
741
+ await applyRetryDecision({
742
742
  job: jobB2,
743
743
  isOneShot: true,
744
744
  errorMsg: "fail",