@femtomc/mu-server 26.2.40 → 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 +47 -0
- package/dist/control_plane.js +325 -16
- 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,120 @@ 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
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
79
|
+
* Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
|
|
80
|
+
* while preserving fenced code blocks verbatim.
|
|
81
|
+
*/
|
|
82
|
+
export function renderTelegramMarkdown(text) {
|
|
83
|
+
const normalized = text.replaceAll("\r\n", "\n");
|
|
84
|
+
const lines = normalized.split("\n");
|
|
85
|
+
const out = [];
|
|
86
|
+
let inFence = false;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const trimmed = line.trimStart();
|
|
89
|
+
if (trimmed.startsWith("```")) {
|
|
90
|
+
inFence = !inFence;
|
|
91
|
+
out.push(line);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (inFence) {
|
|
95
|
+
out.push(line);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let next = line;
|
|
99
|
+
next = next.replace(/^#{1,6}\s+(.+)$/, "*$1*");
|
|
100
|
+
next = next.replace(/\*\*(.+?)\*\*/g, "*$1*");
|
|
101
|
+
next = next.replace(/__(.+?)__/g, "_$1_");
|
|
102
|
+
out.push(next);
|
|
103
|
+
}
|
|
104
|
+
return out.join("\n");
|
|
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
|
+
}
|
|
118
|
+
export function buildTelegramSendMessagePayload(opts) {
|
|
119
|
+
if (!opts.richFormatting || containsTelegramMathNotation(opts.text)) {
|
|
120
|
+
return {
|
|
121
|
+
chat_id: opts.chatId,
|
|
122
|
+
text: opts.text,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
chat_id: opts.chatId,
|
|
127
|
+
text: renderTelegramMarkdown(opts.text),
|
|
128
|
+
parse_mode: "Markdown",
|
|
129
|
+
disable_web_page_preview: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function postTelegramMessage(botToken, payload) {
|
|
133
|
+
return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: { "Content-Type": "application/json" },
|
|
136
|
+
body: JSON.stringify(payload),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const OUTBOX_DRAIN_INTERVAL_MS = 500;
|
|
25
140
|
function buildMessagingOperatorRuntime(opts) {
|
|
26
141
|
if (!opts.config.operator.enabled) {
|
|
27
142
|
return null;
|
|
@@ -50,6 +165,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
50
165
|
const paths = getControlPlanePaths(opts.repoRoot);
|
|
51
166
|
const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
|
|
52
167
|
let pipeline = null;
|
|
168
|
+
let runSupervisor = null;
|
|
53
169
|
let drainInterval = null;
|
|
54
170
|
try {
|
|
55
171
|
await runtime.start();
|
|
@@ -60,10 +176,122 @@ export async function bootstrapControlPlane(opts) {
|
|
|
60
176
|
config: controlPlaneConfig,
|
|
61
177
|
backend: opts.operatorBackend,
|
|
62
178
|
});
|
|
63
|
-
pipeline = new ControlPlaneCommandPipeline({ runtime, operator });
|
|
64
|
-
await pipeline.start();
|
|
65
179
|
const outbox = new ControlPlaneOutbox(paths.outboxPath);
|
|
66
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();
|
|
67
295
|
let telegramBotToken = null;
|
|
68
296
|
const adapterMap = new Map();
|
|
69
297
|
for (const d of detected) {
|
|
@@ -113,42 +341,73 @@ export async function bootstrapControlPlane(opts) {
|
|
|
113
341
|
if (!telegramBotToken) {
|
|
114
342
|
return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
|
|
115
343
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
chat_id: envelope.channel_conversation_id,
|
|
121
|
-
text: envelope.body,
|
|
122
|
-
}),
|
|
344
|
+
const richPayload = buildTelegramSendMessagePayload({
|
|
345
|
+
chatId: envelope.channel_conversation_id,
|
|
346
|
+
text: envelope.body,
|
|
347
|
+
richFormatting: true,
|
|
123
348
|
});
|
|
349
|
+
let res = await postTelegramMessage(telegramBotToken, richPayload);
|
|
350
|
+
// Fallback: if Telegram rejects markdown entities, retry as plain text.
|
|
351
|
+
if (!res.ok && res.status === 400 && richPayload.parse_mode) {
|
|
352
|
+
const plainPayload = buildTelegramSendMessagePayload({
|
|
353
|
+
chatId: envelope.channel_conversation_id,
|
|
354
|
+
text: envelope.body,
|
|
355
|
+
richFormatting: false,
|
|
356
|
+
});
|
|
357
|
+
res = await postTelegramMessage(telegramBotToken, plainPayload);
|
|
358
|
+
}
|
|
124
359
|
if (res.ok) {
|
|
125
360
|
return { kind: "delivered" };
|
|
126
361
|
}
|
|
362
|
+
const responseBody = await res.text().catch(() => "");
|
|
127
363
|
if (res.status === 429 || res.status >= 500) {
|
|
128
364
|
const retryAfter = res.headers.get("retry-after");
|
|
129
365
|
const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
|
|
130
366
|
return {
|
|
131
367
|
kind: "retry",
|
|
132
|
-
error: `telegram sendMessage ${res.status}: ${
|
|
368
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
133
369
|
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
134
370
|
};
|
|
135
371
|
}
|
|
136
372
|
return {
|
|
137
373
|
kind: "retry",
|
|
138
|
-
error: `telegram sendMessage ${res.status}: ${
|
|
374
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
139
375
|
};
|
|
140
376
|
}
|
|
141
377
|
return undefined;
|
|
142
378
|
};
|
|
143
379
|
const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
|
|
144
|
-
|
|
380
|
+
let drainingOutbox = false;
|
|
381
|
+
let drainRequested = false;
|
|
382
|
+
const drainOutboxNow = async () => {
|
|
383
|
+
if (drainingOutbox) {
|
|
384
|
+
drainRequested = true;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
drainingOutbox = true;
|
|
145
388
|
try {
|
|
146
|
-
|
|
389
|
+
do {
|
|
390
|
+
drainRequested = false;
|
|
391
|
+
await dispatcher.drainDue();
|
|
392
|
+
} while (drainRequested);
|
|
147
393
|
}
|
|
148
394
|
catch {
|
|
149
|
-
// Swallow errors — the dispatcher
|
|
395
|
+
// Swallow errors — the dispatcher handles retries internally.
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
drainingOutbox = false;
|
|
150
399
|
}
|
|
151
|
-
}
|
|
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();
|
|
152
411
|
return {
|
|
153
412
|
activeAdapters: [...adapterMap.values()].map((v) => v.info),
|
|
154
413
|
async handleWebhook(path, req) {
|
|
@@ -156,13 +415,57 @@ export async function bootstrapControlPlane(opts) {
|
|
|
156
415
|
if (!entry)
|
|
157
416
|
return null;
|
|
158
417
|
const result = await entry.adapter.ingest(req);
|
|
418
|
+
if (result.outboxRecord) {
|
|
419
|
+
scheduleOutboxDrain();
|
|
420
|
+
}
|
|
159
421
|
return result.response;
|
|
160
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
|
+
},
|
|
161
463
|
async stop() {
|
|
162
464
|
if (drainInterval) {
|
|
163
465
|
clearInterval(drainInterval);
|
|
164
466
|
drainInterval = null;
|
|
165
467
|
}
|
|
468
|
+
await runSupervisor?.stop();
|
|
166
469
|
try {
|
|
167
470
|
await pipeline?.stop();
|
|
168
471
|
}
|
|
@@ -177,6 +480,12 @@ export async function bootstrapControlPlane(opts) {
|
|
|
177
480
|
clearInterval(drainInterval);
|
|
178
481
|
drainInterval = null;
|
|
179
482
|
}
|
|
483
|
+
try {
|
|
484
|
+
await runSupervisor?.stop();
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
// Best effort cleanup.
|
|
488
|
+
}
|
|
180
489
|
try {
|
|
181
490
|
await pipeline?.stop();
|
|
182
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
|
+
}
|