@polpo-ai/server 0.15.44 → 0.15.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 (34) hide show
  1. package/dist/deps.d.ts +5 -3
  2. package/dist/deps.d.ts.map +1 -1
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/routes/schedules.d.ts +2 -5
  8. package/dist/routes/schedules.d.ts.map +1 -1
  9. package/dist/routes/schedules.js +625 -121
  10. package/dist/routes/schedules.js.map +1 -1
  11. package/dist/routes/schedules.test.d.ts +2 -0
  12. package/dist/routes/schedules.test.d.ts.map +1 -0
  13. package/dist/routes/schedules.test.js +328 -0
  14. package/dist/routes/schedules.test.js.map +1 -0
  15. package/dist/schemas.d.ts +10 -10
  16. package/dist/schemas.js +4 -4
  17. package/dist/schemas.js.map +1 -1
  18. package/dist/services/schedules-migration.d.ts +44 -0
  19. package/dist/services/schedules-migration.d.ts.map +1 -0
  20. package/dist/services/schedules-migration.js +308 -0
  21. package/dist/services/schedules-migration.js.map +1 -0
  22. package/dist/services/schedules-migration.test.d.ts +2 -0
  23. package/dist/services/schedules-migration.test.d.ts.map +1 -0
  24. package/dist/services/schedules-migration.test.js +471 -0
  25. package/dist/services/schedules-migration.test.js.map +1 -0
  26. package/dist/services/schedules.d.ts +42 -0
  27. package/dist/services/schedules.d.ts.map +1 -0
  28. package/dist/services/schedules.js +183 -0
  29. package/dist/services/schedules.js.map +1 -0
  30. package/dist/services/schedules.test.d.ts +2 -0
  31. package/dist/services/schedules.test.d.ts.map +1 -0
  32. package/dist/services/schedules.test.js +261 -0
  33. package/dist/services/schedules.test.js.map +1 -0
  34. package/package.json +6 -6
@@ -1,193 +1,697 @@
1
1
  import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
