@femtomc/mu-server 26.2.72 → 26.2.74

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 (55) hide show
  1. package/README.md +54 -49
  2. package/dist/api/control_plane.js +56 -0
  3. package/dist/api/cron.js +2 -23
  4. package/dist/api/heartbeats.js +1 -66
  5. package/dist/api/identities.js +3 -2
  6. package/dist/api/runs.js +0 -83
  7. package/dist/api/session_flash.d.ts +60 -0
  8. package/dist/api/session_flash.js +326 -0
  9. package/dist/api/session_turn.d.ts +38 -0
  10. package/dist/api/session_turn.js +423 -0
  11. package/dist/config.d.ts +9 -4
  12. package/dist/config.js +24 -24
  13. package/dist/control_plane.d.ts +2 -16
  14. package/dist/control_plane.js +57 -83
  15. package/dist/control_plane_adapter_registry.d.ts +19 -0
  16. package/dist/control_plane_adapter_registry.js +74 -0
  17. package/dist/control_plane_bootstrap_helpers.js +4 -1
  18. package/dist/control_plane_contract.d.ts +3 -12
  19. package/dist/control_plane_contract.js +1 -1
  20. package/dist/control_plane_run_queue_coordinator.d.ts +1 -7
  21. package/dist/control_plane_run_queue_coordinator.js +1 -62
  22. package/dist/control_plane_telegram_generation.js +1 -0
  23. package/dist/control_plane_wake_delivery.js +1 -0
  24. package/dist/cron_programs.d.ts +21 -35
  25. package/dist/cron_programs.js +32 -113
  26. package/dist/cron_request.d.ts +0 -6
  27. package/dist/cron_request.js +0 -41
  28. package/dist/heartbeat_programs.d.ts +20 -35
  29. package/dist/heartbeat_programs.js +26 -122
  30. package/dist/index.d.ts +2 -2
  31. package/dist/orchestration_queue.d.ts +44 -0
  32. package/dist/orchestration_queue.js +111 -0
  33. package/dist/outbound_delivery_router.d.ts +12 -0
  34. package/dist/outbound_delivery_router.js +29 -0
  35. package/dist/run_queue.d.ts +1 -1
  36. package/dist/run_queue.js +78 -79
  37. package/dist/run_supervisor.d.ts +2 -17
  38. package/dist/run_supervisor.js +1 -71
  39. package/dist/server.d.ts +0 -5
  40. package/dist/server.js +95 -127
  41. package/dist/server_program_orchestration.d.ts +4 -19
  42. package/dist/server_program_orchestration.js +49 -200
  43. package/dist/server_routing.d.ts +0 -9
  44. package/dist/server_routing.js +19 -151
  45. package/dist/server_runtime.js +0 -1
  46. package/dist/server_types.d.ts +0 -2
  47. package/dist/server_types.js +0 -7
  48. package/package.json +6 -10
  49. package/dist/api/forum.d.ts +0 -2
  50. package/dist/api/forum.js +0 -75
  51. package/dist/api/issues.d.ts +0 -2
  52. package/dist/api/issues.js +0 -173
  53. package/public/assets/index-CxkevQNh.js +0 -100
  54. package/public/assets/index-D_8anM-D.css +0 -1
  55. package/public/index.html +0 -14
@@ -1,25 +1,34 @@
1
1
  import { CronProgramRegistry } from "./cron_programs.js";
2
2
  import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
