@femtomc/mu-server 26.2.69 → 26.2.71
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 +7 -3
- package/dist/api/activities.d.ts +2 -0
- package/dist/api/activities.js +160 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +45 -0
- package/dist/api/control_plane.d.ts +2 -0
- package/dist/api/control_plane.js +28 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +182 -0
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/heartbeats.d.ts +2 -0
- package/dist/api/heartbeats.js +211 -0
- package/dist/api/identities.d.ts +2 -0
- package/dist/api/identities.js +103 -0
- package/dist/api/issues.js +120 -33
- package/dist/api/runs.d.ts +2 -0
- package/dist/api/runs.js +207 -0
- package/dist/cli.js +58 -3
- package/dist/config.d.ts +4 -21
- package/dist/config.js +24 -75
- package/dist/control_plane.d.ts +7 -114
- package/dist/control_plane.js +238 -654
- package/dist/control_plane_bootstrap_helpers.d.ts +16 -0
- package/dist/control_plane_bootstrap_helpers.js +85 -0
- package/dist/control_plane_contract.d.ts +176 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_reload.d.ts +63 -0
- package/dist/control_plane_reload.js +525 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
- package/dist/control_plane_run_queue_coordinator.js +327 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +520 -0
- package/dist/control_plane_wake_delivery.d.ts +50 -0
- package/dist/control_plane_wake_delivery.js +123 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -1
- package/dist/run_queue.d.ts +95 -0
- package/dist/run_queue.js +817 -0
- package/dist/run_supervisor.d.ts +20 -0
- package/dist/run_supervisor.js +25 -1
- package/dist/server.d.ts +12 -49
- package/dist/server.js +365 -2128
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +254 -0
- package/dist/server_routing.d.ts +31 -0
- package/dist/server_routing.js +230 -0
- package/dist/server_runtime.d.ts +30 -0
- package/dist/server_runtime.js +43 -0
- package/dist/server_types.d.ts +3 -0
- package/dist/server_types.js +16 -0
- package/dist/session_lifecycle.d.ts +11 -0
- package/dist/session_lifecycle.js +149 -0
- package/package.json +7 -6
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { reconcileRunQueue, runSnapshotFromQueueSnapshot, } from "./run_queue.js";
|
|
2
|
+
const DEFAULT_RUN_MAX_STEPS = 20;
|
|
3
|
+
const MAX_RECONCILE_TURNS = 256;
|
|
4
|
+
function toPositiveInt(value, fallback) {
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
+
return Math.max(1, Math.trunc(value));
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "string" && /^\d+$/.test(value.trim())) {
|
|
9
|
+
return Math.max(1, Number.parseInt(value, 10));
|
|
10
|
+
}
|
|
11
|
+
return Math.max(1, Math.trunc(fallback));
|
|
12
|
+
}
|
|
13
|
+
function normalizeIssueId(value) {
|
|
14
|
+
if (!value) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!/^mu-[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return trimmed.toLowerCase();
|
|
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
|
+
/**
|
|
30
|
+
* Queue/reconcile adapter for control-plane run lifecycle operations.
|
|
31
|
+
*
|
|
32
|
+
* Keeps inter-root queue execution concerns out of `control_plane.ts` while preserving
|
|
33
|
+
* queue-first semantics:
|
|
34
|
+
* - enqueue intents durably
|
|
35
|
+
* - reconcile/claim/launch deterministically
|
|
36
|
+
* - mirror runtime events back into durable queue state
|
|
37
|
+
*/
|
|
38
|
+
export class ControlPlaneRunQueueCoordinator {
|
|
39
|
+
#runQueue;
|
|
40
|
+
#interRootQueuePolicy;
|
|
41
|
+
#getRunSupervisor;
|
|
42
|
+
#defaultRunMaxSteps;
|
|
43
|
+
#queuedCommandById = new Map();
|
|
44
|
+
#queueReconcileCounter = 0;
|
|
45
|
+
#queueReconcileTail = Promise.resolve();
|
|
46
|
+
constructor(opts) {
|
|
47
|
+
this.#runQueue = opts.runQueue;
|
|
48
|
+
this.#interRootQueuePolicy = opts.interRootQueuePolicy;
|
|
49
|
+
this.#getRunSupervisor = opts.getRunSupervisor;
|
|
50
|
+
this.#defaultRunMaxSteps = toPositiveInt(opts.defaultRunMaxSteps, DEFAULT_RUN_MAX_STEPS);
|
|
51
|
+
}
|
|
52
|
+
runtimeSnapshotsByJobId() {
|
|
53
|
+
const map = new Map();
|
|
54
|
+
for (const run of this.#getRunSupervisor()?.list({ limit: 500 }) ?? []) {
|
|
55
|
+
map.set(run.job_id, run);
|
|
56
|
+
}
|
|
57
|
+
return map;
|
|
58
|
+
}
|
|
59
|
+
async #launchQueueEntry(row, opPrefix) {
|
|
60
|
+
const runSupervisor = this.#getRunSupervisor();
|
|
61
|
+
if (!runSupervisor) {
|
|
62
|
+
throw new Error("run_supervisor_unavailable");
|
|
63
|
+
}
|
|
64
|
+
const command = row.command_id ? (this.#queuedCommandById.get(row.command_id) ?? null) : null;
|
|
65
|
+
try {
|
|
66
|
+
const launched = row.mode === "run_start"
|
|
67
|
+
? await runSupervisor.launchStart({
|
|
68
|
+
prompt: row.prompt ?? "",
|
|
69
|
+
maxSteps: row.max_steps,
|
|
70
|
+
command,
|
|
71
|
+
commandId: row.command_id,
|
|
72
|
+
source: row.source,
|
|
73
|
+
})
|
|
74
|
+
: await runSupervisor.launchResume({
|
|
75
|
+
rootIssueId: row.root_issue_id ?? "",
|
|
76
|
+
maxSteps: row.max_steps,
|
|
77
|
+
command,
|
|
78
|
+
commandId: row.command_id,
|
|
79
|
+
source: row.source,
|
|
80
|
+
});
|
|
81
|
+
if (command?.command_id) {
|
|
82
|
+
this.#queuedCommandById.delete(command.command_id);
|
|
83
|
+
}
|
|
84
|
+
const latestSnapshot = runSupervisor.get(launched.job_id) ?? launched;
|
|
85
|
+
await this.#runQueue.bindRunSnapshot({
|
|
86
|
+
queueId: row.queue_id,
|
|
87
|
+
run: latestSnapshot,
|
|
88
|
+
operationId: `${opPrefix}:bind:${row.queue_id}:${launched.job_id}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
if (command?.command_id) {
|
|
93
|
+
this.#queuedCommandById.delete(command.command_id);
|
|
94
|
+
}
|
|
95
|
+
await this.#runQueue.transition({
|
|
96
|
+
queueId: row.queue_id,
|
|
97
|
+
toState: "failed",
|
|
98
|
+
operationId: `${opPrefix}:failed:${row.queue_id}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async #reconcileQueuedRunsNow(reason) {
|
|
103
|
+
const runSupervisor = this.#getRunSupervisor();
|
|
104
|
+
if (!runSupervisor) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (let turn = 0; turn < MAX_RECONCILE_TURNS; turn += 1) {
|
|
108
|
+
const rows = await this.#runQueue.list({ limit: 500 });
|
|
109
|
+
const plan = reconcileRunQueue(rows, this.#interRootQueuePolicy);
|
|
110
|
+
if (plan.activate_queue_ids.length === 0 && plan.launch_queue_ids.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (plan.activate_queue_ids.length > 0) {
|
|
114
|
+
for (const queueId of plan.activate_queue_ids) {
|
|
115
|
+
await this.#runQueue.claim({
|
|
116
|
+
queueId,
|
|
117
|
+
operationId: `reconcile:${reason}:activate:${queueId}:${turn}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
for (const queueId of plan.launch_queue_ids) {
|
|
123
|
+
const row = await this.#runQueue.get(queueId);
|
|
124
|
+
if (!row || row.state !== "active") {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (row.job_id) {
|
|
128
|
+
const existing = runSupervisor.get(row.job_id);
|
|
129
|
+
if (existing) {
|
|
130
|
+
await this.#runQueue.applyRunSnapshot({
|
|
131
|
+
queueId: row.queue_id,
|
|
132
|
+
run: existing,
|
|
133
|
+
operationId: `reconcile:${reason}:existing:${row.queue_id}:${existing.updated_at_ms}`,
|
|
134
|
+
});
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
await this.#launchQueueEntry(row, `reconcile:${reason}:launch:${turn}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async scheduleReconcile(reason) {
|
|
143
|
+
const token = `${++this.#queueReconcileCounter}:${reason}`;
|
|
144
|
+
this.#queueReconcileTail = this.#queueReconcileTail.then(async () => await this.#reconcileQueuedRunsNow(token), async () => await this.#reconcileQueuedRunsNow(token));
|
|
145
|
+
return await this.#queueReconcileTail;
|
|
146
|
+
}
|
|
147
|
+
async launchQueuedRun(launchOpts) {
|
|
148
|
+
const runSupervisor = this.#getRunSupervisor();
|
|
149
|
+
if (!runSupervisor) {
|
|
150
|
+
throw new Error("run_supervisor_unavailable");
|
|
151
|
+
}
|
|
152
|
+
const dedupeKey = launchOpts.dedupeKey.trim();
|
|
153
|
+
if (!dedupeKey) {
|
|
154
|
+
throw new Error("run_queue_dedupe_key_required");
|
|
155
|
+
}
|
|
156
|
+
const maxSteps = toPositiveInt(launchOpts.maxSteps, this.#defaultRunMaxSteps);
|
|
157
|
+
const queued = await this.#runQueue.enqueue({
|
|
158
|
+
mode: launchOpts.mode,
|
|
159
|
+
prompt: launchOpts.mode === "run_start" ? (launchOpts.prompt ?? null) : null,
|
|
160
|
+
rootIssueId: launchOpts.mode === "run_resume" ? (launchOpts.rootIssueId ?? null) : null,
|
|
161
|
+
maxSteps,
|
|
162
|
+
commandId: launchOpts.command?.command_id ?? null,
|
|
163
|
+
source: launchOpts.source,
|
|
164
|
+
dedupeKey,
|
|
165
|
+
operationId: `enqueue:${dedupeKey}`,
|
|
166
|
+
});
|
|
167
|
+
if (launchOpts.command?.command_id) {
|
|
168
|
+
this.#queuedCommandById.set(launchOpts.command.command_id, launchOpts.command);
|
|
169
|
+
}
|
|
170
|
+
await this.scheduleReconcile(`enqueue:${queued.queue_id}`);
|
|
171
|
+
const refreshed = (await this.#runQueue.get(queued.queue_id)) ?? queued;
|
|
172
|
+
const runtime = refreshed.job_id ? (runSupervisor.get(refreshed.job_id) ?? null) : null;
|
|
173
|
+
return runSnapshotFromQueueSnapshot(refreshed, runtime);
|
|
174
|
+
}
|
|
175
|
+
async launchQueuedRunFromCommand(record) {
|
|
176
|
+
if (record.target_type === "run start") {
|
|
177
|
+
const prompt = record.command_args.join(" ").trim();
|
|
178
|
+
if (!prompt) {
|
|
179
|
+
throw new Error("run_start_prompt_required");
|
|
180
|
+
}
|
|
181
|
+
return await this.launchQueuedRun({
|
|
182
|
+
mode: "run_start",
|
|
183
|
+
prompt,
|
|
184
|
+
source: "command",
|
|
185
|
+
command: record,
|
|
186
|
+
dedupeKey: `command:${record.command_id}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (record.target_type === "run resume") {
|
|
190
|
+
const fallbackRoot = normalizeIssueId(record.target_id);
|
|
191
|
+
const explicitRoot = normalizeIssueId(record.command_args[0] ?? "") ?? fallbackRoot;
|
|
192
|
+
if (!explicitRoot) {
|
|
193
|
+
throw new Error("run_resume_invalid_root_issue_id");
|
|
194
|
+
}
|
|
195
|
+
const maxSteps = toPositiveInt(record.command_args[1], this.#defaultRunMaxSteps);
|
|
196
|
+
return await this.launchQueuedRun({
|
|
197
|
+
mode: "run_resume",
|
|
198
|
+
rootIssueId: explicitRoot,
|
|
199
|
+
maxSteps,
|
|
200
|
+
source: "command",
|
|
201
|
+
command: record,
|
|
202
|
+
dedupeKey: `command:${record.command_id}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
throw new Error("run_queue_invalid_command_target");
|
|
206
|
+
}
|
|
207
|
+
async interruptQueuedRun(opts) {
|
|
208
|
+
const runSupervisor = this.#getRunSupervisor();
|
|
209
|
+
const result = runSupervisor?.interrupt(opts) ?? { ok: false, reason: "not_found", run: null };
|
|
210
|
+
if (result.run) {
|
|
211
|
+
await this.#runQueue
|
|
212
|
+
.applyRunSnapshot({
|
|
213
|
+
run: result.run,
|
|
214
|
+
operationId: `interrupt-sync:${result.run.job_id}:${result.run.updated_at_ms}`,
|
|
215
|
+
createIfMissing: true,
|
|
216
|
+
})
|
|
217
|
+
.catch(() => {
|
|
218
|
+
// Best effort only.
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (result.ok) {
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
const target = opts.jobId?.trim() || opts.rootIssueId?.trim() || "";
|
|
225
|
+
if (!target) {
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
const queued = await this.#runQueue.get(target);
|
|
229
|
+
if (!queued) {
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
if (queued.state === "queued" ||
|
|
233
|
+
queued.state === "active" ||
|
|
234
|
+
queued.state === "waiting_review" ||
|
|
235
|
+
queued.state === "refining") {
|
|
236
|
+
const cancelled = await this.#runQueue.transition({
|
|
237
|
+
queueId: queued.queue_id,
|
|
238
|
+
toState: "cancelled",
|
|
239
|
+
operationId: `interrupt-cancel:${target}`,
|
|
240
|
+
});
|
|
241
|
+
await this.scheduleReconcile(`interrupt:${queued.queue_id}`);
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
reason: null,
|
|
245
|
+
run: runSnapshotFromQueueSnapshot(cancelled),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
ok: false,
|
|
250
|
+
reason: "not_running",
|
|
251
|
+
run: runSnapshotFromQueueSnapshot(queued),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
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
|
+
async onRunEvent(event) {
|
|
307
|
+
const queueEventSnapshot = await this.#runQueue
|
|
308
|
+
.applyRunSnapshot({
|
|
309
|
+
run: event.run,
|
|
310
|
+
operationId: `run-event:${event.seq}:${event.kind}`,
|
|
311
|
+
createIfMissing: false,
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {
|
|
314
|
+
// Best effort queue reconciliation from runtime events.
|
|
315
|
+
return null;
|
|
316
|
+
});
|
|
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
|
+
if (event.kind === "run_completed" || event.kind === "run_failed" || event.kind === "run_cancelled") {
|
|
321
|
+
await this.scheduleReconcile(`terminal:${event.kind}:${event.run.job_id}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
stop() {
|
|
325
|
+
this.#queuedCommandById.clear();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, type ControlPlaneSignalObserver, type ReloadableGenerationIdentity, TelegramControlPlaneAdapter } from "@femtomc/mu-control-plane";
|
|
2
|
+
import type { ControlPlaneConfig, TelegramGenerationReloadResult, TelegramGenerationSwapHooks } from "./control_plane_contract.js";
|
|
3
|
+
export declare class TelegramAdapterGenerationManager {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(opts: {
|
|
6
|
+
pipeline: ControlPlaneCommandPipeline;
|
|
7
|
+
outbox: ControlPlaneOutbox;
|
|
8
|
+
initialConfig: ControlPlaneConfig;
|
|
9
|
+
nowMs?: () => number;
|
|
10
|
+
onOutboxEnqueued?: () => void;
|
|
11
|
+
signalObserver?: ControlPlaneSignalObserver;
|
|
12
|
+
hooks?: TelegramGenerationSwapHooks;
|
|
13
|
+
});
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
hasActiveGeneration(): boolean;
|
|
16
|
+
activeGeneration(): ReloadableGenerationIdentity | null;
|
|
17
|
+
activeBotToken(): string | null;
|
|
18
|
+
activeAdapter(): TelegramControlPlaneAdapter | null;
|
|
19
|
+
canHandleConfig(nextConfig: ControlPlaneConfig, reason: string): boolean;
|
|
20
|
+
reload(opts: {
|
|
21
|
+
config: ControlPlaneConfig;
|
|
22
|
+
reason: string;
|
|
23
|
+
warmupTimeoutMs?: number;
|
|
24
|
+
drainTimeoutMs?: number;
|
|
25
|
+
}): Promise<TelegramGenerationReloadResult>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
}
|