@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.
Files changed (58) hide show
  1. package/README.md +7 -3
  2. package/dist/api/activities.d.ts +2 -0
  3. package/dist/api/activities.js +160 -0
  4. package/dist/api/config.d.ts +2 -0
  5. package/dist/api/config.js +45 -0
  6. package/dist/api/control_plane.d.ts +2 -0
  7. package/dist/api/control_plane.js +28 -0
  8. package/dist/api/cron.d.ts +2 -0
  9. package/dist/api/cron.js +182 -0
  10. package/dist/api/events.js +77 -19
  11. package/dist/api/forum.js +52 -18
  12. package/dist/api/heartbeats.d.ts +2 -0
  13. package/dist/api/heartbeats.js +211 -0
  14. package/dist/api/identities.d.ts +2 -0
  15. package/dist/api/identities.js +103 -0
  16. package/dist/api/issues.js +120 -33
  17. package/dist/api/runs.d.ts +2 -0
  18. package/dist/api/runs.js +207 -0
  19. package/dist/cli.js +58 -3
  20. package/dist/config.d.ts +4 -21
  21. package/dist/config.js +24 -75
  22. package/dist/control_plane.d.ts +7 -114
  23. package/dist/control_plane.js +238 -654
  24. package/dist/control_plane_bootstrap_helpers.d.ts +16 -0
  25. package/dist/control_plane_bootstrap_helpers.js +85 -0
  26. package/dist/control_plane_contract.d.ts +176 -0
  27. package/dist/control_plane_contract.js +1 -0
  28. package/dist/control_plane_reload.d.ts +63 -0
  29. package/dist/control_plane_reload.js +525 -0
  30. package/dist/control_plane_run_outbox.d.ts +7 -0
  31. package/dist/control_plane_run_outbox.js +52 -0
  32. package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
  33. package/dist/control_plane_run_queue_coordinator.js +327 -0
  34. package/dist/control_plane_telegram_generation.d.ts +27 -0
  35. package/dist/control_plane_telegram_generation.js +520 -0
  36. package/dist/control_plane_wake_delivery.d.ts +50 -0
  37. package/dist/control_plane_wake_delivery.js +123 -0
  38. package/dist/cron_request.d.ts +8 -0
  39. package/dist/cron_request.js +65 -0
  40. package/dist/index.d.ts +7 -2
  41. package/dist/index.js +4 -1
  42. package/dist/run_queue.d.ts +95 -0
  43. package/dist/run_queue.js +817 -0
  44. package/dist/run_supervisor.d.ts +20 -0
  45. package/dist/run_supervisor.js +25 -1
  46. package/dist/server.d.ts +12 -49
  47. package/dist/server.js +365 -2128
  48. package/dist/server_program_orchestration.d.ts +38 -0
  49. package/dist/server_program_orchestration.js +254 -0
  50. package/dist/server_routing.d.ts +31 -0
  51. package/dist/server_routing.js +230 -0
  52. package/dist/server_runtime.d.ts +30 -0
  53. package/dist/server_runtime.js +43 -0
  54. package/dist/server_types.d.ts +3 -0
  55. package/dist/server_types.js +16 -0
  56. package/dist/session_lifecycle.d.ts +11 -0
  57. package/dist/session_lifecycle.js +149 -0
  58. package/package.json +7 -6