3
- const AUTO_RUN_HEARTBEAT_REASON = "auto-run-heartbeat";
3
+ function describeError(err) {
4
+ if (err instanceof Error) {
5
+ return err.message;
6
+ }
7
+ return String(err);
8
+ }
4
9
  export function createServerProgramOrchestration(opts) {
5
- const autoRunHeartbeatProgramByJobId = new Map();
6
10
  const heartbeatPrograms = new HeartbeatProgramRegistry({
7
11
  repoRoot: opts.repoRoot,
8
12
  heartbeatScheduler: opts.heartbeatScheduler,
9
- runHeartbeat: async (runOpts) => {
10
- const result = await opts.controlPlaneProxy.heartbeatRun?.({
11
- jobId: runOpts.jobId ?? null,
12
- rootIssueId: runOpts.rootIssueId ?? null,
13
- reason: runOpts.reason ?? null,
14
- wakeMode: runOpts.wakeMode,
15
- });
16
- return result ?? { ok: false, reason: "not_found" };
17
- },
18
- activityHeartbeat: async (activityOpts) => {
19
- return opts.activitySupervisor.heartbeat({
20
- activityId: activityOpts.activityId ?? null,
21
- reason: activityOpts.reason ?? null,
13
+ dispatchWake: async (wakeOpts) => {
14
+ const wakeResult = await opts.emitOperatorWake({
15
+ dedupeKey: `heartbeat-program:${wakeOpts.programId}`,
16
+ message: `Heartbeat wake: ${wakeOpts.title}`,
17
+ payload: {
18
+ wake_source: "heartbeat_program",
19
+ source_ts_ms: wakeOpts.triggeredAtMs,
20
+ program_id: wakeOpts.programId,
21
+ reason: wakeOpts.reason,
22
+ metadata: wakeOpts.metadata,
23
+ },
22
24
  });
25
+ if (wakeResult.status === "coalesced") {
26
+ return { status: "coalesced", reason: wakeResult.reason };
27
+ }
28
+ if (wakeResult.status === "failed") {
29
+ return { status: "failed", reason: wakeResult.reason };
30
+ }
31
+ return { status: "ok" };
23
32
  },
24
33
  onTickEvent: async (event) => {
25
34
  await opts.eventLog.emit("heartbeat_program.tick", {
@@ -32,44 +41,36 @@ export function createServerProgramOrchestration(opts) {
32
41
  program: event.program,
33
42
  },
34
43
  });
35
- await opts.emitOperatorWake({
36
- dedupeKey: `heartbeat-program:${event.program_id}`,
37
- message: event.message,
38
- payload: {
39
- wake_source: "heartbeat_program",
40
- source_ts_ms: event.ts_ms,
41
- program_id: event.program_id,
42
- status: event.status,
43
- reason: event.reason,
44
- wake_mode: event.program.wake_mode,
45
- target_kind: event.program.target.kind,
46
- target: event.program.target.kind === "run"
47
- ? {
48
- job_id: event.program.target.job_id,
49
- root_issue_id: event.program.target.root_issue_id,
50
- }
51
- : { activity_id: event.program.target.activity_id },
52
- },
53
- });
54
44
  },
55
45
  });
56
46
  const cronPrograms = new CronProgramRegistry({
57
47
  repoRoot: opts.repoRoot,
58
48
  heartbeatScheduler: opts.heartbeatScheduler,
59
- runHeartbeat: async (runOpts) => {
60
- const result = await opts.controlPlaneProxy.heartbeatRun?.({
61
- jobId: runOpts.jobId ?? null,
62
- rootIssueId: runOpts.rootIssueId ?? null,
63
- reason: runOpts.reason ?? null,
64
- wakeMode: runOpts.wakeMode,
65
- });
66
- return result ?? { ok: false, reason: "not_found" };
67
- },
68
- activityHeartbeat: async (activityOpts) => {
69
- return opts.activitySupervisor.heartbeat({
70
- activityId: activityOpts.activityId ?? null,
71
- reason: activityOpts.reason ?? null,
72
- });
49
+ dispatchWake: async (wakeOpts) => {
50
+ try {
51
+ const wakeResult = await opts.emitOperatorWake({
52
+ dedupeKey: `cron-program:${wakeOpts.programId}`,
53
+ message: `Cron wake: ${wakeOpts.title}`,
54
+ payload: {
55
+ wake_source: "cron_program",
56
+ source_ts_ms: wakeOpts.triggeredAtMs,
57
+ program_id: wakeOpts.programId,
58
+ reason: wakeOpts.reason,
59
+ schedule: wakeOpts.schedule,
60
+ metadata: wakeOpts.metadata,
61
+ },
62
+ });
63
+ if (wakeResult.status === "coalesced") {
64
+ return { status: "coalesced", reason: wakeResult.reason };
65
+ }
66
+ if (wakeResult.status === "failed") {
67
+ return { status: "failed", reason: wakeResult.reason };
68
+ }
69
+ return { status: "ok" };
70
+ }
71
+ catch (error) {
72
+ return { status: "failed", reason: describeError(error) };
73
+ }
73
74
  },
74
75
  onLifecycleEvent: async (event) => {
75
76
  await opts.eventLog.emit("cron_program.lifecycle", {
@@ -93,162 +94,10 @@ export function createServerProgramOrchestration(opts) {
93
94
  program: event.program,
94
95
  },
95
96
  });
96
- await opts.emitOperatorWake({
97
- dedupeKey: `cron-program:${event.program_id}`,
98
- message: event.message,
99
- payload: {
100
- wake_source: "cron_program",
101
- source_ts_ms: event.ts_ms,
102
- program_id: event.program_id,
103
- status: event.status,
104
- reason: event.reason,
105
- wake_mode: event.program.wake_mode,
106
- target_kind: event.program.target.kind,
107
- target: event.program.target.kind === "run"
108
- ? {
109
- job_id: event.program.target.job_id,
110
- root_issue_id: event.program.target.root_issue_id,
111
- }
112
- : { activity_id: event.program.target.activity_id },
113
- },
114
- });
115
97
  },
116
98
  });
117
- const findAutoRunHeartbeatProgram = async (jobId) => {
118
- const normalizedJobId = jobId.trim();
119
- if (!normalizedJobId) {
120
- return null;
121
- }
122
- const knownProgramId = autoRunHeartbeatProgramByJobId.get(normalizedJobId);
123
- if (knownProgramId) {
124
- const knownProgram = await heartbeatPrograms.get(knownProgramId);
125
- if (knownProgram) {
126
- return knownProgram;
127
- }
128
- autoRunHeartbeatProgramByJobId.delete(normalizedJobId);
129
- }
130
- const programs = await heartbeatPrograms.list({ targetKind: "run", limit: 500 });
131
- for (const program of programs) {
132
- if (program.metadata.auto_run_job_id !== normalizedJobId) {
133
- continue;
134
- }
135
- autoRunHeartbeatProgramByJobId.set(normalizedJobId, program.program_id);
136
- return program;
137
- }
138
- return null;
139
- };
140
- const registerAutoRunHeartbeatProgram = async (run) => {
141
- if (run.source === "command") {
142
- return;
143
- }
144
- const jobId = run.job_id.trim();
145
- if (!jobId || run.status !== "running") {
146
- return;
147
- }
148
- const rootIssueId = typeof run.root_issue_id === "string" ? run.root_issue_id.trim() : "";
149
- const metadata = {
150
- auto_run_heartbeat: true,
151
- auto_run_job_id: jobId,
152
- auto_run_root_issue_id: rootIssueId || null,
153
- auto_disable_on_terminal: true,
154
- run_mode: run.mode,
155
- run_source: run.source,
156
- };
157
- const existing = await findAutoRunHeartbeatProgram(jobId);
158
- if (existing) {
159
- const result = await heartbeatPrograms.update({
160
- programId: existing.program_id,
161
- title: `Run heartbeat: ${rootIssueId || jobId}`,
162
- target: {
163
- kind: "run",
164
- job_id: jobId,
165
- root_issue_id: rootIssueId || null,
166
- },
167
- enabled: true,
168
- everyMs: opts.autoRunHeartbeatEveryMs,
169
- reason: AUTO_RUN_HEARTBEAT_REASON,
170
- wakeMode: "next_heartbeat",
171
- metadata,
172
- });
173
- if (result.ok && result.program) {
174
- autoRunHeartbeatProgramByJobId.set(jobId, result.program.program_id);
175
- await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
176
- source: "mu-server.runs",
177
- payload: {
178
- action: "updated",
179
- run_job_id: jobId,
180
- run_root_issue_id: rootIssueId || null,
181
- program_id: result.program.program_id,
182
- program: result.program,
183
- },
184
- });
185
- }
186
- return;
187
- }
188
- const created = await heartbeatPrograms.create({
189
- title: `Run heartbeat: ${rootIssueId || jobId}`,
190
- target: {
191
- kind: "run",
192
- job_id: jobId,
193
- root_issue_id: rootIssueId || null,
194
- },
195
- everyMs: opts.autoRunHeartbeatEveryMs,
196
- reason: AUTO_RUN_HEARTBEAT_REASON,
197
- wakeMode: "next_heartbeat",
198
- metadata,
199
- enabled: true,
200
- });
201
- autoRunHeartbeatProgramByJobId.set(jobId, created.program_id);
202
- await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
203
- source: "mu-server.runs",
204
- payload: {
205
- action: "registered",
206
- run_job_id: jobId,
207
- run_root_issue_id: rootIssueId || null,
208
- program_id: created.program_id,
209
- program: created,
210
- },
211
- });
212
- };
213
- const disableAutoRunHeartbeatProgram = async (disableOpts) => {
214
- const program = await findAutoRunHeartbeatProgram(disableOpts.jobId);
215
- if (!program) {
216
- return;
217
- }
218
- const metadata = {
219
- ...program.metadata,
220
- auto_disabled_from_status: disableOpts.status,
221
- auto_disabled_reason: disableOpts.reason,
222
- auto_disabled_at_ms: Date.now(),
223
- };
224
- const result = await heartbeatPrograms.update({
225
- programId: program.program_id,
226
- enabled: false,
227
- everyMs: 0,
228
- reason: AUTO_RUN_HEARTBEAT_REASON,
229
- wakeMode: program.wake_mode,
230
- metadata,
231
- });
232
- autoRunHeartbeatProgramByJobId.delete(disableOpts.jobId.trim());
233
- if (!result.ok || !result.program) {
234
- return;
235
- }
236
- await opts.eventLog.emit("run.auto_heartbeat.lifecycle", {
237
- source: "mu-server.runs",
238
- payload: {
239
- action: "disabled",
240
- run_job_id: disableOpts.jobId,
241
- status: disableOpts.status,
242
- reason: disableOpts.reason,
243
- program_id: result.program.program_id,
244
- program: result.program,
245
- },
246
- });
247
- };
248
99
  return {
249
100
  heartbeatPrograms,
250
101
  cronPrograms,
251
- registerAutoRunHeartbeatProgram,
252
- disableAutoRunHeartbeatProgram,
253
102
  };
254
103
  }
