@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
@@ -1,7 +1,13 @@
1
- import { ApprovedCommandBroker, CommandContextResolver, MessagingOperatorRuntime, operatorExtensionPaths, PiMessagingOperatorBackend, } from "@femtomc/mu-agent";
2
- import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneOutboxDispatcher, ControlPlaneRuntime, correlationFromCommandRecord, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapter, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
1
+ import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, DiscordControlPlaneAdapter, getControlPlanePaths, SlackControlPlaneAdapter, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
3
2
  import { DEFAULT_MU_CONFIG } from "./config.js";
3
+ import { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, } from "./control_plane_contract.js";
4
4
  import { ControlPlaneRunSupervisor, } from "./run_supervisor.js";
5
+ import { DurableRunQueue, queueStatesForRunStatusFilter, runSnapshotFromQueueSnapshot } from "./run_queue.js";
6
+ import { buildMessagingOperatorRuntime, createOutboxDrainLoop } from "./control_plane_bootstrap_helpers.js";
7
+ import { ControlPlaneRunQueueCoordinator } from "./control_plane_run_queue_coordinator.js";
8
+ import { enqueueRunEventOutbox } from "./control_plane_run_outbox.js";
9
+ import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
10
+ import { TelegramAdapterGenerationManager } from "./control_plane_telegram_generation.js";
5
11
  function generationTags(generation, component) {
6
12
  return {
7
13
  generation_id: generation.generation_id,
@@ -10,6 +16,25 @@ function generationTags(generation, component) {
10
16
  component,
11
17
  };
12
18
  }
19
+ const WAKE_OUTBOX_MAX_ATTEMPTS = 6;
20
+ function emptyNotifyOperatorsResult() {
21
+ return {
22
+ queued: 0,
23
+ duplicate: 0,
24
+ skipped: 0,
25
+ decisions: [],
26
+ };
27
+ }
28
+ function normalizeIssueId(value) {
29
+ if (!value) {
30
+ return null;
31
+ }
32
+ const trimmed = value.trim();
33
+ if (!/^mu-[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {
34
+ return null;
35
+ }
36
+ return trimmed.toLowerCase();
37
+ }
13
38
  export function detectAdapters(config) {
14
39
  const adapters = [];
15
40
  const slackSecret = config.adapters.slack.signing_secret;
@@ -31,577 +56,6 @@ export function detectAdapters(config) {
31
56
  }
32
57
  return adapters;
33
58
  }
34
- const TELEGRAM_GENERATION_SUPERVISOR_ID = "telegram-adapter";
35
- const TELEGRAM_WARMUP_TIMEOUT_MS = 2_000;
36
- const TELEGRAM_DRAIN_TIMEOUT_MS = 5_000;
37
- function cloneControlPlaneConfig(config) {
38
- return JSON.parse(JSON.stringify(config));
39
- }
40
- function controlPlaneNonTelegramFingerprint(config) {
41
- return JSON.stringify({
42
- adapters: {
43
- slack: config.adapters.slack,
44
- discord: config.adapters.discord,
45
- gmail: config.adapters.gmail,
46
- },
47
- operator: config.operator,
48
- });
49
- }
50
- function telegramAdapterConfigFromControlPlane(config) {
51
- const webhookSecret = config.adapters.telegram.webhook_secret;
52
- if (!webhookSecret) {
53
- return null;
54
- }
55
- return {
56
- webhookSecret,
57
- botToken: config.adapters.telegram.bot_token,
58
- botUsername: config.adapters.telegram.bot_username,
59
- };
60
- }
61
- function applyTelegramAdapterConfig(base, telegram) {
62
- const next = cloneControlPlaneConfig(base);
63
- next.adapters.telegram.webhook_secret = telegram?.webhookSecret ?? null;
64
- next.adapters.telegram.bot_token = telegram?.botToken ?? null;
65
- next.adapters.telegram.bot_username = telegram?.botUsername ?? null;
66
- return next;
67
- }
68
- function cloneTelegramAdapterConfig(config) {
69
- return {
70
- webhookSecret: config.webhookSecret,
71
- botToken: config.botToken,
72
- botUsername: config.botUsername,
73
- };
74
- }
75
- function describeError(err) {
76
- if (err instanceof Error && err.message.trim().length > 0) {
77
- return err.message;
78
- }
79
- return String(err);
80
- }
81
- async function runWithTimeout(opts) {
82
- if (opts.timeoutMs <= 0) {
83
- return await opts.run();
84
- }
85
- return await new Promise((resolve, reject) => {
86
- let settled = false;
87
- const timer = setTimeout(() => {
88
- if (settled) {
89
- return;
90
- }
91
- settled = true;
92
- reject(new Error(opts.timeoutMessage));
93
- }, opts.timeoutMs);
94
- void opts
95
- .run()
96
- .then((value) => {
97
- if (settled) {
98
- return;
99
- }
100
- settled = true;
101
- clearTimeout(timer);
102
- resolve(value);
103
- })
104
- .catch((err) => {
105
- if (settled) {
106
- return;
107
- }
108
- settled = true;
109
- clearTimeout(timer);
110
- reject(err);
111
- });
112
- });
113
- }
114
- class TelegramAdapterGenerationManager {
115
- #pipeline;
116
- #outbox;
117
- #nowMs;
118
- #onOutboxEnqueued;
119
- #signalObserver;
120
- #hooks;
121
- #generationSeq = -1;
122
- #active = null;
123
- #previousConfig = null;
124
- #activeControlPlaneConfig;
125
- constructor(opts) {
126
- this.#pipeline = opts.pipeline;
127
- this.#outbox = opts.outbox;
128
- this.#nowMs = opts.nowMs ?? Date.now;
129
- this.#onOutboxEnqueued = opts.onOutboxEnqueued ?? null;
130
- this.#signalObserver = opts.signalObserver ?? null;
131
- this.#hooks = opts.hooks ?? null;
132
- this.#activeControlPlaneConfig = cloneControlPlaneConfig(opts.initialConfig);
133
- }
134
- #nextGeneration() {
135
- const nextSeq = this.#generationSeq + 1;
136
- return {
137
- generation_id: `${TELEGRAM_GENERATION_SUPERVISOR_ID}-gen-${nextSeq}`,
138
- generation_seq: nextSeq,
139
- };
140
- }
141
- #buildAdapter(config, opts) {
142
- return new TelegramControlPlaneAdapter({
143
- pipeline: this.#pipeline,
144
- outbox: this.#outbox,
145
- webhookSecret: config.webhookSecret,
146
- botUsername: config.botUsername,
147
- deferredIngress: true,
148
- onOutboxEnqueued: this.#onOutboxEnqueued ?? undefined,
149
- signalObserver: this.#signalObserver ?? undefined,
150
- acceptIngress: opts.acceptIngress,
151
- ingressDrainEnabled: opts.ingressDrainEnabled,
152
- nowMs: this.#nowMs,
153
- });
154
- }
155
- async initialize() {
156
- const initial = telegramAdapterConfigFromControlPlane(this.#activeControlPlaneConfig);
157
- if (!initial) {
158
- return;
159
- }
160
- const generation = this.#nextGeneration();
161
- const adapter = this.#buildAdapter(initial, {
162
- acceptIngress: true,
163
- ingressDrainEnabled: true,
164
- });
165
- await adapter.warmup();
166
- const health = await adapter.healthCheck();
167
- if (!health.ok) {
168
- await adapter.stop({ force: true, reason: "startup_health_gate_failed" });
169
- throw new Error(`telegram adapter warmup health failed: ${health.reason}`);
170
- }
171
- this.#active = {
172
- generation,
173
- config: cloneTelegramAdapterConfig(initial),
174
- adapter,
175
- };
176
- this.#generationSeq = generation.generation_seq;
177
- }
178
- hasActiveGeneration() {
179
- return this.#active != null;
180
- }
181
- activeGeneration() {
182
- return this.#active ? { ...this.#active.generation } : null;
183
- }
184
- activeBotToken() {
185
- return this.#active?.config.botToken ?? null;
186
- }
187
- activeAdapter() {
188
- return this.#active?.adapter ?? null;
189
- }
190
- canHandleConfig(nextConfig, reason) {
191
- if (reason === "rollback") {
192
- return true;
193
- }
194
- return (controlPlaneNonTelegramFingerprint(nextConfig) ===
195
- controlPlaneNonTelegramFingerprint(this.#activeControlPlaneConfig));
196
- }
197
- async #rollbackToPrevious(opts) {
198
- if (!opts.previous) {
199
- return { ok: false, error: "rollback_unavailable" };
200
- }
201
- try {
202
- opts.previous.adapter.activateIngress();
203
- this.#active = opts.previous;
204
- this.#previousConfig = cloneTelegramAdapterConfig(opts.failedRecord.config);
205
- await opts.failedRecord.adapter.stop({ force: true, reason: `rollback:${opts.reason}` });
206
- this.#activeControlPlaneConfig = applyTelegramAdapterConfig(this.#activeControlPlaneConfig, opts.previous.config);
207
- return { ok: true };
208
- }
209
- catch (err) {
210
- return { ok: false, error: describeError(err) };
211
- }
212
- }
213
- async reload(opts) {
214
- if (!this.canHandleConfig(opts.config, opts.reason)) {
215
- return {
216
- handled: false,
217
- ok: false,
218
- reason: opts.reason,
219
- route: "/webhooks/telegram",
220
- from_generation: this.#active?.generation ?? null,
221
- to_generation: null,
222
- active_generation: this.#active?.generation ?? null,
223
- warmup: null,
224
- cutover: null,
225
- drain: null,
226
- rollback: {
227
- requested: opts.reason === "rollback",
228
- trigger: null,
229
- attempted: false,
230
- ok: true,
231
- },
232
- };
233
- }
234
- const rollbackRequested = opts.reason === "rollback";
235
- let rollbackTrigger = rollbackRequested ? "manual" : null;
236
- let rollbackAttempted = false;
237
- let rollbackOk = true;
238
- let rollbackError;
239
- const fromGeneration = this.#active?.generation ?? null;
240
- const previousRecord = this.#active;
241
- const warmupTimeoutMs = Math.max(0, Math.trunc(opts.warmupTimeoutMs ?? TELEGRAM_WARMUP_TIMEOUT_MS));
242
- const drainTimeoutMs = Math.max(0, Math.trunc(opts.drainTimeoutMs ?? TELEGRAM_DRAIN_TIMEOUT_MS));
243
- const targetConfig = rollbackRequested
244
- ? this.#previousConfig
245
- : telegramAdapterConfigFromControlPlane(opts.config);
246
- if (rollbackRequested && !targetConfig) {
247
- return {
248
- handled: true,
249
- ok: false,
250
- reason: opts.reason,
251
- route: "/webhooks/telegram",
252
- from_generation: fromGeneration,
253
- to_generation: null,
254
- active_generation: fromGeneration,
255
- warmup: null,
256
- cutover: null,
257
- drain: null,
258
- rollback: {
259
- requested: true,
260
- trigger: "rollback_unavailable",
261
- attempted: false,
262
- ok: false,
263
- error: "rollback_unavailable",
264
- },
265
- error: "rollback_unavailable",
266
- };
267
- }
268
- if (!targetConfig && !previousRecord) {
269
- this.#activeControlPlaneConfig = cloneControlPlaneConfig(opts.config);
270
- return {
271
- handled: true,
272
- ok: true,
273
- reason: opts.reason,
274
- route: "/webhooks/telegram",
275
- from_generation: null,
276
- to_generation: null,
277
- active_generation: null,
278
- warmup: null,
279
- cutover: null,
280
- drain: null,
281
- rollback: {
282
- requested: rollbackRequested,
283
- trigger: rollbackTrigger,
284
- attempted: false,
285
- ok: true,
286
- },
287
- };
288
- }
289
- if (!targetConfig && previousRecord) {
290
- const drainStartedAtMs = Math.trunc(this.#nowMs());
291
- let forcedStop = false;
292
- let drainError;
293
- let drainTimedOut = false;
294
- try {
295
- previousRecord.adapter.beginDrain();
296
- if (this.#hooks?.onDrain) {
297
- await this.#hooks.onDrain({
298
- generation: previousRecord.generation,
299
- reason: opts.reason,
300
- timeout_ms: drainTimeoutMs,
301
- });
302
- }
303
- const drain = await runWithTimeout({
304
- timeoutMs: drainTimeoutMs,
305
- timeoutMessage: "telegram_drain_timeout",
306
- run: async () => await previousRecord.adapter.drain({ timeoutMs: drainTimeoutMs, reason: opts.reason }),
307
- });
308
- drainTimedOut = drain.timed_out;
309
- if (!drain.ok || drain.timed_out) {
310
- forcedStop = true;
311
- await previousRecord.adapter.stop({ force: true, reason: "disable_drain_timeout" });
312
- }
313
- else {
314
- await previousRecord.adapter.stop({ force: false, reason: "disable" });
315
- }
316
- }
317
- catch (err) {
318
- drainError = describeError(err);
319
- forcedStop = true;
320
- drainTimedOut = drainError.includes("timeout");
321
- await previousRecord.adapter.stop({ force: true, reason: "disable_drain_failed" });
322
- }
323
- this.#previousConfig = cloneTelegramAdapterConfig(previousRecord.config);
324
- this.#active = null;
325
- this.#activeControlPlaneConfig = applyTelegramAdapterConfig(this.#activeControlPlaneConfig, null);
326
- return {
327
- handled: true,
328
- ok: drainError == null,
329
- reason: opts.reason,
330
- route: "/webhooks/telegram",
331
- from_generation: fromGeneration,
332
- to_generation: null,
333
- active_generation: null,
334
- warmup: null,
335
- cutover: {
336
- ok: true,
337
- elapsed_ms: 0,
338
- },
339
- drain: {
340
- ok: drainError == null && !drainTimedOut,
341
- elapsed_ms: Math.max(0, Math.trunc(this.#nowMs()) - drainStartedAtMs),
342
- timed_out: drainTimedOut,
343
- forced_stop: forcedStop,
344
- ...(drainError ? { error: drainError } : {}),
345
- },
346
- rollback: {
347
- requested: rollbackRequested,
348
- trigger: rollbackTrigger,
349
- attempted: false,
350
- ok: true,
351
- },
352
- ...(drainError ? { error: drainError } : {}),
353
- };
354
- }
355
- const nextConfig = cloneTelegramAdapterConfig(targetConfig);
356
- const toGeneration = this.#nextGeneration();
357
- const nextAdapter = this.#buildAdapter(nextConfig, {
358
- acceptIngress: false,
359
- ingressDrainEnabled: false,
360
- });
361
- const nextRecord = {
362
- generation: toGeneration,
363
- config: nextConfig,
364
- adapter: nextAdapter,
365
- };
366
- const warmupStartedAtMs = Math.trunc(this.#nowMs());
367
- try {
368
- if (this.#hooks?.onWarmup) {
369
- await this.#hooks.onWarmup({ generation: toGeneration, reason: opts.reason });
370
- }
371
- await runWithTimeout({
372
- timeoutMs: warmupTimeoutMs,
373
- timeoutMessage: "telegram_warmup_timeout",
374
- run: async () => {
375
- await nextAdapter.warmup();
376
- const health = await nextAdapter.healthCheck();
377
- if (!health.ok) {
378
- throw new Error(`telegram_health_gate_failed:${health.reason}`);
379
- }
380
- },
381
- });
382
- }
383
- catch (err) {
384
- const error = describeError(err);
385
- rollbackTrigger = error.includes("health_gate") ? "health_gate_failed" : "warmup_failed";
386
- await nextAdapter.stop({ force: true, reason: "warmup_failed" });
387
- return {
388
- handled: true,
389
- ok: false,
390
- reason: opts.reason,
391
- route: "/webhooks/telegram",
392
- from_generation: fromGeneration,
393
- to_generation: toGeneration,
394
- active_generation: fromGeneration,
395
- warmup: {
396
- ok: false,
397
- elapsed_ms: Math.max(0, Math.trunc(this.#nowMs()) - warmupStartedAtMs),
398
- error,
399
- },
400
- cutover: null,
401
- drain: null,
402
- rollback: {
403
- requested: rollbackRequested,
404
- trigger: rollbackTrigger,
405
- attempted: false,
406
- ok: true,
407
- },
408
- error,
409
- };
410
- }
411
- const cutoverStartedAtMs = Math.trunc(this.#nowMs());
412
- try {
413
- if (this.#hooks?.onCutover) {
414
- await this.#hooks.onCutover({
415
- from_generation: fromGeneration,
416
- to_generation: toGeneration,
417
- reason: opts.reason,
418
- });
419
- }
420
- nextAdapter.activateIngress();
421
- if (previousRecord) {
422
- previousRecord.adapter.beginDrain();
423
- }
424
- this.#active = nextRecord;
425
- this.#generationSeq = toGeneration.generation_seq;
426
- const postCutoverHealth = await nextAdapter.healthCheck();
427
- if (!postCutoverHealth.ok) {
428
- throw new Error(`telegram_post_cutover_health_failed:${postCutoverHealth.reason}`);
429
- }
430
- }
431
- catch (err) {
432
- const error = describeError(err);
433
- rollbackTrigger = error.includes("post_cutover") ? "post_cutover_health_failed" : "cutover_failed";
434
- rollbackAttempted = true;
435
- const rollback = await this.#rollbackToPrevious({
436
- failedRecord: nextRecord,
437
- previous: previousRecord,
438
- reason: opts.reason,
439
- });
440
- rollbackOk = rollback.ok;
441
- rollbackError = rollback.error;
442
- if (!rollback.ok) {
443
- await nextAdapter.stop({ force: true, reason: "rollback_failed" });
444
- this.#active = previousRecord ?? null;
445
- this.#activeControlPlaneConfig = applyTelegramAdapterConfig(this.#activeControlPlaneConfig, previousRecord?.config ?? null);
446
- }
447
- return {
448
- handled: true,
449
- ok: false,
450
- reason: opts.reason,
451
- route: "/webhooks/telegram",
452
- from_generation: fromGeneration,
453
- to_generation: toGeneration,
454
- active_generation: this.#active?.generation ?? fromGeneration,
455
- warmup: {
456
- ok: true,
457
- elapsed_ms: Math.max(0, cutoverStartedAtMs - warmupStartedAtMs),
458
- },
459
- cutover: {
460
- ok: false,
461
- elapsed_ms: Math.max(0, Math.trunc(this.#nowMs()) - cutoverStartedAtMs),
462
- error,
463
- },
464
- drain: null,
465
- rollback: {
466
- requested: rollbackRequested,
467
- trigger: rollbackTrigger,
468
- attempted: rollbackAttempted,
469
- ok: rollbackOk,
470
- ...(rollbackError ? { error: rollbackError } : {}),
471
- },
472
- error,
473
- };
474
- }
475
- let drain = null;
476
- if (previousRecord) {
477
- const drainStartedAtMs = Math.trunc(this.#nowMs());
478
- let forcedStop = false;
479
- let drainTimedOut = false;
480
- let drainError;
481
- try {
482
- if (this.#hooks?.onDrain) {
483
- await this.#hooks.onDrain({
484
- generation: previousRecord.generation,
485
- reason: opts.reason,
486
- timeout_ms: drainTimeoutMs,
487
- });
488
- }
489
- const drained = await runWithTimeout({
490
- timeoutMs: drainTimeoutMs,
491
- timeoutMessage: "telegram_drain_timeout",
492
- run: async () => await previousRecord.adapter.drain({ timeoutMs: drainTimeoutMs, reason: opts.reason }),
493
- });
494
- drainTimedOut = drained.timed_out;
495
- if (!drained.ok || drained.timed_out) {
496
- forcedStop = true;
497
- await previousRecord.adapter.stop({ force: true, reason: "generation_drain_timeout" });
498
- }
499
- else {
500
- await previousRecord.adapter.stop({ force: false, reason: "generation_drained" });
501
- }
502
- }
503
- catch (err) {
504
- drainError = describeError(err);
505
- forcedStop = true;
506
- drainTimedOut = drainError.includes("timeout");
507
- await previousRecord.adapter.stop({ force: true, reason: "generation_drain_failed" });
508
- }
509
- drain = {
510
- ok: drainError == null && !drainTimedOut,
511
- elapsed_ms: Math.max(0, Math.trunc(this.#nowMs()) - drainStartedAtMs),
512
- timed_out: drainTimedOut,
513
- forced_stop: forcedStop,
514
- ...(drainError ? { error: drainError } : {}),
515
- };
516
- }
517
- this.#previousConfig = previousRecord ? cloneTelegramAdapterConfig(previousRecord.config) : this.#previousConfig;
518
- this.#activeControlPlaneConfig = applyTelegramAdapterConfig(this.#activeControlPlaneConfig, nextConfig);
519
- return {
520
- handled: true,
521
- ok: true,
522
- reason: opts.reason,
523
- route: "/webhooks/telegram",
524
- from_generation: fromGeneration,
525
- to_generation: toGeneration,
526
- active_generation: toGeneration,
527
- warmup: {
528
- ok: true,
529
- elapsed_ms: Math.max(0, cutoverStartedAtMs - warmupStartedAtMs),
530
- },
531
- cutover: {
532
- ok: true,
533
- elapsed_ms: Math.max(0, Math.trunc(this.#nowMs()) - cutoverStartedAtMs),
534
- },
535
- drain,
536
- rollback: {
537
- requested: rollbackRequested,
538
- trigger: rollbackTrigger,
539
- attempted: rollbackAttempted,
540
- ok: rollbackOk,
541
- ...(rollbackError ? { error: rollbackError } : {}),
542
- },
543
- };
544
- }
545
- async stop() {
546
- const active = this.#active;
547
- this.#active = null;
548
- if (!active) {
549
- return;
550
- }
551
- await active.adapter.stop({ force: true, reason: "shutdown" });
552
- }
553
- }
554
- function sha256Hex(input) {
555
- const hasher = new Bun.CryptoHasher("sha256");
556
- hasher.update(input);
557
- return hasher.digest("hex");
558
- }
559
- function outboxKindForRunEvent(kind) {
560
- switch (kind) {
561
- case "run_completed":
562
- return "result";
563
- case "run_failed":
564
- return "error";
565
- default:
566
- return "lifecycle";
567
- }
568
- }
569
- async function enqueueRunEventOutbox(opts) {
570
- const command = opts.event.command;
571
- if (!command) {
572
- return null;
573
- }
574
- const baseCorrelation = correlationFromCommandRecord(command);
575
- const correlation = {
576
- ...baseCorrelation,
577
- run_root_id: opts.event.run.root_issue_id ?? baseCorrelation.run_root_id,
578
- };
579
- const envelope = {
580
- v: 1,
581
- ts_ms: opts.nowMs,
582
- channel: command.channel,
583
- channel_tenant_id: command.channel_tenant_id,
584
- channel_conversation_id: command.channel_conversation_id,
585
- request_id: command.request_id,
586
- response_id: `resp-${sha256Hex(`run-event:${opts.event.run.job_id}:${opts.event.seq}:${opts.nowMs}`).slice(0, 20)}`,
587
- kind: outboxKindForRunEvent(opts.event.kind),
588
- body: opts.event.message,
589
- correlation,
590
- metadata: {
591
- async_run: true,
592
- run_event_kind: opts.event.kind,
593
- run_event_seq: opts.event.seq,
594
- run: opts.event.run,
595
- },
596
- };
597
- const decision = await opts.outbox.enqueue({
598
- dedupeKey: `run-event:${opts.event.run.job_id}:${opts.event.seq}`,
599
- envelope,
600
- nowMs: opts.nowMs,
601
- maxAttempts: 6,
602
- });
603
- return decision.record;
604
- }
605
59
  /**
606
60
  * Telegram supports a markdown dialect that uses single markers for emphasis.
607
61
  * Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
@@ -664,26 +118,6 @@ async function postTelegramMessage(botToken, payload) {
664
118
  body: JSON.stringify(payload),
665
119
  });
666
120
  }
667
- const OUTBOX_DRAIN_INTERVAL_MS = 500;
668
- function buildMessagingOperatorRuntime(opts) {
669
- if (!opts.config.operator.enabled) {
670
- return null;
671
- }
672
- const backend = opts.backend ??
673
- new PiMessagingOperatorBackend({
674
- provider: opts.config.operator.provider ?? undefined,
675
- model: opts.config.operator.model ?? undefined,
676
- extensionPaths: operatorExtensionPaths,
677
- });
678
- return new MessagingOperatorRuntime({
679
- backend,
680
- broker: new ApprovedCommandBroker({
681
- runTriggersEnabled: opts.config.operator.run_triggers_enabled,
682
- contextResolver: new CommandContextResolver({ allowedRepoRoots: [opts.repoRoot] }),
683
- }),
684
- enabled: true,
685
- });
686
- }
687
121
  export async function bootstrapControlPlane(opts) {
688
122
  const controlPlaneConfig = opts.config ?? DEFAULT_MU_CONFIG.control_plane;
689
123
  const detected = detectAdapters(controlPlaneConfig);
@@ -709,7 +143,9 @@ export async function bootstrapControlPlane(opts) {
709
143
  const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
710
144
  let pipeline = null;
711
145
  let runSupervisor = null;
712
- let drainInterval = null;
146
+ let outboxDrainLoop = null;
147
+ let wakeDeliveryObserver = opts.wakeDeliveryObserver ?? null;
148
+ const outboundDeliveryChannels = new Set(["telegram"]);
713
149
  const adapterMap = new Map();
714
150
  try {
715
151
  await runtime.start();
@@ -725,12 +161,20 @@ export async function bootstrapControlPlane(opts) {
725
161
  });
726
162
  await outbox.load();
727
163
  let scheduleOutboxDrainRef = null;
164
+ const runQueue = new DurableRunQueue({ repoRoot: opts.repoRoot });
165
+ const interRootQueuePolicy = normalizeInterRootQueuePolicy(opts.interRootQueuePolicy ?? DEFAULT_INTER_ROOT_QUEUE_POLICY);
166
+ const runQueueCoordinator = new ControlPlaneRunQueueCoordinator({
167
+ runQueue,
168
+ interRootQueuePolicy,
169
+ getRunSupervisor: () => runSupervisor,
170
+ });
728
171
  runSupervisor = new ControlPlaneRunSupervisor({
729
172
  repoRoot: opts.repoRoot,
730
173
  heartbeatScheduler: opts.heartbeatScheduler,
731
174
  heartbeatIntervalMs: opts.runSupervisorHeartbeatIntervalMs,
732
175
  spawnProcess: opts.runSupervisorSpawnProcess,
733
176
  onEvent: async (event) => {
177
+ await runQueueCoordinator.onRunEvent(event);
734
178
  const outboxRecord = await enqueueRunEventOutbox({
735
179
  outbox,
736
180
  event,
@@ -741,6 +185,7 @@ export async function bootstrapControlPlane(opts) {
741
185
  }
742
186
  },
743
187
  });
188
+ await runQueueCoordinator.scheduleReconcile("bootstrap");
744
189
  pipeline = new ControlPlaneCommandPipeline({
745
190
  runtime,
746
191
  operator,
@@ -836,10 +281,7 @@ export async function bootstrapControlPlane(opts) {
836
281
  }
837
282
  if (record.target_type === "run start" || record.target_type === "run resume") {
838
283
  try {
839
- const launched = await runSupervisor?.startFromCommand(record);
840
- if (!launched) {
841
- return null;
842
- }
284
+ const launched = await runQueueCoordinator.launchQueuedRunFromCommand(record);
843
285
  return {
844
286
  terminalState: "completed",
845
287
  result: {
@@ -863,6 +305,8 @@ export async function bootstrapControlPlane(opts) {
863
305
  run_mode: launched.mode,
864
306
  run_root_id: launched.root_issue_id,
865
307
  run_source: launched.source,
308
+ queue_id: launched.queue_id ?? null,
309
+ queue_state: launched.queue_state ?? null,
866
310
  },
867
311
  },
868
312
  ],
@@ -871,7 +315,7 @@ export async function bootstrapControlPlane(opts) {
871
315
  catch (err) {
872
316
  return {
873
317
  terminalState: "failed",
874
- errorCode: err instanceof Error && err.message ? err.message : "run_supervisor_start_failed",
318
+ errorCode: err instanceof Error && err.message ? err.message : "run_queue_start_failed",
875
319
  trace: {
876
320
  cliCommandKind: record.target_type.replaceAll(" ", "_"),
877
321
  runRootId: record.target_id,
@@ -880,9 +324,9 @@ export async function bootstrapControlPlane(opts) {
880
324
  }
881
325
  }
882
326
  if (record.target_type === "run interrupt") {
883
- const result = runSupervisor?.interrupt({
327
+ const result = await runQueueCoordinator.interruptQueuedRun({
884
328
  rootIssueId: record.target_id,
885
- }) ?? { ok: false, reason: "not_found", run: null };
329
+ });
886
330
  if (!result.ok) {
887
331
  return {
888
332
  terminalState: "failed",
@@ -1001,6 +445,104 @@ export async function bootstrapControlPlane(opts) {
1001
445
  },
1002
446
  isActive: () => telegramManager.hasActiveGeneration(),
1003
447
  });
448
+ const notifyOperators = async (notifyOpts) => {
449
+ if (!pipeline) {
450
+ return emptyNotifyOperatorsResult();
451
+ }
452
+ const message = notifyOpts.message.trim();
453
+ const dedupeKey = notifyOpts.dedupeKey.trim();
454
+ if (!message || !dedupeKey) {
455
+ return emptyNotifyOperatorsResult();
456
+ }
457
+ const wakeSource = typeof notifyOpts.wake?.wakeSource === "string" ? notifyOpts.wake.wakeSource.trim() : "";
458
+ const wakeProgramId = typeof notifyOpts.wake?.programId === "string" ? notifyOpts.wake.programId.trim() : "";
459
+ const wakeSourceTsMsRaw = notifyOpts.wake?.sourceTsMs;
460
+ const wakeSourceTsMs = typeof wakeSourceTsMsRaw === "number" && Number.isFinite(wakeSourceTsMsRaw)
461
+ ? Math.trunc(wakeSourceTsMsRaw)
462
+ : null;
463
+ const wakeId = typeof notifyOpts.wake?.wakeId === "string" && notifyOpts.wake.wakeId.trim().length > 0
464
+ ? notifyOpts.wake.wakeId.trim()
465
+ : `wake-${(() => {
466
+ const hasher = new Bun.CryptoHasher("sha256");
467
+ hasher.update(`${dedupeKey}:${message}`);
468
+ return hasher.digest("hex").slice(0, 16);
469
+ })()}`;
470
+ const context = {
471
+ wakeId,
472
+ dedupeKey,
473
+ wakeSource: wakeSource || null,
474
+ programId: wakeProgramId || null,
475
+ sourceTsMs: wakeSourceTsMs,
476
+ };
477
+ const nowMs = Math.trunc(Date.now());
478
+ const telegramBotToken = telegramManager.activeBotToken();
479
+ const bindings = pipeline.identities
480
+ .listBindings({ includeInactive: false })
481
+ .filter((binding) => binding.scopes.includes("cp.ops.admin"));
482
+ const result = emptyNotifyOperatorsResult();
483
+ for (const binding of bindings) {
484
+ const bindingDedupeKey = wakeFanoutDedupeKey({
485
+ dedupeKey,
486
+ wakeId,
487
+ binding,
488
+ });
489
+ const capability = resolveWakeFanoutCapability({
490
+ binding,
491
+ isChannelDeliverySupported: (channel) => outboundDeliveryChannels.has(channel),
492
+ telegramBotToken,
493
+ });
494
+ if (!capability.ok) {
495
+ result.skipped += 1;
496
+ result.decisions.push({
497
+ state: "skipped",
498
+ reason_code: capability.reasonCode,
499
+ binding_id: binding.binding_id,
500
+ channel: binding.channel,
501
+ dedupe_key: bindingDedupeKey,
502
+ outbox_id: null,
503
+ });
504
+ continue;
505
+ }
506
+ const envelope = buildWakeOutboundEnvelope({
507
+ repoRoot: opts.repoRoot,
508
+ nowMs,
509
+ message,
510
+ binding,
511
+ context,
512
+ metadata: notifyOpts.metadata,
513
+ });
514
+ const enqueueDecision = await outbox.enqueue({
515
+ dedupeKey: bindingDedupeKey,
516
+ envelope,
517
+ nowMs,
518
+ maxAttempts: WAKE_OUTBOX_MAX_ATTEMPTS,
519
+ });
520
+ if (enqueueDecision.kind === "enqueued") {
521
+ result.queued += 1;
522
+ scheduleOutboxDrainRef?.();
523
+ result.decisions.push({
524
+ state: "queued",
525
+ reason_code: "outbox_enqueued",
526
+ binding_id: binding.binding_id,
527
+ channel: binding.channel,
528
+ dedupe_key: bindingDedupeKey,
529
+ outbox_id: enqueueDecision.record.outbox_id,
530
+ });
531
+ }
532
+ else {
533
+ result.duplicate += 1;
534
+ result.decisions.push({
535
+ state: "duplicate",
536
+ reason_code: "outbox_duplicate",
537
+ binding_id: binding.binding_id,
538
+ channel: binding.channel,
539
+ dedupe_key: bindingDedupeKey,
540
+ outbox_id: enqueueDecision.record.outbox_id,
541
+ });
542
+ }
543
+ }
544
+ return result;
545
+ };
1004
546
  const deliver = async (record) => {
1005
547
  const { envelope } = record;
1006
548
  if (envelope.channel === "telegram") {
@@ -1043,38 +585,38 @@ export async function bootstrapControlPlane(opts) {
1043
585
  }
1044
586
  return undefined;
1045
587
  };
1046
- const dispatcher = new ControlPlaneOutboxDispatcher({ outbox, deliver });
1047
- let drainingOutbox = false;
1048
- let drainRequested = false;
1049
- const drainOutboxNow = async () => {
1050
- if (drainingOutbox) {
1051
- drainRequested = true;
1052
- return;
1053
- }
1054
- drainingOutbox = true;
1055
- try {
1056
- do {
1057
- drainRequested = false;
1058
- await dispatcher.drainDue();
1059
- } while (drainRequested);
1060
- }
1061
- catch {
1062
- // Swallow errors — the dispatcher handles retries internally.
1063
- }
1064
- finally {
1065
- drainingOutbox = false;
1066
- }
1067
- };
1068
- const scheduleOutboxDrain = () => {
1069
- queueMicrotask(() => {
1070
- void drainOutboxNow();
1071
- });
1072
- };
588
+ const outboxDrain = createOutboxDrainLoop({
589
+ outbox,
590
+ deliver,
591
+ onOutcome: async (outcome) => {
592
+ if (!wakeDeliveryObserver) {
593
+ return;
594
+ }
595
+ const metadata = wakeDeliveryMetadataFromOutboxRecord(outcome.record);
596
+ if (!metadata) {
597
+ return;
598
+ }
599
+ const state = outcome.kind === "delivered" ? "delivered" : outcome.kind === "retried" ? "retried" : "dead_letter";
600
+ await wakeDeliveryObserver({
601
+ state,
602
+ reason_code: wakeDispatchReasonCode({
603
+ state,
604
+ lastError: outcome.record.last_error,
605
+ deadLetterReason: outcome.record.dead_letter_reason,
606
+ }),
607
+ wake_id: metadata.wakeId,
608
+ dedupe_key: metadata.wakeDedupeKey,
609
+ binding_id: metadata.bindingId,
610
+ channel: metadata.channel,
611
+ outbox_id: metadata.outboxId,
612
+ outbox_dedupe_key: metadata.outboxDedupeKey,
613
+ attempt_count: outcome.record.attempt_count,
614
+ });
615
+ },
616
+ });
617
+ const scheduleOutboxDrain = outboxDrain.scheduleOutboxDrain;
1073
618
  scheduleOutboxDrainRef = scheduleOutboxDrain;
1074
- drainInterval = setInterval(() => {
1075
- scheduleOutboxDrain();
1076
- }, OUTBOX_DRAIN_INTERVAL_MS);
1077
- scheduleOutboxDrain();
619
+ outboxDrainLoop = outboxDrain;
1078
620
  return {
1079
621
  get activeAdapters() {
1080
622
  return [...adapterMap.values()].filter((entry) => entry.isActive()).map((v) => v.info);
@@ -1089,6 +631,12 @@ export async function bootstrapControlPlane(opts) {
1089
631
  }
1090
632
  return result.response;
1091
633
  },
634
+ async notifyOperators(notifyOpts) {
635
+ return await notifyOperators(notifyOpts);
636
+ },
637
+ setWakeDeliveryObserver(observer) {
638
+ wakeDeliveryObserver = observer;
639
+ },
1092
640
  async reloadTelegramGeneration(reloadOpts) {
1093
641
  const result = await telegramManager.reload({
1094
642
  config: reloadOpts.config,
@@ -1100,41 +648,74 @@ export async function bootstrapControlPlane(opts) {
1100
648
  return result;
1101
649
  },
1102
650
  async listRuns(opts = {}) {
1103
- return (runSupervisor?.list({
651
+ const limit = Math.max(1, Math.min(500, Math.trunc(opts.limit ?? 100)));
652
+ const fallbackStatusFilter = queueStatesForRunStatusFilter(opts.status);
653
+ if (Array.isArray(fallbackStatusFilter) && fallbackStatusFilter.length === 0) {
654
+ return [];
655
+ }
656
+ const queued = await runQueue.listRunSnapshots({
1104
657
  status: opts.status,
1105
- limit: opts.limit,
1106
- }) ?? []);
658
+ limit,
659
+ runtimeByJobId: runQueueCoordinator.runtimeSnapshotsByJobId(),
660
+ });
661
+ const seen = new Set(queued.map((run) => run.job_id));
662
+ const fallbackRuns = runSupervisor?.list({ limit: 500 }) ?? [];
663
+ for (const run of fallbackRuns) {
664
+ if (seen.has(run.job_id)) {
665
+ continue;
666
+ }
667
+ if (fallbackStatusFilter && fallbackStatusFilter.length > 0) {
668
+ const mapped = run.status === "completed"
669
+ ? "done"
670
+ : run.status === "failed"
671
+ ? "failed"
672
+ : run.status === "cancelled"
673
+ ? "cancelled"
674
+ : "active";
675
+ if (!fallbackStatusFilter.includes(mapped)) {
676
+ continue;
677
+ }
678
+ }
679
+ queued.push(run);
680
+ seen.add(run.job_id);
681
+ }
682
+ return queued.slice(0, limit);
1107
683
  },
1108
684
  async getRun(idOrRoot) {
685
+ const queued = await runQueue.get(idOrRoot);
686
+ if (queued) {
687
+ const runtime = queued.job_id ? (runSupervisor?.get(queued.job_id) ?? null) : null;
688
+ return runSnapshotFromQueueSnapshot(queued, runtime);
689
+ }
1109
690
  return runSupervisor?.get(idOrRoot) ?? null;
1110
691
  },
1111
692
  async startRun(startOpts) {
1112
- const run = await runSupervisor?.launchStart({
693
+ return await runQueueCoordinator.launchQueuedRun({
694
+ mode: "run_start",
1113
695
  prompt: startOpts.prompt,
1114
696
  maxSteps: startOpts.maxSteps,
1115
697
  source: "api",
698
+ dedupeKey: `api:run_start:${crypto.randomUUID()}`,
1116
699
  });
1117
- if (!run) {
1118
- throw new Error("run_supervisor_unavailable");
1119
- }
1120
- return run;
1121
700
  },
1122
701
  async resumeRun(resumeOpts) {
1123
- const run = await runSupervisor?.launchResume({
1124
- rootIssueId: resumeOpts.rootIssueId,
702
+ const rootIssueId = normalizeIssueId(resumeOpts.rootIssueId);
703
+ if (!rootIssueId) {
704
+ throw new Error("run_resume_invalid_root_issue_id");
705
+ }
706
+ return await runQueueCoordinator.launchQueuedRun({
707
+ mode: "run_resume",
708
+ rootIssueId,
1125
709
  maxSteps: resumeOpts.maxSteps,
1126
710
  source: "api",
711
+ dedupeKey: `api:run_resume:${rootIssueId}:${crypto.randomUUID()}`,
1127
712
  });
1128
- if (!run) {
1129
- throw new Error("run_supervisor_unavailable");
1130
- }
1131
- return run;
1132
713
  },
1133
714
  async interruptRun(interruptOpts) {
1134
- return runSupervisor?.interrupt(interruptOpts) ?? { ok: false, reason: "not_found", run: null };
715
+ return await runQueueCoordinator.interruptQueuedRun(interruptOpts);
1135
716
  },
1136
717
  async heartbeatRun(heartbeatOpts) {
1137
- return runSupervisor?.heartbeat(heartbeatOpts) ?? { ok: false, reason: "not_found", run: null };
718
+ return await runQueueCoordinator.heartbeatQueuedRun(heartbeatOpts);
1138
719
  },
1139
720
  async traceRun(traceOpts) {
1140
721
  return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
@@ -1146,9 +727,11 @@ export async function bootstrapControlPlane(opts) {
1146
727
  return await pipeline.handleTerminalInbound(terminalOpts);
1147
728
  },
1148
729
  async stop() {
1149
- if (drainInterval) {
1150
- clearInterval(drainInterval);
1151
- drainInterval = null;
730
+ wakeDeliveryObserver = null;
731
+ runQueueCoordinator.stop();
732
+ if (outboxDrainLoop) {
733
+ outboxDrainLoop.stop();
734
+ outboxDrainLoop = null;
1152
735
  }
1153
736
  for (const { adapter } of adapterMap.values()) {
1154
737
  try {
@@ -1169,9 +752,10 @@ export async function bootstrapControlPlane(opts) {
1169
752
  };
1170
753
  }
1171
754
  catch (err) {
1172
- if (drainInterval) {
1173
- clearInterval(drainInterval);
1174
- drainInterval = null;
755
+ wakeDeliveryObserver = null;
756
+ if (outboxDrainLoop) {
757
+ outboxDrainLoop.stop();
758
+ outboxDrainLoop = null;
1175
759
  }
1176
760
  for (const { adapter } of adapterMap.values()) {
1177
761
  try {