2
- // ── Schemas ───────────────────────────────────────────────────────────
3
- const CreateScheduleSchema = z.object({
2
+ import { ScheduleConflictError, ScheduleInvalidStateError, ScheduleNotFoundError, translateLegacyMissionSchedule, } from "@polpo-ai/core/scheduling";
3
+ import { ScheduleServiceError, } from "../services/schedules.js";
4
+ import { legacyMissionScheduleId } from "../services/schedules-migration.js";
5
+ const MetadataSchema = z.record(z.string(), z.unknown());
6
+ const TimingSchema = z.discriminatedUnion("kind", [
7
+ z.object({
8
+ kind: z.literal("cron"),
9
+ expression: z.string().min(1),
10
+ timezone: z.string().min(1),
11
+ }).passthrough(),
12
+ z.object({
13
+ kind: z.literal("once"),
14
+ at: z.string().min(1),
15
+ timezone: z.string().min(1),
16
+ }).passthrough(),
17
+ ]);
18
+ const InvocationSchema = z.discriminatedUnion("surface", [
19
+ z.object({ surface: z.literal("agent") }).passthrough(),
20
+ z.object({ surface: z.literal("task") }).passthrough(),
21
+ z.object({ surface: z.literal("channel") }).passthrough(),
22
+ z.object({ surface: z.literal("webhook") }).passthrough(),
23
+ z.object({ surface: z.literal("legacy_mission") }).passthrough(),
24
+ ]);
25
+ const PolicySchema = z.object({
26
+ catchUp: z.enum(["skip", "latest"]).optional(),
27
+ misfireGraceSeconds: z.number().int().nonnegative().optional(),
28
+ maxConcurrency: z.number().int().positive().optional(),
29
+ }).passthrough();
30
+ const V2CreateScheduleSchema = z.object({
31
+ id: z.string().min(1).optional(),
32
+ name: z.string().min(1).optional(),
33
+ description: z.string().min(1).optional(),
34
+ timing: TimingSchema,
35
+ invocation: InvocationSchema,
36
+ status: z.enum(["active", "paused"]).optional(),
37
+ policy: PolicySchema.optional(),
38
+ metadata: MetadataSchema.optional(),
39
+ }).passthrough();
40
+ const LegacyCreateScheduleSchema = z.object({
4
41
  missionId: z.string().min(1),
5
42
  expression: z.string().min(1),
6
43
  recurring: z.boolean().optional(),
7
44
  endDate: z.string().datetime().optional(),
8
- });
9
- const UpdateScheduleSchema = z.object({
45
+ }).strict();
46
+ const V2UpdateScheduleSchema = z.object({
47
+ name: z.string().min(1).nullable().optional(),
48
+ description: z.string().min(1).nullable().optional(),
49
+ timing: TimingSchema.optional(),
50
+ invocation: InvocationSchema.optional(),
51
+ status: z.enum(["active", "paused", "completed"]).optional(),
52
+ policy: PolicySchema.optional(),
53
+ metadata: MetadataSchema.optional(),
54
+ }).passthrough();
55
+ const LegacyUpdateScheduleSchema = z.object({
10
56
  expression: z.string().min(1).optional(),
11
57
  recurring: z.boolean().optional(),
12
58
  enabled: z.boolean().optional(),
13
59
  endDate: z.string().datetime().nullable().optional(),
60
+ }).strict().refine((value) => Object.keys(value).length > 0, {
61
+ message: "Schedule update must include at least one field",
62
+ });
63
+ const ScheduleIdParams = z.object({ scheduleId: z.string().min(1) });
64
+ const RevisionHeaders = z.object({ "if-match": z.string().optional() });
65
+ const SuccessSchema = z.object({ ok: z.literal(true), data: z.any() });
66
+ const ErrorSchema = z.object({
67
+ ok: z.literal(false),
68
+ error: z.string(),
69
+ code: z.string(),
70
+ retryable: z.boolean().optional(),
14
71
  });
15
- // ── Route definitions ─────────────────────────────────────────────────
16
- const listSchedulesRoute = createRoute({
72
+ const commonErrors = {
73
+ 400: {
74
+ content: { "application/json": { schema: ErrorSchema } },
75
+ description: "Invalid request",
76
+ },
77
+ 404: {
78
+ content: { "application/json": { schema: ErrorSchema } },
79
+ description: "Schedule not found",
80
+ },
81
+ 409: {
82
+ content: { "application/json": { schema: ErrorSchema } },
83
+ description: "Schedule conflict",
84
+ },
85
+ 500: {
86
+ content: { "application/json": { schema: ErrorSchema } },
87
+ description: "Schedule operation failed",
88
+ },
89
+ 503: {
90
+ content: { "application/json": { schema: ErrorSchema } },
91
+ description: "Schedule service unavailable",
92
+ },
93
+ };
94
+ const listRoute = createRoute({
17
95
  method: "get",
18
96
  path: "/",
19
97
  tags: ["Schedules"],
20
98
  summary: "List schedules",
99
+ request: {
100
+ query: z.object({
101
+ status: z.enum(["active", "paused", "completed", "deleted"]).optional(),
102
+ surface: z.enum([
103
+ "agent",
104
+ "task",
105
+ "channel",
106
+ "webhook",
107
+ "legacy_mission",
108
+ ]).optional(),
109
+ includeDeleted: z.enum(["true", "false"]).optional(),
110
+ }),
111
+ },
21
112
  responses: {
22
113
  200: {
23
- content: { "application/json": { schema: z.object({ ok: z.boolean(), data: z.array(z.any()) }) } },
24
- description: "List of schedule entries",
114
+ content: { "application/json": { schema: SuccessSchema } },
115
+ description: "Schedules",
25
116
  },
117
+ ...commonErrors,
26
118
  },
27
119
  });
28
- const createScheduleRoute = createRoute({
120
+ const createRouteDefinition = createRoute({
29
121
  method: "post",
30
122
  path: "/",
31
123
  tags: ["Schedules"],
32
- summary: "Create schedule",
124
+ summary: "Create a schedule",
33
125
  request: {
34
- body: { content: { "application/json": { schema: CreateScheduleSchema } } },
126
+ body: {
127
+ content: {
128
+ "application/json": {
129
+ schema: z.union([
130
+ V2CreateScheduleSchema,
131
+ LegacyCreateScheduleSchema,
132
+ ]),
133
+ },
134
+ },
135
+ },
35
136
  },
36
137
  responses: {
37
138
  201: {
38
- content: { "application/json": { schema: z.object({ ok: z.boolean(), data: z.any() }) } },
139
+ content: { "application/json": { schema: SuccessSchema } },
39
140
  description: "Schedule created",
40
141
  },
41
- 400: {
42
- content: { "application/json": { schema: z.object({ ok: z.boolean(), error: z.string(), code: z.string() }) } },
43
- description: "Invalid expression or scheduler unavailable",
44
- },
45
- 404: {
46
- content: { "application/json": { schema: z.object({ ok: z.boolean(), error: z.string(), code: z.string() }) } },
47
- description: "Mission not found",
142
+ ...commonErrors,
143
+ },
144
+ });
145
+ const getRoute = createRoute({
146
+ method: "get",
147
+ path: "/{scheduleId}",
148
+ tags: ["Schedules"],
149
+ summary: "Get a schedule",
150
+ request: { params: ScheduleIdParams },
151
+ responses: {
152
+ 200: {
153
+ content: { "application/json": { schema: SuccessSchema } },
154
+ description: "Schedule",
48
155
  },
156
+ ...commonErrors,
49
157
  },
50
158
  });
51
- const updateScheduleRoute = createRoute({
159
+ const updateRoute = createRoute({
52
160
  method: "patch",
53
- path: "/{missionId}",
161
+ path: "/{scheduleId}",
54
162
  tags: ["Schedules"],
55
- summary: "Update schedule",
163
+ summary: "Update a schedule",
56
164
  request: {
57
- params: z.object({ missionId: z.string() }),
58
- body: { content: { "application/json": { schema: UpdateScheduleSchema } } },
165
+ params: ScheduleIdParams,
166
+ headers: RevisionHeaders,
167
+ body: {
168
+ content: {
169
+ "application/json": {
170
+ schema: z.union([
171
+ LegacyUpdateScheduleSchema,
172
+ V2UpdateScheduleSchema,
173
+ ]),
174
+ },
175
+ },
176
+ },
59
177
  },
60
178
  responses: {
61
179
  200: {
62
- content: { "application/json": { schema: z.object({ ok: z.boolean(), data: z.any() }) } },
180
+ content: { "application/json": { schema: SuccessSchema } },
63
181
  description: "Schedule updated",
64
182
  },
65
- 404: {
66
- content: { "application/json": { schema: z.object({ ok: z.boolean(), error: z.string(), code: z.string() }) } },
67
- description: "Schedule not found",
68
- },
183
+ ...commonErrors,
69
184
  },
70
185
  });
71
- const deleteScheduleRoute = createRoute({
186
+ const deleteRoute = createRoute({
72
187
  method: "delete",
73
- path: "/{missionId}",
188
+ path: "/{scheduleId}",
189
+ tags: ["Schedules"],
190
+ summary: "Delete a schedule",
191
+ request: { params: ScheduleIdParams, headers: RevisionHeaders },
192
+ responses: {
193
+ 200: {
194
+ content: { "application/json": { schema: SuccessSchema } },
195
+ description: "Schedule deleted",
196
+ },
197
+ ...commonErrors,
198
+ },
199
+ });
200
+ const lifecycleRoute = (action) => createRoute({
201
+ method: "post",
202
+ path: `/{scheduleId}/${action}`,
203
+ tags: ["Schedules"],
204
+ summary: `${action === "pause" ? "Pause" : "Resume"} a schedule`,
205
+ request: { params: ScheduleIdParams, headers: RevisionHeaders },
206
+ responses: {
207
+ 200: {
208
+ content: { "application/json": { schema: SuccessSchema } },
209
+ description: `Schedule ${action}d`,
210
+ },
211
+ ...commonErrors,
212
+ },
213
+ });
214
+ const pauseRoute = lifecycleRoute("pause");
215
+ const resumeRoute = lifecycleRoute("resume");
216
+ const listRunsRoute = createRoute({
217
+ method: "get",
218
+ path: "/{scheduleId}/runs",
74
219
  tags: ["Schedules"],
75
- summary: "Delete schedule",
220
+ summary: "List schedule runs",
76
221
  request: {
77
- params: z.object({ missionId: z.string() }),
222
+ params: ScheduleIdParams,
223
+ query: z.object({
224
+ status: z.enum([
225
+ "pending",
226
+ "claimed",
227
+ "running",
228
+ "succeeded",
229
+ "failed",
230
+ "skipped",
231
+ "cancelled",
232
+ ]).optional(),
233
+ limit: z.string().regex(/^\d+$/).refine((value) => Number(value) >= 1 && Number(value) <= 1_000, "Run history limit must be between 1 and 1000").optional(),
234
+ order: z.enum(["asc", "desc"]).optional(),
235
+ }),
78
236
  },
79
237
  responses: {
80
238
  200: {
81
- content: { "application/json": { schema: z.object({ ok: z.boolean(), data: z.object({ deleted: z.boolean() }) }) } },
82
- description: "Schedule deleted",
239
+ content: { "application/json": { schema: SuccessSchema } },
240
+ description: "Schedule runs",
83
241
  },
84
- 404: {
85
- content: { "application/json": { schema: z.object({ ok: z.boolean(), error: z.string(), code: z.string() }) } },
86
- description: "Schedule not found",
242
+ ...commonErrors,
243
+ },
244
+ });
245
+ const triggerRoute = createRoute({
246
+ method: "post",
247
+ path: "/{scheduleId}/runs",
248
+ tags: ["Schedules"],
249
+ summary: "Create an idempotent manual schedule run",
250
+ request: {
251
+ params: ScheduleIdParams,
252
+ body: {
253
+ content: {
254
+ "application/json": {
255
+ schema: z.object({
256
+ idempotencyKey: z.string().min(1),
257
+ }).strict(),
258
+ },
259
+ },
87
260
  },
88
261
  },
262
+ responses: {
263
+ 202: {
264
+ content: { "application/json": { schema: SuccessSchema } },
265
+ description: "Run accepted",
266
+ },
267
+ ...commonErrors,
268
+ },
89
269
  });
90
- // ── Route handlers ────────────────────────────────────────────────────
91
270
  export function scheduleRoutes(getDeps) {
92
271
  const app = new OpenAPIHono();
93
- // GET /schedules list all schedule entries
94
- app.openapi(listSchedulesRoute, (c) => {
272
+ app.openapi(listRoute, async (c) => {
95
273
  const deps = getDeps();
96
- const schedules = deps.getScheduler()?.getAllSchedules() ?? [];
97
- return c.json({ ok: true, data: schedules });
274
+ if (!deps.scheduleService)
275
+ return legacyList(c, deps);
276
+ try {
277
+ const query = c.req.valid("query");
278
+ const filter = {
279
+ ...(query.status === undefined ? {} : { status: query.status }),
280
+ ...(query.surface === undefined ? {} : { surface: query.surface }),
281
+ includeDeleted: query.includeDeleted === "true",
282
+ };
283
+ const schedules = await deps.scheduleService.list(filter);
284
+ return c.json({
285
+ ok: true,
286
+ data: schedules.map(withLegacyCompatibility),
287
+ }, 200);
288
+ }
289
+ catch (error) {
290
+ return scheduleError(c, error);
291
+ }
98
292
  });
99
- // POST /schedules create a schedule for a mission
100
- app.openapi(createScheduleRoute, async (c) => {
293
+ app.openapi(createRouteDefinition, async (c) => {
101
294
  const deps = getDeps();
102
- const scheduler = deps.getScheduler();
103
- if (!scheduler) {
104
- return c.json({ ok: false, error: "Scheduler not available", code: "SCHEDULER_UNAVAILABLE" }, 400);
105
- }
106
295
  const body = c.req.valid("json");
107
- const mission = await deps.getMission(body.missionId);
108
- if (!mission) {
109
- return c.json({ ok: false, error: `Mission "${body.missionId}" not found`, code: "NOT_FOUND" }, 404);
110
- }
111
- const isRecurring = body.recurring ?? false;
112
- const newStatus = isRecurring ? "recurring" : "scheduled";
113
- const missionUpdate = {
114
- schedule: body.expression,
115
- status: newStatus,
116
- };
117
- if (body.endDate !== undefined) {
118
- missionUpdate.endDate = body.endDate;
119
- }
120
- const updatedMission = await deps.updateMission(body.missionId, missionUpdate);
121
- const entry = scheduler.registerMission(updatedMission);
122
- if (!entry) {
123
- return c.json({ ok: false, error: "Could not create schedule. Expression may be invalid or timestamp is in the past.", code: "INVALID_EXPRESSION" }, 400);
124
- }
125
- return c.json({ ok: true, data: entry }, 201);
296
+ if (isLegacyCreate(body)) {
297
+ if (!deps.scheduleService)
298
+ return legacyCreate(c, deps, body);
299
+ try {
300
+ const mission = await deps.getMission?.(body.missionId);
301
+ if (!mission)
302
+ throw new ScheduleNotFoundError("Schedule", body.missionId);
303
+ const translated = translateLegacyRequest(body);
304
+ const created = await deps.scheduleService.create({
305
+ ...translated,
306
+ id: legacyMissionScheduleId(body.missionId),
307
+ });
308
+ return c.json({
309
+ ok: true,
310
+ data: withLegacyCompatibility(created),
311
+ }, 201);
312
+ }
313
+ catch (error) {
314
+ return scheduleError(c, error);
315
+ }
316
+ }
317
+ const service = deps.scheduleService;
318
+ if (!service)
319
+ return unavailable(c);
320
+ try {
321
+ const created = await service.create(body);
322
+ return c.json({ ok: true, data: created }, 201);
323
+ }
324
+ catch (error) {
325
+ return scheduleError(c, error);
326
+ }
126
327
  });
127
- // PATCH /schedules/:missionId update a schedule
128
- app.openapi(updateScheduleRoute, async (c) => {
328
+ app.openapi(getRoute, async (c) => {
129
329
  const deps = getDeps();
130
- const scheduler = deps.getScheduler();
131
- if (!scheduler) {
132
- return c.json({ ok: false, error: "Scheduler not available", code: "SCHEDULER_UNAVAILABLE" }, 404);
330
+ const service = deps.scheduleService;
331
+ if (!service)
332
+ return unavailable(c);
333
+ try {
334
+ const schedule = await resolveSchedule(service, c.req.valid("param").scheduleId);
335
+ return c.json({
336
+ ok: true,
337
+ data: withLegacyCompatibility(schedule),
338
+ }, 200);
133
339
  }
134
- const { missionId } = c.req.valid("param");
135
- const existing = scheduler.getScheduleByMissionId(missionId);
136
- if (!existing) {
137
- return c.json({ ok: false, error: `No schedule found for mission "${missionId}"`, code: "NOT_FOUND" }, 404);
340
+ catch (error) {
341
+ return scheduleError(c, error);
138
342
  }
343
+ });
344
+ app.openapi(updateRoute, async (c) => {
345
+ const deps = getDeps();
139
346
  const body = c.req.valid("json");
140
- if (body.expression !== undefined) {
141
- const mission = await deps.getMission(missionId);
142
- if (!mission) {
143
- return c.json({ ok: false, error: `Mission "${missionId}" not found`, code: "NOT_FOUND" }, 404);
347
+ const { scheduleId } = c.req.valid("param");
348
+ if (!deps.scheduleService) {
349
+ if (isLegacyUpdate(body))
350
+ return legacyUpdate(c, deps, scheduleId, body);
351
+ return unavailable(c);
352
+ }
353
+ try {
354
+ const expectedRevision = parseRevision(c.req.header("if-match"));
355
+ if (isLegacyUpdate(body)) {
356
+ const existing = await resolveSchedule(deps.scheduleService, scheduleId);
357
+ if (existing.invocation.surface !== "legacy_mission") {
358
+ throw new ScheduleServiceError("INVALID_REQUEST", `Schedule "${scheduleId}" is not a legacy mission schedule`, false);
359
+ }
360
+ const patch = legacyUpdatePatch(existing, body);
361
+ const updated = await deps.scheduleService.update(existing.id, patch, expectedRevision === undefined ? {} : { expectedRevision });
362
+ return c.json({
363
+ ok: true,
364
+ data: withLegacyCompatibility(updated),
365
+ }, 200);
144
366
  }
145
- const isRecurring = body.recurring ?? existing.recurring;
146
- const newStatus = isRecurring ? "recurring" : "scheduled";
147
- const updated = await deps.updateMission(missionId, {
148
- schedule: body.expression,
149
- status: newStatus,
150
- });
151
- scheduler.unregisterMission(missionId);
152
- scheduler.registerMission(updated);
153
- }
154
- if (body.recurring !== undefined && body.expression === undefined) {
155
- const isRecurring = body.recurring;
156
- existing.recurring = isRecurring;
157
- const newStatus = isRecurring ? "recurring" : "scheduled";
158
- await deps.updateMission(missionId, { status: newStatus });
159
- const mission = await deps.getMission(missionId);
160
- if (mission) {
161
- scheduler.unregisterMission(missionId);
162
- scheduler.registerMission(mission);
367
+ const updated = await deps.scheduleService.update(scheduleId, body, expectedRevision === undefined ? {} : { expectedRevision });
368
+ return c.json({ ok: true, data: updated }, 200);
369
+ }
370
+ catch (error) {
371
+ return scheduleError(c, error);
372
+ }
373
+ });
374
+ app.openapi(deleteRoute, async (c) => {
375
+ const deps = getDeps();
376
+ const { scheduleId } = c.req.valid("param");
377
+ if (!deps.scheduleService)
378
+ return legacyDelete(c, deps, scheduleId);
379
+ try {
380
+ const expectedRevision = parseRevision(c.req.header("if-match"));
381
+ const existing = await resolveSchedule(deps.scheduleService, scheduleId);
382
+ const deleted = await deps.scheduleService.delete(existing.id, expectedRevision === undefined ? {} : { expectedRevision });
383
+ if (existing.invocation.surface === "legacy_mission") {
384
+ return c.json({
385
+ ok: true,
386
+ data: { deleted: true, schedule: withLegacyCompatibility(deleted) },
387
+ }, 200);
163
388
  }
389
+ return c.json({ ok: true, data: deleted }, 200);
390
+ }
391
+ catch (error) {
392
+ return scheduleError(c, error);
164
393
  }
165
- if (body.enabled !== undefined) {
166
- existing.enabled = body.enabled;
394
+ });
395
+ app.openapi(pauseRoute, async (c) => {
396
+ const service = getDeps().scheduleService;
397
+ if (!service)
398
+ return unavailable(c);
399
+ try {
400
+ const expectedRevision = parseRevision(c.req.header("if-match"));
401
+ const paused = await service.pause(c.req.valid("param").scheduleId, expectedRevision === undefined ? {} : { expectedRevision });
402
+ return c.json({ ok: true, data: paused }, 200);
167
403
  }
168
- if (body.endDate !== undefined) {
169
- const endDate = body.endDate ?? undefined;
170
- await deps.updateMission(missionId, { endDate });
404
+ catch (error) {
405
+ return scheduleError(c, error);
171
406
  }
172
- const updated = scheduler.getScheduleByMissionId(missionId);
173
- return c.json({ ok: true, data: updated }, 200);
174
407
  });
175
- // DELETE /schedules/:missionId delete a schedule
176
- app.openapi(deleteScheduleRoute, async (c) => {
177
- const deps = getDeps();
178
- const scheduler = deps.getScheduler();
179
- if (!scheduler) {
180
- return c.json({ ok: false, error: "Scheduler not available", code: "SCHEDULER_UNAVAILABLE" }, 404);
181
- }
182
- const { missionId } = c.req.valid("param");
183
- const deleted = scheduler.unregisterMission(missionId);
184
- if (!deleted) {
185
- return c.json({ ok: false, error: `No schedule found for mission "${missionId}"`, code: "NOT_FOUND" }, 404);
186
- }
187
- // Clear schedule from mission and reset status to draft
188
- await deps.updateMission(missionId, { schedule: undefined, status: "draft" });
189
- return c.json({ ok: true, data: { deleted: true } }, 200);
408
+ app.openapi(resumeRoute, async (c) => {
409
+ const service = getDeps().scheduleService;
410
+ if (!service)
411
+ return unavailable(c);
412
+ try {
413
+ const expectedRevision = parseRevision(c.req.header("if-match"));
414
+ const resumed = await service.resume(c.req.valid("param").scheduleId, expectedRevision === undefined ? {} : { expectedRevision });
415
+ return c.json({ ok: true, data: resumed }, 200);
416
+ }
417
+ catch (error) {
418
+ return scheduleError(c, error);
419
+ }
420
+ });
421
+ app.openapi(listRunsRoute, async (c) => {
422
+ const service = getDeps().scheduleService;
423
+ if (!service)
424
+ return unavailable(c);
425
+ try {
426
+ const query = c.req.valid("query");
427
+ const filter = {
428
+ ...(query.status === undefined ? {} : { status: query.status }),
429
+ ...(query.limit === undefined ? {} : { limit: Number(query.limit) }),
430
+ ...(query.order === undefined ? {} : { order: query.order }),
431
+ };
432
+ const runs = await service.listRuns(c.req.valid("param").scheduleId, filter);
433
+ return c.json({ ok: true, data: runs }, 200);
434
+ }
435
+ catch (error) {
436
+ return scheduleError(c, error);
437
+ }
438
+ });
439
+ app.openapi(triggerRoute, async (c) => {
440
+ const service = getDeps().scheduleService;
441
+ if (!service)
442
+ return unavailable(c);
443
+ try {
444
+ const run = await service.trigger(c.req.valid("param").scheduleId, c.req.valid("json"));
445
+ return c.json({ ok: true, data: run }, 202);
446
+ }
447
+ catch (error) {
448
+ return scheduleError(c, error);
449
+ }
190
450
  });
191
451
  return app;
192
452
  }
453
+ function isLegacyCreate(body) {
454
+ return "missionId" in body;
455
+ }
456
+ function isLegacyUpdate(body) {
457
+ return ["expression", "recurring", "enabled", "endDate"]
458
+ .some((key) => key in body);
459
+ }
460
+ async function resolveSchedule(service, idOrMissionId) {
461
+ try {
462
+ return await service.get(idOrMissionId);
463
+ }
464
+ catch (error) {
465
+ if (!(error instanceof ScheduleNotFoundError))
466
+ throw error;
467
+ }
468
+ const legacy = (await service.list({ surface: "legacy_mission" }))
469
+ .find((schedule) => schedule.invocation.surface === "legacy_mission"
470
+ && schedule.invocation.missionId === idOrMissionId);
471
+ if (!legacy)
472
+ throw new ScheduleNotFoundError("Schedule", idOrMissionId);
473
+ return legacy;
474
+ }
475
+ function withLegacyCompatibility(schedule) {
476
+ if (schedule.invocation.surface !== "legacy_mission")
477
+ return schedule;
478
+ const compatibility = compatibilityMetadata(schedule);
479
+ return {
480
+ ...schedule,
481
+ missionId: schedule.invocation.missionId,
482
+ expression: timingExpression(schedule),
483
+ recurring: compatibility.recurring === true,
484
+ enabled: schedule.status === "active",
485
+ ...(schedule.nextOccurrenceAt === undefined
486
+ ? {}
487
+ : { nextRunAt: schedule.nextOccurrenceAt }),
488
+ ...(schedule.lastOccurrenceAt === undefined
489
+ ? {}
490
+ : { lastRunAt: schedule.lastOccurrenceAt }),
491
+ };
492
+ }
493
+ function legacyUpdatePatch(existing, body) {
494
+ if (existing.invocation.surface !== "legacy_mission") {
495
+ throw new ScheduleServiceError("INVALID_REQUEST", "Legacy update requires a legacy mission invocation", false);
496
+ }
497
+ const missionId = existing.invocation.missionId;
498
+ const currentCompatibility = compatibilityMetadata(existing);
499
+ const expression = body.expression ?? timingExpression(existing);
500
+ const recurring = body.recurring
501
+ ?? (currentCompatibility.recurring === true);
502
+ const endDate = body.endDate === undefined
503
+ ? stringValue(currentCompatibility.endDate)
504
+ : body.endDate ?? undefined;
505
+ const translated = translateLegacyRequest({
506
+ missionId,
507
+ expression,
508
+ recurring,
509
+ ...(endDate === undefined ? {} : { endDate }),
510
+ }, { allowPastOnce: true });
511
+ return {
512
+ ...(body.expression === undefined && body.recurring === undefined
513
+ ? {}
514
+ : { timing: translated.timing }),
515
+ invocation: translated.invocation,
516
+ metadata: {
517
+ ...existing.metadata,
518
+ ...translated.metadata,
519
+ },
520
+ ...(body.enabled === undefined
521
+ ? {}
522
+ : { status: body.enabled ? "active" : "paused" }),
523
+ };
524
+ }
525
+ function compatibilityMetadata(schedule) {
526
+ const value = schedule.metadata.compatibility;
527
+ if (!value || typeof value !== "object" || Array.isArray(value))
528
+ return {};
529
+ return value;
530
+ }
531
+ function timingExpression(schedule) {
532
+ return schedule.timing.kind === "cron"
533
+ ? schedule.timing.expression
534
+ : schedule.timing.at;
535
+ }
536
+ function stringValue(value) {
537
+ return typeof value === "string" ? value : undefined;
538
+ }
539
+ function parseRevision(value) {
540
+ if (value === undefined)
541
+ return undefined;
542
+ const normalized = value.trim().replace(/^W\//, "").replace(/^"|"$/g, "");
543
+ if (!/^[1-9]\d*$/.test(normalized)) {
544
+ throw new ScheduleServiceError("INVALID_REQUEST", "If-Match must contain a positive schedule revision", false);
545
+ }
546
+ const revision = Number(normalized);
547
+ if (!Number.isSafeInteger(revision)) {
548
+ throw new ScheduleServiceError("INVALID_REQUEST", "If-Match must contain a safe positive schedule revision", false);
549
+ }
550
+ return revision;
551
+ }
552
+ function unavailable(c) {
553
+ return c.json({
554
+ ok: false,
555
+ error: "Schedule service is not enabled on this host",
556
+ code: "SCHEDULE_SERVICE_UNAVAILABLE",
557
+ }, 503);
558
+ }
559
+ function scheduleError(c, error) {
560
+ if (error instanceof ScheduleNotFoundError
561
+ || (error instanceof ScheduleServiceError && error.code === "NOT_FOUND")) {
562
+ return c.json({
563
+ ok: false,
564
+ error: error.message,
565
+ code: "NOT_FOUND",
566
+ retryable: false,
567
+ }, 404);
568
+ }
569
+ if (error instanceof ScheduleConflictError) {
570
+ return c.json({
571
+ ok: false,
572
+ error: error.message,
573
+ code: "CONFLICT",
574
+ retryable: false,
575
+ }, 409);
576
+ }
577
+ if (error instanceof ScheduleInvalidStateError
578
+ || (error instanceof ScheduleServiceError && error.code === "INVALID_STATE")) {
579
+ return c.json({
580
+ ok: false,
581
+ error: error.message,
582
+ code: "INVALID_STATE",
583
+ retryable: false,
584
+ }, 409);
585
+ }
586
+ if (error instanceof ScheduleServiceError) {
587
+ return c.json({
588
+ ok: false,
589
+ error: error.message,
590
+ code: error.code,
591
+ retryable: error.retryable,
592
+ }, 400);
593
+ }
594
+ return c.json({
595
+ ok: false,
596
+ error: "Schedule operation failed",
597
+ code: "SCHEDULE_OPERATION_FAILED",
598
+ }, 500);
599
+ }
600
+ function translateLegacyRequest(value, options = {}) {
601
+ try {
602
+ return translateLegacyMissionSchedule(value, options);
603
+ }
604
+ catch (cause) {
605
+ throw new ScheduleServiceError("INVALID_REQUEST", cause instanceof Error ? cause.message : "Invalid legacy schedule request", false, { cause });
606
+ }
607
+ }
608
+ async function legacyList(c, deps) {
609
+ const schedules = deps.getScheduler?.()?.getAllSchedules() ?? [];
610
+ return c.json({ ok: true, data: schedules }, 200);
611
+ }
612
+ async function legacyCreate(c, deps, body) {
613
+ const scheduler = deps.getScheduler?.();
614
+ if (!scheduler)
615
+ return unavailable(c);
616
+ const mission = await deps.getMission?.(body.missionId);
617
+ if (!mission) {
618
+ return c.json({
619
+ ok: false,
620
+ error: `Mission "${body.missionId}" not found`,
621
+ code: "NOT_FOUND",
622
+ }, 404);
623
+ }
624
+ const updatedMission = await deps.updateMission?.(body.missionId, {
625
+ schedule: body.expression,
626
+ status: body.recurring ? "recurring" : "scheduled",
627
+ ...(body.endDate === undefined ? {} : { endDate: body.endDate }),
628
+ });
629
+ const entry = scheduler.registerMission(updatedMission);
630
+ if (!entry) {
631
+ return c.json({
632
+ ok: false,
633
+ error: "Could not create schedule",
634
+ code: "INVALID_EXPRESSION",
635
+ }, 400);
636
+ }
637
+ return c.json({ ok: true, data: entry }, 201);
638
+ }
639
+ async function legacyUpdate(c, deps, missionId, body) {
640
+ const scheduler = deps.getScheduler?.();
641
+ if (!scheduler)
642
+ return unavailable(c);
643
+ const existing = scheduler.getScheduleByMissionId(missionId);
644
+ if (!existing) {
645
+ return c.json({
646
+ ok: false,
647
+ error: `No schedule found for mission "${missionId}"`,
648
+ code: "NOT_FOUND",
649
+ }, 404);
650
+ }
651
+ if (body.expression !== undefined || body.recurring !== undefined) {
652
+ const mission = await deps.getMission?.(missionId);
653
+ if (!mission) {
654
+ return c.json({
655
+ ok: false,
656
+ error: `Mission "${missionId}" not found`,
657
+ code: "NOT_FOUND",
658
+ }, 404);
659
+ }
660
+ const recurring = body.recurring ?? existing.recurring;
661
+ const updated = await deps.updateMission?.(missionId, {
662
+ schedule: body.expression ?? existing.expression,
663
+ status: recurring ? "recurring" : "scheduled",
664
+ });
665
+ scheduler.unregisterMission(missionId);
666
+ scheduler.registerMission(updated);
667
+ }
668
+ if (body.enabled !== undefined)
669
+ existing.enabled = body.enabled;
670
+ if (body.endDate !== undefined) {
671
+ await deps.updateMission?.(missionId, {
672
+ endDate: body.endDate ?? undefined,
673
+ });
674
+ }
675
+ return c.json({
676
+ ok: true,
677
+ data: scheduler.getScheduleByMissionId(missionId),
678
+ }, 200);
679
+ }
680
+ async function legacyDelete(c, deps, missionId) {
681
+ const scheduler = deps.getScheduler?.();
682
+ if (!scheduler)
683
+ return unavailable(c);
684
+ if (!scheduler.unregisterMission(missionId)) {
685
+ return c.json({
686
+ ok: false,
687
+ error: `No schedule found for mission "${missionId}"`,
688
+ code: "NOT_FOUND",
689
+ }, 404);
690
+ }
691
+ await deps.updateMission?.(missionId, {
692
+ schedule: undefined,
693
+ status: "draft",
694
+ });
695
+ return c.json({ ok: true, data: { deleted: true } }, 200);
696
+ }
193
697
  //# sourceMappingURL=schedules.js.map