@@ -3,7 +3,6 @@ import type { MuConfig } from "./config.js";
3
3
  import type { ControlPlaneHandle } from "./control_plane_contract.js";
4
4
  import type { CronProgramRegistry } from "./cron_programs.js";
5
5
  import type { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
6
- import type { AutoHeartbeatRunSnapshot } from "./server_program_orchestration.js";
7
6
  import type { ServerContext } from "./server.js";
8
7
  export type ServerRoutingDependencies = {
9
8
  context: ServerContext;
@@ -17,15 +16,7 @@ export type ServerRoutingDependencies = {
17
16
  ok: boolean;
18
17
  }>;
19
18
  getControlPlaneStatus: () => unknown;
20
- registerAutoRunHeartbeatProgram: (run: AutoHeartbeatRunSnapshot) => Promise<void>;
21
- disableAutoRunHeartbeatProgram: (opts: {
22
- jobId: string;
23
- status: string;
24
- reason: string;
25
- }) => Promise<void>;
26
19
  describeError: (error: unknown) => string;
27
20
  initiateShutdown?: () => Promise<void>;
28
- publicDir?: string;
29
- mimeTypes?: Record<string, string>;
30
21
  };
31
22
  export declare function createServerRequestHandler(deps: ServerRoutingDependencies): (request: Request) => Promise<Response>;
@@ -1,30 +1,14 @@
1
- import { extname, join, resolve } from "node:path";
2
1
  import { activityRoutes } from "./api/activities.js";
3
2
  import { configRoutes } from "./api/config.js";
4
3
  import { controlPlaneRoutes } from "./api/control_plane.js";
5
4
  import { cronRoutes } from "./api/cron.js";
6
5
  import { eventRoutes } from "./api/events.js";
7
- import { forumRoutes } from "./api/forum.js";
8
6
  import { heartbeatRoutes } from "./api/heartbeats.js";
9
7
  import { identityRoutes } from "./api/identities.js";
10
- import { issueRoutes } from "./api/issues.js";
11
8
  import { runRoutes } from "./api/runs.js";
12
- const DEFAULT_MIME_TYPES = {
13
- ".html": "text/html; charset=utf-8",
14
- ".js": "text/javascript; charset=utf-8",
15
- ".css": "text/css; charset=utf-8",
16
- ".json": "application/json",
17
- ".png": "image/png",
18
- ".jpg": "image/jpeg",
19
- ".svg": "image/svg+xml",
20
- ".ico": "image/x-icon",
21
- ".woff": "font/woff",
22
- ".woff2": "font/woff2",
23
- };
24
- const DEFAULT_PUBLIC_DIR = join(new URL(".", import.meta.url).pathname, "..", "public");
9
+ import { sessionFlashRoutes } from "./api/session_flash.js";
10
+ import { sessionTurnRoutes } from "./api/session_turn.js";
25
11
  export function createServerRequestHandler(deps) {
26
- const publicDir = deps.publicDir ?? DEFAULT_PUBLIC_DIR;
27
- const mimeTypes = deps.mimeTypes ?? DEFAULT_MIME_TYPES;
28
12
  return async (request) => {
29
13
  const url = new URL(request.url);
30
14
  const path = url.pathname;
@@ -47,121 +31,32 @@ export function createServerRequestHandler(deps) {
47
31
  return Response.json({ error: "shutdown not supported" }, { status: 501, headers });
48
32
  }
49
33
  const shutdown = deps.initiateShutdown;
50
- // Respond before shutting down so the client receives the response.
51
- setTimeout(() => { void shutdown(); }, 100);
34
+ setTimeout(() => {
35
+ void shutdown();
36
+ }, 100);
52
37
  return Response.json({ ok: true, message: "shutdown initiated" }, { headers });
53
38
  }
54
39
  if (path === "/api/config") {
55
40
  return configRoutes(request, url, deps, headers);
56
41
  }
57
- if (path === "/api/control-plane/reload" || path === "/api/control-plane/rollback") {
42
+ if (path === "/api/control-plane/reload" ||
43
+ path === "/api/control-plane/rollback" ||
44
+ path === "/api/control-plane/channels") {
58
45
  return controlPlaneRoutes(request, url, deps, headers);
59
46
  }
60
47
  if (path === "/api/status") {
61
- const issues = await deps.context.issueStore.list();
62
- const openIssues = issues.filter((i) => i.status === "open");
63
- const readyIssues = await deps.context.issueStore.ready();
64
- const controlPlane = deps.getControlPlaneStatus();
65
48
  return Response.json({
66
49
  repo_root: deps.context.repoRoot,
67
- open_count: openIssues.length,
68
- ready_count: readyIssues.length,
69
- control_plane: controlPlane,
50
+ control_plane: deps.getControlPlaneStatus(),
70
51
  }, { headers });
71
52
  }
72
- if (path === "/api/commands/submit") {
73
- if (request.method !== "POST") {
74
- return Response.json({ error: "Method Not Allowed" }, { status: 405, headers });
75
- }
76
- let body;
77
- try {
78
- body = (await request.json());
79
- }
80
- catch {
81
- return Response.json({ error: "invalid json body" }, { status: 400, headers });
82
- }
83
- const kind = typeof body.kind === "string" ? body.kind.trim() : "";
84
- if (!kind) {
85
- return Response.json({ error: "kind is required" }, { status: 400, headers });
86
- }
87
- let commandText;
88
- switch (kind) {
89
- case "run_start": {
90
- const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
91
- if (!prompt) {
92
- return Response.json({ error: "prompt is required for run_start" }, { status: 400, headers });
93
- }
94
- const maxStepsSuffix = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
95
- ? ` --max-steps ${Math.max(1, Math.trunc(body.max_steps))}`
96
- : "";
97
- commandText = `mu! run start ${prompt}${maxStepsSuffix}`;
98
- break;
99
- }
100
- case "run_resume": {
101
- const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
102
- const maxSteps = typeof body.max_steps === "number" && Number.isFinite(body.max_steps)
103
- ? ` ${Math.max(1, Math.trunc(body.max_steps))}`
104
- : "";
105
- commandText = `mu! run resume${rootId ? ` ${rootId}` : ""}${maxSteps}`;
106
- break;
107
- }
108
- case "run_interrupt": {
109
- const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
110
- commandText = `mu! run interrupt${rootId ? ` ${rootId}` : ""}`;
111
- break;
112
- }
113
- case "reload":
114
- commandText = "/mu reload";
115
- break;
116
- case "update":
117
- commandText = "/mu update";
118
- break;
119
- case "status":
120
- commandText = "/mu status";
121
- break;
122
- case "issue_list":
123
- commandText = "/mu issue list";
124
- break;
125
- case "issue_get": {
126
- const issueId = typeof body.issue_id === "string" ? body.issue_id.trim() : "";
127
- commandText = `/mu issue get${issueId ? ` ${issueId}` : ""}`;
128
- break;
129
- }
130
- case "forum_read": {
131
- const topic = typeof body.topic === "string" ? body.topic.trim() : "";
132
- const limit = typeof body.limit === "number" && Number.isFinite(body.limit)
133
- ? ` ${Math.max(1, Math.trunc(body.limit))}`
134
- : "";
135
- commandText = `/mu forum read${topic ? ` ${topic}` : ""}${limit}`;
136
- break;
137
- }
138
- case "run_list":
139
- commandText = "/mu run list";
140
- break;
141
- case "run_status": {
142
- const rootId = typeof body.root_issue_id === "string" ? body.root_issue_id.trim() : "";
143
- commandText = `/mu run status${rootId ? ` ${rootId}` : ""}`;
144
- break;
145
- }
146
- case "ready":
147
- commandText = "/mu ready";
148
- break;
149
- default:
150
- return Response.json({ error: `unknown command kind: ${kind}` }, { status: 400, headers });
151
- }
152
- try {
153
- if (!deps.controlPlaneProxy.submitTerminalCommand) {
154
- return Response.json({ error: "control plane not available" }, { status: 503, headers });
155
- }
156
- const result = await deps.controlPlaneProxy.submitTerminalCommand({
157
- commandText,
158
- repoRoot: deps.context.repoRoot,
159
- });
160
- return Response.json({ ok: true, result }, { headers });
161
- }
162
- catch (err) {
163
- return Response.json({ error: `command failed: ${deps.describeError(err)}` }, { status: 500, headers });
164
- }
53
+ if (path === "/api/session-flash" ||
54
+ path === "/api/session-flash/ack" ||
55
+ path.startsWith("/api/session-flash/")) {
56
+ return sessionFlashRoutes(request, url, deps, headers);
57
+ }
58
+ if (path === "/api/session-turn") {
59
+ return sessionTurnRoutes(request, url, deps, headers);
165
60
  }
166
61
  if (path === "/api/runs" || path.startsWith("/api/runs/")) {
167
62
  return runRoutes(request, url, deps, headers);
@@ -178,20 +73,6 @@ export function createServerRequestHandler(deps) {
178
73
  if (path === "/api/identities" || path === "/api/identities/link" || path === "/api/identities/unlink") {
179
74
  return identityRoutes(request, url, deps, headers);
180
75
  }
181
- if (path.startsWith("/api/issues")) {
182
- const response = await issueRoutes(request, deps.context);
183
- headers.forEach((value, key) => {
184
- response.headers.set(key, value);
185
- });
186
- return response;
187
- }
188
- if (path.startsWith("/api/forum")) {
189
- const response = await forumRoutes(request, deps.context);
190
- headers.forEach((value, key) => {
191
- response.headers.set(key, value);
192
- });
193
- return response;
194
- }
195
76
  if (path.startsWith("/api/events")) {
196
77
  const response = await eventRoutes(request, deps.context);
197
78
  headers.forEach((value, key) => {
@@ -207,23 +88,10 @@ export function createServerRequestHandler(deps) {
207
88
  });
208
89
  return response;
209
90
  }
91
+ return new Response("Not Found", { status: 404, headers });
210
92
  }
211
- const filePath = resolve(publicDir, `.${path === "/" ? "/index.html" : path}`);
212
- if (!filePath.startsWith(publicDir)) {
213
- return new Response("Forbidden", { status: 403, headers });
214
- }
215
- const file = Bun.file(filePath);
216
- if (await file.exists()) {
217
- const ext = extname(filePath);
218
- const mime = mimeTypes[ext] ?? "application/octet-stream";
219
- headers.set("Content-Type", mime);
220
- return new Response(await file.arrayBuffer(), { status: 200, headers });
221
- }
222
- const indexPath = join(publicDir, "index.html");
223
- const indexFile = Bun.file(indexPath);
224
- if (await indexFile.exists()) {
225
- headers.set("Content-Type", "text/html; charset=utf-8");
226
- return new Response(await indexFile.arrayBuffer(), { status: 200, headers });
93
+ if (path.startsWith("/api/")) {
94
+ return Response.json({ error: "Not Found" }, { status: 404, headers });
227
95
  }
228
96
  return new Response("Not Found", { status: 404, headers });
229
97
  };
@@ -22,7 +22,6 @@ export async function composeServerRuntime(options = {}) {
22
22
  : await bootstrapControlPlane({
23
23
  repoRoot,
24
24
  config: config.control_plane,
25
- heartbeatScheduler,
26
25
  generation: {
27
26
  generation_id: "control-plane-gen-0",
28
27
  generation_seq: 0,
@@ -1,3 +1 @@
1
- export type ProgramWakeMode = "immediate" | "next_heartbeat";
2
- export declare function normalizeWakeMode(value: unknown): ProgramWakeMode;
3
1
  export declare function toNonNegativeInt(value: unknown, fallback: number): number;
@@ -1,10 +1,3 @@
1
- export function normalizeWakeMode(value) {
2
- if (typeof value !== "string") {
3
- return "immediate";
4
- }
5
- const normalized = value.trim().toLowerCase().replaceAll("-", "_");
6
- return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
7
- }
8
1
  export function toNonNegativeInt(value, fallback) {
9
2
  if (typeof value === "number" && Number.isFinite(value)) {
10
3
  return Math.max(0, Math.trunc(value));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.72",
4
- "description": "HTTP API server for mu status, work items, messaging setup, and web UI.",
3
+ "version": "26.2.74",
4
+ "description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
5
5
  "keywords": [
6
6
  "mu",
7
7
  "server",
@@ -22,8 +22,7 @@
22
22
  }
23
23
  },
24
24
  "files": [
25
- "dist/**",
26
- "public/**"
25
+ "dist/**"
27
26
  ],
28
27
  "scripts": {
29
28
  "build": "tsc -p tsconfig.build.json",
@@ -31,11 +30,8 @@
31
30
  "start": "bun run dist/cli.js"
32
31
  },
33
32
  "dependencies": {
34
- "@femtomc/mu-agent": "26.2.72",
35
- "@femtomc/mu-control-plane": "26.2.72",
36
- "@femtomc/mu-core": "26.2.72",
37
- "@femtomc/mu-forum": "26.2.72",
38
- "@femtomc/mu-issue": "26.2.72",
39
- "@femtomc/mu-orchestrator": "26.2.72"
33
+ "@femtomc/mu-agent": "26.2.74",
34
+ "@femtomc/mu-control-plane": "26.2.74",
35
+ "@femtomc/mu-core": "26.2.74"
40
36
  }
41
37
  }
@@ -1,2 +0,0 @@
1
- import type { ServerContext } from "../server.js";
2
- export declare function forumRoutes(request: Request, context: ServerContext): Promise<Response>;
package/dist/api/forum.js DELETED
@@ -1,75 +0,0 @@
1
- import { DEFAULT_FORUM_TOPICS_LIMIT, ForumStoreValidationError, normalizeForumPrefix, normalizeForumReadLimit, normalizeForumTopic, normalizeForumTopicsLimit, } from "@femtomc/mu-forum";
2
- async function readJsonBody(request) {
3
- let body;
4
- try {
5
- body = await request.json();
6
- }
7
- catch {
8
- throw new ForumStoreValidationError("invalid json body");
9
- }
10
- if (!body || typeof body !== "object" || Array.isArray(body)) {
11
- throw new ForumStoreValidationError("json body must be an object");
12
- }
13
- return body;
14
- }
15
- function errorResponse(status, message) {
16
- return new Response(JSON.stringify({ error: message }), {
17
- status,
18
- headers: { "Content-Type": "application/json" },
19
- });
20
- }
21
- function mapForumRouteError(error) {
22
- if (error instanceof ForumStoreValidationError) {
23
- return errorResponse(400, error.message);
24
- }
25
- if (error instanceof Error && error.name === "ZodError") {
26
- return errorResponse(400, error.message);
27
- }
28
- console.error("Forum API error:", error);
29
- return errorResponse(500, error instanceof Error ? error.message : "Internal server error");
30
- }
31
- export async function forumRoutes(request, context) {
32
- const url = new URL(request.url);
33
- const path = url.pathname.replace("/api/forum", "") || "/";
34
- const method = request.method;
35
- try {
36
- // List topics - GET /api/forum/topics
37
- if (path === "/topics" && method === "GET") {
38
- const prefix = normalizeForumPrefix(url.searchParams.get("prefix"));
39
- const limit = normalizeForumTopicsLimit(url.searchParams.get("limit"), {
40
- defaultLimit: DEFAULT_FORUM_TOPICS_LIMIT,
41
- });
42
- const topics = await context.forumStore.topics(prefix, { limit });
43
- return Response.json(topics);
44
- }
45
- // Read messages - GET /api/forum/read
46
- if (path === "/read" && method === "GET") {
47
- const topic = normalizeForumTopic(url.searchParams.get("topic"));
48
- const limit = normalizeForumReadLimit(url.searchParams.get("limit"));
49
- const messages = await context.forumStore.read(topic, limit);
50
- return Response.json(messages);
51
- }
52
- // Post message - POST /api/forum/post
53
- if (path === "/post" && method === "POST") {
54
- const body = await readJsonBody(request);
55
- const topic = normalizeForumTopic(body.topic);
56
- if (typeof body.body !== "string" || body.body.trim().length === 0) {
57
- return errorResponse(400, "body is required");
58
- }
59
- const messageBody = body.body;
60
- let author = "system";
61
- if (body.author != null) {
62
- if (typeof body.author !== "string" || body.author.trim().length === 0) {
63
- return errorResponse(400, "author must be a non-empty string when provided");
64
- }
65
- author = body.author.trim();
66
- }
67
- const message = await context.forumStore.post(topic, messageBody, author);
68
- return Response.json(message, { status: 201 });
69
- }
70
- return new Response("Not Found", { status: 404 });
71
- }
72
- catch (error) {
73
- return mapForumRouteError(error);
74
- }
75
- }
@@ -1,2 +0,0 @@
1
- import type { ServerContext } from "../server.js";
2
- export declare function issueRoutes(request: Request, context: ServerContext): Promise<Response>;