@femtomc/mu-server 26.2.70 → 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.
Files changed (42) hide show
  1. package/dist/api/activities.d.ts +2 -0
  2. package/dist/api/activities.js +160 -0
  3. package/dist/api/config.d.ts +2 -0
  4. package/dist/api/config.js +45 -0
  5. package/dist/api/control_plane.d.ts +2 -0
  6. package/dist/api/control_plane.js +28 -0
  7. package/dist/api/cron.d.ts +2 -0
  8. package/dist/api/cron.js +182 -0
  9. package/dist/api/heartbeats.d.ts +2 -0
  10. package/dist/api/heartbeats.js +211 -0
  11. package/dist/api/identities.d.ts +2 -0
  12. package/dist/api/identities.js +103 -0
  13. package/dist/api/runs.d.ts +2 -0
  14. package/dist/api/runs.js +207 -0
  15. package/dist/cli.js +58 -3
  16. package/dist/config.d.ts +4 -21
  17. package/dist/config.js +24 -75
  18. package/dist/control_plane.d.ts +4 -2
  19. package/dist/control_plane.js +226 -25
  20. package/dist/control_plane_bootstrap_helpers.d.ts +2 -1
  21. package/dist/control_plane_bootstrap_helpers.js +11 -1
  22. package/dist/control_plane_contract.d.ts +57 -0
  23. package/dist/control_plane_contract.js +1 -1
  24. package/dist/control_plane_reload.d.ts +63 -0
  25. package/dist/control_plane_reload.js +525 -0
  26. package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
  27. package/dist/control_plane_run_queue_coordinator.js +327 -0
  28. package/dist/control_plane_telegram_generation.js +0 -1
  29. package/dist/control_plane_wake_delivery.d.ts +50 -0
  30. package/dist/control_plane_wake_delivery.js +123 -0
  31. package/dist/index.d.ts +4 -1
  32. package/dist/index.js +2 -0
  33. package/dist/run_queue.d.ts +95 -0
  34. package/dist/run_queue.js +817 -0
  35. package/dist/run_supervisor.d.ts +20 -0
  36. package/dist/run_supervisor.js +25 -1
  37. package/dist/server.d.ts +5 -10
  38. package/dist/server.js +337 -528
  39. package/dist/server_program_orchestration.js +2 -0
  40. package/dist/server_routing.d.ts +3 -2
  41. package/dist/server_routing.js +28 -900
  42. 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
+ }
@@ -10,7 +10,6 @@ function controlPlaneNonTelegramFingerprint(config) {
10
10
  adapters: {
11
11
  slack: config.adapters.slack,
12
12
  discord: config.adapters.discord,
13
- gmail: config.adapters.gmail,
14
13
  },
15
14
  operator: config.operator,
16
15
  });
