@femtomc/mu-server 26.2.56 → 26.2.57

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.
package/dist/server.js CHANGED
@@ -9,8 +9,9 @@ import { forumRoutes } from "./api/forum.js";
9
9
  import { issueRoutes } from "./api/issues.js";
10
10
  import { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
11
11
  import { bootstrapControlPlane, } from "./control_plane.js";
12
+ import { CronProgramRegistry } from "./cron_programs.js";
12
13
  import { ControlPlaneGenerationSupervisor } from "./generation_supervisor.js";
13
- import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
14
+ import { HeartbeatProgramRegistry, } from "./heartbeat_programs.js";
14
15
  import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
15
16
  const MIME_TYPES = {
16
17
  ".html": "text/html; charset=utf-8",
@@ -26,6 +27,25 @@ const MIME_TYPES = {
26
27
  };
27
28
  // Resolve public/ dir relative to this file (works in npm global installs)
28
29
  const PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
30
+ const DEFAULT_OPERATOR_WAKE_COALESCE_MS = 2_000;
31
+ const DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS = 15_000;
32
+ const AUTO_RUN_HEARTBEAT_REASON = "auto-run-heartbeat";
33
+ function normalizeWakeMode(value) {
34
+ if (typeof value !== "string") {
35
+ return "immediate";
36
+ }
37
+ const normalized = value.trim().toLowerCase().replaceAll("-", "_");
38
+ return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
39
+ }
40
+ function toNonNegativeInt(value, fallback) {
41
+ if (typeof value === "number" && Number.isFinite(value)) {
42
+ return Math.max(0, Math.trunc(value));
43
+ }
44
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
45
+ return Math.max(0, Number.parseInt(value, 10));
46
+ }
47
+ return Math.max(0, Math.trunc(fallback));
48
+ }
29
49
  function describeError(err) {
30
50
  if (err instanceof Error)
31
51
  return err.message;
@@ -41,6 +61,71 @@ function summarizeControlPlane(handle) {
41
61
  routes: handle.activeAdapters.map((adapter) => ({ name: adapter.name, route: adapter.route })),
42
62
  };
43
63
  }
64
+ function parseCronTarget(body) {
65
+ const targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
66
+ if (targetKind === "run") {
67
+ const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
68
+ const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
69
+ if (!jobId && !rootIssueId) {
70
+ return {
71
+ target: null,
72
+ error: "run target requires run_job_id or run_root_issue_id",
73
+ };
74
+ }
75
+ return {
76
+ target: {
77
+ kind: "run",
78
+ job_id: jobId || null,
79
+ root_issue_id: rootIssueId || null,
80
+ },
81
+ error: null,
82
+ };
83
+ }
84
+ if (targetKind === "activity") {
85
+ const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
86
+ if (!activityId) {
87
+ return {
88
+ target: null,
89
+ error: "activity target requires activity_id",
90
+ };
91
+ }
92
+ return {
93
+ target: {
94
+ kind: "activity",
95
+ activity_id: activityId,
96
+ },
97
+ error: null,
98
+ };
99
+ }
100
+ return {
101
+ target: null,
102
+ error: "target_kind must be run or activity",
103
+ };
104
+ }
105
+ function hasCronScheduleInput(body) {
106
+ return (body.schedule != null ||
107
+ body.schedule_kind != null ||
108
+ body.at_ms != null ||
109
+ body.at != null ||
110
+ body.every_ms != null ||
111
+ body.anchor_ms != null ||
112
+ body.expr != null ||
113
+ body.tz != null);
114
+ }
115
+ function cronScheduleInputFromBody(body) {
116
+ if (body.schedule && typeof body.schedule === "object" && !Array.isArray(body.schedule)) {
117
+ return { ...body.schedule };
118
+ }
119
+ return {
120
+ kind: typeof body.schedule_kind === "string" ? body.schedule_kind : undefined,
121
+ at_ms: body.at_ms,
122
+ at: body.at,
123
+ every_ms: body.every_ms,
124
+ anchor_ms: body.anchor_ms,
125
+ expr: body.expr,
126
+ tz: body.tz,
127
+ };
128
+ }
44
129
  export function createContext(repoRoot) {
45
130
  const paths = getStorePaths(repoRoot);
46
131
  const eventsStore = new FsJsonlStore(paths.eventsPath);
@@ -76,6 +161,33 @@ export function createServer(options = {}) {
76
161
  });
77
162
  },
