@femtomc/mu-server 26.2.73 → 26.2.75
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/README.md +54 -66
- package/dist/api/control_plane.js +56 -0
- package/dist/api/cron.js +2 -23
- package/dist/api/heartbeats.js +1 -66
- package/dist/api/identities.js +3 -2
- package/dist/api/runs.js +0 -83
- package/dist/api/session_flash.d.ts +60 -0
- package/dist/api/session_flash.js +326 -0
- package/dist/api/session_turn.d.ts +38 -0
- package/dist/api/session_turn.js +423 -0
- package/dist/config.d.ts +9 -4
- package/dist/config.js +24 -24
- package/dist/control_plane.d.ts +2 -16
- package/dist/control_plane.js +57 -83
- package/dist/control_plane_adapter_registry.d.ts +19 -0
- package/dist/control_plane_adapter_registry.js +74 -0
- package/dist/control_plane_contract.d.ts +1 -7
- package/dist/control_plane_run_queue_coordinator.d.ts +1 -7
- package/dist/control_plane_run_queue_coordinator.js +1 -62
- package/dist/control_plane_telegram_generation.js +1 -0
- package/dist/control_plane_wake_delivery.js +1 -0
- package/dist/cron_programs.d.ts +21 -35
- package/dist/cron_programs.js +32 -113
- package/dist/cron_request.d.ts +0 -6
- package/dist/cron_request.js +0 -41
- package/dist/heartbeat_programs.d.ts +20 -35
- package/dist/heartbeat_programs.js +26 -122
- package/dist/index.d.ts +2 -2
- package/dist/outbound_delivery_router.d.ts +12 -0
- package/dist/outbound_delivery_router.js +29 -0
- package/dist/run_supervisor.d.ts +1 -16
- package/dist/run_supervisor.js +0 -70
- package/dist/server.d.ts +0 -5
- package/dist/server.js +95 -127
- package/dist/server_program_orchestration.d.ts +4 -19
- package/dist/server_program_orchestration.js +49 -200
- package/dist/server_routing.d.ts +0 -9
- package/dist/server_routing.js +19 -654
- package/dist/server_runtime.js +0 -1
- package/dist/server_types.d.ts +0 -2
- package/dist/server_types.js +0 -7
- package/package.json +6 -9
- package/dist/api/context.d.ts +0 -5
- package/dist/api/context.js +0 -1147
- package/dist/api/forum.d.ts +0 -2
- package/dist/api/forum.js +0 -75
- package/dist/api/issues.d.ts +0 -2
- package/dist/api/issues.js +0 -173
- package/public/assets/index-CxkevQNh.js +0 -100
- package/public/assets/index-D_8anM-D.css +0 -1
- package/public/index.html +0 -14
package/dist/control_plane.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime,
|
|
1
|
+
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
|
|
2
2
|
import { DEFAULT_MU_CONFIG } from "./config.js";
|
|
3
3
|
import { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, } from "./control_plane_contract.js";
|
|
4
4
|
import { ControlPlaneRunSupervisor, } from "./run_supervisor.js";
|
|
@@ -7,6 +7,8 @@ import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_
|
|
|
7
7
|
import { ControlPlaneRunQueueCoordinator } from "./control_plane_run_queue_coordinator.js";
|
|
8
8
|
import { enqueueRunEventOutbox } from "./control_plane_run_outbox.js";
|
|
9
9
|
import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
|
|
10
|
+
import { createStaticAdaptersFromDetected, detectAdapters, } from "./control_plane_adapter_registry.js";
|
|
11
|
+
import { OutboundDeliveryRouter } from "./outbound_delivery_router.js";
|
|
10
12
|
import { TelegramAdapterGenerationManager } from "./control_plane_telegram_generation.js";
|
|
11
13
|
function generationTags(generation, component) {
|
|
12
14
|
return {
|
|
@@ -35,27 +37,7 @@ function normalizeIssueId(value) {
|
|
|
35
37
|
}
|
|
36
38
|
return trimmed.toLowerCase();
|
|
37
39
|
}
|
|
38
|
-
export
|
|
39
|
-
const adapters = [];
|
|
40
|
-
const slackSecret = config.adapters.slack.signing_secret;
|
|
41
|
-
if (slackSecret) {
|
|
42
|
-
adapters.push({ name: "slack", signingSecret: slackSecret });
|
|
43
|
-
}
|
|
44
|
-
const discordSecret = config.adapters.discord.signing_secret;
|
|
45
|
-
if (discordSecret) {
|
|
46
|
-
adapters.push({ name: "discord", signingSecret: discordSecret });
|
|
47
|
-
}
|
|
48
|
-
const telegramSecret = config.adapters.telegram.webhook_secret;
|
|
49
|
-
if (telegramSecret) {
|
|
50
|
-
adapters.push({
|
|
51
|
-
name: "telegram",
|
|
52
|
-
webhookSecret: telegramSecret,
|
|
53
|
-
botToken: config.adapters.telegram.bot_token,
|
|
54
|
-
botUsername: config.adapters.telegram.bot_username,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
return adapters;
|
|
58
|
-
}
|
|
40
|
+
export { detectAdapters };
|
|
59
41
|
/**
|
|
60
42
|
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
61
43
|
* Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
|
|
@@ -145,7 +127,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
145
127
|
let runSupervisor = null;
|
|
146
128
|
let outboxDrainLoop = null;
|
|
147
129
|
let wakeDeliveryObserver = opts.wakeDeliveryObserver ?? null;
|
|
148
|
-
const outboundDeliveryChannels = new Set(
|
|
130
|
+
const outboundDeliveryChannels = new Set();
|
|
149
131
|
const adapterMap = new Map();
|
|
150
132
|
try {
|
|
151
133
|
await runtime.start();
|
|
@@ -170,8 +152,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
170
152
|
});
|
|
171
153
|
runSupervisor = new ControlPlaneRunSupervisor({
|
|
172
154
|
repoRoot: opts.repoRoot,
|
|
173
|
-
heartbeatScheduler: opts.heartbeatScheduler,
|
|
174
|
-
heartbeatIntervalMs: opts.runSupervisorHeartbeatIntervalMs,
|
|
175
155
|
spawnProcess: opts.runSupervisorSpawnProcess,
|
|
176
156
|
onEvent: async (event) => {
|
|
177
157
|
await runQueueCoordinator.onRunEvent(event);
|
|
@@ -384,21 +364,11 @@ export async function bootstrapControlPlane(opts) {
|
|
|
384
364
|
hooks: opts.telegramGenerationHooks,
|
|
385
365
|
});
|
|
386
366
|
await telegramManager.initialize();
|
|
387
|
-
for (const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
? new SlackControlPlaneAdapter({
|
|
393
|
-
pipeline,
|
|
394
|
-
outbox,
|
|
395
|
-
signingSecret: d.signingSecret,
|
|
396
|
-
})
|
|
397
|
-
: new DiscordControlPlaneAdapter({
|
|
398
|
-
pipeline,
|
|
399
|
-
outbox,
|
|
400
|
-
signingSecret: d.signingSecret,
|
|
401
|
-
});
|
|
367
|
+
for (const adapter of createStaticAdaptersFromDetected({
|
|
368
|
+
detected,
|
|
369
|
+
pipeline,
|
|
370
|
+
outbox,
|
|
371
|
+
})) {
|
|
402
372
|
const route = adapter.spec.route;
|
|
403
373
|
if (adapterMap.has(route)) {
|
|
404
374
|
throw new Error(`duplicate control-plane webhook route: ${route}`);
|
|
@@ -445,6 +415,52 @@ export async function bootstrapControlPlane(opts) {
|
|
|
445
415
|
},
|
|
446
416
|
isActive: () => telegramManager.hasActiveGeneration(),
|
|
447
417
|
});
|
|
418
|
+
const deliveryRouter = new OutboundDeliveryRouter([
|
|
419
|
+
{
|
|
420
|
+
channel: "telegram",
|
|
421
|
+
deliver: async (record) => {
|
|
422
|
+
const telegramBotToken = telegramManager.activeBotToken();
|
|
423
|
+
if (!telegramBotToken) {
|
|
424
|
+
return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
|
|
425
|
+
}
|
|
426
|
+
const richPayload = buildTelegramSendMessagePayload({
|
|
427
|
+
chatId: record.envelope.channel_conversation_id,
|
|
428
|
+
text: record.envelope.body,
|
|
429
|
+
richFormatting: true,
|
|
430
|
+
});
|
|
431
|
+
let res = await postTelegramMessage(telegramBotToken, richPayload);
|
|
432
|
+
// Fallback: if Telegram rejects markdown entities, retry as plain text.
|
|
433
|
+
if (!res.ok && res.status === 400 && richPayload.parse_mode) {
|
|
434
|
+
const plainPayload = buildTelegramSendMessagePayload({
|
|
435
|
+
chatId: record.envelope.channel_conversation_id,
|
|
436
|
+
text: record.envelope.body,
|
|
437
|
+
richFormatting: false,
|
|
438
|
+
});
|
|
439
|
+
res = await postTelegramMessage(telegramBotToken, plainPayload);
|
|
440
|
+
}
|
|
441
|
+
if (res.ok) {
|
|
442
|
+
return { kind: "delivered" };
|
|
443
|
+
}
|
|
444
|
+
const responseBody = await res.text().catch(() => "");
|
|
445
|
+
if (res.status === 429 || res.status >= 500) {
|
|
446
|
+
const retryAfter = res.headers.get("retry-after");
|
|
447
|
+
const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
|
|
448
|
+
return {
|
|
449
|
+
kind: "retry",
|
|
450
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
451
|
+
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
kind: "retry",
|
|
456
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
]);
|
|
461
|
+
for (const channel of deliveryRouter.supportedChannels()) {
|
|
462
|
+
outboundDeliveryChannels.add(channel);
|
|
463
|
+
}
|
|
448
464
|
const notifyOperators = async (notifyOpts) => {
|
|
449
465
|
if (!pipeline) {
|
|
450
466
|
return emptyNotifyOperatorsResult();
|
|
@@ -544,46 +560,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
544
560
|
return result;
|
|
545
561
|
};
|
|
546
562
|
const deliver = async (record) => {
|
|
547
|
-
|
|
548
|
-
if (envelope.channel === "telegram") {
|
|
549
|
-
const telegramBotToken = telegramManager.activeBotToken();
|
|
550
|
-
if (!telegramBotToken) {
|
|
551
|
-
return { kind: "retry", error: "telegram bot token not configured in .mu/config.json" };
|
|
552
|
-
}
|
|
553
|
-
const richPayload = buildTelegramSendMessagePayload({
|
|
554
|
-
chatId: envelope.channel_conversation_id,
|
|
555
|
-
text: envelope.body,
|
|
556
|
-
richFormatting: true,
|
|
557
|
-
});
|
|
558
|
-
let res = await postTelegramMessage(telegramBotToken, richPayload);
|
|
559
|
-
// Fallback: if Telegram rejects markdown entities, retry as plain text.
|
|
560
|
-
if (!res.ok && res.status === 400 && richPayload.parse_mode) {
|
|
561
|
-
const plainPayload = buildTelegramSendMessagePayload({
|
|
562
|
-
chatId: envelope.channel_conversation_id,
|
|
563
|
-
text: envelope.body,
|
|
564
|
-
richFormatting: false,
|
|
565
|
-
});
|
|
566
|
-
res = await postTelegramMessage(telegramBotToken, plainPayload);
|
|
567
|
-
}
|
|
568
|
-
if (res.ok) {
|
|
569
|
-
return { kind: "delivered" };
|
|
570
|
-
}
|
|
571
|
-
const responseBody = await res.text().catch(() => "");
|
|
572
|
-
if (res.status === 429 || res.status >= 500) {
|
|
573
|
-
const retryAfter = res.headers.get("retry-after");
|
|
574
|
-
const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
|
|
575
|
-
return {
|
|
576
|
-
kind: "retry",
|
|
577
|
-
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
578
|
-
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
579
|
-
};
|
|
580
|
-
}
|
|
581
|
-
return {
|
|
582
|
-
kind: "retry",
|
|
583
|
-
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
return undefined;
|
|
563
|
+
return await deliveryRouter.deliver(record);
|
|
587
564
|
};
|
|
588
565
|
const outboxDrain = createOutboxDrainLoop({
|
|
589
566
|
outbox,
|
|
@@ -714,9 +691,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
714
691
|
async interruptRun(interruptOpts) {
|
|
715
692
|
return await runQueueCoordinator.interruptQueuedRun(interruptOpts);
|
|
716
693
|
},
|
|
717
|
-
async heartbeatRun(heartbeatOpts) {
|
|
718
|
-
return await runQueueCoordinator.heartbeatQueuedRun(heartbeatOpts);
|
|
719
|
-
},
|
|
720
694
|
async traceRun(traceOpts) {
|
|
721
695
|
return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
|
|
722
696
|
},
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ControlPlaneAdapter, type ControlPlaneCommandPipeline, type ControlPlaneOutbox } from "@femtomc/mu-control-plane";
|
|
2
|
+
import type { ControlPlaneConfig } from "./control_plane_contract.js";
|
|
3
|
+
export type DetectedStaticAdapter = {
|
|
4
|
+
name: "slack" | "discord" | "neovim";
|
|
5
|
+
secret: string;
|
|
6
|
+
};
|
|
7
|
+
export type DetectedTelegramAdapter = {
|
|
8
|
+
name: "telegram";
|
|
9
|
+
webhookSecret: string;
|
|
10
|
+
botToken: string | null;
|
|
11
|
+
botUsername: string | null;
|
|
12
|
+
};
|
|
13
|
+
export type DetectedAdapter = DetectedStaticAdapter | DetectedTelegramAdapter;
|
|
14
|
+
export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
|
|
15
|
+
export declare function createStaticAdaptersFromDetected(opts: {
|
|
16
|
+
detected: readonly DetectedAdapter[];
|
|
17
|
+
pipeline: ControlPlaneCommandPipeline;
|
|
18
|
+
outbox: ControlPlaneOutbox;
|
|
19
|
+
}): ControlPlaneAdapter[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DiscordControlPlaneAdapter, NeovimControlPlaneAdapter, SlackControlPlaneAdapter, } from "@femtomc/mu-control-plane";
|
|
2
|
+
const STATIC_ADAPTER_MODULES = [
|
|
3
|
+
{
|
|
4
|
+
name: "slack",
|
|
5
|
+
detectSecret: (config) => config.adapters.slack.signing_secret,
|
|
6
|
+
create: (opts) => new SlackControlPlaneAdapter({
|
|
7
|
+
pipeline: opts.pipeline,
|
|
8
|
+
outbox: opts.outbox,
|
|
9
|
+
signingSecret: opts.secret,
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: "discord",
|
|
14
|
+
detectSecret: (config) => config.adapters.discord.signing_secret,
|
|
15
|
+
create: (opts) => new DiscordControlPlaneAdapter({
|
|
16
|
+
pipeline: opts.pipeline,
|
|
17
|
+
outbox: opts.outbox,
|
|
18
|
+
signingSecret: opts.secret,
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "neovim",
|
|
23
|
+
detectSecret: (config) => config.adapters.neovim.shared_secret,
|
|
24
|
+
create: (opts) => new NeovimControlPlaneAdapter({
|
|
25
|
+
pipeline: opts.pipeline,
|
|
26
|
+
sharedSecret: opts.secret,
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
const STATIC_ADAPTER_BY_NAME = new Map(STATIC_ADAPTER_MODULES.map((module) => [module.name, module]));
|
|
31
|
+
function isStaticAdapter(adapter) {
|
|
32
|
+
return adapter.name !== "telegram";
|
|
33
|
+
}
|
|
34
|
+
export function detectAdapters(config) {
|
|
35
|
+
const detected = [];
|
|
36
|
+
for (const module of STATIC_ADAPTER_MODULES) {
|
|
37
|
+
const secret = module.detectSecret(config);
|
|
38
|
+
if (!secret) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
detected.push({
|
|
42
|
+
name: module.name,
|
|
43
|
+
secret,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const telegramSecret = config.adapters.telegram.webhook_secret;
|
|
47
|
+
if (telegramSecret) {
|
|
48
|
+
detected.push({
|
|
49
|
+
name: "telegram",
|
|
50
|
+
webhookSecret: telegramSecret,
|
|
51
|
+
botToken: config.adapters.telegram.bot_token,
|
|
52
|
+
botUsername: config.adapters.telegram.bot_username,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return detected;
|
|
56
|
+
}
|
|
57
|
+
export function createStaticAdaptersFromDetected(opts) {
|
|
58
|
+
const adapters = [];
|
|
59
|
+
for (const detected of opts.detected) {
|
|
60
|
+
if (!isStaticAdapter(detected)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const module = STATIC_ADAPTER_BY_NAME.get(detected.name);
|
|
64
|
+
if (!module) {
|
|
65
|
+
throw new Error(`missing static adapter module: ${detected.name}`);
|
|
66
|
+
}
|
|
67
|
+
adapters.push(module.create({
|
|
68
|
+
pipeline: opts.pipeline,
|
|
69
|
+
outbox: opts.outbox,
|
|
70
|
+
secret: detected.secret,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
return adapters;
|
|
74
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Channel, CommandPipelineResult, ReloadableGenerationIdentity } from "@femtomc/mu-control-plane";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunTrace } from "./run_supervisor.js";
|
|
3
3
|
import type { MuConfig } from "./config.js";
|
|
4
4
|
/**
|
|
5
5
|
* Boundary contracts for server/control-plane composition.
|
|
@@ -154,12 +154,6 @@ export type ControlPlaneHandle = {
|
|
|
154
154
|
jobId?: string | null;
|
|
155
155
|
rootIssueId?: string | null;
|
|
156
156
|
}): Promise<ControlPlaneRunInterruptResult>;
|
|
157
|
-
heartbeatRun?(opts: {
|
|
158
|
-
jobId?: string | null;
|
|
159
|
-
rootIssueId?: string | null;
|
|
160
|
-
reason?: string | null;
|
|
161
|
-
wakeMode?: string | null;
|
|
162
|
-
}): Promise<ControlPlaneRunHeartbeatResult>;
|
|
163
157
|
traceRun?(opts: {
|
|
164
158
|
idOrRoot: string;
|
|
165
159
|
limit?: number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CommandRecord } from "@femtomc/mu-control-plane";
|
|
2
2
|
import type { InterRootQueuePolicy } from "./control_plane_contract.js";
|
|
3
3
|
import { DurableRunQueue } from "./run_queue.js";
|
|
4
|
-
import type { ControlPlaneRunEvent,
|
|
4
|
+
import type { ControlPlaneRunEvent, ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunSupervisor } from "./run_supervisor.js";
|
|
5
5
|
export type QueueLaunchOpts = {
|
|
6
6
|
mode: "run_start" | "run_resume";
|
|
7
7
|
prompt?: string;
|
|
@@ -37,12 +37,6 @@ export declare class ControlPlaneRunQueueCoordinator {
|
|
|
37
37
|
jobId?: string | null;
|
|
38
38
|
rootIssueId?: string | null;
|
|
39
39
|
}): Promise<ControlPlaneRunInterruptResult>;
|
|
40
|
-
heartbeatQueuedRun(opts: {
|
|
41
|
-
jobId?: string | null;
|
|
42
|
-
rootIssueId?: string | null;
|
|
43
|
-
reason?: string | null;
|
|
44
|
-
wakeMode?: string | null;
|
|
45
|
-
}): Promise<ControlPlaneRunHeartbeatResult>;
|
|
46
40
|
onRunEvent(event: ControlPlaneRunEvent): Promise<void>;
|
|
47
41
|
stop(): void;
|
|
48
42
|
}
|
|
@@ -20,12 +20,6 @@ function normalizeIssueId(value) {
|
|
|
20
20
|
}
|
|
21
21
|
return trimmed.toLowerCase();
|
|
22
22
|
}
|
|
23
|
-
function isTerminalQueueState(state) {
|
|
24
|
-
return state === "done" || state === "failed" || state === "cancelled";
|
|
25
|
-
}
|
|
26
|
-
function isInFlightQueueState(state) {
|
|
27
|
-
return state === "active" || state === "waiting_review" || state === "refining";
|
|
28
|
-
}
|
|
29
23
|
/**
|
|
30
24
|
* Queue/reconcile adapter for control-plane run lifecycle operations.
|
|
31
25
|
*
|
|
@@ -251,60 +245,8 @@ export class ControlPlaneRunQueueCoordinator {
|
|
|
251
245
|
run: runSnapshotFromQueueSnapshot(queued),
|
|
252
246
|
};
|
|
253
247
|
}
|
|
254
|
-
async heartbeatQueuedRun(opts) {
|
|
255
|
-
const runSupervisor = this.#getRunSupervisor();
|
|
256
|
-
const result = runSupervisor?.heartbeat(opts) ?? { ok: false, reason: "not_found", run: null };
|
|
257
|
-
let syncedQueue = null;
|
|
258
|
-
if (result.run) {
|
|
259
|
-
syncedQueue = await this.#runQueue
|
|
260
|
-
.applyRunSnapshot({
|
|
261
|
-
run: result.run,
|
|
262
|
-
operationId: `heartbeat-sync:${result.run.job_id}:${result.run.updated_at_ms}`,
|
|
263
|
-
createIfMissing: true,
|
|
264
|
-
})
|
|
265
|
-
.catch(() => {
|
|
266
|
-
// Best effort only.
|
|
267
|
-
return null;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
if (result.ok || result.reason === "missing_target") {
|
|
271
|
-
if (syncedQueue && isTerminalQueueState(syncedQueue.state)) {
|
|
272
|
-
await this.scheduleReconcile(`heartbeat-terminal:${syncedQueue.queue_id}`);
|
|
273
|
-
}
|
|
274
|
-
return result;
|
|
275
|
-
}
|
|
276
|
-
const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
|
|
277
|
-
if (!target) {
|
|
278
|
-
return result;
|
|
279
|
-
}
|
|
280
|
-
const queued = await this.#runQueue.get(target);
|
|
281
|
-
if (!queued) {
|
|
282
|
-
return result;
|
|
283
|
-
}
|
|
284
|
-
if (result.reason === "not_found" && queued.job_id && isInFlightQueueState(queued.state)) {
|
|
285
|
-
const failed = await this.#runQueue.transition({
|
|
286
|
-
queueId: queued.queue_id,
|
|
287
|
-
toState: "failed",
|
|
288
|
-
operationId: `heartbeat-fallback:not-found:${queued.queue_id}`,
|
|
289
|
-
});
|
|
290
|
-
await this.scheduleReconcile(`heartbeat-fallback:not-found:${queued.queue_id}`);
|
|
291
|
-
return {
|
|
292
|
-
ok: false,
|
|
293
|
-
reason: "not_running",
|
|
294
|
-
run: runSnapshotFromQueueSnapshot(failed),
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
if ((syncedQueue && isTerminalQueueState(syncedQueue.state)) || (result.reason === "not_running" && isInFlightQueueState(queued.state))) {
|
|
298
|
-
await this.scheduleReconcile(`heartbeat-wake:${queued.queue_id}:${result.reason ?? "unknown"}`);
|
|
299
|
-
}
|
|
300
|
-
return {
|
|
301
|
-
ok: false,
|
|
302
|
-
reason: "not_running",
|
|
303
|
-
run: runSnapshotFromQueueSnapshot(queued),
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
248
|
async onRunEvent(event) {
|
|
307
|
-
|
|
249
|
+
await this.#runQueue
|
|
308
250
|
.applyRunSnapshot({
|
|
309
251
|
run: event.run,
|
|
310
252
|
operationId: `run-event:${event.seq}:${event.kind}`,
|
|
@@ -314,9 +256,6 @@ export class ControlPlaneRunQueueCoordinator {
|
|
|
314
256
|
// Best effort queue reconciliation from runtime events.
|
|
315
257
|
return null;
|
|
316
258
|
});
|
|
317
|
-
if (event.kind === "run_heartbeat" && queueEventSnapshot && isInFlightQueueState(queueEventSnapshot.state)) {
|
|
318
|
-
await this.scheduleReconcile(`event-wake:run_heartbeat:${queueEventSnapshot.queue_id}:${event.seq}`);
|
|
319
|
-
}
|
|
320
259
|
if (event.kind === "run_completed" || event.kind === "run_failed" || event.kind === "run_cancelled") {
|
|
321
260
|
await this.scheduleReconcile(`terminal:${event.kind}:${event.run.job_id}`);
|
|
322
261
|
}
|
package/dist/cron_programs.d.ts
CHANGED
|
@@ -2,15 +2,6 @@ import type { JsonlStore } from "@femtomc/mu-core";
|
|
|
2
2
|
import { type CronProgramSchedule } from "./cron_schedule.js";
|
|
3
3
|
import { CronTimerRegistry } from "./cron_timer.js";
|
|
4
4
|
import type { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
5
|
-
export type CronProgramTarget = {
|
|
6
|
-
kind: "run";
|
|
7
|
-
job_id: string | null;
|
|
8
|
-
root_issue_id: string | null;
|
|
9
|
-
} | {
|
|
10
|
-
kind: "activity";
|
|
11
|
-
activity_id: string;
|
|
12
|
-
};
|
|
13
|
-
export type CronProgramWakeMode = "immediate" | "next_heartbeat";
|
|
14
5
|
export type CronProgramSnapshot = {
|
|
15
6
|
v: 1;
|
|
16
7
|
program_id: string;
|
|
@@ -18,14 +9,12 @@ export type CronProgramSnapshot = {
|
|
|
18
9
|
enabled: boolean;
|
|
19
10
|
schedule: CronProgramSchedule;
|
|
20
11
|
reason: string;
|
|
21
|
-
wake_mode: CronProgramWakeMode;
|
|
22
|
-
target: CronProgramTarget;
|
|
23
12
|
metadata: Record<string, unknown>;
|
|
24
13
|
created_at_ms: number;
|
|
25
14
|
updated_at_ms: number;
|
|
26
15
|
next_run_at_ms: number | null;
|
|
27
16
|
last_triggered_at_ms: number | null;
|
|
28
|
-
last_result: "ok" | "
|
|
17
|
+
last_result: "ok" | "coalesced" | "failed" | null;
|
|
29
18
|
last_error: string | null;
|
|
30
19
|
};
|
|
31
20
|
export type CronProgramLifecycleAction = "created" | "updated" | "deleted" | "scheduled" | "disabled" | "oneshot_completed";
|
|
@@ -40,13 +29,13 @@ export type CronProgramTickEvent = {
|
|
|
40
29
|
ts_ms: number;
|
|
41
30
|
program_id: string;
|
|
42
31
|
message: string;
|
|
43
|
-
status: "ok" | "
|
|
32
|
+
status: "ok" | "coalesced" | "failed";
|
|
44
33
|
reason: string | null;
|
|
45
34
|
program: CronProgramSnapshot;
|
|
46
35
|
};
|
|
47
36
|
export type CronProgramOperationResult = {
|
|
48
37
|
ok: boolean;
|
|
49
|
-
reason: "not_found" | "missing_target" | "
|
|
38
|
+
reason: "not_found" | "missing_target" | "invalid_schedule" | "not_running" | "failed" | null;
|
|
50
39
|
program: CronProgramSnapshot | null;
|
|
51
40
|
};
|
|
52
41
|
export type CronProgramStatusSnapshot = {
|
|
@@ -58,28 +47,30 @@ export type CronProgramStatusSnapshot = {
|
|
|
58
47
|
due_at_ms: number;
|
|
59
48
|
}>;
|
|
60
49
|
};
|
|
50
|
+
export type CronProgramDispatchResult = {
|
|
51
|
+
status: "ok";
|
|
52
|
+
reason?: string | null;
|
|
53
|
+
} | {
|
|
54
|
+
status: "coalesced";
|
|
55
|
+
reason?: string | null;
|
|
56
|
+
} | {
|
|
57
|
+
status: "failed";
|
|
58
|
+
reason: string;
|
|
59
|
+
};
|
|
61
60
|
export type CronProgramRegistryOpts = {
|
|
62
61
|
repoRoot: string;
|
|
63
62
|
heartbeatScheduler: ActivityHeartbeatScheduler;
|
|
64
63
|
nowMs?: () => number;
|
|
65
64
|
timer?: CronTimerRegistry;
|
|
66
65
|
store?: JsonlStore<CronProgramSnapshot>;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
reason
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}>;
|
|
76
|
-
activityHeartbeat: (opts: {
|
|
77
|
-
activityId?: string | null;
|
|
78
|
-
reason?: string | null;
|
|
79
|
-
}) => Promise<{
|
|
80
|
-
ok: boolean;
|
|
81
|
-
reason: "not_found" | "not_running" | "missing_target" | null;
|
|
82
|
-
}>;
|
|
66
|
+
dispatchWake: (opts: {
|
|
67
|
+
programId: string;
|
|
68
|
+
title: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
metadata: Record<string, unknown>;
|
|
71
|
+
triggeredAtMs: number;
|
|
72
|
+
schedule: CronProgramSchedule;
|
|
73
|
+
}) => Promise<CronProgramDispatchResult>;
|
|
83
74
|
onTickEvent?: (event: CronProgramTickEvent) => void | Promise<void>;
|
|
84
75
|
onLifecycleEvent?: (event: CronProgramLifecycleEvent) => void | Promise<void>;
|
|
85
76
|
};
|
|
@@ -88,7 +79,6 @@ export declare class CronProgramRegistry {
|
|
|
88
79
|
constructor(opts: CronProgramRegistryOpts);
|
|
89
80
|
list(opts?: {
|
|
90
81
|
enabled?: boolean;
|
|
91
|
-
targetKind?: "run" | "activity";
|
|
92
82
|
scheduleKind?: "at" | "every" | "cron";
|
|
93
83
|
limit?: number;
|
|
94
84
|
}): Promise<CronProgramSnapshot[]>;
|
|
@@ -96,10 +86,8 @@ export declare class CronProgramRegistry {
|
|
|
96
86
|
get(programId: string): Promise<CronProgramSnapshot | null>;
|
|
97
87
|
create(opts: {
|
|
98
88
|
title: string;
|
|
99
|
-
target: CronProgramTarget;
|
|
100
89
|
schedule: unknown;
|
|
101
90
|
reason?: string;
|
|
102
|
-
wakeMode?: CronProgramWakeMode;
|
|
103
91
|
enabled?: boolean;
|
|
104
92
|
metadata?: Record<string, unknown>;
|
|
105
93
|
}): Promise<CronProgramSnapshot>;
|
|
@@ -107,9 +95,7 @@ export declare class CronProgramRegistry {
|
|
|
107
95
|
programId: string;
|
|
108
96
|
title?: string;
|
|
109
97
|
reason?: string;
|
|
110
|
-
wakeMode?: CronProgramWakeMode;
|
|
111
98
|
enabled?: boolean;
|
|
112
|
-
target?: CronProgramTarget;
|
|
113
99
|
schedule?: unknown;
|
|
114
100
|
metadata?: Record<string, unknown>;
|
|
115
101
|
}): Promise<CronProgramOperationResult>;
|