@femtomc/mu-server 26.2.72 → 26.2.74
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 -49
- 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_bootstrap_helpers.js +4 -1
- package/dist/control_plane_contract.d.ts +3 -12
- package/dist/control_plane_contract.js +1 -1
- 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/orchestration_queue.d.ts +44 -0
- package/dist/orchestration_queue.js +111 -0
- package/dist/outbound_delivery_router.d.ts +12 -0
- package/dist/outbound_delivery_router.js +29 -0
- package/dist/run_queue.d.ts +1 -1
- package/dist/run_queue.js +78 -79
- package/dist/run_supervisor.d.ts +2 -17
- package/dist/run_supervisor.js +1 -71
- 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 -151
- 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 -10
- 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,44 @@
|
|
|
1
|
+
export type OrchestrationQueueState = "queued" | "active" | "waiting_review" | "refining" | "done" | "failed" | "cancelled";
|
|
2
|
+
/**
|
|
3
|
+
* Inter-root scheduler policy knob for durable queue drain.
|
|
4
|
+
* - sequential: exactly one active root (`max_active_roots=1`)
|
|
5
|
+
* - parallel: bounded fanout (`max_active_roots>=1`)
|
|
6
|
+
*/
|
|
7
|
+
export type InterRootQueuePolicy = {
|
|
8
|
+
mode: "sequential";
|
|
9
|
+
max_active_roots: 1;
|
|
10
|
+
} | {
|
|
11
|
+
mode: "parallel";
|
|
12
|
+
max_active_roots: number;
|
|
13
|
+
};
|
|
14
|
+
export declare const DEFAULT_INTER_ROOT_QUEUE_POLICY: InterRootQueuePolicy;
|
|
15
|
+
export declare function normalizeInterRootQueuePolicy(policy: InterRootQueuePolicy | null | undefined): InterRootQueuePolicy;
|
|
16
|
+
/**
|
|
17
|
+
* Allowed queue transitions. Queue tests should enforce this table exactly.
|
|
18
|
+
*/
|
|
19
|
+
export declare const ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS: Record<OrchestrationQueueState, readonly OrchestrationQueueState[]>;
|
|
20
|
+
export declare const ORCHESTRATION_QUEUE_INVARIANTS: readonly ["ORCH-QUEUE-001: Queue writes are durable before acknowledging enqueue/start/resume requests.", "ORCH-QUEUE-002: Queue dispatch must claim exactly one active item at a time per root slot.", "ORCH-QUEUE-003: Terminal states (done|failed|cancelled) are immutable.", "ORCH-QUEUE-004: Review path is active -> waiting_review -> (done | refining).", "ORCH-QUEUE-005: Refinement re-enters execution only via refining -> queued.", "ORCH-QUEUE-006: sequential policy permits <=1 active root; parallel permits <=max_active_roots active roots."];
|
|
21
|
+
export type InterRootQueueSnapshot = {
|
|
22
|
+
queue_id: string;
|
|
23
|
+
root_issue_id: string | null;
|
|
24
|
+
state: OrchestrationQueueState;
|
|
25
|
+
job_id: string | null;
|
|
26
|
+
created_at_ms: number;
|
|
27
|
+
};
|
|
28
|
+
export type InterRootQueueReconcilePlan = {
|
|
29
|
+
policy: InterRootQueuePolicy;
|
|
30
|
+
max_active_roots: number;
|
|
31
|
+
active_root_count: number;
|
|
32
|
+
available_root_slots: number;
|
|
33
|
+
activate_queue_ids: string[];
|
|
34
|
+
launch_queue_ids: string[];
|
|
35
|
+
};
|
|
36
|
+
export declare const INTER_ROOT_QUEUE_RECONCILE_INVARIANTS: readonly ["ORCH-INTER-ROOT-RECON-001: one queue snapshot + policy yields one deterministic activation/launch plan.", "ORCH-INTER-ROOT-RECON-002: activation order is FIFO (`created_at_ms`, then `queue_id`) with per-root slot dedupe.", "ORCH-INTER-ROOT-RECON-003: sequential policy admits <=1 occupied root; parallel admits <=max_active_roots roots.", "ORCH-INTER-ROOT-RECON-004: launch candidates are active rows without bound job ids, one launch per root slot."];
|
|
37
|
+
/**
|
|
38
|
+
* Deterministic inter-root queue reconcile primitive.
|
|
39
|
+
*
|
|
40
|
+
* Computes queue activation/launch intentions from durable queue state and policy. The caller is
|
|
41
|
+
* responsible for performing side effects (claim, launch, bind, transition) and reconciling again
|
|
42
|
+
* against refreshed queue/runtime snapshots.
|
|
43
|
+
*/
|
|
44
|
+
export declare function reconcileInterRootQueue<Row extends InterRootQueueSnapshot>(rows: readonly Row[], policy: InterRootQueuePolicy): InterRootQueueReconcilePlan;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export const DEFAULT_INTER_ROOT_QUEUE_POLICY = {
|
|
2
|
+
mode: "sequential",
|
|
3
|
+
max_active_roots: 1,
|
|
4
|
+
};
|
|
5
|
+
export function normalizeInterRootQueuePolicy(policy) {
|
|
6
|
+
if (!policy) {
|
|
7
|
+
return DEFAULT_INTER_ROOT_QUEUE_POLICY;
|
|
8
|
+
}
|
|
9
|
+
if (policy.mode === "parallel") {
|
|
10
|
+
return {
|
|
11
|
+
mode: "parallel",
|
|
12
|
+
max_active_roots: Math.max(1, Math.trunc(policy.max_active_roots)),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
return DEFAULT_INTER_ROOT_QUEUE_POLICY;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Allowed queue transitions. Queue tests should enforce this table exactly.
|
|
19
|
+
*/
|
|
20
|
+
export const ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS = {
|
|
21
|
+
queued: ["active", "cancelled"],
|
|
22
|
+
active: ["waiting_review", "done", "failed", "cancelled"],
|
|
23
|
+
waiting_review: ["refining", "done", "failed", "cancelled"],
|
|
24
|
+
refining: ["queued", "failed", "cancelled"],
|
|
25
|
+
done: [],
|
|
26
|
+
failed: [],
|
|
27
|
+
cancelled: [],
|
|
28
|
+
};
|
|
29
|
+
export const ORCHESTRATION_QUEUE_INVARIANTS = [
|
|
30
|
+
"ORCH-QUEUE-001: Queue writes are durable before acknowledging enqueue/start/resume requests.",
|
|
31
|
+
"ORCH-QUEUE-002: Queue dispatch must claim exactly one active item at a time per root slot.",
|
|
32
|
+
"ORCH-QUEUE-003: Terminal states (done|failed|cancelled) are immutable.",
|
|
33
|
+
"ORCH-QUEUE-004: Review path is active -> waiting_review -> (done | refining).",
|
|
34
|
+
"ORCH-QUEUE-005: Refinement re-enters execution only via refining -> queued.",
|
|
35
|
+
"ORCH-QUEUE-006: sequential policy permits <=1 active root; parallel permits <=max_active_roots active roots.",
|
|
36
|
+
];
|
|
37
|
+
const INTER_ROOT_OCCUPIED_STATES = new Set(["active", "waiting_review", "refining"]);
|
|
38
|
+
export const INTER_ROOT_QUEUE_RECONCILE_INVARIANTS = [
|
|
39
|
+
"ORCH-INTER-ROOT-RECON-001: one queue snapshot + policy yields one deterministic activation/launch plan.",
|
|
40
|
+
"ORCH-INTER-ROOT-RECON-002: activation order is FIFO (`created_at_ms`, then `queue_id`) with per-root slot dedupe.",
|
|
41
|
+
"ORCH-INTER-ROOT-RECON-003: sequential policy admits <=1 occupied root; parallel admits <=max_active_roots roots.",
|
|
42
|
+
"ORCH-INTER-ROOT-RECON-004: launch candidates are active rows without bound job ids, one launch per root slot.",
|
|
43
|
+
];
|
|
44
|
+
function stableCompare(a, b) {
|
|
45
|
+
if (a.created_at_ms !== b.created_at_ms) {
|
|
46
|
+
return a.created_at_ms - b.created_at_ms;
|
|
47
|
+
}
|
|
48
|
+
return a.queue_id.localeCompare(b.queue_id);
|
|
49
|
+
}
|
|
50
|
+
function normalizeMaxActiveRoots(policy) {
|
|
51
|
+
if (policy.mode === "parallel") {
|
|
52
|
+
return Math.max(1, Math.trunc(policy.max_active_roots));
|
|
53
|
+
}
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
function queueRootSlotKey(row) {
|
|
57
|
+
return row.root_issue_id ? `root:${row.root_issue_id}` : `queue:${row.queue_id}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Deterministic inter-root queue reconcile primitive.
|
|
61
|
+
*
|
|
62
|
+
* Computes queue activation/launch intentions from durable queue state and policy. The caller is
|
|
63
|
+
* responsible for performing side effects (claim, launch, bind, transition) and reconciling again
|
|
64
|
+
* against refreshed queue/runtime snapshots.
|
|
65
|
+
*/
|
|
66
|
+
export function reconcileInterRootQueue(rows, policy) {
|
|
67
|
+
const sorted = [...rows].sort(stableCompare);
|
|
68
|
+
const maxActiveRoots = normalizeMaxActiveRoots(policy);
|
|
69
|
+
const occupiedRoots = new Set();
|
|
70
|
+
const launchRoots = new Set();
|
|
71
|
+
const launchQueueIds = [];
|
|
72
|
+
for (const row of sorted) {
|
|
73
|
+
if (!INTER_ROOT_OCCUPIED_STATES.has(row.state)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const slotKey = queueRootSlotKey(row);
|
|
77
|
+
occupiedRoots.add(slotKey);
|
|
78
|
+
if (row.state !== "active" || row.job_id != null || launchRoots.has(slotKey)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
launchRoots.add(slotKey);
|
|
82
|
+
launchQueueIds.push(row.queue_id);
|
|
83
|
+
}
|
|
84
|
+
const availableRootSlots = Math.max(0, maxActiveRoots - occupiedRoots.size);
|
|
85
|
+
const claimedRoots = new Set();
|
|
86
|
+
const activateQueueIds = [];
|
|
87
|
+
if (availableRootSlots > 0) {
|
|
88
|
+
for (const row of sorted) {
|
|
89
|
+
if (row.state !== "queued") {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const slotKey = queueRootSlotKey(row);
|
|
93
|
+
if (occupiedRoots.has(slotKey) || claimedRoots.has(slotKey)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
claimedRoots.add(slotKey);
|
|
97
|
+
activateQueueIds.push(row.queue_id);
|
|
98
|
+
if (activateQueueIds.length >= availableRootSlots) {
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
policy,
|
|
105
|
+
max_active_roots: maxActiveRoots,
|
|
106
|
+
active_root_count: occupiedRoots.size,
|
|
107
|
+
available_root_slots: availableRootSlots,
|
|
108
|
+
activate_queue_ids: activateQueueIds,
|
|
109
|
+
launch_queue_ids: launchQueueIds,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -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_queue.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JsonlStore } from "@femtomc/mu-core";
|
|
2
|
-
import { type InterRootQueuePolicy, type InterRootQueueReconcilePlan, type OrchestrationQueueState } from "
|
|
2
|
+
import { type InterRootQueuePolicy, type InterRootQueueReconcilePlan, type OrchestrationQueueState } from "./orchestration_queue.js";
|
|
3
3
|
import type { ControlPlaneRunMode, ControlPlaneRunSnapshot, ControlPlaneRunStatus } from "./run_supervisor.js";
|
|
4
4
|
export type DurableRunQueueState = OrchestrationQueueState;
|
|
5
5
|
export type DurableRunQueueSnapshot = {
|