78
163
  });
164
+ const operatorWakeCoalesceMs = toNonNegativeInt(options.operatorWakeCoalesceMs, DEFAULT_OPERATOR_WAKE_COALESCE_MS);
165
+ const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
166
+ const operatorWakeLastByKey = new Map();
167
+ const autoRunHeartbeatProgramByJobId = new Map();
168
+ const emitOperatorWake = async (opts) => {
169
+ const dedupeKey = opts.dedupeKey.trim();
170
+ if (!dedupeKey) {
171
+ return false;
172
+ }
173
+ const nowMs = Date.now();
174
+ const coalesceMs = Math.max(0, Math.trunc(opts.coalesceMs ?? operatorWakeCoalesceMs));
175
+ const previous = operatorWakeLastByKey.get(dedupeKey);
176
+ if (typeof previous === "number" && nowMs - previous < coalesceMs) {
177
+ return false;
178
+ }
179
+ operatorWakeLastByKey.set(dedupeKey, nowMs);
180
+ await context.eventLog.emit("operator.wake", {
181
+ source: "mu-server.operator-wake",
182
+ payload: {
183
+ message: opts.message,
184
+ dedupe_key: dedupeKey,
185
+ coalesce_ms: coalesceMs,
186
+ ...opts.payload,
187
+ },
188
+ });
189
+ return true;
190
+ };
79
191
  let controlPlaneCurrent = options.controlPlane ?? null;
80
192
  let reloadInFlight = null;
81
193
  const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
@@ -174,6 +286,7 @@ export function createServer(options = {}) {
174
286
  jobId: opts.jobId ?? null,
175
287
  rootIssueId: opts.rootIssueId ?? null,
176
288
  reason: opts.reason ?? null,
289
+ wakeMode: opts.wakeMode,
177
290
  });
178
291
  return result ?? { ok: false, reason: "not_found" };
179
292
  },
@@ -194,8 +307,217 @@ export function createServer(options = {}) {
194
307
  program: event.program,
195
308
  },
196
309
  });
310
+ await emitOperatorWake({
311
+ dedupeKey: `heartbeat-program:${event.program_id}`,
312
+ message: event.message,
313
+ payload: {
314
+ wake_source: "heartbeat_program",
315
+ program_id: event.program_id,
316
+ status: event.status,
317
+ reason: event.reason,
318
+ wake_mode: event.program.wake_mode,
319
+ target_kind: event.program.target.kind,
320
+ target: event.program.target.kind === "run"
321
+ ? {
322
+ job_id: event.program.target.job_id,
323
+ root_issue_id: event.program.target.root_issue_id,
324
+ }
325
+ : { activity_id: event.program.target.activity_id },
326
+ },
327
+ });
197
328
  },
198
329
  });
