@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
package/dist/server.js CHANGED
@@ -1,9 +1,7 @@
1
- import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
1
+ import { GenerationTelemetryRecorder, presentPipelineResultMessage, } from "@femtomc/mu-control-plane";
2
2
  import { currentRunId, EventLog, FsJsonlStore, getStorePaths, JsonlEventSink } from "@femtomc/mu-core/node";
3
- import { ForumStore } from "@femtomc/mu-forum";
4
- import { IssueStore } from "@femtomc/mu-issue";
5
3
  import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
6
- import { DEFAULT_MU_CONFIG, readMuConfigFile, writeMuConfigFile, } from "./config.js";
4
+ import { DEFAULT_MU_CONFIG, readMuConfigFile, writeMuConfigFile } from "./config.js";
7
5
  import { bootstrapControlPlane } from "./control_plane.js";
8
6
  import { createReloadManager, } from "./control_plane_reload.js";
9
7
  import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
@@ -12,7 +10,6 @@ import { createServerProgramOrchestration } from "./server_program_orchestration
12
10
  import { createServerRequestHandler } from "./server_routing.js";
13
11
  import { toNonNegativeInt } from "./server_types.js";
14
12
  const DEFAULT_OPERATOR_WAKE_COALESCE_MS = 2_000;
15
- const DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS = 15_000;
16
13
  export { createProcessSessionLifecycle };
17
14
  function describeError(err) {
18
15
  if (err instanceof Error)
@@ -27,19 +24,6 @@ function emptyNotifyOperatorsResult() {
27
24
  decisions: [],
28
25
  };
29
26
  }