@@ -0,0 +1,525 @@
1
+ import { ControlPlaneGenerationSupervisor } from "./generation_supervisor.js";
2
+ export function summarizeControlPlane(handle) {
3
+ if (!handle) {
4
+ return { active: false, adapters: [], routes: [] };
5
+ }
6
+ return {
7
+ active: handle.activeAdapters.length > 0,
8
+ adapters: handle.activeAdapters.map((adapter) => adapter.name),
9
+ routes: handle.activeAdapters.map((adapter) => ({ name: adapter.name, route: adapter.route })),
10
+ };
11
+ }
12
+ function describeError(err) {
13
+ if (err instanceof Error)
14
+ return err.message;
15
+ return String(err);
16
+ }
17
+ export function createReloadManager(deps) {
18
+ let controlPlaneCurrent = deps.initialControlPlane;
19
+ let reloadInFlight = null;
20
+ const generationTelemetry = deps.generationTelemetry;
21
+ const generationSupervisor = new ControlPlaneGenerationSupervisor({
22
+ supervisorId: "control-plane",
23
+ initialGeneration: controlPlaneCurrent
24
+ ? {
25
+ generation_id: "control-plane-gen-0",
26
+ generation_seq: 0,
27
+ }
28
+ : null,
29
+ });
30
+ const generationTagsFor = (generation, component) => ({
31
+ generation_id: generation.generation_id,
32
+ generation_seq: generation.generation_seq,
33
+ supervisor: "control_plane",
34
+ component,
35
+ });
36
+ const performControlPlaneReload = async (reason) => {
37
+ const startedAtMs = Date.now();
38
+ const planned = generationSupervisor.beginReload(reason);
39
+ const attempt = planned.attempt;
40
+ const previous = controlPlaneCurrent;
41
+ const previousSummary = summarizeControlPlane(previous);
42
+ const tags = generationTagsFor(attempt.to_generation, "server.reload");
43
+ const baseFields = {
44
+ reason,
45
+ attempt_id: attempt.attempt_id,
46
+ coalesced: planned.coalesced,
47
+ from_generation_id: attempt.from_generation?.generation_id ?? null,
48
+ };
49
+ const logLifecycle = (opts) => {
50
+ generationTelemetry.log({
51
+ level: opts.level,
52
+ message: `reload transition ${opts.stage}:${opts.state}`,
53
+ fields: {
54
+ ...tags,
55
+ ...baseFields,
56
+ ...(opts.extra ?? {}),
57
+ },
58
+ });
59
+ };
60
+ let swapped = false;
61
+ let failedStage = "warmup";
62
+ let drainDurationMs = 0;
63
+ let drainStartedAtMs = null;
64
+ let nextHandle = null;
65
+ try {
66
+ logLifecycle({ level: "info", stage: "warmup", state: "start" });
67
+ const latestConfig = await deps.loadConfigFromDisk();
68
+ const telegramGeneration = (await previous?.reloadTelegramGeneration?.({
69
+ config: latestConfig.control_plane,
70
+ reason,
71
+ })) ?? null;
72
+ if (telegramGeneration?.handled) {
73
+ if (telegramGeneration.warmup) {
74
+ logLifecycle({
75
+ level: telegramGeneration.warmup.ok ? "info" : "error",
76
+ stage: "warmup",
77
+ state: telegramGeneration.warmup.ok ? "complete" : "failed",
78
+ extra: {
79
+ warmup_elapsed_ms: telegramGeneration.warmup.elapsed_ms,
80
+ error: telegramGeneration.warmup.error,
81
+ telegram_generation_id: telegramGeneration.to_generation?.generation_id ?? null,
82
+ },
83
+ });
84
+ }
85
+ else {
86
+ logLifecycle({
87
+ level: "info",
88
+ stage: "warmup",
89
+ state: "skipped",
90
+ extra: {
91
+ warmup_reason: "telegram_generation_no_warmup",
92
+ telegram_generation_id: telegramGeneration.to_generation?.generation_id ?? null,
93
+ },
94
+ });
95
+ }
96
+ if (telegramGeneration.cutover) {
97
+ logLifecycle({ level: "info", stage: "cutover", state: "start" });
98
+ logLifecycle({
99
+ level: telegramGeneration.cutover.ok ? "info" : "error",
100
+ stage: "cutover",
101
+ state: telegramGeneration.cutover.ok ? "complete" : "failed",
102
+ extra: {
103
+ cutover_elapsed_ms: telegramGeneration.cutover.elapsed_ms,
104
+ error: telegramGeneration.cutover.error,
105
+ active_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
106
+ },
107
+ });
108
+ }
109
+ else {
110
+ logLifecycle({
111
+ level: "info",
112
+ stage: "cutover",
113
+ state: "skipped",
114
+ extra: {
115
+ cutover_reason: "telegram_generation_no_cutover",
116
+ active_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
117
+ },
118
+ });
119
+ }
120
+ if (telegramGeneration.drain) {
121
+ logLifecycle({ level: "info", stage: "drain", state: "start" });
122
+ drainDurationMs = Math.max(0, Math.trunc(telegramGeneration.drain.elapsed_ms));
123
+ generationTelemetry.recordDrainDuration(tags, {
124
+ durationMs: drainDurationMs,
125
+ timedOut: telegramGeneration.drain.timed_out,
126
+ metadata: {
127
+ ...baseFields,
128
+ telegram_forced_stop: telegramGeneration.drain.forced_stop,
129
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
130
+ },
131
+ });
132
+ logLifecycle({
133
+ level: telegramGeneration.drain.ok ? "info" : "warn",
134
+ stage: "drain",
135
+ state: telegramGeneration.drain.ok ? "complete" : "failed",
136
+ extra: {
137
+ drain_duration_ms: telegramGeneration.drain.elapsed_ms,
138
+ drain_timed_out: telegramGeneration.drain.timed_out,
139
+ forced_stop: telegramGeneration.drain.forced_stop,
140
+ error: telegramGeneration.drain.error,
141
+ },
142
+ });
143
+ }
144
+ else {
145
+ logLifecycle({
146
+ level: "info",
147
+ stage: "drain",
148
+ state: "skipped",
149
+ extra: {
150
+ drain_reason: "telegram_generation_no_drain",
151
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
152
+ },
153
+ });
154
+ }
155
+ const shouldLogRollbackStart = telegramGeneration.rollback.requested ||
156
+ telegramGeneration.rollback.attempted ||
157
+ telegramGeneration.rollback.trigger != null ||
158
+ !telegramGeneration.ok;
159
+ if (shouldLogRollbackStart) {
160
+ logLifecycle({
161
+ level: telegramGeneration.rollback.ok ? "warn" : "error",
162
+ stage: "rollback",
163
+ state: "start",
164
+ extra: {
165
+ rollback_requested: telegramGeneration.rollback.requested,
166
+ rollback_trigger: telegramGeneration.rollback.trigger,
167
+ rollback_attempted: telegramGeneration.rollback.attempted,
168
+ },
169
+ });
170
+ logLifecycle({
171
+ level: telegramGeneration.rollback.ok ? "info" : "error",
172
+ stage: "rollback",
173
+ state: telegramGeneration.rollback.ok ? "complete" : "failed",
174
+ extra: {
175
+ rollback_requested: telegramGeneration.rollback.requested,
176
+ rollback_trigger: telegramGeneration.rollback.trigger,
177
+ rollback_attempted: telegramGeneration.rollback.attempted,
178
+ error: telegramGeneration.rollback.error,
179
+ },
180
+ });
181
+ }
182
+ else {
183
+ logLifecycle({
184
+ level: "debug",
185
+ stage: "rollback",
186
+ state: "skipped",
187
+ extra: {
188
+ rollback_reason: "not_requested",
189
+ },
190
+ });
191
+ }
192
+ if (telegramGeneration.ok) {
193
+ swapped = generationSupervisor.markSwapInstalled(attempt.attempt_id);
194
+ generationSupervisor.finishReload(attempt.attempt_id, "success");
195
+ const elapsedMs = Math.max(0, Date.now() - startedAtMs);
196
+ generationTelemetry.recordReloadSuccess(tags, {
197
+ ...baseFields,
198
+ elapsed_ms: elapsedMs,
199
+ drain_duration_ms: drainDurationMs,
200
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
201
+ telegram_rollback_attempted: telegramGeneration.rollback.attempted,
202
+ telegram_rollback_trigger: telegramGeneration.rollback.trigger,
203
+ });
204
+ generationTelemetry.trace({
205
+ name: "control_plane.reload",
206
+ status: "ok",
207
+ durationMs: elapsedMs,
208
+ fields: {
209
+ ...tags,
210
+ ...baseFields,
211
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
212
+ },
213
+ });
214
+ return {
215
+ ok: true,
216
+ reason,
217
+ previous_control_plane: previousSummary,
218
+ control_plane: summarizeControlPlane(controlPlaneCurrent),
219
+ generation: {
220
+ attempt_id: attempt.attempt_id,
221
+ coalesced: planned.coalesced,
222
+ from_generation: attempt.from_generation,
223
+ to_generation: attempt.to_generation,
224
+ active_generation: generationSupervisor.activeGeneration(),
225
+ outcome: "success",
226
+ },
227
+ telegram_generation: telegramGeneration,
228
+ };
229
+ }
230
+ generationSupervisor.finishReload(attempt.attempt_id, "failure");
231
+ const error = telegramGeneration.error ?? "telegram_generation_reload_failed";
232
+ const elapsedMs = Math.max(0, Date.now() - startedAtMs);
233
+ generationTelemetry.recordReloadFailure(tags, {
234
+ ...baseFields,
235
+ elapsed_ms: elapsedMs,
236
+ drain_duration_ms: drainDurationMs,
237
+ error,
238
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
239
+ telegram_rollback_trigger: telegramGeneration.rollback.trigger,
240
+ });
241
+ generationTelemetry.trace({
242
+ name: "control_plane.reload",
243
+ status: "error",
244
+ durationMs: elapsedMs,
245
+ fields: {
246
+ ...tags,
247
+ ...baseFields,
248
+ error,
249
+ telegram_generation_id: telegramGeneration.active_generation?.generation_id ?? null,
250
+ telegram_rollback_trigger: telegramGeneration.rollback.trigger,
251
+ },
252
+ });
253
+ return {
254
+ ok: false,
255
+ reason,
256
+ previous_control_plane: previousSummary,
257
+ control_plane: summarizeControlPlane(controlPlaneCurrent),
258
+ generation: {
259
+ attempt_id: attempt.attempt_id,
260
+ coalesced: planned.coalesced,
261
+ from_generation: attempt.from_generation,
262
+ to_generation: attempt.to_generation,
263
+ active_generation: generationSupervisor.activeGeneration(),
264
+ outcome: "failure",
265
+ },
266
+ telegram_generation: telegramGeneration,
267
+ error,
268
+ };
269
+ }
270
+ const next = await deps.controlPlaneReloader({
271
+ repoRoot: deps.repoRoot,
272
+ previous,
273
+ config: latestConfig.control_plane,
274
+ generation: attempt.to_generation,
275
+ });
276
+ nextHandle = next;
277
+ logLifecycle({ level: "info", stage: "warmup", state: "complete" });
278
+ failedStage = "cutover";
279
+ logLifecycle({ level: "info", stage: "cutover", state: "start" });
280
+ controlPlaneCurrent = next;
281
+ swapped = generationSupervisor.markSwapInstalled(attempt.attempt_id);
282
+ logLifecycle({
283
+ level: "info",
284
+ stage: "cutover",
285
+ state: "complete",
286
+ extra: {
287
+ active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
288
+ },
289
+ });
290
+ failedStage = "drain";
291
+ if (previous && previous !== next) {
292
+ logLifecycle({ level: "info", stage: "drain", state: "start" });
293
+ drainStartedAtMs = Date.now();
294
+ await previous.stop();
295
+ drainDurationMs = Math.max(0, Date.now() - drainStartedAtMs);
296
+ generationTelemetry.recordDrainDuration(tags, {
297
+ durationMs: drainDurationMs,
298
+ metadata: {
299
+ ...baseFields,
300
+ },
301
+ });
302
+ logLifecycle({
303
+ level: "info",
304
+ stage: "drain",
305
+ state: "complete",
306
+ extra: {
307
+ drain_duration_ms: drainDurationMs,
308
+ },
309
+ });
310
+ }
311
+ else {
312
+ logLifecycle({
313
+ level: "info",
314
+ stage: "drain",
315
+ state: "skipped",
316
+ extra: {
317
+ drain_reason: "no_previous_generation",
318
+ },
319
+ });
320
+ }
321
+ logLifecycle({
322
+ level: "debug",
323
+ stage: "rollback",
324
+ state: "skipped",
325
+ extra: {
326
+ rollback_reason: "not_requested",
327
+ },
328
+ });
329
+ generationSupervisor.finishReload(attempt.attempt_id, "success");
330
+ const elapsedMs = Math.max(0, Date.now() - startedAtMs);
331
+ generationTelemetry.recordReloadSuccess(tags, {
332
+ ...baseFields,
333
+ elapsed_ms: elapsedMs,
334
+ drain_duration_ms: drainDurationMs,
335
+ });
336
+ generationTelemetry.trace({
337
+ name: "control_plane.reload",
338
+ status: "ok",
339
+ durationMs: elapsedMs,
340
+ fields: {
341
+ ...tags,
342
+ ...baseFields,
343
+ },
344
+ });
345
+ return {
346
+ ok: true,
347
+ reason,
348
+ previous_control_plane: previousSummary,
349
+ control_plane: summarizeControlPlane(next),
350
+ generation: {
351
+ attempt_id: attempt.attempt_id,
352
+ coalesced: planned.coalesced,
353
+ from_generation: attempt.from_generation,
354
+ to_generation: attempt.to_generation,
355
+ active_generation: generationSupervisor.activeGeneration(),
356
+ outcome: "success",
357
+ },
358
+ };
359
+ }
360
+ catch (err) {
361
+ const error = describeError(err);
362
+ if (failedStage === "drain" && drainStartedAtMs != null) {
363
+ drainDurationMs = Math.max(0, Date.now() - drainStartedAtMs);
364
+ generationTelemetry.recordDrainDuration(tags, {
365
+ durationMs: drainDurationMs,
366
+ metadata: {
367
+ ...baseFields,
368
+ error,
369
+ },
370
+ });
371
+ }
372
+ logLifecycle({
373
+ level: "error",
374
+ stage: failedStage,
375
+ state: "failed",
376
+ extra: {
377
+ error,
378
+ drain_duration_ms: failedStage === "drain" ? drainDurationMs : undefined,
379
+ },
380
+ });
381
+ if (swapped) {
382
+ logLifecycle({
383
+ level: "warn",
384
+ stage: "rollback",
385
+ state: "start",
386
+ extra: {
387
+ rollback_reason: "reload_failed_after_cutover",
388
+ rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
389
+ rollback_source_generation_id: attempt.to_generation.generation_id,
390
+ },
391
+ });
392
+ if (!previous) {
393
+ logLifecycle({
394
+ level: "error",
395
+ stage: "rollback",
396
+ state: "failed",
397
+ extra: {
398
+ rollback_reason: "no_previous_generation",
399
+ rollback_source_generation_id: attempt.to_generation.generation_id,
400
+ },
401
+ });
402
+ }
403
+ else {
404
+ try {
405
+ const restored = generationSupervisor.rollbackSwapInstalled(attempt.attempt_id);
406
+ if (!restored) {
407
+ throw new Error("generation_rollback_state_mismatch");
408
+ }
409
+ controlPlaneCurrent = previous;
410
+ if (nextHandle && nextHandle !== previous) {
411
+ await nextHandle.stop();
412
+ }
413
+ logLifecycle({
414
+ level: "info",
415
+ stage: "rollback",
416
+ state: "complete",
417
+ extra: {
418
+ active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
419
+ rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
420
+ },
421
+ });
422
+ }
423
+ catch (rollbackErr) {
424
+ logLifecycle({
425
+ level: "error",
426
+ stage: "rollback",
427
+ state: "failed",
428
+ extra: {
429
+ error: describeError(rollbackErr),
430
+ active_generation_id: generationSupervisor.activeGeneration()?.generation_id ?? null,
431
+ rollback_target_generation_id: attempt.from_generation?.generation_id ?? null,
432
+ rollback_source_generation_id: attempt.to_generation.generation_id,
433
+ },
434
+ });
435
+ }
436
+ }
437
+ }
438
+ else {
439
+ logLifecycle({
440
+ level: "debug",
441
+ stage: "rollback",
442
+ state: "skipped",
443
+ extra: {
444
+ rollback_reason: "cutover_not_installed",
445
+ },
446
+ });
447
+ }
448
+ generationSupervisor.finishReload(attempt.attempt_id, "failure");
449
+ const elapsedMs = Math.max(0, Date.now() - startedAtMs);
450
+ generationTelemetry.recordReloadFailure(tags, {
451
+ ...baseFields,
452
+ elapsed_ms: elapsedMs,
453
+ drain_duration_ms: drainDurationMs,
454
+ error,
455
+ });
456
+ generationTelemetry.trace({
457
+ name: "control_plane.reload",
458
+ status: "error",
459
+ durationMs: elapsedMs,
460
+ fields: {
461
+ ...tags,
462
+ ...baseFields,
463
+ error,
464
+ },
465
+ });
466
+ return {
467
+ ok: false,
468
+ reason,
469
+ previous_control_plane: previousSummary,
470
+ control_plane: summarizeControlPlane(controlPlaneCurrent),
471
+ generation: {
472
+ attempt_id: attempt.attempt_id,
473
+ coalesced: planned.coalesced,
474
+ from_generation: attempt.from_generation,
475
+ to_generation: attempt.to_generation,
476
+ active_generation: generationSupervisor.activeGeneration(),
477
+ outcome: "failure",
478
+ },
479
+ error,
480
+ };
481
+ }
482
+ };
483
+ const reloadControlPlane = async (reason) => {
484
+ if (reloadInFlight) {
485
+ const pending = generationSupervisor.pendingReload();
486
+ const fallbackGeneration = generationSupervisor.activeGeneration() ??
487
+ generationSupervisor.snapshot().last_reload?.to_generation ??
488
+ null;
489
+ const generation = pending?.to_generation ?? fallbackGeneration;
490
+ if (generation) {
491
+ generationTelemetry.recordDuplicateSignal(generationTagsFor(generation, "server.reload"), {
492
+ source: "server_reload",
493
+ signal: "coalesced_reload_request",
494
+ dedupe_key: pending?.attempt_id ?? "reload_in_flight",
495
+ record_id: pending?.attempt_id ?? "reload_in_flight",
496
+ metadata: {
497
+ reason,
498
+ pending_reason: pending?.reason ?? null,
499
+ },
500
+ });
501
+ }
502
+ return await reloadInFlight;
503
+ }
504
+ reloadInFlight = performControlPlaneReload(reason).finally(() => {
505
+ reloadInFlight = null;
506
+ });
507
+ return await reloadInFlight;
508
+ };
509
+ return {
510
+ reloadControlPlane,
511
+ getControlPlaneStatus: () => ({
512
+ ...summarizeControlPlane(controlPlaneCurrent),
513
+ generation: generationSupervisor.snapshot(),
514
+ observability: {
515
+ counters: generationTelemetry.counters(),
516
+ },
517
+ }),
518
+ getControlPlaneCurrent: () => controlPlaneCurrent,
519
+ setControlPlaneCurrent: (handle) => {
520
+ controlPlaneCurrent = handle;
521
+ },
522
+ generationSupervisor,
523
+ generationTelemetry,
524
+ };
525
+ }
@@ -0,0 +1,7 @@
1
+ import { type ControlPlaneOutbox, type OutboxRecord } from "@femtomc/mu-control-plane";
2
+ import type { ControlPlaneRunEvent } from "./run_supervisor.js";
3
+ export declare function enqueueRunEventOutbox(opts: {
4
+ outbox: ControlPlaneOutbox;
5
+ event: ControlPlaneRunEvent;
6
+ nowMs: number;
7
+ }): Promise<OutboxRecord | null>;
@@ -0,0 +1,52 @@
1
+ import { correlationFromCommandRecord, } from "@femtomc/mu-control-plane";
2
+ function sha256Hex(input) {
3
+ const hasher = new Bun.CryptoHasher("sha256");
4
+ hasher.update(input);
5
+ return hasher.digest("hex");
6
+ }
7
+ function outboxKindForRunEvent(kind) {
8
+ switch (kind) {
9
+ case "run_completed":
10
+ return "result";
11
+ case "run_failed":
12
+ return "error";
13
+ default:
14
+ return "lifecycle";
15
+ }
16
+ }
17
+ export async function enqueueRunEventOutbox(opts) {
18
+ const command = opts.event.command;
19
+ if (!command) {
20
+ return null;
21
+ }
22
+ const baseCorrelation = correlationFromCommandRecord(command);
23
+ const correlation = {
24
+ ...baseCorrelation,
25
+ run_root_id: opts.event.run.root_issue_id ?? baseCorrelation.run_root_id,
26
+ };
27
+ const envelope = {
28
+ v: 1,
29
+ ts_ms: opts.nowMs,
30
+ channel: command.channel,
31
+ channel_tenant_id: command.channel_tenant_id,
32
+ channel_conversation_id: command.channel_conversation_id,
33
+ request_id: command.request_id,
34
+ response_id: `resp-${sha256Hex(`run-event:${opts.event.run.job_id}:${opts.event.seq}:${opts.nowMs}`).slice(0, 20)}`,
35
+ kind: outboxKindForRunEvent(opts.event.kind),
36
+ body: opts.event.message,
37
+ correlation,
38
+ metadata: {
39
+ async_run: true,
40
+ run_event_kind: opts.event.kind,
41
+ run_event_seq: opts.event.seq,
42
+ run: opts.event.run,
43
+ },
44
+ };
45
+ const decision = await opts.outbox.enqueue({
46
+ dedupeKey: `run-event:${opts.event.run.job_id}:${opts.event.seq}`,
47
+ envelope,
48
+ nowMs: opts.nowMs,
49
+ maxAttempts: 6,
50
+ });
51
+ return decision.record;
52
+ }
@@ -0,0 +1,48 @@
1
+ import type { CommandRecord } from "@femtomc/mu-control-plane";
2
+ import type { InterRootQueuePolicy } from "./control_plane_contract.js";
3
+ import { DurableRunQueue } from "./run_queue.js";
4
+ import type { ControlPlaneRunEvent, ControlPlaneRunHeartbeatResult, ControlPlaneRunInterruptResult, ControlPlaneRunSnapshot, ControlPlaneRunSupervisor } from "./run_supervisor.js";
5
+ export type QueueLaunchOpts = {
6
+ mode: "run_start" | "run_resume";
7
+ prompt?: string;
8
+ rootIssueId?: string | null;
9
+ maxSteps?: number;
10
+ source: "command" | "api";
11
+ command?: CommandRecord | null;
12
+ dedupeKey: string;
13
+ };
14
+ export type ControlPlaneRunQueueCoordinatorOpts = {
15
+ runQueue: DurableRunQueue;
16
+ interRootQueuePolicy: InterRootQueuePolicy;
17
+ getRunSupervisor: () => ControlPlaneRunSupervisor | null;
18
+ defaultRunMaxSteps?: number;
19
+ };
20
+ /**
21
+ * Queue/reconcile adapter for control-plane run lifecycle operations.
22
+ *
23
+ * Keeps inter-root queue execution concerns out of `control_plane.ts` while preserving
24
+ * queue-first semantics:
25
+ * - enqueue intents durably
26
+ * - reconcile/claim/launch deterministically
27
+ * - mirror runtime events back into durable queue state
28
+ */
29
+ export declare class ControlPlaneRunQueueCoordinator {
30
+ #private;
31
+ constructor(opts: ControlPlaneRunQueueCoordinatorOpts);
32
+ runtimeSnapshotsByJobId(): Map<string, ControlPlaneRunSnapshot>;
33
+ scheduleReconcile(reason: string): Promise<void>;
34
+ launchQueuedRun(launchOpts: QueueLaunchOpts): Promise<ControlPlaneRunSnapshot>;
35
+ launchQueuedRunFromCommand(record: CommandRecord): Promise<ControlPlaneRunSnapshot>;
36
+ interruptQueuedRun(opts: {
37
+ jobId?: string | null;
38
+ rootIssueId?: string | null;
39
+ }): Promise<ControlPlaneRunInterruptResult>;
40
+ heartbeatQueuedRun(opts: {
41
+ jobId?: string | null;
42
+ rootIssueId?: string | null;
43
+ reason?: string | null;
44
+ wakeMode?: string | null;
45
+ }): Promise<ControlPlaneRunHeartbeatResult>;
46
+ onRunEvent(event: ControlPlaneRunEvent): Promise<void>;
47
+ stop(): void;
48
+ }