330
+ const cronPrograms = new CronProgramRegistry({
331
+ repoRoot,
332
+ heartbeatScheduler,
333
+ runHeartbeat: async (opts) => {
334
+ const result = await controlPlaneProxy.heartbeatRun?.({
335
+ jobId: opts.jobId ?? null,
336
+ rootIssueId: opts.rootIssueId ?? null,
337
+ reason: opts.reason ?? null,
338
+ wakeMode: opts.wakeMode,
339
+ });
340
+ return result ?? { ok: false, reason: "not_found" };
341
+ },
342
+ activityHeartbeat: async (opts) => {
343
+ return activitySupervisor.heartbeat({
344
+ activityId: opts.activityId ?? null,
345
+ reason: opts.reason ?? null,
346
+ });
347
+ },
348
+ onLifecycleEvent: async (event) => {
349
+ await context.eventLog.emit("cron_program.lifecycle", {
350
+ source: "mu-server.cron-programs",
351
+ payload: {
352
+ action: event.action,
353
+ program_id: event.program_id,
354
+ message: event.message,
355
+ program: event.program,
356
+ },
357
+ });
358
+ },
359
+ onTickEvent: async (event) => {
360
+ await context.eventLog.emit("cron_program.tick", {
361
+ source: "mu-server.cron-programs",
362
+ payload: {
363
+ program_id: event.program_id,
364
+ status: event.status,
365
+ reason: event.reason,
366
+ message: event.message,
367
+ program: event.program,
368
+ },
369
+ });
370
+ await emitOperatorWake({
371
+ dedupeKey: `cron-program:${event.program_id}`,
372
+ message: event.message,
373
+ payload: {
374
+ wake_source: "cron_program",
375
+ program_id: event.program_id,
376
+ status: event.status,
377
+ reason: event.reason,
378
+ wake_mode: event.program.wake_mode,
379
+ target_kind: event.program.target.kind,
380
+ target: event.program.target.kind === "run"
381
+ ? {
382
+ job_id: event.program.target.job_id,
383
+ root_issue_id: event.program.target.root_issue_id,
384
+ }
385
+ : { activity_id: event.program.target.activity_id },
386
+ },
387
+ });
388
+ },
389
+ });
390
+ const findAutoRunHeartbeatProgram = async (jobId) => {
391
+ const normalizedJobId = jobId.trim();
392
+ if (!normalizedJobId) {
393
+ return null;
394
+ }
395
+ const knownProgramId = autoRunHeartbeatProgramByJobId.get(normalizedJobId);
396
+ if (knownProgramId) {
397
+ const knownProgram = await heartbeatPrograms.get(knownProgramId);
398
+ if (knownProgram) {
399
+ return knownProgram;
400
+ }
401
+ autoRunHeartbeatProgramByJobId.delete(normalizedJobId);
402
+ }
403
+ const programs = await heartbeatPrograms.list({ targetKind: "run", limit: 500 });
404
+ for (const program of programs) {
405
+ if (program.metadata.auto_run_job_id !== normalizedJobId) {
406
+ continue;
407
+ }
408
+ autoRunHeartbeatProgramByJobId.set(normalizedJobId, program.program_id);
409
+ return program;
410
+ }
411
+ return null;
412
+ };
413
+ const registerAutoRunHeartbeatProgram = async (run) => {
414
+ if (run.source === "command") {
415
+ return;
416
+ }
417
+ const jobId = run.job_id.trim();
418
+ if (!jobId || run.status !== "running") {
419
+ return;
420
+ }
421
+ const rootIssueId = typeof run.root_issue_id === "string" ? run.root_issue_id.trim() : "";
422
+ const metadata = {
423
+ auto_run_heartbeat: true,
424
+ auto_run_job_id: jobId,
425
+ auto_run_root_issue_id: rootIssueId || null,
426
+ auto_disable_on_terminal: true,
427
+ run_mode: run.mode,
428
+ run_source: run.source,
429
+ };
430
+ const existing = await findAutoRunHeartbeatProgram(jobId);
431
+ if (existing) {
432
+ const result = await heartbeatPrograms.update({
433
+ programId: existing.program_id,
434
+ title: `Run heartbeat: ${rootIssueId || jobId}`,
435
+ target: {
436
+ kind: "run",
437
+ job_id: jobId,
438
+ root_issue_id: rootIssueId || null,
439
+ },
440
+ enabled: true,
441
+ everyMs: autoRunHeartbeatEveryMs,
442
+ reason: AUTO_RUN_HEARTBEAT_REASON,
443
+ wakeMode: "next_heartbeat",
444
+ metadata,
445
+ });
446
+ if (result.ok && result.program) {
447
+ autoRunHeartbeatProgramByJobId.set(jobId, result.program.program_id);
448
+ await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
449
+ source: "mu-server.runs",
450
+ payload: {
451
+ action: "updated",
452
+ run_job_id: jobId,
453
+ run_root_issue_id: rootIssueId || null,
454
+ program_id: result.program.program_id,
455
+ program: result.program,
456
+ },
457
+ });
458
+ }
459
+ return;
460
+ }
461
+ const created = await heartbeatPrograms.create({
462
+ title: `Run heartbeat: ${rootIssueId || jobId}`,
463
+ target: {
464
+ kind: "run",
465
+ job_id: jobId,
466
+ root_issue_id: rootIssueId || null,
467
+ },
468
+ everyMs: autoRunHeartbeatEveryMs,
469
+ reason: AUTO_RUN_HEARTBEAT_REASON,
470
+ wakeMode: "next_heartbeat",
471
+ metadata,
472
+ enabled: true,
473
+ });
474
+ autoRunHeartbeatProgramByJobId.set(jobId, created.program_id);
475
+ await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
476
+ source: "mu-server.runs",
477
+ payload: {
478
+ action: "registered",
479
+ run_job_id: jobId,
480
+ run_root_issue_id: rootIssueId || null,
481
+ program_id: created.program_id,
482
+ program: created,
483
+ },
484
+ });
485
+ };
486
+ const disableAutoRunHeartbeatProgram = async (opts) => {
487
+ const program = await findAutoRunHeartbeatProgram(opts.jobId);
488
+ if (!program) {
489
+ return;
490
+ }
491
+ const metadata = {
492
+ ...program.metadata,
493
+ auto_disabled_from_status: opts.status,
494
+ auto_disabled_reason: opts.reason,
495
+ auto_disabled_at_ms: Date.now(),
496
+ };
497
+ const result = await heartbeatPrograms.update({
498
+ programId: program.program_id,
499
+ enabled: false,
500
+ everyMs: 0,
501
+ reason: AUTO_RUN_HEARTBEAT_REASON,
502
+ wakeMode: program.wake_mode,
503
+ metadata,
504
+ });
505
+ autoRunHeartbeatProgramByJobId.delete(opts.jobId.trim());
506
+ if (!result.ok || !result.program) {
507
+ return;
508
+ }
509
+ await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
510
+ source: "mu-server.runs",
511
+ payload: {
512
+ action: "disabled",
513
+ run_job_id: opts.jobId,
514
+ status: opts.status,
515
+ reason: opts.reason,
516
+ program_id: result.program.program_id,
517
+ program: result.program,
518
+ },
519
+ });
520
+ };
199
521
  const loadConfigFromDisk = async () => {
200
522
  try {
201
523
  return await readConfig(context.repoRoot);
@@ -813,6 +1135,16 @@ export function createServer(options = {}) {
813
1135
  if (!run) {
814
1136
  return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
815
1137
  }
1138
+ await registerAutoRunHeartbeatProgram(run).catch(async (error) => {
1139
+ await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
1140
+ source: "mu-server.runs",
1141
+ payload: {
1142
+ action: "register_failed",
1143
+ run_job_id: run.job_id,
1144
+ error: describeError(error),
1145
+ },
1146
+ });
1147
+ });
816
1148
  return Response.json({ ok: true, run }, { status: 201, headers });
817
1149
  }
