@femtomc/mu-server 26.2.69 → 26.2.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/api/activities.d.ts +2 -0
- package/dist/api/activities.js +160 -0
- package/dist/api/config.d.ts +2 -0
- package/dist/api/config.js +45 -0
- package/dist/api/control_plane.d.ts +2 -0
- package/dist/api/control_plane.js +28 -0
- package/dist/api/cron.d.ts +2 -0
- package/dist/api/cron.js +182 -0
- package/dist/api/events.js +77 -19
- package/dist/api/forum.js +52 -18
- package/dist/api/heartbeats.d.ts +2 -0
- package/dist/api/heartbeats.js +211 -0
- package/dist/api/identities.d.ts +2 -0
- package/dist/api/identities.js +103 -0
- package/dist/api/issues.js +120 -33
- package/dist/api/runs.d.ts +2 -0
- package/dist/api/runs.js +207 -0
- package/dist/cli.js +58 -3
- package/dist/config.d.ts +4 -21
- package/dist/config.js +24 -75
- package/dist/control_plane.d.ts +7 -114
- package/dist/control_plane.js +238 -654
- package/dist/control_plane_bootstrap_helpers.d.ts +16 -0
- package/dist/control_plane_bootstrap_helpers.js +85 -0
- package/dist/control_plane_contract.d.ts +176 -0
- package/dist/control_plane_contract.js +1 -0
- package/dist/control_plane_reload.d.ts +63 -0
- package/dist/control_plane_reload.js +525 -0
- package/dist/control_plane_run_outbox.d.ts +7 -0
- package/dist/control_plane_run_outbox.js +52 -0
- package/dist/control_plane_run_queue_coordinator.d.ts +48 -0
- package/dist/control_plane_run_queue_coordinator.js +327 -0
- package/dist/control_plane_telegram_generation.d.ts +27 -0
- package/dist/control_plane_telegram_generation.js +520 -0
- package/dist/control_plane_wake_delivery.d.ts +50 -0
- package/dist/control_plane_wake_delivery.js +123 -0
- package/dist/cron_request.d.ts +8 -0
- package/dist/cron_request.js +65 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +4 -1
- package/dist/run_queue.d.ts +95 -0
- package/dist/run_queue.js +817 -0
- package/dist/run_supervisor.d.ts +20 -0
- package/dist/run_supervisor.js +25 -1
- package/dist/server.d.ts +12 -49
- package/dist/server.js +365 -2128
- package/dist/server_program_orchestration.d.ts +38 -0
- package/dist/server_program_orchestration.js +254 -0
- package/dist/server_routing.d.ts +31 -0
- package/dist/server_routing.js +230 -0
- package/dist/server_runtime.d.ts +30 -0
- package/dist/server_runtime.js +43 -0
- package/dist/server_types.d.ts +3 -0
- package/dist/server_types.js +16 -0
- package/dist/session_lifecycle.d.ts +11 -0
- package/dist/session_lifecycle.js +149 -0
- package/package.json +7 -6
package/dist/control_plane.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
|
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 : "
|
|
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 =
|
|
327
|
+
const result = await runQueueCoordinator.interruptQueuedRun({
|
|
884
328
|
rootIssueId: record.target_id,
|
|
885
|
-
})
|
|
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
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1124
|
-
|
|
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
|
|
715
|
+
return await runQueueCoordinator.interruptQueuedRun(interruptOpts);
|
|
1135
716
|
},
|
|
1136
717
|
async heartbeatRun(heartbeatOpts) {
|
|
1137
|
-
return
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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 {
|