@femtomc/mu-server 26.2.41 → 26.2.42

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.
@@ -1,6 +1,7 @@
1
1
  import { ApprovedCommandBroker, CommandContextResolver, MessagingOperatorRuntime, PiMessagingOperatorBackend, serveExtensionPaths, } from "@femtomc/mu-agent";
2
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, } from "@femtomc/mu-control-plane";
2
+ import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, correlationFromCommandRecord, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, } from "@femtomc/mu-control-plane";
3
3
  import { DEFAULT_MU_CONFIG } from "./config.js";
4
+ import { ControlPlaneRunSupervisor, } from "./run_supervisor.js";
4
5
  export function detectAdapters(config) {
5
6
  const adapters = [];
6
7
  const slackSecret = config.adapters.slack.signing_secret;
@@ -22,6 +23,57 @@ export function detectAdapters(config) {
22
23
  }
23
24
  return adapters;
24
25
  }
26
+ function sha256Hex(input) {
27
+ const hasher = new Bun.CryptoHasher("sha256");
28
+ hasher.update(input);
29
+ return hasher.digest("hex");
30
+ }
31
+ function outboxKindForRunEvent(kind) {
32
+ switch (kind) {
33
+ case "run_completed":
34
+ return "result";
35
+ case "run_failed":
36
+ return "error";
37
+ default:
38
+ return "lifecycle";
39
+ }
40
+ }
41
+ async function enqueueRunEventOutbox(opts) {
42
+ const command = opts.event.command;
43
+ if (!command) {
44
+ return null;
45
+ }
46
+ const baseCorrelation = correlationFromCommandRecord(command);
47
+ const correlation = {
48
+ ...baseCorrelation,
49
+ run_root_id: opts.event.run.root_issue_id ?? baseCorrelation.run_root_id,
50
+ };
51
+ const envelope = {
52
+ v: 1,
53
+ ts_ms: opts.nowMs,
54
+ channel: command.channel,
55
+ channel_tenant_id: command.channel_tenant_id,
56
+ channel_conversation_id: command.channel_conversation_id,
57
+ request_id: command.request_id,
58
+ response_id: `resp-${sha256Hex(`run-event:${opts.event.run.job_id}:${opts.event.seq}:${opts.nowMs}`).slice(0, 20)}`,
59
+ kind: outboxKindForRunEvent(opts.event.kind),
60
+ body: opts.event.message,
61
+ correlation,
62
+ metadata: {
63
+ async_run: true,
64
+ run_event_kind: opts.event.kind,
65
+ run_event_seq: opts.event.seq,
66
+ run: opts.event.run,
67
+ },
68
+ };
69
+ const decision = await opts.outbox.enqueue({
70
+ dedupeKey: `run-event:${opts.event.run.job_id}:${opts.event.seq}`,
71
+ envelope,
72
+ nowMs: opts.nowMs,
73
+ maxAttempts: 6,
74
+ });
75
+ return decision.record;
76
+ }
25
77
  /**
26
78
  * Telegram supports a markdown dialect that uses single markers for emphasis.
27
79
  * Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
@@ -51,8 +103,20 @@ export function renderTelegramMarkdown(text) {
51
103
  }
52
104
  return out.join("\n");
53
105
  }
106
+ const TELEGRAM_MATH_PATTERNS = [
107
+ /\$\$[\s\S]+?\$\$/m,
108
+ /(^|[^\\])\$[^$\n]+\$/m,
109
+ /\\\([\s\S]+?\\\)/m,
110
+ /\\\[[\s\S]+?\\\]/m,
111
+ ];
112
+ export function containsTelegramMathNotation(text) {
113
+ if (text.trim().length === 0) {
114
+ return false;
115
+ }
116
+ return TELEGRAM_MATH_PATTERNS.some((pattern) => pattern.test(text));
117
+ }
54
118
  export function buildTelegramSendMessagePayload(opts) {
55
- if (!opts.richFormatting) {
119
+ if (!opts.richFormatting || containsTelegramMathNotation(opts.text)) {
56
120
  return {
57
121
  chat_id: opts.chatId,
58
122
  text: opts.text,
@@ -72,6 +136,7 @@ async function postTelegramMessage(botToken, payload) {
72
136
  body: JSON.stringify(payload),
73
137
  });
74
138
  }
139
+ const OUTBOX_DRAIN_INTERVAL_MS = 500;
75
140
  function buildMessagingOperatorRuntime(opts) {
76
141
  if (!opts.config.operator.enabled) {
77
142
  return null;
@@ -100,6 +165,7 @@ export async function bootstrapControlPlane(opts) {
100
165
  const paths = getControlPlanePaths(opts.repoRoot);
101
166
  const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
102
167
  let pipeline = null;
168
+ let runSupervisor = null;
103
169
  let drainInterval = null;
104
170
  try {
105
171
  await runtime.start();
@@ -110,10 +176,122 @@ export async function bootstrapControlPlane(opts) {
110
176
  config: controlPlaneConfig,
111
177
  backend: opts.operatorBackend,
112
178
  });
113
- pipeline = new ControlPlaneCommandPipeline({ runtime, operator });
114
- await pipeline.start();
115
179
  const outbox = new ControlPlaneOutbox(paths.outboxPath);
116
180
  await outbox.load();
181
+ let scheduleOutboxDrainRef = null;
182
+ runSupervisor = new ControlPlaneRunSupervisor({
183
+ repoRoot: opts.repoRoot,
184
+ heartbeatScheduler: opts.heartbeatScheduler,
185
+ onEvent: async (event) => {
186
+ const outboxRecord = await enqueueRunEventOutbox({
187
+ outbox,
188
+ event,
189
+ nowMs: Math.trunc(Date.now()),
190
+ });
191
+ if (outboxRecord) {
192
+ scheduleOutboxDrainRef?.();
193
+ }
194
+ },
195
+ });
196
+ pipeline = new ControlPlaneCommandPipeline({
197
+ runtime,
198
+ operator,
199
+ mutationExecutor: async (record) => {
200
+ if (record.target_type === "run start" || record.target_type === "run resume") {
201
+ try {
202
+ const launched = await runSupervisor?.startFromCommand(record);
203
+ if (!launched) {
204
+ return null;
205
+ }
206
+ return {
207
+ terminalState: "completed",
208
+ result: {
209
+ ok: true,
210
+ async_run: true,
211
+ run_job_id: launched.job_id,
212
+ run_root_id: launched.root_issue_id,
213
+ run_status: launched.status,
214
+ run_mode: launched.mode,
215
+ run_source: launched.source,
216
+ },
217
+ trace: {
218
+ cliCommandKind: launched.mode,
219
+ runRootId: launched.root_issue_id,
220
+ },
221
+ mutatingEvents: [
222
+ {
223
+ eventType: "run.supervisor.start",
224
+ payload: {
225
+ run_job_id: launched.job_id,
226
+ run_mode: launched.mode,
227
+ run_root_id: launched.root_issue_id,
228
+ run_source: launched.source,
229
+ },
230
+ },
231
+ ],
232
+ };
233
+ }
234
+ catch (err) {
235
+ return {
236
+ terminalState: "failed",
237
+ errorCode: err instanceof Error && err.message ? err.message : "run_supervisor_start_failed",
238
+ trace: {
239
+ cliCommandKind: record.target_type.replaceAll(" ", "_"),
240
+ runRootId: record.target_id,
241
+ },
242
+ };
243
+ }
244
+ }
245
+ if (record.target_type === "run interrupt") {
246
+ const result = runSupervisor?.interrupt({
247
+ rootIssueId: record.target_id,
248
+ }) ?? { ok: false, reason: "not_found", run: null };
249
+ if (!result.ok) {
250
+ return {
251
+ terminalState: "failed",
252
+ errorCode: result.reason ?? "run_interrupt_failed",
253
+ trace: {
254
+ cliCommandKind: "run_interrupt",
255
+ runRootId: result.run?.root_issue_id ?? record.target_id,
256
+ },
257
+ mutatingEvents: [
258
+ {
259
+ eventType: "run.supervisor.interrupt.failed",
260
+ payload: {
261
+ reason: result.reason,
262
+ target: record.target_id,
263
+ },
264
+ },
265
+ ],
266
+ };
267
+ }
268
+ return {
269
+ terminalState: "completed",
270
+ result: {
271
+ ok: true,
272
+ async_run: true,
273
+ interrupted: true,
274
+ run: result.run,
275
+ },
276
+ trace: {
277
+ cliCommandKind: "run_interrupt",
278
+ runRootId: result.run?.root_issue_id ?? record.target_id,
279
+ },
280
+ mutatingEvents: [
281
+ {
282
+ eventType: "run.supervisor.interrupt",
283
+ payload: {
284
+ target: record.target_id,
285
+ run: result.run,
286
+ },
287
+ },
288
+ ],
289
+ };
290
+ }
291
+ return null;
292
+ },
293
+ });
294
+ await pipeline.start();
117
295
  let telegramBotToken = null;
118
296
  const adapterMap = new Map();
119
297
  for (const d of detected) {
@@ -199,14 +377,37 @@ export async function bootstrapControlPlane(opts) {
199
377
  return undefined;
200
378
  };
201
379
  const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
202
- drainInterval = setInterval(async () => {
380
+ let drainingOutbox = false;
381
+ let drainRequested = false;
382
+ const drainOutboxNow = async () => {
383
+ if (drainingOutbox) {
384
+ drainRequested = true;
385
+ return;
386
+ }
387
+ drainingOutbox = true;
203
388
  try {
204
- await dispatcher.drainDue();
389
+ do {
390
+ drainRequested = false;
391
+ await dispatcher.drainDue();
392
+ } while (drainRequested);
205
393
  }
206
394
  catch {
207
- // Swallow errors — the dispatcher already handles retries internally.
395
+ // Swallow errors — the dispatcher handles retries internally.
208
396
  }
209
- }, 2_000);
397
+ finally {
398
+ drainingOutbox = false;
399
+ }
400
+ };
401
+ const scheduleOutboxDrain = () => {
402
+ queueMicrotask(() => {
403
+ void drainOutboxNow();
404
+ });
405
+ };
406
+ scheduleOutboxDrainRef = scheduleOutboxDrain;
407
+ drainInterval = setInterval(() => {
408
+ scheduleOutboxDrain();
409
+ }, OUTBOX_DRAIN_INTERVAL_MS);
410
+ scheduleOutboxDrain();
210
411
  return {
211
412
  activeAdapters: [...adapterMap.values()].map((v) => v.info),
212
413
  async handleWebhook(path, req) {
@@ -214,13 +415,57 @@ export async function bootstrapControlPlane(opts) {
214
415
  if (!entry)
215
416
  return null;
216
417
  const result = await entry.adapter.ingest(req);
418
+ if (result.outboxRecord) {
419
+ scheduleOutboxDrain();
420
+ }
217
421
  return result.response;
218
422
  },
423
+ async listRuns(opts = {}) {
424
+ return (runSupervisor?.list({
425
+ status: opts.status,
426
+ limit: opts.limit,
427
+ }) ?? []);
428
+ },
429
+ async getRun(idOrRoot) {
430
+ return runSupervisor?.get(idOrRoot) ?? null;
431
+ },
432
+ async startRun(startOpts) {
433
+ const run = await runSupervisor?.launchStart({
434
+ prompt: startOpts.prompt,
435
+ maxSteps: startOpts.maxSteps,
436
+ source: "api",
437
+ });
438
+ if (!run) {
439
+ throw new Error("run_supervisor_unavailable");
440
+ }
441
+ return run;
442
+ },
443
+ async resumeRun(resumeOpts) {
444
+ const run = await runSupervisor?.launchResume({
445
+ rootIssueId: resumeOpts.rootIssueId,
446
+ maxSteps: resumeOpts.maxSteps,
447
+ source: "api",
448
+ });
449
+ if (!run) {
450
+ throw new Error("run_supervisor_unavailable");
451
+ }
452
+ return run;
453
+ },
454
+ async interruptRun(interruptOpts) {
455
+ return runSupervisor?.interrupt(interruptOpts) ?? { ok: false, reason: "not_found", run: null };
456
+ },
457
+ async heartbeatRun(heartbeatOpts) {
458
+ return runSupervisor?.heartbeat(heartbeatOpts) ?? { ok: false, reason: "not_found", run: null };
459
+ },
460
+ async traceRun(traceOpts) {
461
+ return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
462
+ },
219
463
  async stop() {
220
464
  if (drainInterval) {
221
465
  clearInterval(drainInterval);
222
466
  drainInterval = null;
223
467
  }
468
+ await runSupervisor?.stop();
224
469
  try {
225
470
  await pipeline?.stop();
226
471
  }
@@ -235,6 +480,12 @@ export async function bootstrapControlPlane(opts) {
235
480
  clearInterval(drainInterval);
236
481
  drainInterval = null;
237
482
  }
483
+ try {
484
+ await runSupervisor?.stop();
485
+ }
486
+ catch {
487
+ // Best effort cleanup.
488
+ }
238
489
  try {
239
490
  await pipeline?.stop();
240
491
  }
@@ -0,0 +1,93 @@
1
+ import type { JsonlStore } from "@femtomc/mu-core";
2
+ import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
3
+ export type HeartbeatProgramTarget = {
4
+ kind: "run";
5
+ job_id: string | null;
6
+ root_issue_id: string | null;
7
+ } | {
8
+ kind: "activity";
9
+ activity_id: string;
10
+ };
11
+ export type HeartbeatProgramSnapshot = {
12
+ v: 1;
13
+ program_id: string;
14
+ title: string;
15
+ enabled: boolean;
16
+ every_ms: number;
17
+ reason: string;
18
+ target: HeartbeatProgramTarget;
19
+ metadata: Record<string, unknown>;
20
+ created_at_ms: number;
21
+ updated_at_ms: number;
22
+ last_triggered_at_ms: number | null;
23
+ last_result: "ok" | "not_found" | "not_running" | "failed" | null;
24
+ last_error: string | null;
25
+ };
26
+ export type HeartbeatProgramOperationResult = {
27
+ ok: boolean;
28
+ reason: "not_found" | "missing_target" | "invalid_target" | "not_running" | "failed" | null;
29
+ program: HeartbeatProgramSnapshot | null;
30
+ };
31
+ export type HeartbeatProgramTickEvent = {
32
+ ts_ms: number;
33
+ program_id: string;
34
+ message: string;
35
+ status: "ok" | "not_found" | "not_running" | "failed";
36
+ reason: string | null;
37
+ program: HeartbeatProgramSnapshot;
38
+ };
39
+ export type HeartbeatProgramRegistryOpts = {
40
+ repoRoot: string;
41
+ heartbeatScheduler: ActivityHeartbeatScheduler;
42
+ nowMs?: () => number;
43
+ store?: JsonlStore<HeartbeatProgramSnapshot>;
44
+ runHeartbeat: (opts: {
45
+ jobId?: string | null;
46
+ rootIssueId?: string | null;
47
+ reason?: string | null;
48
+ }) => Promise<{
49
+ ok: boolean;
50
+ reason: "not_found" | "not_running" | "missing_target" | null;
51
+ }>;
52
+ activityHeartbeat: (opts: {
53
+ activityId?: string | null;
54
+ reason?: string | null;
55
+ }) => Promise<{
56
+ ok: boolean;
57
+ reason: "not_found" | "not_running" | "missing_target" | null;
58
+ }>;
59
+ onTickEvent?: (event: HeartbeatProgramTickEvent) => void | Promise<void>;
60
+ };
61
+ export declare class HeartbeatProgramRegistry {
62
+ #private;
63
+ constructor(opts: HeartbeatProgramRegistryOpts);
64
+ list(opts?: {
65
+ enabled?: boolean;
66
+ targetKind?: "run" | "activity";
67
+ limit?: number;
68
+ }): Promise<HeartbeatProgramSnapshot[]>;
69
+ get(programId: string): Promise<HeartbeatProgramSnapshot | null>;
70
+ create(opts: {
71
+ title: string;
72
+ target: HeartbeatProgramTarget;
73
+ everyMs?: number;
74
+ reason?: string;
75
+ enabled?: boolean;
76
+ metadata?: Record<string, unknown>;
77
+ }): Promise<HeartbeatProgramSnapshot>;
78
+ update(opts: {
79
+ programId: string;
80
+ title?: string;
81
+ everyMs?: number;
82
+ reason?: string;
83
+ enabled?: boolean;
84
+ target?: HeartbeatProgramTarget;
85
+ metadata?: Record<string, unknown>;
86
+ }): Promise<HeartbeatProgramOperationResult>;
87
+ remove(programId: string): Promise<HeartbeatProgramOperationResult>;
88
+ trigger(opts: {
89
+ programId?: string | null;
90
+ reason?: string | null;
91
+ }): Promise<HeartbeatProgramOperationResult>;
92
+ stop(): void;
93
+ }