@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.
- package/dist/activity_supervisor.d.ts +81 -0
- package/dist/activity_supervisor.js +306 -0
- package/dist/control_plane.d.ts +30 -0
- package/dist/control_plane.js +259 -8
- package/dist/heartbeat_programs.d.ts +93 -0
- package/dist/heartbeat_programs.js +415 -0
- package/dist/heartbeat_scheduler.d.ts +38 -0
- package/dist/heartbeat_scheduler.js +238 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/run_supervisor.d.ts +101 -0
- package/dist/run_supervisor.js +480 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +628 -3
- package/package.json +6 -6
package/dist/control_plane.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
389
|
+
do {
|
|
390
|
+
drainRequested = false;
|
|
391
|
+
await dispatcher.drainDue();
|
|
392
|
+
} while (drainRequested);
|
|
205
393
|
}
|
|
206
394
|
catch {
|
|
207
|
-
// Swallow errors — the dispatcher
|
|
395
|
+
// Swallow errors — the dispatcher handles retries internally.
|
|
208
396
|
}
|
|
209
|
-
|
|
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
|
+
}
|