@@ -0,0 +1,50 @@
1
+ import type { Channel, IdentityBinding, OutboundEnvelope } from "@femtomc/mu-control-plane";
2
+ export type WakeFanoutSkipReasonCode = "channel_delivery_unsupported" | "telegram_bot_token_missing";
3
+ export type WakeFanoutContext = {
4
+ wakeId: string;
5
+ dedupeKey: string;
6
+ wakeSource: string | null;
7
+ programId: string | null;
8
+ sourceTsMs: number | null;
9
+ };
10
+ export type WakeDeliveryMetadata = {
11
+ wakeId: string;
12
+ wakeDedupeKey: string;
13
+ bindingId: string;
14
+ channel: Channel;
15
+ outboxId: string;
16
+ outboxDedupeKey: string;
17
+ };
18
+ export declare function wakeFanoutDedupeKey(opts: {
19
+ dedupeKey: string;
20
+ wakeId: string;
21
+ binding: Pick<IdentityBinding, "channel" | "binding_id">;
22
+ }): string;
23
+ export declare function resolveWakeFanoutCapability(opts: {
24
+ binding: IdentityBinding;
25
+ isChannelDeliverySupported: (channel: Channel) => boolean;
26
+ telegramBotToken: string | null;
27
+ }): {
28
+ ok: true;
29
+ } | {
30
+ ok: false;
31
+ reasonCode: WakeFanoutSkipReasonCode;
32
+ };
33
+ export declare function buildWakeOutboundEnvelope(opts: {
34
+ repoRoot: string;
35
+ nowMs: number;
36
+ message: string;
37
+ binding: IdentityBinding;
38
+ context: WakeFanoutContext;
39
+ metadata?: Record<string, unknown>;
40
+ }): OutboundEnvelope;
41
+ export declare function wakeDeliveryMetadataFromOutboxRecord(record: {
42
+ outbox_id: string;
43
+ dedupe_key: string;
44
+ envelope: Pick<OutboundEnvelope, "channel" | "metadata">;
45
+ }): WakeDeliveryMetadata | null;
46
+ export declare function wakeDispatchReasonCode(opts: {
47
+ state: "delivered" | "retried" | "dead_letter";
48
+ lastError: string | null;
49
+ deadLetterReason: string | null;
50
+ }): string;
@@ -0,0 +1,123 @@
1
+ function sha256Hex(input) {
2
+ const hasher = new Bun.CryptoHasher("sha256");
3
+ hasher.update(input);
4
+ return hasher.digest("hex");
5
+ }
6
+ function normalizeString(value) {
7
+ if (typeof value !== "string") {
8
+ return null;
9
+ }
10
+ const trimmed = value.trim();
11
+ return trimmed.length > 0 ? trimmed : null;
12
+ }
13
+ function normalizeChannel(value) {
14
+ switch (value) {
15
+ case "slack":
16
+ case "discord":
17
+ case "telegram":
18
+ case "terminal":
19
+ return value;
20
+ default:
21
+ return null;
22
+ }
23
+ }
24
+ export function wakeFanoutDedupeKey(opts) {
25
+ const base = opts.dedupeKey.trim().length > 0 ? opts.dedupeKey.trim() : `wake:${opts.wakeId}`;
26
+ return `${base}:wake:${opts.wakeId}:${opts.binding.channel}:${opts.binding.binding_id}`;
27
+ }
28
+ export function resolveWakeFanoutCapability(opts) {
29
+ const { binding } = opts;
30
+ if (!opts.isChannelDeliverySupported(binding.channel)) {
31
+ return { ok: false, reasonCode: "channel_delivery_unsupported" };
32
+ }
33
+ if (binding.channel === "telegram" && (!opts.telegramBotToken || opts.telegramBotToken.trim().length === 0)) {
34
+ return { ok: false, reasonCode: "telegram_bot_token_missing" };
35
+ }
36
+ return { ok: true };
37
+ }
38
+ export function buildWakeOutboundEnvelope(opts) {
39
+ const channelConversationId = opts.binding.channel_actor_id;
40
+ const requestId = `wake-req-${sha256Hex(`${opts.context.wakeId}:${opts.binding.binding_id}`).slice(0, 20)}`;
41
+ const responseId = `wake-resp-${sha256Hex(`${opts.context.wakeId}:${opts.binding.binding_id}:${opts.nowMs}`).slice(0, 20)}`;
42
+ return {
43
+ v: 1,
44
+ ts_ms: opts.nowMs,
45
+ channel: opts.binding.channel,
46
+ channel_tenant_id: opts.binding.channel_tenant_id,
47
+ channel_conversation_id: channelConversationId,
48
+ request_id: requestId,
49
+ response_id: responseId,
50
+ kind: "lifecycle",
51
+ body: opts.message,
52
+ correlation: {
53
+ command_id: `wake-${opts.context.wakeId}-${opts.binding.binding_id}`,
54
+ idempotency_key: `wake-idem-${opts.context.wakeId}-${opts.binding.binding_id}`,
55
+ request_id: requestId,
56
+ channel: opts.binding.channel,
57
+ channel_tenant_id: opts.binding.channel_tenant_id,
58
+ channel_conversation_id: channelConversationId,
59
+ actor_id: opts.binding.channel_actor_id,
60
+ actor_binding_id: opts.binding.binding_id,
61
+ assurance_tier: opts.binding.assurance_tier,
62
+ repo_root: opts.repoRoot,
63
+ scope_required: "cp.ops.admin",
64
+ scope_effective: "cp.ops.admin",
65
+ target_type: "operator_wake",
66
+ target_id: opts.context.wakeId,
67
+ attempt: 1,
68
+ state: "completed",
69
+ error_code: null,
70
+ operator_session_id: null,
71
+ operator_turn_id: null,
72
+ cli_invocation_id: null,
73
+ cli_command_kind: null,
74
+ run_root_id: null,
75
+ },
76
+ metadata: {
77
+ wake_delivery: true,
78
+ wake_id: opts.context.wakeId,
79
+ wake_dedupe_key: opts.context.dedupeKey,
80
+ wake_binding_id: opts.binding.binding_id,
81
+ wake_channel: opts.binding.channel,
82
+ wake_source: opts.context.wakeSource,
83
+ wake_program_id: opts.context.programId,
84
+ wake_source_ts_ms: opts.context.sourceTsMs,
85
+ ...(opts.metadata ?? {}),
86
+ },
87
+ };
88
+ }
89
+ export function wakeDeliveryMetadataFromOutboxRecord(record) {
90
+ const metadata = record.envelope.metadata;
91
+ if (metadata.wake_delivery !== true) {
92
+ return null;
93
+ }
94
+ const wakeId = normalizeString(metadata.wake_id);
95
+ const bindingId = normalizeString(metadata.wake_binding_id);
96
+ const wakeDedupeKey = normalizeString(metadata.wake_dedupe_key) ?? record.dedupe_key;
97
+ const metadataChannel = normalizeChannel(metadata.wake_channel);
98
+ const envelopeChannel = normalizeChannel(record.envelope.channel);
99
+ const channel = metadataChannel ?? envelopeChannel;
100
+ if (!wakeId || !bindingId || !channel) {
101
+ return null;
102
+ }
103
+ return {
104
+ wakeId,
105
+ wakeDedupeKey,
106
+ bindingId,
107
+ channel,
108
+ outboxId: record.outbox_id,
109
+ outboxDedupeKey: record.dedupe_key,
110
+ };
111
+ }
112
+ export function wakeDispatchReasonCode(opts) {
113
+ switch (opts.state) {
114
+ case "delivered":
115
+ return "outbox_delivered";
116
+ case "retried":
117
+ return opts.lastError && opts.lastError.trim().length > 0 ? opts.lastError : "outbox_retry";
118
+ case "dead_letter":
119
+ return opts.deadLetterReason && opts.deadLetterReason.trim().length > 0
120
+ ? opts.deadLetterReason
121
+ : "outbox_dead_letter";
122
+ }
123
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,10 @@ export type { ControlPlaneActivityEvent, ControlPlaneActivityEventKind, ControlP
2
2
  export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
3
3
  export type { MuConfig, MuConfigPatch, MuConfigPresence } from "./config.js";
4
4
  export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
5
- export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, } from "./control_plane_contract.js";
5
+ export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, InterRootQueuePolicy, NotifyOperatorsOpts, NotifyOperatorsResult, OrchestrationQueueState, WakeDeliveryEvent, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
6
+ export { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS, ORCHESTRATION_QUEUE_INVARIANTS, } from "./control_plane_contract.js";
7
+ export type { DurableRunQueueClaimOpts, DurableRunQueueEnqueueOpts, DurableRunQueueOpts, DurableRunQueueSnapshot, DurableRunQueueState, DurableRunQueueTransitionOpts, RunQueueReconcilePlan, } from "./run_queue.js";
8
+ export { DurableRunQueue, queueStatesForRunStatusFilter, reconcileRunQueue, RUN_QUEUE_RECONCILE_INVARIANTS, runQueuePath, runSnapshotFromQueueSnapshot, runStatusFromQueueState, } from "./run_queue.js";
6
9
  export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
7
10
  export type { CronProgramLifecycleAction, CronProgramLifecycleEvent, CronProgramOperationResult, CronProgramRegistryOpts, CronProgramSnapshot, CronProgramStatusSnapshot, CronProgramTarget, CronProgramTickEvent, CronProgramWakeMode, } from "./cron_programs.js";
8
11
  export { CronProgramRegistry } from "./cron_programs.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export { ControlPlaneActivitySupervisor } from "./activity_supervisor.js";
2
2
  export { applyMuConfigPatch, DEFAULT_MU_CONFIG, getMuConfigPath, muConfigPresence, normalizeMuConfig, readMuConfigFile, redactMuConfigSecrets, writeMuConfigFile, } from "./config.js";
3
+ export { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, ORCHESTRATION_QUEUE_ALLOWED_TRANSITIONS, ORCHESTRATION_QUEUE_INVARIANTS, } from "./control_plane_contract.js";
4
+ export { DurableRunQueue, queueStatesForRunStatusFilter, reconcileRunQueue, RUN_QUEUE_RECONCILE_INVARIANTS, runQueuePath, runSnapshotFromQueueSnapshot, runStatusFromQueueState, } from "./run_queue.js";
3
5
  export { bootstrapControlPlane, detectAdapters } from "./control_plane.js";
4
6
  export { CronProgramRegistry } from "./cron_programs.js";
5
7
  export { computeNextScheduleRunAtMs, normalizeCronSchedule } from "./cron_schedule.js";
@@ -0,0 +1,95 @@
1
+ import type { JsonlStore } from "@femtomc/mu-core";
2
+ import { type InterRootQueuePolicy, type InterRootQueueReconcilePlan, type OrchestrationQueueState } from "@femtomc/mu-orchestrator";
3
+ import type { ControlPlaneRunMode, ControlPlaneRunSnapshot, ControlPlaneRunStatus } from "./run_supervisor.js";
4
+ export type DurableRunQueueState = OrchestrationQueueState;
5
+ export type DurableRunQueueSnapshot = {
6
+ v: 1;
7
+ queue_id: string;
8
+ dedupe_key: string;
9
+ mode: ControlPlaneRunMode;
10
+ state: DurableRunQueueState;
11
+ prompt: string | null;
12
+ root_issue_id: string | null;
13
+ max_steps: number;
14
+ command_id: string | null;
15
+ source: "command" | "api";
16
+ job_id: string | null;
17
+ started_at_ms: number | null;
18
+ updated_at_ms: number;
19
+ finished_at_ms: number | null;
20
+ exit_code: number | null;
21
+ pid: number | null;
22
+ last_progress: string | null;
23
+ created_at_ms: number;
24
+ revision: number;
25
+ applied_operation_ids: string[];
26
+ };
27
+ export type DurableRunQueueOpts = {
28
+ repoRoot: string;
29
+ nowMs?: () => number;
30
+ store?: JsonlStore<unknown>;
31
+ maxOperationIds?: number;
32
+ };
33
+ export type DurableRunQueueEnqueueOpts = {
34
+ mode: ControlPlaneRunMode;
35
+ prompt: string | null;
36
+ rootIssueId: string | null;
37
+ maxSteps?: number;
38
+ commandId?: string | null;
39
+ source: "command" | "api";
40
+ dedupeKey: string;
41
+ operationId?: string | null;
42
+ nowMs?: number;
43
+ };
44
+ export type DurableRunQueueTransitionOpts = {
45
+ queueId: string;
46
+ toState: DurableRunQueueState;
47
+ operationId?: string | null;
48
+ nowMs?: number;
49
+ };
50
+ export type DurableRunQueueClaimOpts = {
51
+ queueId?: string | null;
52
+ operationId?: string | null;
53
+ nowMs?: number;
54
+ };
55
+ export declare function runStatusFromQueueState(state: DurableRunQueueState): ControlPlaneRunStatus;
56
+ export declare function queueStatesForRunStatusFilter(status: string | null | undefined): DurableRunQueueState[] | null;
57
+ export type RunQueueReconcilePlan = InterRootQueueReconcilePlan;
58
+ export declare const RUN_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."];
59
+ /**
60
+ * Server adapter wrapper around the orchestrator-owned inter-root planner.
61
+ */
62
+ export declare function reconcileRunQueue(rows: readonly DurableRunQueueSnapshot[], policy: InterRootQueuePolicy): RunQueueReconcilePlan;
63
+ export declare function runQueuePath(repoRoot: string): string;
64
+ export declare function runSnapshotFromQueueSnapshot(queue: DurableRunQueueSnapshot, runtime?: ControlPlaneRunSnapshot | null): ControlPlaneRunSnapshot;
65
+ export declare class DurableRunQueue {
66
+ #private;
67
+ constructor(opts: DurableRunQueueOpts);
68
+ enqueue(opts: DurableRunQueueEnqueueOpts): Promise<DurableRunQueueSnapshot>;
69
+ claim(opts?: DurableRunQueueClaimOpts): Promise<DurableRunQueueSnapshot | null>;
70
+ activate(opts: DurableRunQueueClaimOpts): Promise<DurableRunQueueSnapshot | null>;
71
+ transition(opts: DurableRunQueueTransitionOpts): Promise<DurableRunQueueSnapshot>;
72
+ bindRunSnapshot(opts: {
73
+ queueId: string;
74
+ run: ControlPlaneRunSnapshot;
75
+ operationId?: string | null;
76
+ nowMs?: number;
77
+ }): Promise<DurableRunQueueSnapshot>;
78
+ applyRunSnapshot(opts: {
79
+ run: ControlPlaneRunSnapshot;
80
+ queueId?: string | null;
81
+ operationId?: string | null;
82
+ nowMs?: number;
83
+ createIfMissing?: boolean;
84
+ }): Promise<DurableRunQueueSnapshot | null>;
85
+ get(idOrRoot: string): Promise<DurableRunQueueSnapshot | null>;
86
+ list(opts?: {
87
+ states?: readonly DurableRunQueueState[];
88
+ limit?: number;
89
+ }): Promise<DurableRunQueueSnapshot[]>;
90
+ listRunSnapshots(opts?: {
91
+ status?: string;
92
+ limit?: number;
93
+ runtimeByJobId?: Map<string, ControlPlaneRunSnapshot>;
94
+ }): Promise<ControlPlaneRunSnapshot[]>;
95
+ }