@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
|
@@ -4,43 +4,6 @@ const HEARTBEAT_PROGRAMS_FILENAME = "heartbeats.jsonl";
|
|
|
4
4
|
function defaultNowMs() {
|
|
5
5
|
return Date.now();
|
|
6
6
|
}
|
|
7
|
-
function normalizeTarget(input) {
|
|
8
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
const record = input;
|
|
12
|
-
const kind = typeof record.kind === "string" ? record.kind.trim().toLowerCase() : "";
|
|
13
|
-
if (kind === "run") {
|
|
14
|
-
const jobId = typeof record.job_id === "string" ? record.job_id.trim() : "";
|
|
15
|
-
const rootIssueId = typeof record.root_issue_id === "string" ? record.root_issue_id.trim() : "";
|
|
16
|
-
if (!jobId && !rootIssueId) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
kind: "run",
|
|
21
|
-
job_id: jobId || null,
|
|
22
|
-
root_issue_id: rootIssueId || null,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
if (kind === "activity") {
|
|
26
|
-
const activityId = typeof record.activity_id === "string" ? record.activity_id.trim() : "";
|
|
27
|
-
if (!activityId) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
kind: "activity",
|
|
32
|
-
activity_id: activityId,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
function normalizeWakeMode(value) {
|
|
38
|
-
if (typeof value !== "string") {
|
|
39
|
-
return "immediate";
|
|
40
|
-
}
|
|
41
|
-
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
42
|
-
return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
|
|
43
|
-
}
|
|
44
7
|
function sanitizeMetadata(value) {
|
|
45
8
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
46
9
|
return {};
|
|
@@ -54,8 +17,7 @@ function normalizeProgram(row) {
|
|
|
54
17
|
const record = row;
|
|
55
18
|
const programId = typeof record.program_id === "string" ? record.program_id.trim() : "";
|
|
56
19
|
const title = typeof record.title === "string" ? record.title.trim() : "";
|
|
57
|
-
|
|
58
|
-
if (!programId || !title || !target) {
|
|
20
|
+
if (!programId || !title) {
|
|
59
21
|
return null;
|
|
60
22
|
}
|
|
61
23
|
const everyMsRaw = record.every_ms;
|
|
@@ -70,14 +32,8 @@ function normalizeProgram(row) {
|
|
|
70
32
|
? Math.trunc(record.last_triggered_at_ms)
|
|
71
33
|
: null;
|
|
72
34
|
const lastResultRaw = typeof record.last_result === "string" ? record.last_result.trim().toLowerCase() : null;
|
|
73
|
-
const lastResult = lastResultRaw === "ok" ||
|
|
74
|
-
lastResultRaw === "not_found" ||
|
|
75
|
-
lastResultRaw === "not_running" ||
|
|
76
|
-
lastResultRaw === "failed"
|
|
77
|
-
? lastResultRaw
|
|
78
|
-
: null;
|
|
35
|
+
const lastResult = lastResultRaw === "ok" || lastResultRaw === "coalesced" || lastResultRaw === "failed" ? lastResultRaw : null;
|
|
79
36
|
const reason = typeof record.reason === "string" && record.reason.trim().length > 0 ? record.reason.trim() : "scheduled";
|
|
80
|
-
const wakeMode = normalizeWakeMode(record.wake_mode);
|
|
81
37
|
return {
|
|
82
38
|
v: 1,
|
|
83
39
|
program_id: programId,
|
|
@@ -85,8 +41,6 @@ function normalizeProgram(row) {
|
|
|
85
41
|
enabled: record.enabled !== false,
|
|
86
42
|
every_ms: everyMs,
|
|
87
43
|
reason,
|
|
88
|
-
wake_mode: wakeMode,
|
|
89
|
-
target,
|
|
90
44
|
metadata: sanitizeMetadata(record.metadata),
|
|
91
45
|
created_at_ms: createdAt,
|
|
92
46
|
updated_at_ms: updatedAt,
|
|
@@ -106,16 +60,14 @@ function sortPrograms(programs) {
|
|
|
106
60
|
export class HeartbeatProgramRegistry {
|
|
107
61
|
#store;
|
|
108
62
|
#heartbeatScheduler;
|
|
109
|
-
#
|
|
110
|
-
#activityHeartbeat;
|
|
63
|
+
#dispatchWake;
|
|
111
64
|
#onTickEvent;
|
|
112
65
|
#nowMs;
|
|
113
66
|
#programs = new Map();
|
|
114
67
|
#loaded = null;
|
|
115
68
|
constructor(opts) {
|
|
116
69
|
this.#heartbeatScheduler = opts.heartbeatScheduler;
|
|
117
|
-
this.#
|
|
118
|
-
this.#activityHeartbeat = opts.activityHeartbeat;
|
|
70
|
+
this.#dispatchWake = opts.dispatchWake;
|
|
119
71
|
this.#onTickEvent = opts.onTickEvent;
|
|
120
72
|
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
121
73
|
this.#store =
|
|
@@ -128,7 +80,6 @@ export class HeartbeatProgramRegistry {
|
|
|
128
80
|
#snapshot(program) {
|
|
129
81
|
return {
|
|
130
82
|
...program,
|
|
131
|
-
target: program.target.kind === "run" ? { ...program.target } : { ...program.target },
|
|
132
83
|
metadata: { ...program.metadata },
|
|
133
84
|
};
|
|
134
85
|
}
|
|
@@ -187,50 +138,38 @@ export class HeartbeatProgramRegistry {
|
|
|
187
138
|
const nowMs = Math.trunc(this.#nowMs());
|
|
188
139
|
program.last_triggered_at_ms = nowMs;
|
|
189
140
|
program.updated_at_ms = nowMs;
|
|
190
|
-
let
|
|
141
|
+
let tickResult;
|
|
191
142
|
let eventStatus = "ok";
|
|
192
143
|
let eventReason = heartbeatReason;
|
|
193
|
-
let eventMessage = `heartbeat program
|
|
144
|
+
let eventMessage = `heartbeat program dispatched wake: ${program.title}`;
|
|
194
145
|
try {
|
|
195
|
-
const result =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
activityId: program.target.activity_id,
|
|
204
|
-
reason: heartbeatReason,
|
|
205
|
-
});
|
|
206
|
-
if (result.ok) {
|
|
146
|
+
const result = await this.#dispatchWake({
|
|
147
|
+
programId: program.program_id,
|
|
148
|
+
title: program.title,
|
|
149
|
+
reason: heartbeatReason,
|
|
150
|
+
metadata: { ...program.metadata },
|
|
151
|
+
triggeredAtMs: nowMs,
|
|
152
|
+
});
|
|
153
|
+
if (result.status === "ok") {
|
|
207
154
|
program.last_result = "ok";
|
|
208
155
|
program.last_error = null;
|
|
209
|
-
|
|
156
|
+
tickResult = { status: "ran" };
|
|
210
157
|
}
|
|
211
|
-
else if (result.
|
|
212
|
-
program.last_result = "
|
|
158
|
+
else if (result.status === "coalesced") {
|
|
159
|
+
program.last_result = "coalesced";
|
|
213
160
|
program.last_error = null;
|
|
214
|
-
eventStatus = "
|
|
215
|
-
eventReason = result.reason;
|
|
216
|
-
eventMessage = `heartbeat program
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
else if (result.reason === "not_found") {
|
|
220
|
-
program.last_result = "not_found";
|
|
221
|
-
program.last_error = null;
|
|
222
|
-
eventStatus = "not_found";
|
|
223
|
-
eventReason = result.reason;
|
|
224
|
-
eventMessage = `heartbeat program skipped (not found): ${program.title}`;
|
|
225
|
-
heartbeatResult = { status: "skipped", reason: "not_found" };
|
|
161
|
+
eventStatus = "coalesced";
|
|
162
|
+
eventReason = result.reason ?? "coalesced";
|
|
163
|
+
eventMessage = `heartbeat program coalesced wake: ${program.title}`;
|
|
164
|
+
tickResult = { status: "skipped", reason: "coalesced" };
|
|
226
165
|
}
|
|
227
166
|
else {
|
|
228
167
|
program.last_result = "failed";
|
|
229
|
-
program.last_error = result.reason
|
|
168
|
+
program.last_error = result.reason;
|
|
230
169
|
eventStatus = "failed";
|
|
231
|
-
eventReason =
|
|
170
|
+
eventReason = result.reason;
|
|
232
171
|
eventMessage = `heartbeat program failed: ${program.title}`;
|
|
233
|
-
|
|
172
|
+
tickResult = { status: "failed", reason: result.reason };
|
|
234
173
|
}
|
|
235
174
|
}
|
|
236
175
|
catch (err) {
|
|
@@ -239,23 +178,7 @@ export class HeartbeatProgramRegistry {
|
|
|
239
178
|
eventStatus = "failed";
|
|
240
179
|
eventReason = program.last_error;
|
|
241
180
|
eventMessage = `heartbeat program failed: ${program.title}`;
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
const shouldAutoDisableOnTerminal = program.target.kind === "run" &&
|
|
245
|
-
program.metadata.auto_disable_on_terminal === true &&
|
|
246
|
-
heartbeatResult.status === "skipped" &&
|
|
247
|
-
(heartbeatResult.reason === "not_running" || heartbeatResult.reason === "not_found");
|
|
248
|
-
if (shouldAutoDisableOnTerminal) {
|
|
249
|
-
program.enabled = false;
|
|
250
|
-
program.every_ms = 0;
|
|
251
|
-
program.updated_at_ms = Math.trunc(this.#nowMs());
|
|
252
|
-
program.metadata = {
|
|
253
|
-
...program.metadata,
|
|
254
|
-
auto_disabled_at_ms: Math.trunc(this.#nowMs()),
|
|
255
|
-
auto_disabled_reason: heartbeatResult.status === "skipped" ? (heartbeatResult.reason ?? null) : null,
|
|
256
|
-
};
|
|
257
|
-
this.#heartbeatScheduler.unregister(this.#scheduleId(program.program_id));
|
|
258
|
-
eventMessage = `${eventMessage} (auto-disabled)`;
|
|
181
|
+
tickResult = { status: "failed", reason: program.last_error };
|
|
259
182
|
}
|
|
260
183
|
await this.#persist();
|
|
261
184
|
await this.#emitTickEvent({
|
|
@@ -268,7 +191,7 @@ export class HeartbeatProgramRegistry {
|
|
|
268
191
|
}).catch(() => {
|
|
269
192
|
// best effort only
|
|
270
193
|
});
|
|
271
|
-
return
|
|
194
|
+
return tickResult;
|
|
272
195
|
}
|
|
273
196
|
async list(opts = {}) {
|
|
274
197
|
await this.#ensureLoaded();
|
|
@@ -278,9 +201,6 @@ export class HeartbeatProgramRegistry {
|
|
|
278
201
|
if (typeof opts.enabled === "boolean" && program.enabled !== opts.enabled) {
|
|
279
202
|
return false;
|
|
280
203
|
}
|
|
281
|
-
if (opts.targetKind && program.target.kind !== opts.targetKind) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
204
|
return true;
|
|
285
205
|
})
|
|
286
206
|
.slice(0, limit)
|
|
@@ -297,10 +217,6 @@ export class HeartbeatProgramRegistry {
|
|
|
297
217
|
if (!title) {
|
|
298
218
|
throw new Error("heartbeat_program_title_required");
|
|
299
219
|
}
|
|
300
|
-
const target = normalizeTarget(opts.target);
|
|
301
|
-
if (!target) {
|
|
302
|
-
throw new Error("heartbeat_program_invalid_target");
|
|
303
|
-
}
|
|
304
220
|
const nowMs = Math.trunc(this.#nowMs());
|
|
305
221
|
const program = {
|
|
306
222
|
v: 1,
|
|
@@ -311,8 +227,6 @@ export class HeartbeatProgramRegistry {
|
|
|
311
227
|
? Math.max(0, Math.trunc(opts.everyMs))
|
|
312
228
|
: 15_000,
|
|
313
229
|
reason: opts.reason?.trim() || "scheduled",
|
|
314
|
-
wake_mode: normalizeWakeMode(opts.wakeMode),
|
|
315
|
-
target,
|
|
316
230
|
metadata: sanitizeMetadata(opts.metadata),
|
|
317
231
|
created_at_ms: nowMs,
|
|
318
232
|
updated_at_ms: nowMs,
|
|
@@ -344,19 +258,9 @@ export class HeartbeatProgramRegistry {
|
|
|
344
258
|
if (typeof opts.reason === "string") {
|
|
345
259
|
program.reason = opts.reason.trim() || "scheduled";
|
|
346
260
|
}
|
|
347
|
-
if (typeof opts.wakeMode === "string") {
|
|
348
|
-
program.wake_mode = normalizeWakeMode(opts.wakeMode);
|
|
349
|
-
}
|
|
350
261
|
if (typeof opts.enabled === "boolean") {
|
|
351
262
|
program.enabled = opts.enabled;
|
|
352
263
|
}
|
|
353
|
-
if (opts.target) {
|
|
354
|
-
const target = normalizeTarget(opts.target);
|
|
355
|
-
if (!target) {
|
|
356
|
-
return { ok: false, reason: "invalid_target", program: this.#snapshot(program) };
|
|
357
|
-
}
|
|
358
|
-
program.target = target;
|
|
359
|
-
}
|
|
360
264
|
if (opts.metadata) {
|
|
361
265
|
program.metadata = sanitizeMetadata(opts.metadata);
|
|
362
266
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,13 +7,13 @@ export { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, ORCHEST
|
|
|
7
7
|
export type { DurableRunQueueClaimOpts, DurableRunQueueEnqueueOpts, DurableRunQueueOpts, DurableRunQueueSnapshot, DurableRunQueueState, DurableRunQueueTransitionOpts, RunQueueReconcilePlan, } from "./run_queue.js";
|
|
8
8
|
export { DurableRunQueue, queueStatesForRunStatusFilter, reconcileRunQueue, RUN_QUEUE_RECONCILE_INVARIANTS, runQueuePath, runSnapshotFromQueueSnapshot, runStatusFromQueueState, } from "./run_queue.js";
|
|
9
9
|
export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
|
|
10
|
-
export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot,
|
|
10
|
+
export type { CronProgramDispatchResult, CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTickEvent, } from "./cron_programs.js";
|
|
11
11
|
export { CronProgramRegistry } from "./cron_programs.js";
|
|
12
12
|
export type { CronProgramSchedule as CronSchedule, CronProgramSchedule } from "./cron_schedule.js";
|
|
13
13
|
export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
|
|
14
14
|
export type { CronTimerRegistryOpts, CronTimerSnapshot } from "./cron_timer.js";
|
|
15
15
|
export { CronTimerRegistry } from "./cron_timer.js";
|
|
16
|
-
export type { HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, HeartbeatProgramSnapshot,
|
|
16
|
+
export type { HeartbeatProgramDispatchResult, HeartbeatProgramOperationResult, HeartbeatProgramRegistryOpts, HeartbeatProgramSnapshot, HeartbeatProgramTickEvent, } from "./heartbeat_programs.js";
|
|
17
17
|
export { HeartbeatProgramRegistry } from "./heartbeat_programs.js";
|
|
18
18
|
export type { ActivityHeartbeatSchedulerOpts, HeartbeatRunResult, HeartbeatTickHandler, } from "./heartbeat_scheduler.js";
|
|
19
19
|
export { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Channel, type OutboxDeliveryHandlerResult, type OutboxRecord } from "@femtomc/mu-control-plane";
|
|
2
|
+
export type OutboundDeliveryDriver = {
|
|
3
|
+
channel: Channel;
|
|
4
|
+
deliver: (record: OutboxRecord) => Promise<OutboxDeliveryHandlerResult>;
|
|
5
|
+
};
|
|
6
|
+
export declare class OutboundDeliveryRouter {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(drivers: readonly OutboundDeliveryDriver[]);
|
|
9
|
+
supportsChannel(channel: Channel): boolean;
|
|
10
|
+
supportedChannels(): Channel[];
|
|
11
|
+
deliver(record: OutboxRecord): Promise<undefined | OutboxDeliveryHandlerResult>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ChannelSchema, } from "@femtomc/mu-control-plane";
|
|
2
|
+
export class OutboundDeliveryRouter {
|
|
3
|
+
#driversByChannel = new Map();
|
|
4
|
+
constructor(drivers) {
|
|
5
|
+
for (const driver of drivers) {
|
|
6
|
+
if (this.#driversByChannel.has(driver.channel)) {
|
|
7
|
+
throw new Error(`duplicate outbound delivery driver: ${driver.channel}`);
|
|
8
|
+
}
|
|
9
|
+
this.#driversByChannel.set(driver.channel, driver);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
supportsChannel(channel) {
|
|
13
|
+
return this.#driversByChannel.has(channel);
|
|
14
|
+
}
|
|
15
|
+
supportedChannels() {
|
|
16
|
+
return [...this.#driversByChannel.keys()].sort((a, b) => a.localeCompare(b));
|
|
17
|
+
}
|
|
18
|
+
async deliver(record) {
|
|
19
|
+
const parsedChannel = ChannelSchema.safeParse(record.envelope.channel);
|
|
20
|
+
if (!parsedChannel.success) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const driver = this.#driversByChannel.get(parsedChannel.data);
|
|
24
|
+
if (!driver) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return await driver.deliver(record);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/run_supervisor.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { CommandRecord } from "@femtomc/mu-control-plane";
|
|
2
|
-
import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
3
2
|
export type ControlPlaneRunMode = "run_start" | "run_resume";
|
|
4
3
|
export type ControlPlaneRunStatus = "running" | "completed" | "failed" | "cancelled";
|
|
5
|
-
export type ControlPlaneRunWakeMode = "immediate" | "next_heartbeat";
|
|
6
4
|
export type ControlPlaneRunSnapshot = {
|
|
7
5
|
job_id: string;
|
|
8
6
|
mode: ControlPlaneRunMode;
|
|
@@ -33,12 +31,7 @@ export type ControlPlaneRunInterruptResult = {
|
|
|
33
31
|
reason: "not_found" | "not_running" | "missing_target" | null;
|
|
34
32
|
run: ControlPlaneRunSnapshot | null;
|
|
35
33
|
};
|
|
36
|
-
export type
|
|
37
|
-
ok: boolean;
|
|
38
|
-
reason: "not_found" | "not_running" | "missing_target" | null;
|
|
39
|
-
run: ControlPlaneRunSnapshot | null;
|
|
40
|
-
};
|
|
41
|
-
export type ControlPlaneRunEventKind = "run_started" | "run_root_discovered" | "run_progress" | "run_heartbeat" | "run_interrupt_requested" | "run_completed" | "run_failed" | "run_cancelled";
|
|
34
|
+
export type ControlPlaneRunEventKind = "run_started" | "run_root_discovered" | "run_progress" | "run_interrupt_requested" | "run_completed" | "run_failed" | "run_cancelled";
|
|
42
35
|
export type ControlPlaneRunEvent = {
|
|
43
36
|
seq: number;
|
|
44
37
|
ts_ms: number;
|
|
@@ -61,8 +54,6 @@ export type ControlPlaneRunSupervisorOpts = {
|
|
|
61
54
|
argv: string[];
|
|
62
55
|
cwd: string;
|
|
63
56
|
}) => ControlPlaneRunProcess;
|
|
64
|
-
heartbeatIntervalMs?: number;
|
|
65
|
-
heartbeatScheduler?: ActivityHeartbeatScheduler;
|
|
66
57
|
maxStoredLines?: number;
|
|
67
58
|
maxHistory?: number;
|
|
68
59
|
onEvent?: (event: ControlPlaneRunEvent) => void | Promise<void>;
|
|
@@ -112,12 +103,6 @@ export declare class ControlPlaneRunSupervisor {
|
|
|
112
103
|
jobId?: string | null;
|
|
113
104
|
rootIssueId?: string | null;
|
|
114
105
|
}): ControlPlaneRunInterruptResult;
|
|
115
|
-
heartbeat(opts: {
|
|
116
|
-
jobId?: string | null;
|
|
117
|
-
rootIssueId?: string | null;
|
|
118
|
-
reason?: string | null;
|
|
119
|
-
wakeMode?: string | null;
|
|
120
|
-
}): ControlPlaneRunHeartbeatResult;
|
|
121
106
|
startFromCommand(command: CommandRecord): Promise<ControlPlaneRunSnapshot | null>;
|
|
122
107
|
stop(): Promise<void>;
|
|
123
108
|
}
|
package/dist/run_supervisor.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readdir } from "node:fs/promises";
|
|
2
2
|
import { join, relative } from "node:path";
|
|
3
|
-
import { ActivityHeartbeatScheduler } from "./heartbeat_scheduler.js";
|
|
4
3
|
const DEFAULT_MAX_STEPS = 20;
|
|
5
4
|
const ROOT_RE = /\bRoot:\s*(mu-[a-z0-9][a-z0-9-]*)\b/i;
|
|
6
5
|
const STEP_RE = /^(Step|Done)\s+\d+\/\d+\s+/;
|
|
@@ -45,13 +44,6 @@ function normalizeIssueId(value) {
|
|
|
45
44
|
}
|
|
46
45
|
return trimmed.toLowerCase();
|
|
47
46
|
}
|
|
48
|
-
function normalizeWakeMode(value) {
|
|
49
|
-
if (typeof value !== "string") {
|
|
50
|
-
return "immediate";
|
|
51
|
-
}
|
|
52
|
-
const normalized = value.trim().toLowerCase().replaceAll("-", "_");
|
|
53
|
-
return normalized === "next_heartbeat" ? "next_heartbeat" : "immediate";
|
|
54
|
-
}
|
|
55
47
|
function pushBounded(lines, line, maxLines) {
|
|
56
48
|
lines.push(line);
|
|
57
49
|
if (lines.length <= maxLines) {
|
|
@@ -109,9 +101,6 @@ export class ControlPlaneRunSupervisor {
|
|
|
109
101
|
#repoRoot;
|
|
110
102
|
#nowMs;
|
|
111
103
|
#spawnProcess;
|
|
112
|
-
#heartbeatIntervalMs;
|
|
113
|
-
#heartbeatScheduler;
|
|
114
|
-
#ownsHeartbeatScheduler;
|
|
115
104
|
#maxStoredLines;
|
|
116
105
|
#maxHistory;
|
|
117
106
|
#onEvent;
|
|
@@ -123,9 +112,6 @@ export class ControlPlaneRunSupervisor {
|
|
|
123
112
|
this.#repoRoot = opts.repoRoot;
|
|
124
113
|
this.#nowMs = opts.nowMs ?? defaultNowMs;
|
|
125
114
|
this.#spawnProcess = opts.spawnProcess ?? defaultSpawnProcess;
|
|
126
|
-
this.#heartbeatIntervalMs = Math.max(2_000, Math.trunc(opts.heartbeatIntervalMs ?? 15_000));
|
|
127
|
-
this.#heartbeatScheduler = opts.heartbeatScheduler ?? new ActivityHeartbeatScheduler();
|
|
128
|
-
this.#ownsHeartbeatScheduler = !opts.heartbeatScheduler;
|
|
129
115
|
this.#maxStoredLines = Math.max(50, Math.trunc(opts.maxStoredLines ?? 1_000));
|
|
130
116
|
this.#maxHistory = Math.max(20, Math.trunc(opts.maxHistory ?? 200));
|
|
131
117
|
this.#onEvent = opts.onEvent ?? null;
|
|
@@ -253,7 +239,6 @@ export class ControlPlaneRunSupervisor {
|
|
|
253
239
|
stderr_lines: [],
|
|
254
240
|
log_hints: new Set(),
|
|
255
241
|
interrupt_requested: false,
|
|
256
|
-
next_heartbeat_reason: null,
|
|
257
242
|
hard_kill_timer: null,
|
|
258
243
|
};
|
|
259
244
|
this.#jobsById.set(snapshot.job_id, job);
|
|
@@ -261,34 +246,11 @@ export class ControlPlaneRunSupervisor {
|
|
|
261
246
|
this.#jobIdByRootIssueId.set(snapshot.root_issue_id, snapshot.job_id);
|
|
262
247
|
}
|
|
263
248
|
this.#emit("run_started", job, `🚀 Started ${describeRun(snapshot)} (job ${snapshot.job_id}, pid ${snapshot.pid ?? "?"})`);
|
|
264
|
-
this.#heartbeatScheduler.register({
|
|
265
|
-
activityId: snapshot.job_id,
|
|
266
|
-
everyMs: this.#heartbeatIntervalMs,
|
|
267
|
-
handler: async ({ reason }) => {
|
|
268
|
-
if (job.snapshot.status !== "running") {
|
|
269
|
-
return { status: "skipped", reason: "not_running" };
|
|
270
|
-
}
|
|
271
|
-
const normalizedReason = reason?.trim();
|
|
272
|
-
const heartbeatReason = normalizedReason && normalizedReason.length > 0 && normalizedReason !== "interval"
|
|
273
|
-
? normalizedReason
|
|
274
|
-
: job.next_heartbeat_reason;
|
|
275
|
-
if (heartbeatReason) {
|
|
276
|
-
job.next_heartbeat_reason = null;
|
|
277
|
-
}
|
|
278
|
-
const elapsedSec = Math.max(0, Math.trunc((this.#nowMs() - job.snapshot.started_at_ms) / 1_000));
|
|
279
|
-
const root = job.snapshot.root_issue_id ?? job.snapshot.job_id;
|
|
280
|
-
const progress = job.snapshot.last_progress ? ` · ${job.snapshot.last_progress}` : "";
|
|
281
|
-
const reasonSuffix = heartbeatReason ? ` · wake=${heartbeatReason}` : "";
|
|
282
|
-
this.#emit("run_heartbeat", job, `⏱ ${root} running for ${elapsedSec}s${progress}${reasonSuffix}`);
|
|
283
|
-
return { status: "ran" };
|
|
284
|
-
},
|
|
285
|
-
});
|
|
286
249
|
void (async () => {
|
|
287
250
|
const stdoutTask = consumeStreamLines(process.stdout, (line) => this.#handleLine(job, "stdout", line));
|
|
288
251
|
const stderrTask = consumeStreamLines(process.stderr, (line) => this.#handleLine(job, "stderr", line));
|
|
289
252
|
const exitCode = await process.exited.catch(() => -1);
|
|
290
253
|
await Promise.allSettled([stdoutTask, stderrTask]);
|
|
291
|
-
this.#heartbeatScheduler.unregister(job.snapshot.job_id);
|
|
292
254
|
if (job.hard_kill_timer) {
|
|
293
255
|
clearTimeout(job.hard_kill_timer);
|
|
294
256
|
job.hard_kill_timer = null;
|
|
@@ -451,34 +413,6 @@ export class ControlPlaneRunSupervisor {
|
|
|
451
413
|
this.#emit("run_interrupt_requested", job, `⚠️ Interrupt requested for ${root}.`);
|
|
452
414
|
return { ok: true, reason: null, run: this.#snapshot(job) };
|
|
453
415
|
}
|
|
454
|
-
heartbeat(opts) {
|
|
455
|
-
const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
|
|
456
|
-
if (target.length === 0) {
|
|
457
|
-
return { ok: false, reason: "missing_target", run: null };
|
|
458
|
-
}
|
|
459
|
-
const job = this.#resolveJob(target);
|
|
460
|
-
if (!job) {
|
|
461
|
-
return { ok: false, reason: "not_found", run: null };
|
|
462
|
-
}
|
|
463
|
-
if (job.snapshot.status !== "running") {
|
|
464
|
-
return { ok: false, reason: "not_running", run: this.#snapshot(job) };
|
|
465
|
-
}
|
|
466
|
-
const reason = opts.reason?.trim() || "manual";
|
|
467
|
-
const wakeMode = normalizeWakeMode(opts.wakeMode);
|
|
468
|
-
if (wakeMode === "next_heartbeat") {
|
|
469
|
-
job.next_heartbeat_reason = reason;
|
|
470
|
-
this.#touch(job);
|
|
471
|
-
return { ok: true, reason: null, run: this.#snapshot(job) };
|
|
472
|
-
}
|
|
473
|
-
if (reason !== "interval") {
|
|
474
|
-
job.next_heartbeat_reason = null;
|
|
475
|
-
}
|
|
476
|
-
this.#heartbeatScheduler.requestNow(job.snapshot.job_id, {
|
|
477
|
-
reason,
|
|
478
|
-
coalesceMs: 0,
|
|
479
|
-
});
|
|
480
|
-
return { ok: true, reason: null, run: this.#snapshot(job) };
|
|
481
|
-
}
|
|
482
416
|
async startFromCommand(command) {
|
|
483
417
|
switch (command.target_type) {
|
|
484
418
|
case "run start": {
|
|
@@ -508,7 +442,6 @@ export class ControlPlaneRunSupervisor {
|
|
|
508
442
|
}
|
|
509
443
|
async stop() {
|
|
510
444
|
for (const job of this.#jobsById.values()) {
|
|
511
|
-
this.#heartbeatScheduler.unregister(job.snapshot.job_id);
|
|
512
445
|
if (job.hard_kill_timer) {
|
|
513
446
|
clearTimeout(job.hard_kill_timer);
|
|
514
447
|
job.hard_kill_timer = null;
|
|
@@ -522,8 +455,5 @@ export class ControlPlaneRunSupervisor {
|
|
|
522
455
|
}
|
|
523
456
|
}
|
|
524
457
|
}
|
|
525
|
-
if (this.#ownsHeartbeatScheduler) {
|
|
526
|
-
this.#heartbeatScheduler.stop();
|
|
527
|
-
}
|
|
528
458
|
}
|
|
529
459
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { GenerationTelemetryRecorder } from "@femtomc/mu-control-plane";
|
|
2
2
|
import type { EventEnvelope, JsonlStore } from "@femtomc/mu-core";
|
|
3
3
|
import { EventLog } from "@femtomc/mu-core/node";
|
|
4
|
-
import { ForumStore } from "@femtomc/mu-forum";
|
|
5
|
-
import { IssueStore } from "@femtomc/mu-issue";
|
|
6
4
|
import { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
|
|
7
5
|
import { type MuConfig } from "./config.js";
|
|
8
6
|
import type { ControlPlaneHandle, ControlPlaneSessionLifecycle } from "./control_plane_contract.js";
|
|
@@ -21,7 +19,6 @@ export type ServerOptions = {
|
|
|
21
19
|
controlPlaneReloader?: ControlPlaneReloader;
|
|
22
20
|
generationTelemetry?: GenerationTelemetryRecorder;
|
|
23
21
|
operatorWakeCoalesceMs?: number;
|
|
24
|
-
autoRunHeartbeatEveryMs?: number;
|
|
25
22
|
config?: MuConfig;
|
|
26
23
|
configReader?: ConfigReader;
|
|
27
24
|
configWriter?: ConfigWriter;
|
|
@@ -31,8 +28,6 @@ export type ServerOptions = {
|
|
|
31
28
|
export type ServerInstanceOptions = Omit<ServerOptions, "repoRoot" | "controlPlane" | "heartbeatScheduler" | "generationTelemetry" | "config" | "sessionLifecycle">;
|
|
32
29
|
export type ServerContext = {
|
|
33
30
|
repoRoot: string;
|
|
34
|
-
issueStore: IssueStore;
|
|
35
|
-
forumStore: ForumStore;
|
|
36
31
|
eventLog: EventLog;
|
|
37
32
|
eventsStore: JsonlStore<EventEnvelope>;
|
|
38
33
|
};
|