30
- function normalizeWakeTurnMode(value) {
31
- if (typeof value !== "string") {
32
- return "off";
33
- }
34
- const normalized = value.trim().toLowerCase();
35
- if (normalized === "shadow") {
36
- return "shadow";
37
- }
38
- if (normalized === "active") {
39
- return "active";
40
- }
41
- return "off";
42
- }
43
27
  function stringField(payload, key) {
44
28
  const value = payload[key];
45
29
  if (typeof value !== "string") {
@@ -55,47 +39,52 @@ function numberField(payload, key) {
55
39
  }
56
40
  return Math.trunc(value);
57
41
  }
58
- function computeWakeId(opts) {
59
- const source = stringField(opts.payload, "wake_source") ?? "unknown";
60
- const programId = stringField(opts.payload, "program_id") ?? "unknown";
61
- const sourceTsMs = numberField(opts.payload, "source_ts_ms");
62
- const target = Object.hasOwn(opts.payload, "target") ? opts.payload.target : null;
63
- let targetFingerprint = "null";
42
+ function stablePayloadSnapshot(payload) {
64
43
  try {
65
- targetFingerprint = JSON.stringify(target) ?? "null";
44
+ return JSON.stringify(payload) ?? "{}";
66
45
  }
67
46
  catch {
68
- targetFingerprint = "[unserializable]";
47
+ return "[unserializable]";
69
48
  }
49
+ }
50
+ function computeWakeId(opts) {
51
+ const source = stringField(opts.payload, "wake_source") ?? "unknown";
52
+ const programId = stringField(opts.payload, "program_id") ?? "unknown";
53
+ const sourceTsMs = numberField(opts.payload, "source_ts_ms");
54
+ const payloadSnapshot = stablePayloadSnapshot(opts.payload);
70
55
  const hasher = new Bun.CryptoHasher("sha256");
71
- hasher.update(`${source}|${programId}|${sourceTsMs ?? "na"}|${opts.dedupeKey}|${targetFingerprint}`);
56
+ hasher.update(`${source}|${programId}|${sourceTsMs ?? "na"}|${opts.dedupeKey}|${payloadSnapshot}`);
72
57
  return hasher.digest("hex").slice(0, 16);
73
58
  }
59
+ function extractWakeTurnReply(turnResult) {
60
+ if (turnResult.kind === "operator_response") {
61
+ const message = turnResult.message.trim();
62
+ return message.length > 0 ? message : null;
63
+ }
64
+ const presented = presentPipelineResultMessage(turnResult);
65
+ const payload = presented.message.payload;
66
+ const payloadMessage = typeof payload.message === "string" ? payload.message.trim() : "";
67
+ if (payloadMessage.length > 0) {
68
+ return payloadMessage;
69
+ }
70
+ const compact = presented.compact.trim();
71
+ return compact.length > 0 ? compact : null;
72
+ }
74
73
  function buildWakeTurnCommandText(opts) {
75
74
  const wakeSource = stringField(opts.payload, "wake_source") ?? "unknown";
76
75
  const programId = stringField(opts.payload, "program_id") ?? "unknown";
77
- const wakeMode = stringField(opts.payload, "wake_mode") ?? "immediate";
78
- const targetKind = stringField(opts.payload, "target_kind") ?? "unknown";
79
76
  const reason = stringField(opts.payload, "reason") ?? "scheduled";
80
- let target = "null";
81
- try {
82
- target = JSON.stringify(Object.hasOwn(opts.payload, "target") ? opts.payload.target : null) ?? "null";
83
- }
84
- catch {
85
- target = "[unserializable]";
86
- }
77
+ const payloadSnapshot = stablePayloadSnapshot(opts.payload);
87
78
  return [
88
79
  "Autonomous wake turn triggered by heartbeat/cron scheduler.",
89
80
  `wake_id=${opts.wakeId}`,
90
81
  `wake_source=${wakeSource}`,
91
82
  `program_id=${programId}`,
92
- `wake_mode=${wakeMode}`,
93
- `target_kind=${targetKind}`,
94
83
  `reason=${reason}`,
95
- `message=${opts.message}`,
96
- `target=${target}`,
84
+ `trigger_message=${opts.message}`,
85
+ `payload=${payloadSnapshot}`,
97
86
  "",
98
- "If an action is needed, produce exactly one `/mu ...` command. If no action is needed, provide a short operator response.",
87
+ "If action is needed, produce exactly one `/mu ...` command. If no action is needed, return a short operator response that can be broadcast verbatim.",
99
88
  ].join("\n");
100
89
  }
101
90
  export function createContext(repoRoot) {
@@ -104,9 +93,7 @@ export function createContext(repoRoot) {
104
93
  const eventLog = new EventLog(new JsonlEventSink(eventsStore), {
105
94
  runIdProvider: currentRunId,
106
95
  });
107
- const issueStore = new IssueStore(new FsJsonlStore(paths.issuesPath), { events: eventLog });
108
- const forumStore = new ForumStore(new FsJsonlStore(paths.forumPath), { events: eventLog });
109
- return { repoRoot, issueStore, forumStore, eventLog, eventsStore };
96
+ return { repoRoot, eventLog, eventsStore };
110
97
  }
111
98
  function createServer(options = {}) {
112
99
  const repoRoot = options.repoRoot || process.cwd();
@@ -134,7 +121,6 @@ function createServer(options = {}) {
134
121
  },
135
122
  });
136
123
  const operatorWakeCoalesceMs = toNonNegativeInt(options.operatorWakeCoalesceMs, DEFAULT_OPERATOR_WAKE_COALESCE_MS);
137
- const autoRunHeartbeatEveryMs = Math.max(1_000, toNonNegativeInt(options.autoRunHeartbeatEveryMs, DEFAULT_AUTO_RUN_HEARTBEAT_EVERY_MS));
138
124
  const operatorWakeLastByKey = new Map();
139
125
  const sessionLifecycle = options.sessionLifecycle ?? createProcessSessionLifecycle({ repoRoot });
140
126
  const emitWakeDeliveryEvent = async (payload) => {
@@ -146,61 +132,28 @@ function createServer(options = {}) {
146
132
  const emitOperatorWake = async (opts) => {
147
133
  const dedupeKey = opts.dedupeKey.trim();
148
134
  if (!dedupeKey) {
149
- return false;
135
+ return { status: "failed", reason: "missing_dedupe_key" };
150
136
  }
151
137
  const nowMs = Date.now();
152
138
  const coalesceMs = Math.max(0, Math.trunc(opts.coalesceMs ?? operatorWakeCoalesceMs));
153
139
  const previous = operatorWakeLastByKey.get(dedupeKey);
154
140
  if (typeof previous === "number" && nowMs - previous < coalesceMs) {
155
- return false;
141
+ return { status: "coalesced", reason: "coalesced_window" };
156
142
  }
157
143
  operatorWakeLastByKey.set(dedupeKey, nowMs);
158
144
  const wakeId = computeWakeId({ dedupeKey, payload: opts.payload });
159
- const selectedWakeMode = stringField(opts.payload, "wake_mode");
160
145
  const wakeSource = stringField(opts.payload, "wake_source");
161
146
  const programId = stringField(opts.payload, "program_id");
162
147
  const sourceTsMs = numberField(opts.payload, "source_ts_ms");
163
- let wakeTurnMode = normalizeWakeTurnMode(fallbackConfig.control_plane.operator.wake_turn_mode);
164
- let configReadError = null;
165
- try {
166
- const config = await loadConfigFromDisk();
167
- wakeTurnMode = normalizeWakeTurnMode(config.control_plane.operator.wake_turn_mode);
168
- }
169
- catch (err) {
170
- configReadError = describeError(err);
171
- }
172
148
  let decision;
173
- if (wakeTurnMode === "off") {
174
- decision = {
175
- outcome: "skipped",
176
- reason: "feature_disabled",
177
- wakeTurnMode,
178
- selectedWakeMode,
179
- turnRequestId: null,
180
- turnResultKind: null,
181
- error: configReadError,
182
- };
183
- }
184
- else if (wakeTurnMode === "shadow") {
185
- decision = {
186
- outcome: "skipped",
187
- reason: "shadow_mode",
188
- wakeTurnMode,
189
- selectedWakeMode,
190
- turnRequestId: null,
191
- turnResultKind: null,
192
- error: configReadError,
193
- };
194
- }
195
- else if (typeof controlPlaneProxy.submitTerminalCommand !== "function") {
149
+ if (typeof controlPlaneProxy.submitTerminalCommand !== "function") {
196
150
  decision = {
197
151
  outcome: "fallback",
198
152
  reason: "control_plane_unavailable",
199
- wakeTurnMode,
200
- selectedWakeMode,
201
153
  turnRequestId: null,
202
154
  turnResultKind: null,
203
- error: configReadError,
155
+ turnReply: null,
156
+ error: null,
204
157
  };
205
158
  }
206
159
  else {
@@ -219,23 +172,34 @@ function createServer(options = {}) {
219
172
  decision = {
220
173
  outcome: "fallback",
221
174
  reason: `turn_result_${turnResult.kind}`,
222
- wakeTurnMode,
223
- selectedWakeMode,
224
175
  turnRequestId,
225
176
  turnResultKind: turnResult.kind,
226
- error: configReadError,
177
+ turnReply: null,
178
+ error: null,
227
179
  };
228
180
  }
229
181
  else {
230
- decision = {
231
- outcome: "triggered",
232
- reason: "turn_invoked",
233
- wakeTurnMode,
234
- selectedWakeMode,
235
- turnRequestId,
236
- turnResultKind: turnResult.kind,
237
- error: configReadError,
238
- };
182
+ const turnReply = extractWakeTurnReply(turnResult);
183
+ if (!turnReply) {
184
+ decision = {
185
+ outcome: "fallback",
186
+ reason: "turn_reply_empty",
187
+ turnRequestId,
188
+ turnResultKind: turnResult.kind,
189
+ turnReply: null,
190
+ error: null,
191
+ };
192
+ }
193
+ else {
194
+ decision = {
195
+ outcome: "triggered",
196
+ reason: "turn_invoked",
197
+ turnRequestId,
198
+ turnResultKind: turnResult.kind,
199
+ turnReply,
200
+ error: null,
201
+ };
202
+ }
239
203
  }
240
204
  }
241
205
  catch (err) {
@@ -243,10 +207,9 @@ function createServer(options = {}) {
243
207
  decision = {
244
208
  outcome: "fallback",
245
209
  reason: error === "control_plane_unavailable" ? "control_plane_unavailable" : "turn_execution_failed",
246
- wakeTurnMode,
247
- selectedWakeMode,
248
210
  turnRequestId,
249
211
  turnResultKind: null,
212
+ turnReply: null,
250
213
  error,
251
214
  };
252
215
  }
@@ -259,22 +222,27 @@ function createServer(options = {}) {
259
222
  wake_source: wakeSource,
260
223
  program_id: programId,
261
224
  source_ts_ms: sourceTsMs,
262
- selected_wake_mode: selectedWakeMode,
263
- wake_turn_mode: decision.wakeTurnMode,
264
- wake_turn_feature_enabled: decision.wakeTurnMode === "active",
265
- outcome: decision.outcome,
266
- reason: decision.reason,
225
+ wake_turn_outcome: decision.outcome,
226
+ wake_turn_reason: decision.reason,
267
227
  turn_request_id: decision.turnRequestId,
268
228
  turn_result_kind: decision.turnResultKind,
269
- error: decision.error,
229
+ turn_reply_present: decision.turnReply != null,
230
+ wake_turn_error: decision.error,
270
231
  },
271
232
  });
272
233
  let notifyResult = emptyNotifyOperatorsResult();
273
234
  let notifyError = null;
274
- if (typeof controlPlaneProxy.notifyOperators === "function") {
235
+ let deliverySkippedReason = null;
236
+ if (!decision.turnReply) {
237
+ deliverySkippedReason = "no_turn_reply";
238
+ }
239
+ else if (typeof controlPlaneProxy.notifyOperators !== "function") {
240
+ deliverySkippedReason = "notify_operators_unavailable";
241
+ }
242
+ else {
275
243
  try {
276
244
  notifyResult = await controlPlaneProxy.notifyOperators({
277
- message: opts.message,
245
+ message: decision.turnReply,
278
246
  dedupeKey,
279
247
  wake: {
280
248
  wakeId,
@@ -286,6 +254,7 @@ function createServer(options = {}) {
286
254
  wake_delivery_reason: "heartbeat_cron_wake",
287
255
  wake_turn_outcome: decision.outcome,
288
256
  wake_turn_reason: decision.reason,
257
+ wake_turn_result_kind: decision.turnResultKind,
289
258
  },
290
259
  });
291
260
  }
@@ -312,19 +281,18 @@ function createServer(options = {}) {
312
281
  await context.eventLog.emit("operator.wake", {
313
282
  source: "mu-server.operator-wake",
314
283
  payload: {
315
- message: opts.message,
284
+ trigger_message: opts.message,
285
+ broadcast_message: decision.turnReply,
286
+ broadcast_message_present: decision.turnReply != null,
316
287
  dedupe_key: dedupeKey,
317
288
  coalesce_ms: coalesceMs,
318
289
  ...opts.payload,
319
290
  wake_id: wakeId,
320
- decision_outcome: decision.outcome,
321
- decision_reason: decision.reason,
322
- wake_turn_mode: decision.wakeTurnMode,
323
- selected_wake_mode: decision.selectedWakeMode,
324
- wake_turn_feature_enabled: decision.wakeTurnMode === "active",
291
+ wake_turn_outcome: decision.outcome,
292
+ wake_turn_reason: decision.reason,
325
293
  turn_request_id: decision.turnRequestId,
326
294
  turn_result_kind: decision.turnResultKind,
327
- decision_error: decision.error,
295
+ wake_turn_error: decision.error,
328
296
  delivery: {
329
297
  queued: notifyResult.queued,
330
298
  duplicate: notifyResult.duplicate,
@@ -336,10 +304,23 @@ function createServer(options = {}) {
336
304
  skipped: notifyResult.skipped,
337
305
  total: notifyResult.decisions.length,
338
306
  },
307
+ delivery_skipped_reason: deliverySkippedReason,
339
308
  delivery_error: notifyError,
340
309
  },
341
310
  });
342
- return true;
311
+ if (decision.outcome !== "triggered") {
312
+ return { status: "failed", reason: decision.reason };
313
+ }
314
+ if (!decision.turnReply) {
315
+ return { status: "failed", reason: "no_turn_reply" };
316
+ }
317
+ if (notifyError) {
318
+ return { status: "failed", reason: "notify_failed" };
319
+ }
320
+ if (deliverySkippedReason === "notify_operators_unavailable") {
321
+ return { status: "failed", reason: deliverySkippedReason };
322
+ }
323
+ return { status: "dispatched", reason: "operator_reply_broadcast" };
343
324
  };
344
325
  const generationTelemetry = options.generationTelemetry ?? new GenerationTelemetryRecorder();
345
326
  const loadConfigFromDisk = async () => {
@@ -358,7 +339,6 @@ function createServer(options = {}) {
358
339
  return await bootstrapControlPlane({
359
340
  repoRoot,
360
341
  config,
361
- heartbeatScheduler,
362
342
  generation,
363
343
  telemetry: generationTelemetry,
364
344
  sessionLifecycle,
@@ -461,13 +441,6 @@ function createServer(options = {}) {
461
441
  }
462
442
  return await handle.interruptRun(opts);
463
443
  },
464
- async heartbeatRun(opts) {
465
- const handle = reloadManager.getControlPlaneCurrent();
466
- if (!handle?.heartbeatRun) {
467
- return { ok: false, reason: "not_found", run: null };
468
- }
469
- return await handle.heartbeatRun(opts);
470
- },
471
444
  async traceRun(opts) {
472
445
  const handle = reloadManager.getControlPlaneCurrent();
473
446
  if (!handle?.traceRun)
@@ -488,13 +461,10 @@ function createServer(options = {}) {
488
461
  await handle?.stop();
489
462
  },
490
463
  };
491
- const { heartbeatPrograms, cronPrograms, registerAutoRunHeartbeatProgram, disableAutoRunHeartbeatProgram, } = createServerProgramOrchestration({
464
+ const { heartbeatPrograms, cronPrograms } = createServerProgramOrchestration({
492
465
  repoRoot,
493
466
  heartbeatScheduler,
494
- controlPlaneProxy,
495
- activitySupervisor,
496
467
  eventLog: context.eventLog,
497
- autoRunHeartbeatEveryMs,
498
468
  emitOperatorWake,
499
469
  });
500
470
  const handleRequest = createServerRequestHandler({
@@ -507,8 +477,6 @@ function createServer(options = {}) {
507
477
  writeConfig,
508
478
  reloadControlPlane,
509
479
  getControlPlaneStatus: reloadManager.getControlPlaneStatus,
510
- registerAutoRunHeartbeatProgram,
511
- disableAutoRunHeartbeatProgram,
512
480
  describeError,
513
481
  initiateShutdown: options.initiateShutdown,
514
482
  });
@@ -1,38 +1,23 @@
1
1
  import type { EventLog } from "@femtomc/mu-core/node";
2
- import type { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
3
- import type { ControlPlaneHandle } from "./control_plane_contract.js";
4
2
  import { CronProgramRegistry } from "./cron_programs.js";
5
3
  import { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
6
4
  import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
7
- export type AutoHeartbeatRunSnapshot = {
8
- job_id: string;
9
- root_issue_id: string | null;
10
- status: string;
11
- source: "command" | "api";
12
- mode: string;
13
- };
14
5
  type OperatorWakeEmitter = (opts: {
15
6
  dedupeKey: string;
16
7
  message: string;
17
8
  payload: Record<string, unknown>;
18
9
  coalesceMs?: number;
19
- }) => Promise<boolean>;
10
+ }) => Promise<{
11
+ status: "dispatched" | "coalesced" | "failed";
12
+ reason: string;
13
+ }>;
20
14
  export declare function createServerProgramOrchestration(opts: {
21
15
  repoRoot: string;
22
16
  heartbeatScheduler: ActivityHeartbeatScheduler;
23
- controlPlaneProxy: ControlPlaneHandle;
24
- activitySupervisor: ControlPlaneActivitySupervisor;
25
17
  eventLog: EventLog;
26
- autoRunHeartbeatEveryMs: number;
27
18
  emitOperatorWake: OperatorWakeEmitter;
28
19
  }): {
29
20
  heartbeatPrograms: HeartbeatProgramRegistry;
30
21
  cronPrograms: CronProgramRegistry;
31
- registerAutoRunHeartbeatProgram: (run: AutoHeartbeatRunSnapshot) => Promise<void>;
32
- disableAutoRunHeartbeatProgram: (opts: {
33
- jobId: string;
34
- status: string;
35
- reason: string;
36
- }) => Promise<void>;
37
22
  };
38
23
  export {};