818
1150
  catch (err) {
@@ -842,6 +1174,16 @@ export function createServer(options = {}) {
842
1174
  if (!run) {
843
1175
  return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
844
1176
  }
1177
+ await registerAutoRunHeartbeatProgram(run).catch(async (error) => {
1178
+ await context.eventLog.emit("run.auto_heartbeat.lifecycle", {
1179
+ source: "mu-server.runs",
1180
+ payload: {
1181
+ action: "register_failed",
1182
+ run_job_id: run.job_id,
1183
+ error: describeError(error),
1184
+ },
1185
+ });
1186
+ });
845
1187
  return Response.json({ ok: true, run }, { status: 201, headers });
846
1188
  }
847
1189
  catch (err) {
@@ -868,6 +1210,15 @@ export function createServer(options = {}) {
868
1210
  if (!result) {
869
1211
  return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
870
1212
  }
1213
+ if (!result.ok && result.reason === "not_running" && result.run) {
1214
+ await disableAutoRunHeartbeatProgram({
1215
+ jobId: result.run.job_id,
1216
+ status: result.run.status,
1217
+ reason: "interrupt_not_running",
1218
+ }).catch(() => {
1219
+ // best effort cleanup only
1220
+ });
1221
+ }
871
1222
  return Response.json(result, { status: result.ok ? 200 : 404, headers });
872
1223
  }
873
1224
  if (path === "/api/runs/heartbeat") {
@@ -884,14 +1235,25 @@ export function createServer(options = {}) {
884
1235
  const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
885
1236
  const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
886
1237
  const reason = typeof body.reason === "string" ? body.reason.trim() : null;
1238
+ const wakeMode = normalizeWakeMode(body.wake_mode);
887
1239
  const result = await controlPlaneProxy.heartbeatRun?.({
888
1240
  rootIssueId,
889
1241
  jobId,
890
1242
  reason,
1243
+ wakeMode,
891
1244
  });
892
1245
  if (!result) {
893
1246
  return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
894
1247
  }
1248
+ if (!result.ok && result.reason === "not_running" && result.run) {
1249
+ await disableAutoRunHeartbeatProgram({
1250
+ jobId: result.run.job_id,
1251
+ status: result.run.status,
1252
+ reason: "run_not_running",
1253
+ }).catch(() => {
1254
+ // best effort cleanup only
1255
+ });
1256
+ }
895
1257
  if (result.ok) {
896
1258
  return Response.json(result, { status: 200, headers });
897
1259
  }
@@ -931,8 +1293,193 @@ export function createServer(options = {}) {
931
1293
  if (!run) {
932
1294
  return Response.json({ error: "run not found" }, { status: 404, headers });
933
1295
  }
1296
+ if (run.status !== "running") {
1297
+ await disableAutoRunHeartbeatProgram({
1298
+ jobId: run.job_id,
1299
+ status: run.status,
1300
+ reason: "run_terminal_snapshot",
1301
+ }).catch(() => {
1302
+ // best effort cleanup only
1303
+ });
1304
+ }
934
1305
  return Response.json(run, { headers });
935
1306
  }
1307
+ if (path === "/api/cron/status") {
1308
+ if (request.method !== "GET") {
1309
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1310
+ }
1311
+ const status = await cronPrograms.status();
1312
+ return Response.json(status, { headers });
1313
+ }
1314
+ if (path === "/api/cron") {
1315
+ if (request.method !== "GET") {
1316
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1317
+ }
1318
+ const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
1319
+ const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
1320
+ const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
1321
+ const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
1322
+ const scheduleKindRaw = url.searchParams.get("schedule_kind")?.trim().toLowerCase();
1323
+ const scheduleKind = scheduleKindRaw === "at" || scheduleKindRaw === "every" || scheduleKindRaw === "cron"
1324
+ ? scheduleKindRaw
1325
+ : undefined;
1326
+ const limitRaw = url.searchParams.get("limit");
1327
+ const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
1328
+ const programs = await cronPrograms.list({ enabled, targetKind, scheduleKind, limit });
1329
+ return Response.json({ count: programs.length, programs }, { headers });
1330
+ }
1331
+ if (path === "/api/cron/create") {
1332
+ if (request.method !== "POST") {
1333
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1334
+ }
1335
+ let body;
1336
+ try {
1337
+ body = (await request.json());
1338
+ }
1339
+ catch {
1340
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
1341
+ }
1342
+ const title = typeof body.title === "string" ? body.title.trim() : "";
1343
+ if (!title) {
1344
+ return Response.json({ error: "title is required" }, { status: 400, headers });
1345
+ }
1346
+ const parsedTarget = parseCronTarget(body);
1347
+ if (!parsedTarget.target) {
1348
+ return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
1349
+ }
1350
+ if (!hasCronScheduleInput(body)) {
1351
+ return Response.json({ error: "schedule is required" }, { status: 400, headers });
1352
+ }
1353
+ const schedule = cronScheduleInputFromBody(body);
1354
+ const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
1355
+ const wakeMode = normalizeWakeMode(body.wake_mode);
1356
+ const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
1357
+ try {
1358
+ const program = await cronPrograms.create({
1359
+ title,
1360
+ target: parsedTarget.target,
1361
+ schedule,
1362
+ reason,
1363
+ wakeMode,
1364
+ enabled,
1365
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
1366
+ ? body.metadata
1367
+ : undefined,
1368
+ });
1369
+ return Response.json({ ok: true, program }, { status: 201, headers });
1370
+ }
1371
+ catch (err) {
1372
+ return Response.json({ error: describeError(err) }, { status: 400, headers });
1373
+ }
1374
+ }
1375
+ if (path === "/api/cron/update") {
1376
+ if (request.method !== "POST") {
1377
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1378
+ }
1379
+ let body;
1380
+ try {
1381
+ body = (await request.json());
1382
+ }
1383
+ catch {
1384
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
1385
+ }
1386
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
1387
+ if (!programId) {
1388
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
1389
+ }
1390
+ let target;
1391
+ if (typeof body.target_kind === "string") {
1392
+ const parsedTarget = parseCronTarget(body);
1393
+ if (!parsedTarget.target) {
1394
+ return Response.json({ error: parsedTarget.error ?? "invalid target" }, { status: 400, headers });
1395
+ }
1396
+ target = parsedTarget.target;
1397
+ }
1398
+ const schedule = hasCronScheduleInput(body) ? cronScheduleInputFromBody(body) : undefined;
1399
+ const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
1400
+ try {
1401
+ const result = await cronPrograms.update({
1402
+ programId,
1403
+ title: typeof body.title === "string" ? body.title : undefined,
1404
+ reason: typeof body.reason === "string" ? body.reason : undefined,
1405
+ wakeMode,
1406
+ enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
1407
+ target,
1408
+ schedule,
1409
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
1410
+ ? body.metadata
1411
+ : undefined,
1412
+ });
1413
+ if (result.ok) {
1414
+ return Response.json(result, { headers });
1415
+ }
1416
+ if (result.reason === "not_found") {
1417
+ return Response.json(result, { status: 404, headers });
1418
+ }
1419
+ return Response.json(result, { status: 400, headers });
1420
+ }
1421
+ catch (err) {
1422
+ return Response.json({ error: describeError(err) }, { status: 400, headers });
1423
+ }
1424
+ }
1425
+ if (path === "/api/cron/delete") {
1426
+ if (request.method !== "POST") {
1427
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1428
+ }
1429
+ let body;
1430
+ try {
1431
+ body = (await request.json());
1432
+ }
1433
+ catch {
1434
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
1435
+ }
1436
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
1437
+ if (!programId) {
1438
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
1439
+ }
1440
+ const result = await cronPrograms.remove(programId);
1441
+ return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
1442
+ }
1443
+ if (path === "/api/cron/trigger") {
1444
+ if (request.method !== "POST") {
1445
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1446
+ }
1447
+ let body;
1448
+ try {
1449
+ body = (await request.json());
1450
+ }
1451
+ catch {
1452
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
1453
+ }
1454
+ const result = await cronPrograms.trigger({
1455
+ programId: typeof body.program_id === "string" ? body.program_id : null,
1456
+ reason: typeof body.reason === "string" ? body.reason : null,
1457
+ });
1458
+ if (result.ok) {
1459
+ return Response.json(result, { headers });
1460
+ }
1461
+ if (result.reason === "missing_target") {
1462
+ return Response.json(result, { status: 400, headers });
1463
+ }
1464
+ if (result.reason === "not_found") {
1465
+ return Response.json(result, { status: 404, headers });
1466
+ }
1467
+ return Response.json(result, { status: 409, headers });
1468
+ }
1469
+ if (path.startsWith("/api/cron/")) {
1470
+ if (request.method !== "GET") {
1471
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
1472
+ }
1473
+ const id = decodeURIComponent(path.slice("/api/cron/".length)).trim();
1474
+ if (!id) {
1475
+ return Response.json({ error: "missing program id" }, { status: 400, headers });
1476
+ }
1477
+ const program = await cronPrograms.get(id);
1478
+ if (!program) {
1479
+ return Response.json({ error: "program not found" }, { status: 404, headers });
1480
+ }
1481
+ return Response.json(program, { headers });
1482
+ }
936
1483
  if (path === "/api/heartbeats") {
937
1484
  if (request.method !== "GET") {
938
1485
  return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
@@ -992,6 +1539,7 @@ export function createServer(options = {}) {
992
1539
  ? Math.max(0, Math.trunc(body.every_ms))
993
1540
  : undefined;
994
1541
  const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
1542
+ const wakeMode = normalizeWakeMode(body.wake_mode);
995
1543
  const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
996
1544
  try {
997
1545
  const program = await heartbeatPrograms.create({
@@ -999,6 +1547,7 @@ export function createServer(options = {}) {
999
1547
  target,
1000
1548
  everyMs,
1001
1549
  reason,
1550
+ wakeMode,
1002
1551
  enabled,
1003
1552
  metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
1004
1553
  ? body.metadata
@@ -1054,6 +1603,7 @@ export function createServer(options = {}) {
1054
1603
  return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
1055
1604
  }
1056
1605
  }
1606
+ const wakeMode = Object.hasOwn(body, "wake_mode") ? normalizeWakeMode(body.wake_mode) : undefined;
1057
1607
  try {
1058
1608
  const result = await heartbeatPrograms.update({
1059
1609
  programId,
@@ -1063,6 +1613,7 @@ export function createServer(options = {}) {
1063
1613
  ? Math.max(0, Math.trunc(body.every_ms))
1064
1614
  : undefined,
1065
1615
  reason: typeof body.reason === "string" ? body.reason : undefined,
1616
+ wakeMode,
1066
1617
  enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
1067
1618
  metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
1068
1619
  ? body.metadata
@@ -1447,6 +1998,7 @@ export function createServer(options = {}) {
1447
1998
  controlPlane: controlPlaneProxy,
1448
1999
  activitySupervisor,
1449
2000
  heartbeatPrograms,
2001
+ cronPrograms,
1450
2002
  };
1451
2003
  return server;
1452
2004
  }