@femtomc/mu-server 26.2.41 → 26.2.43

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
@@ -6,7 +6,10 @@ import { eventRoutes } from "./api/events.js";
6
6
  import { forumRoutes } from "./api/forum.js";
7
7
  import { issueRoutes } from "./api/issues.js";
8
8
  import { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
9
+ import { ControlPlaneActivitySupervisor, } from "./activity_supervisor.js";
9
10
  import { bootstrapControlPlane } from "./control_plane.js";
11
+ import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
12
+ import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
10
13
  const MIME_TYPES = {
11
14
  ".html": "text/html; charset=utf-8",
12
15
  ".js": "text/javascript; charset=utf-8",
@@ -52,11 +55,30 @@ export function createServer(options = {}) {
52
55
  const readConfig = options.configReader ?? readMuConfigFile;
53
56
  const writeConfig = options.configWriter ?? writeMuConfigFile;
54
57
  const fallbackConfig = options.config ?? DEFAULT_MU_CONFIG;
58
+ const heartbeatScheduler = options.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
59
+ const activitySupervisor = options.activitySupervisor ??
60
+ new ControlPlaneActivitySupervisor({
61
+ heartbeatScheduler,
62
+ onEvent: async (event) => {
63
+ await context.eventLog.emit(`activity.${event.kind}`, {
64
+ source: "mu-server.activity-supervisor",
65
+ payload: {
66
+ seq: event.seq,
67
+ message: event.message,
68
+ activity_id: event.activity.activity_id,
69
+ kind: event.activity.kind,
70
+ status: event.activity.status,
71
+ heartbeat_count: event.activity.heartbeat_count,
72
+ last_progress: event.activity.last_progress,
73
+ },
74
+ });
75
+ },
76
+ });
55
77
  let controlPlaneCurrent = options.controlPlane ?? null;
56
78
  let reloadInFlight = null;
57
79
  const controlPlaneReloader = options.controlPlaneReloader ??
58
80
  (async ({ repoRoot, config }) => {
59
- return await bootstrapControlPlane({ repoRoot, config });
81
+ return await bootstrapControlPlane({ repoRoot, config, heartbeatScheduler });
60
82
  });
61
83
  const controlPlaneProxy = {
62
84
  get activeAdapters() {
@@ -68,12 +90,88 @@ export function createServer(options = {}) {
68
90
  return null;
69
91
  return await handle.handleWebhook(path, req);
70
92
  },
93
+ async listRuns(opts) {
94
+ const handle = controlPlaneCurrent;
95
+ if (!handle?.listRuns)
96
+ return [];
97
+ return await handle.listRuns(opts);
98
+ },
99
+ async getRun(idOrRoot) {
100
+ const handle = controlPlaneCurrent;
101
+ if (!handle?.getRun)
102
+ return null;
103
+ return await handle.getRun(idOrRoot);
104
+ },
105
+ async startRun(opts) {
106
+ const handle = controlPlaneCurrent;
107
+ if (!handle?.startRun) {
108
+ throw new Error("run_supervisor_unavailable");
109
+ }
110
+ return await handle.startRun(opts);
111
+ },
112
+ async resumeRun(opts) {
113
+ const handle = controlPlaneCurrent;
114
+ if (!handle?.resumeRun) {
115
+ throw new Error("run_supervisor_unavailable");
116
+ }
117
+ return await handle.resumeRun(opts);
118
+ },
119
+ async interruptRun(opts) {
120
+ const handle = controlPlaneCurrent;
121
+ if (!handle?.interruptRun) {
122
+ return { ok: false, reason: "not_found", run: null };
123
+ }
124
+ return await handle.interruptRun(opts);
125
+ },
126
+ async heartbeatRun(opts) {
127
+ const handle = controlPlaneCurrent;
128
+ if (!handle?.heartbeatRun) {
129
+ return { ok: false, reason: "not_found", run: null };
130
+ }
131
+ return await handle.heartbeatRun(opts);
132
+ },
133
+ async traceRun(opts) {
134
+ const handle = controlPlaneCurrent;
135
+ if (!handle?.traceRun)
136
+ return null;
137
+ return await handle.traceRun(opts);
138
+ },
71
139
  async stop() {
72
140
  const handle = controlPlaneCurrent;
73
141
  controlPlaneCurrent = null;
74
142
  await handle?.stop();
75
143
  },
76
144
  };
145
+ const heartbeatPrograms = new HeartbeatProgramRegistry({
146
+ repoRoot,
147
+ heartbeatScheduler,
148
+ runHeartbeat: async (opts) => {
149
+ const result = await controlPlaneProxy.heartbeatRun?.({
150
+ jobId: opts.jobId ?? null,
151
+ rootIssueId: opts.rootIssueId ?? null,
152
+ reason: opts.reason ?? null,
153
+ });
154
+ return result ?? { ok: false, reason: "not_found" };
155
+ },
156
+ activityHeartbeat: async (opts) => {
157
+ return activitySupervisor.heartbeat({
158
+ activityId: opts.activityId ?? null,
159
+ reason: opts.reason ?? null,
160
+ });
161
+ },
162
+ onTickEvent: async (event) => {
163
+ await context.eventLog.emit("heartbeat_program.tick", {
164
+ source: "mu-server.heartbeat-programs",
165
+ payload: {
166
+ program_id: event.program_id,
167
+ status: event.status,
168
+ reason: event.reason,
169
+ message: event.message,
170
+ program: event.program,
171
+ },
172
+ });
173
+ },
174
+ });
77
175
  const loadConfigFromDisk = async () => {
78
176
  try {
79
177
  return await readConfig(context.repoRoot);
@@ -212,6 +310,526 @@ export function createServer(options = {}) {
212
310
  control_plane: controlPlane,
213
311
  }, { headers });
214
312
  }
313
+ if (path === "/api/runs") {
314
+ if (request.method !== "GET") {
315
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
316
+ }
317
+ const status = url.searchParams.get("status")?.trim() || undefined;
318
+ const limitRaw = url.searchParams.get("limit");
319
+ const limit = limitRaw && /^\d+$/.test(limitRaw) ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10))) : undefined;
320
+ const runs = await controlPlaneProxy.listRuns?.({ status, limit });
321
+ return Response.json({ count: runs?.length ?? 0, runs: runs ?? [] }, { headers });
322
+ }
323
+ if (path === "/api/runs/start") {
324
+ if (request.method !== "POST") {
325
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
326
+ }
327
+ let body;
328
+ try {
329
+ body = (await request.json());
330
+ }
331
+ catch {
332
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
333
+ }
334
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
335
+ if (prompt.length === 0) {
336
+ return Response.json({ error: "prompt is required" }, { status: 400, headers });
337
+ }
338
+ const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
339
+ ? Math.max(1, Math.trunc(body.max_steps))
340
+ : undefined;
341
+ try {
342
+ const run = await controlPlaneProxy.startRun?.({ prompt, maxSteps });
343
+ if (!run) {
344
+ return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
345
+ }
346
+ return Response.json({ ok: true, run }, { status: 201, headers });
347
+ }
348
+ catch (err) {
349
+ return Response.json({ error: describeError(err) }, { status: 500, headers });
350
+ }
351
+ }
352
+ if (path === "/api/runs/resume") {
353
+ if (request.method !== "POST") {
354
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
355
+ }
356
+ let body;
357
+ try {
358
+ body = (await request.json());
359
+ }
360
+ catch {
361
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
362
+ }
363
+ const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
364
+ if (rootIssueId.length === 0) {
365
+ return Response.json({ error: "root_issue_id is required" }, { status: 400, headers });
366
+ }
367
+ const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
368
+ ? Math.max(1, Math.trunc(body.max_steps))
369
+ : undefined;
370
+ try {
371
+ const run = await controlPlaneProxy.resumeRun?.({ rootIssueId, maxSteps });
372
+ if (!run) {
373
+ return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
374
+ }
375
+ return Response.json({ ok: true, run }, { status: 201, headers });
376
+ }
377
+ catch (err) {
378
+ return Response.json({ error: describeError(err) }, { status: 500, headers });
379
+ }
380
+ }
381
+ if (path === "/api/runs/interrupt") {
382
+ if (request.method !== "POST") {
383
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
384
+ }
385
+ let body;
386
+ try {
387
+ body = (await request.json());
388
+ }
389
+ catch {
390
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
391
+ }
392
+ const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
393
+ const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
394
+ const result = await controlPlaneProxy.interruptRun?.({
395
+ rootIssueId,
396
+ jobId,
397
+ });
398
+ if (!result) {
399
+ return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
400
+ }
401
+ return Response.json(result, { status: result.ok ? 200 : 404, headers });
402
+ }
403
+ if (path === "/api/runs/heartbeat") {
404
+ if (request.method !== "POST") {
405
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
406
+ }
407
+ let body;
408
+ try {
409
+ body = (await request.json());
410
+ }
411
+ catch {
412
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
413
+ }
414
+ const rootIssueId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : null;
415
+ const jobId = typeof body.job_id === "string" ? body.job_id.trim() : null;
416
+ const reason = typeof body.reason === "string" ? body.reason.trim() : null;
417
+ const result = await controlPlaneProxy.heartbeatRun?.({
418
+ rootIssueId,
419
+ jobId,
420
+ reason,
421
+ });
422
+ if (!result) {
423
+ return Response.json({ error: "run supervisor unavailable" }, { status: 503, headers });
424
+ }
425
+ if (result.ok) {
426
+ return Response.json(result, { status: 200, headers });
427
+ }
428
+ if (result.reason === "missing_target") {
429
+ return Response.json(result, { status: 400, headers });
430
+ }
431
+ if (result.reason === "not_running") {
432
+ return Response.json(result, { status: 409, headers });
433
+ }
434
+ return Response.json(result, { status: 404, headers });
435
+ }
436
+ if (path.startsWith("/api/runs/")) {
437
+ const rest = path.slice("/api/runs/".length);
438
+ const [rawId, maybeSub] = rest.split("/");
439
+ const idOrRoot = decodeURIComponent(rawId ?? "").trim();
440
+ if (idOrRoot.length === 0) {
441
+ return Response.json({ error: "missing run id" }, { status: 400, headers });
442
+ }
443
+ if (maybeSub === "trace") {
444
+ if (request.method !== "GET") {
445
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
446
+ }
447
+ const limitRaw = url.searchParams.get("limit");
448
+ const limit = limitRaw && /^\d+$/.test(limitRaw)
449
+ ? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
450
+ : undefined;
451
+ const trace = await controlPlaneProxy.traceRun?.({ idOrRoot, limit });
452
+ if (!trace) {
453
+ return Response.json({ error: "run trace not found" }, { status: 404, headers });
454
+ }
455
+ return Response.json(trace, { headers });
456
+ }
457
+ if (request.method !== "GET") {
458
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
459
+ }
460
+ const run = await controlPlaneProxy.getRun?.(idOrRoot);
461
+ if (!run) {
462
+ return Response.json({ error: "run not found" }, { status: 404, headers });
463
+ }
464
+ return Response.json(run, { headers });
465
+ }
466
+ if (path === "/api/heartbeats") {
467
+ if (request.method !== "GET") {
468
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
469
+ }
470
+ const enabledRaw = url.searchParams.get("enabled")?.trim().toLowerCase();
471
+ const enabled = enabledRaw === "true" ? true : enabledRaw === "false" ? false : undefined;
472
+ const targetKindRaw = url.searchParams.get("target_kind")?.trim().toLowerCase();
473
+ const targetKind = targetKindRaw === "run" || targetKindRaw === "activity" ? targetKindRaw : undefined;
474
+ const limitRaw = url.searchParams.get("limit");
475
+ const limit = limitRaw && /^\d+$/.test(limitRaw)
476
+ ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10)))
477
+ : undefined;
478
+ const programs = await heartbeatPrograms.list({ enabled, targetKind, limit });
479
+ return Response.json({ count: programs.length, programs }, { headers });
480
+ }
481
+ if (path === "/api/heartbeats/create") {
482
+ if (request.method !== "POST") {
483
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
484
+ }
485
+ let body;
486
+ try {
487
+ body = (await request.json());
488
+ }
489
+ catch {
490
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
491
+ }
492
+ const title = typeof body.title === "string" ? body.title.trim() : "";
493
+ if (!title) {
494
+ return Response.json({ error: "title is required" }, { status: 400, headers });
495
+ }
496
+ const targetKind = typeof body.target_kind === "string" ? body.target_kind.trim().toLowerCase() : "";
497
+ let target = null;
498
+ if (targetKind === "run") {
499
+ const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
500
+ const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
501
+ if (!jobId && !rootIssueId) {
502
+ return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
503
+ }
504
+ target = {
505
+ kind: "run",
506
+ job_id: jobId || null,
507
+ root_issue_id: rootIssueId || null,
508
+ };
509
+ }
510
+ else if (targetKind === "activity") {
511
+ const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
512
+ if (!activityId) {
513
+ return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
514
+ }
515
+ target = {
516
+ kind: "activity",
517
+ activity_id: activityId,
518
+ };
519
+ }
520
+ else {
521
+ return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
522
+ }
523
+ const everyMs = typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
524
+ ? Math.max(0, Math.trunc(body.every_ms))
525
+ : undefined;
526
+ const reason = typeof body.reason === "string" ? body.reason.trim() : undefined;
527
+ const enabled = typeof body.enabled === "boolean" ? body.enabled : undefined;
528
+ try {
529
+ const program = await heartbeatPrograms.create({
530
+ title,
531
+ target,
532
+ everyMs,
533
+ reason,
534
+ enabled,
535
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
536
+ ? body.metadata
537
+ : undefined,
538
+ });
539
+ return Response.json({ ok: true, program }, { status: 201, headers });
540
+ }
541
+ catch (err) {
542
+ return Response.json({ error: describeError(err) }, { status: 400, headers });
543
+ }
544
+ }
545
+ if (path === "/api/heartbeats/update") {
546
+ if (request.method !== "POST") {
547
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
548
+ }
549
+ let body;
550
+ try {
551
+ body = (await request.json());
552
+ }
553
+ catch {
554
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
555
+ }
556
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
557
+ if (!programId) {
558
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
559
+ }
560
+ let target;
561
+ if (typeof body.target_kind === "string") {
562
+ const targetKind = body.target_kind.trim().toLowerCase();
563
+ if (targetKind === "run") {
564
+ const jobId = typeof body.run_job_id === "string" ? body.run_job_id.trim() : "";
565
+ const rootIssueId = typeof body.run_root_issue_id === "string" ? body.run_root_issue_id.trim() : "";
566
+ if (!jobId && !rootIssueId) {
567
+ return Response.json({ error: "run target requires run_job_id or run_root_issue_id" }, { status: 400, headers });
568
+ }
569
+ target = {
570
+ kind: "run",
571
+ job_id: jobId || null,
572
+ root_issue_id: rootIssueId || null,
573
+ };
574
+ }
575
+ else if (targetKind === "activity") {
576
+ const activityId = typeof body.activity_id === "string" ? body.activity_id.trim() : "";
577
+ if (!activityId) {
578
+ return Response.json({ error: "activity target requires activity_id" }, { status: 400, headers });
579
+ }
580
+ target = {
581
+ kind: "activity",
582
+ activity_id: activityId,
583
+ };
584
+ }
585
+ else {
586
+ return Response.json({ error: "target_kind must be run or activity" }, { status: 400, headers });
587
+ }
588
+ }
589
+ try {
590
+ const result = await heartbeatPrograms.update({
591
+ programId,
592
+ title: typeof body.title === "string" ? body.title : undefined,
593
+ target,
594
+ everyMs: typeof body.every_ms === "number" && Number.isFinite(body.every_ms)
595
+ ? Math.max(0, Math.trunc(body.every_ms))
596
+ : undefined,
597
+ reason: typeof body.reason === "string" ? body.reason : undefined,
598
+ enabled: typeof body.enabled === "boolean" ? body.enabled : undefined,
599
+ metadata: body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)
600
+ ? body.metadata
601
+ : undefined,
602
+ });
603
+ if (result.ok) {
604
+ return Response.json(result, { headers });
605
+ }
606
+ return Response.json(result, { status: result.reason === "not_found" ? 404 : 400, headers });
607
+ }
608
+ catch (err) {
609
+ return Response.json({ error: describeError(err) }, { status: 400, headers });
610
+ }
611
+ }
612
+ if (path === "/api/heartbeats/delete") {
613
+ if (request.method !== "POST") {
614
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
615
+ }
616
+ let body;
617
+ try {
618
+ body = (await request.json());
619
+ }
620
+ catch {
621
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
622
+ }
623
+ const programId = typeof body.program_id === "string" ? body.program_id.trim() : "";
624
+ if (!programId) {
625
+ return Response.json({ error: "program_id is required" }, { status: 400, headers });
626
+ }
627
+ const result = await heartbeatPrograms.remove(programId);
628
+ return Response.json(result, { status: result.ok ? 200 : result.reason === "not_found" ? 404 : 400, headers });
629
+ }
630
+ if (path === "/api/heartbeats/trigger") {
631
+ if (request.method !== "POST") {
632
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
633
+ }
634
+ let body;
635
+ try {
636
+ body = (await request.json());
637
+ }
638
+ catch {
639
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
640
+ }
641
+ const result = await heartbeatPrograms.trigger({
642
+ programId: typeof body.program_id === "string" ? body.program_id : null,
643
+ reason: typeof body.reason === "string" ? body.reason : null,
644
+ });
645
+ if (result.ok) {
646
+ return Response.json(result, { headers });
647
+ }
648
+ if (result.reason === "missing_target") {
649
+ return Response.json(result, { status: 400, headers });
650
+ }
651
+ if (result.reason === "not_found") {
652
+ return Response.json(result, { status: 404, headers });
653
+ }
654
+ return Response.json(result, { status: 409, headers });
655
+ }
656
+ if (path.startsWith("/api/heartbeats/")) {
657
+ if (request.method !== "GET") {
658
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
659
+ }
660
+ const id = decodeURIComponent(path.slice("/api/heartbeats/".length)).trim();
661
+ if (!id) {
662
+ return Response.json({ error: "missing program id" }, { status: 400, headers });
663
+ }
664
+ const program = await heartbeatPrograms.get(id);
665
+ if (!program) {
666
+ return Response.json({ error: "program not found" }, { status: 404, headers });
667
+ }
668
+ return Response.json(program, { headers });
669
+ }
670
+ if (path === "/api/activities") {
671
+ if (request.method !== "GET") {
672
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
673
+ }
674
+ const statusRaw = url.searchParams.get("status")?.trim().toLowerCase();
675
+ const status = statusRaw === "running" ||
676
+ statusRaw === "completed" ||
677
+ statusRaw === "failed" ||
678
+ statusRaw === "cancelled"
679
+ ? statusRaw
680
+ : undefined;
681
+ const kind = url.searchParams.get("kind")?.trim() || undefined;
682
+ const limitRaw = url.searchParams.get("limit");
683
+ const limit = limitRaw && /^\d+$/.test(limitRaw)
684
+ ? Math.max(1, Math.min(500, Number.parseInt(limitRaw, 10)))
685
+ : undefined;
686
+ const activities = activitySupervisor.list({ status, kind, limit });
687
+ return Response.json({ count: activities.length, activities }, { headers });
688
+ }
689
+ if (path === "/api/activities/start") {
690
+ if (request.method !== "POST") {
691
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
692
+ }
693
+ let body;
694
+ try {
695
+ body = (await request.json());
696
+ }
697
+ catch {
698
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
699
+ }
700
+ const title = typeof body.title === "string" ? body.title.trim() : "";
701
+ if (!title) {
702
+ return Response.json({ error: "title is required" }, { status: 400, headers });
703
+ }
704
+ const kind = typeof body.kind === "string" ? body.kind.trim() : undefined;
705
+ const heartbeatEveryMs = typeof body.heartbeat_every_ms === "number" && Number.isFinite(body.heartbeat_every_ms)
706
+ ? Math.max(0, Math.trunc(body.heartbeat_every_ms))
707
+ : undefined;
708
+ const source = body.source === "api" || body.source === "command" || body.source === "system"
709
+ ? body.source
710
+ : "api";
711
+ try {
712
+ const activity = activitySupervisor.start({
713
+ title,
714
+ kind,
715
+ heartbeatEveryMs,
716
+ metadata: body.metadata ?? undefined,
717
+ source,
718
+ });
719
+ return Response.json({ ok: true, activity }, { status: 201, headers });
720
+ }
721
+ catch (err) {
722
+ return Response.json({ error: describeError(err) }, { status: 400, headers });
723
+ }
724
+ }
725
+ if (path === "/api/activities/progress") {
726
+ if (request.method !== "POST") {
727
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
728
+ }
729
+ let body;
730
+ try {
731
+ body = (await request.json());
732
+ }
733
+ catch {
734
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
735
+ }
736
+ const result = activitySupervisor.progress({
737
+ activityId: typeof body.activity_id === "string" ? body.activity_id : null,
738
+ message: typeof body.message === "string" ? body.message : null,
739
+ });
740
+ if (result.ok) {
741
+ return Response.json(result, { headers });
742
+ }
743
+ if (result.reason === "missing_target") {
744
+ return Response.json(result, { status: 400, headers });
745
+ }
746
+ if (result.reason === "not_running") {
747
+ return Response.json(result, { status: 409, headers });
748
+ }
749
+ return Response.json(result, { status: 404, headers });
750
+ }
751
+ if (path === "/api/activities/heartbeat") {
752
+ if (request.method !== "POST") {
753
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
754
+ }
755
+ let body;
756
+ try {
757
+ body = (await request.json());
758
+ }
759
+ catch {
760
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
761
+ }
762
+ const result = activitySupervisor.heartbeat({
763
+ activityId: typeof body.activity_id === "string" ? body.activity_id : null,
764
+ reason: typeof body.reason === "string" ? body.reason : null,
765
+ });
766
+ if (result.ok) {
767
+ return Response.json(result, { headers });
768
+ }
769
+ if (result.reason === "missing_target") {
770
+ return Response.json(result, { status: 400, headers });
771
+ }
772
+ if (result.reason === "not_running") {
773
+ return Response.json(result, { status: 409, headers });
774
+ }
775
+ return Response.json(result, { status: 404, headers });
776
+ }
777
+ if (path === "/api/activities/complete" || path === "/api/activities/fail" || path === "/api/activities/cancel") {
778
+ if (request.method !== "POST") {
779
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
780
+ }
781
+ let body;
782
+ try {
783
+ body = (await request.json());
784
+ }
785
+ catch {
786
+ return Response.json({ error: "invalid json body" }, { status: 400, headers });
787
+ }
788
+ const activityId = typeof body.activity_id === "string" ? body.activity_id : null;
789
+ const message = typeof body.message === "string" ? body.message : null;
790
+ const result = path === "/api/activities/complete"
791
+ ? activitySupervisor.complete({ activityId, message })
792
+ : path === "/api/activities/fail"
793
+ ? activitySupervisor.fail({ activityId, message })
794
+ : activitySupervisor.cancel({ activityId, message });
795
+ if (result.ok) {
796
+ return Response.json(result, { headers });
797
+ }
798
+ if (result.reason === "missing_target") {
799
+ return Response.json(result, { status: 400, headers });
800
+ }
801
+ if (result.reason === "not_running") {
802
+ return Response.json(result, { status: 409, headers });
803
+ }
804
+ return Response.json(result, { status: 404, headers });
805
+ }
806
+ if (path.startsWith("/api/activities/")) {
807
+ if (request.method !== "GET") {
808
+ return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
809
+ }
810
+ const rest = path.slice("/api/activities/".length);
811
+ const [rawId, maybeSub] = rest.split("/");
812
+ const activityId = decodeURIComponent(rawId ?? "").trim();
813
+ if (activityId.length === 0) {
814
+ return Response.json({ error: "missing activity id" }, { status: 400, headers });
815
+ }
816
+ if (maybeSub === "events") {
817
+ const limitRaw = url.searchParams.get("limit");
818
+ const limit = limitRaw && /^\d+$/.test(limitRaw)
819
+ ? Math.max(1, Math.min(2_000, Number.parseInt(limitRaw, 10)))
820
+ : undefined;
821
+ const events = activitySupervisor.events(activityId, { limit });
822
+ if (!events) {
823
+ return Response.json({ error: "activity not found" }, { status: 404, headers });
824
+ }
825
+ return Response.json({ count: events.length, events }, { headers });
826
+ }
827
+ const activity = activitySupervisor.get(activityId);
828
+ if (!activity) {
829
+ return Response.json({ error: "activity not found" }, { status: 404, headers });
830
+ }
831
+ return Response.json(activity, { headers });
832
+ }
215
833
  if (path.startsWith("/api/issues")) {
216
834
  const response = await issueRoutes(request, context);
217
835
  headers.forEach((value, key) => {
@@ -266,14 +884,21 @@ export function createServer(options = {}) {
266
884
  fetch: handleRequest,
267
885
  hostname: "0.0.0.0",
268
886
  controlPlane: controlPlaneProxy,
887
+ activitySupervisor,
888
+ heartbeatPrograms,
269
889
  };
270
890
  return server;
271
891
  }
272
892
  export async function createServerAsync(options = {}) {
273
893
  const repoRoot = options.repoRoot || process.cwd();
274
894
  const config = options.config ?? (await readMuConfigFile(repoRoot));
275
- const controlPlane = await bootstrapControlPlane({ repoRoot, config: config.control_plane });
276
- const serverConfig = createServer({ ...options, controlPlane, config });
895
+ const heartbeatScheduler = options.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
896
+ const controlPlane = await bootstrapControlPlane({
897
+ repoRoot,
898
+ config: config.control_plane,
899
+ heartbeatScheduler,
900
+ });
901
+ const serverConfig = createServer({ ...options, heartbeatScheduler, controlPlane, config });
277
902
  return {
278
903
  serverConfig,
279
904
  controlPlane: serverConfig.controlPlane,