@femtomc/mu-server 26.2.90 → 26.2.92
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 +23 -10
- package/dist/api/control_plane.js +64 -5
- package/dist/config.d.ts +3 -3
- package/dist/config.js +20 -15
- package/dist/control_plane.d.ts +20 -5
- package/dist/control_plane.js +303 -245
- package/dist/control_plane_adapter_registry.d.ts +1 -0
- package/dist/control_plane_adapter_registry.js +2 -0
- package/dist/control_plane_bootstrap_helpers.js +0 -1
- package/dist/control_plane_contract.d.ts +0 -35
- package/dist/control_plane_contract.js +1 -1
- package/dist/control_plane_telegram_generation.js +1 -0
- package/dist/control_plane_wake_delivery.d.ts +2 -1
- package/dist/control_plane_wake_delivery.js +3 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +0 -2
- package/dist/server.js +2 -41
- package/dist/{server_program_orchestration.d.ts → server_program_coordination.d.ts} +1 -1
- package/dist/{server_program_orchestration.js → server_program_coordination.js} +1 -1
- package/package.json +4 -4
- package/dist/api/runs.d.ts +0 -2
- package/dist/api/runs.js +0 -124
- package/dist/control_plane_run_outbox.d.ts +0 -7
- package/dist/control_plane_run_outbox.js +0 -52
- package/dist/control_plane_run_queue_coordinator.d.ts +0 -42
- package/dist/control_plane_run_queue_coordinator.js +0 -266
- package/dist/orchestration_queue.d.ts +0 -44
- package/dist/orchestration_queue.js +0 -111
- package/dist/run_queue.d.ts +0 -95
- package/dist/run_queue.js +0 -816
- package/dist/run_supervisor.d.ts +0 -108
- package/dist/run_supervisor.js +0 -460
package/dist/control_plane.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { ControlPlaneCommandPipeline, ControlPlaneOutbox, ControlPlaneRuntime, getControlPlanePaths, TelegramControlPlaneAdapterSpec, } from "@femtomc/mu-control-plane";
|
|
2
2
|
import { DEFAULT_MU_CONFIG } from "./config.js";
|
|
3
|
-
import { DEFAULT_INTER_ROOT_QUEUE_POLICY, normalizeInterRootQueuePolicy, } from "./control_plane_contract.js";
|
|
4
|
-
import { ControlPlaneRunSupervisor, } from "./run_supervisor.js";
|
|
5
|
-
import { DurableRunQueue, queueStatesForRunStatusFilter, runSnapshotFromQueueSnapshot } from "./run_queue.js";
|
|
6
3
|
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
4
|
import { buildWakeOutboundEnvelope, resolveWakeFanoutCapability, wakeDeliveryMetadataFromOutboxRecord, wakeDispatchReasonCode, wakeFanoutDedupeKey, } from "./control_plane_wake_delivery.js";
|
|
10
5
|
import { createStaticAdaptersFromDetected, detectAdapters, } from "./control_plane_adapter_registry.js";
|
|
11
6
|
import { OutboundDeliveryRouter } from "./outbound_delivery_router.js";
|
|
@@ -27,17 +22,8 @@ function emptyNotifyOperatorsResult() {
|
|
|
27
22
|
decisions: [],
|
|
28
23
|
};
|
|
29
24
|
}
|
|
30
|
-
function normalizeIssueId(value) {
|
|
31
|
-
if (!value) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
const trimmed = value.trim();
|
|
35
|
-
if (!/^mu-[a-z0-9][a-z0-9-]*$/i.test(trimmed)) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
return trimmed.toLowerCase();
|
|
39
|
-
}
|
|
40
25
|
export { detectAdapters };
|
|
26
|
+
const TELEGRAM_CAPTION_MAX_LEN = 1_024;
|
|
41
27
|
/**
|
|
42
28
|
* Telegram supports a markdown dialect that uses single markers for emphasis.
|
|
43
29
|
* Normalize the most common LLM/GitHub-style markers (`**bold**`, `__italic__`, headings)
|
|
@@ -100,6 +86,287 @@ async function postTelegramMessage(botToken, payload) {
|
|
|
100
86
|
body: JSON.stringify(payload),
|
|
101
87
|
});
|
|
102
88
|
}
|
|
89
|
+
async function postTelegramApiJson(botToken, method, payload) {
|
|
90
|
+
return await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: JSON.stringify(payload),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async function postTelegramApiMultipart(botToken, method, form) {
|
|
97
|
+
return await fetch(`https://api.telegram.org/bot${botToken}/${method}`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: form,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function truncateTelegramCaption(text) {
|
|
103
|
+
const normalized = text.trim();
|
|
104
|
+
if (normalized.length <= TELEGRAM_CAPTION_MAX_LEN) {
|
|
105
|
+
return normalized;
|
|
106
|
+
}
|
|
107
|
+
if (TELEGRAM_CAPTION_MAX_LEN <= 16) {
|
|
108
|
+
return normalized.slice(0, TELEGRAM_CAPTION_MAX_LEN);
|
|
109
|
+
}
|
|
110
|
+
const suffix = "…(truncated)";
|
|
111
|
+
const headLen = Math.max(0, TELEGRAM_CAPTION_MAX_LEN - suffix.length);
|
|
112
|
+
return `${normalized.slice(0, headLen)}${suffix}`;
|
|
113
|
+
}
|
|
114
|
+
function chooseTelegramMediaMethod(attachment) {
|
|
115
|
+
const mime = attachment.mime_type?.toLowerCase() ?? "";
|
|
116
|
+
const filename = attachment.filename?.toLowerCase() ?? "";
|
|
117
|
+
const declaredType = attachment.type.toLowerCase();
|
|
118
|
+
const isSvg = mime === "image/svg+xml" || filename.endsWith(".svg");
|
|
119
|
+
const isImageMime = mime.startsWith("image/");
|
|
120
|
+
if ((declaredType === "image" || isImageMime) && !isSvg) {
|
|
121
|
+
return "sendPhoto";
|
|
122
|
+
}
|
|
123
|
+
return "sendDocument";
|
|
124
|
+
}
|
|
125
|
+
function parseRetryDelayMs(res) {
|
|
126
|
+
const retryAfter = res.headers.get("retry-after");
|
|
127
|
+
if (!retryAfter) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const parsed = Number.parseInt(retryAfter, 10);
|
|
131
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return parsed * 1000;
|
|
135
|
+
}
|
|
136
|
+
export async function deliverTelegramOutboxRecord(opts) {
|
|
137
|
+
const { botToken, record } = opts;
|
|
138
|
+
const fallbackMessagePayload = buildTelegramSendMessagePayload({
|
|
139
|
+
chatId: record.envelope.channel_conversation_id,
|
|
140
|
+
text: record.envelope.body,
|
|
141
|
+
richFormatting: true,
|
|
142
|
+
});
|
|
143
|
+
const firstAttachment = record.envelope.attachments?.[0] ?? null;
|
|
144
|
+
if (!firstAttachment) {
|
|
145
|
+
let res = await postTelegramMessage(botToken, fallbackMessagePayload);
|
|
146
|
+
if (!res.ok && res.status === 400 && fallbackMessagePayload.parse_mode) {
|
|
147
|
+
res = await postTelegramMessage(botToken, buildTelegramSendMessagePayload({
|
|
148
|
+
chatId: record.envelope.channel_conversation_id,
|
|
149
|
+
text: record.envelope.body,
|
|
150
|
+
richFormatting: false,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
if (res.ok) {
|
|
154
|
+
return { kind: "delivered" };
|
|
155
|
+
}
|
|
156
|
+
const responseBody = await res.text().catch(() => "");
|
|
157
|
+
if (res.status === 429 || res.status >= 500) {
|
|
158
|
+
return {
|
|
159
|
+
kind: "retry",
|
|
160
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
161
|
+
retryDelayMs: parseRetryDelayMs(res),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
kind: "retry",
|
|
166
|
+
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const mediaMethod = chooseTelegramMediaMethod(firstAttachment);
|
|
170
|
+
const mediaField = mediaMethod === "sendPhoto" ? "photo" : "document";
|
|
171
|
+
const mediaReference = firstAttachment.reference.file_id ?? firstAttachment.reference.url ?? null;
|
|
172
|
+
if (!mediaReference) {
|
|
173
|
+
return { kind: "retry", error: "telegram media attachment missing reference" };
|
|
174
|
+
}
|
|
175
|
+
const mediaCaption = truncateTelegramCaption(record.envelope.body);
|
|
176
|
+
let mediaResponse;
|
|
177
|
+
if (firstAttachment.reference.file_id) {
|
|
178
|
+
mediaResponse = await postTelegramApiJson(botToken, mediaMethod, mediaMethod === "sendPhoto"
|
|
179
|
+
? {
|
|
180
|
+
chat_id: record.envelope.channel_conversation_id,
|
|
181
|
+
photo: firstAttachment.reference.file_id,
|
|
182
|
+
caption: mediaCaption,
|
|
183
|
+
}
|
|
184
|
+
: {
|
|
185
|
+
chat_id: record.envelope.channel_conversation_id,
|
|
186
|
+
document: firstAttachment.reference.file_id,
|
|
187
|
+
caption: mediaCaption,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const sourceUrl = firstAttachment.reference.url;
|
|
192
|
+
const sourceRes = await fetch(sourceUrl);
|
|
193
|
+
if (!sourceRes.ok) {
|
|
194
|
+
const sourceErr = await sourceRes.text().catch(() => "");
|
|
195
|
+
return {
|
|
196
|
+
kind: "retry",
|
|
197
|
+
error: `telegram attachment fetch ${sourceRes.status}: ${sourceErr}`,
|
|
198
|
+
retryDelayMs: sourceRes.status === 429 || sourceRes.status >= 500 ? parseRetryDelayMs(sourceRes) : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const body = await sourceRes.arrayBuffer();
|
|
202
|
+
const contentType = firstAttachment.mime_type ?? sourceRes.headers.get("content-type") ?? "application/octet-stream";
|
|
203
|
+
const filename = firstAttachment.filename ?? `${firstAttachment.type || "attachment"}.bin`;
|
|
204
|
+
const form = new FormData();
|
|
205
|
+
form.append("chat_id", record.envelope.channel_conversation_id);
|
|
206
|
+
if (mediaCaption.length > 0) {
|
|
207
|
+
form.append("caption", mediaCaption);
|
|
208
|
+
}
|
|
209
|
+
form.append(mediaField, new Blob([body], { type: contentType }), filename);
|
|
210
|
+
mediaResponse = await postTelegramApiMultipart(botToken, mediaMethod, form);
|
|
211
|
+
}
|
|
212
|
+
if (mediaResponse.ok) {
|
|
213
|
+
return { kind: "delivered" };
|
|
214
|
+
}
|
|
215
|
+
const mediaBody = await mediaResponse.text().catch(() => "");
|
|
216
|
+
if (mediaResponse.status === 429 || mediaResponse.status >= 500) {
|
|
217
|
+
return {
|
|
218
|
+
kind: "retry",
|
|
219
|
+
error: `telegram ${mediaMethod} ${mediaResponse.status}: ${mediaBody}`,
|
|
220
|
+
retryDelayMs: parseRetryDelayMs(mediaResponse),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const fallbackPlainPayload = buildTelegramSendMessagePayload({
|
|
224
|
+
chatId: record.envelope.channel_conversation_id,
|
|
225
|
+
text: record.envelope.body,
|
|
226
|
+
richFormatting: false,
|
|
227
|
+
});
|
|
228
|
+
const fallbackRes = await postTelegramMessage(botToken, fallbackPlainPayload);
|
|
229
|
+
if (fallbackRes.ok) {
|
|
230
|
+
return { kind: "delivered" };
|
|
231
|
+
}
|
|
232
|
+
const fallbackBody = await fallbackRes.text().catch(() => "");
|
|
233
|
+
if (fallbackRes.status === 429 || fallbackRes.status >= 500) {
|
|
234
|
+
return {
|
|
235
|
+
kind: "retry",
|
|
236
|
+
error: `telegram media fallback sendMessage ${fallbackRes.status}: ${fallbackBody}`,
|
|
237
|
+
retryDelayMs: parseRetryDelayMs(fallbackRes),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
kind: "retry",
|
|
242
|
+
error: `telegram media fallback sendMessage ${fallbackRes.status}: ${fallbackBody} (media_error=${mediaMethod} ${mediaResponse.status}: ${mediaBody})`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
async function postSlackJson(opts) {
|
|
246
|
+
const response = await fetch(`https://slack.com/api/${opts.method}`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: {
|
|
249
|
+
Authorization: `Bearer ${opts.botToken}`,
|
|
250
|
+
"Content-Type": "application/json",
|
|
251
|
+
},
|
|
252
|
+
body: JSON.stringify(opts.payload),
|
|
253
|
+
});
|
|
254
|
+
const payload = (await response.json().catch(() => null));
|
|
255
|
+
return { response, payload };
|
|
256
|
+
}
|
|
257
|
+
export async function deliverSlackOutboxRecord(opts) {
|
|
258
|
+
const { botToken, record } = opts;
|
|
259
|
+
const attachments = record.envelope.attachments ?? [];
|
|
260
|
+
if (attachments.length === 0) {
|
|
261
|
+
const delivered = await postSlackJson({
|
|
262
|
+
botToken,
|
|
263
|
+
method: "chat.postMessage",
|
|
264
|
+
payload: {
|
|
265
|
+
channel: record.envelope.channel_conversation_id,
|
|
266
|
+
text: record.envelope.body,
|
|
267
|
+
unfurl_links: false,
|
|
268
|
+
unfurl_media: false,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
if (delivered.response.ok && delivered.payload?.ok) {
|
|
272
|
+
return { kind: "delivered" };
|
|
273
|
+
}
|
|
274
|
+
const status = delivered.response.status;
|
|
275
|
+
const err = delivered.payload?.error ?? "unknown_error";
|
|
276
|
+
if (status === 429 || status >= 500) {
|
|
277
|
+
return {
|
|
278
|
+
kind: "retry",
|
|
279
|
+
error: `slack chat.postMessage ${status}: ${err}`,
|
|
280
|
+
retryDelayMs: parseRetryDelayMs(delivered.response),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return { kind: "retry", error: `slack chat.postMessage ${status}: ${err}` };
|
|
284
|
+
}
|
|
285
|
+
let firstError = null;
|
|
286
|
+
for (const [index, attachment] of attachments.entries()) {
|
|
287
|
+
const referenceUrl = attachment.reference.url;
|
|
288
|
+
if (!referenceUrl) {
|
|
289
|
+
return {
|
|
290
|
+
kind: "retry",
|
|
291
|
+
error: `slack attachment ${index + 1} missing reference.url`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const source = await fetch(referenceUrl);
|
|
295
|
+
if (!source.ok) {
|
|
296
|
+
const sourceErr = await source.text().catch(() => "");
|
|
297
|
+
if (source.status === 429 || source.status >= 500) {
|
|
298
|
+
return {
|
|
299
|
+
kind: "retry",
|
|
300
|
+
error: `slack attachment fetch ${source.status}: ${sourceErr}`,
|
|
301
|
+
retryDelayMs: parseRetryDelayMs(source),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return { kind: "retry", error: `slack attachment fetch ${source.status}: ${sourceErr}` };
|
|
305
|
+
}
|
|
306
|
+
const bytes = await source.arrayBuffer();
|
|
307
|
+
const contentType = attachment.mime_type ?? source.headers.get("content-type") ?? "application/octet-stream";
|
|
308
|
+
const filename = attachment.filename ?? `attachment-${index + 1}`;
|
|
309
|
+
const form = new FormData();
|
|
310
|
+
form.set("channels", record.envelope.channel_conversation_id);
|
|
311
|
+
form.set("filename", filename);
|
|
312
|
+
form.set("title", filename);
|
|
313
|
+
if (index === 0 && record.envelope.body.trim().length > 0) {
|
|
314
|
+
form.set("initial_comment", record.envelope.body);
|
|
315
|
+
}
|
|
316
|
+
form.set("file", new Blob([bytes], { type: contentType }), filename);
|
|
317
|
+
const uploaded = await fetch("https://slack.com/api/files.upload", {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: {
|
|
320
|
+
Authorization: `Bearer ${botToken}`,
|
|
321
|
+
},
|
|
322
|
+
body: form,
|
|
323
|
+
});
|
|
324
|
+
const uploadPayload = (await uploaded.json().catch(() => null));
|
|
325
|
+
if (!(uploaded.ok && uploadPayload?.ok)) {
|
|
326
|
+
const status = uploaded.status;
|
|
327
|
+
const err = uploadPayload?.error ?? "unknown_error";
|
|
328
|
+
if (status === 429 || status >= 500) {
|
|
329
|
+
return {
|
|
330
|
+
kind: "retry",
|
|
331
|
+
error: `slack files.upload ${status}: ${err}`,
|
|
332
|
+
retryDelayMs: parseRetryDelayMs(uploaded),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (!firstError) {
|
|
336
|
+
firstError = `slack files.upload ${status}: ${err}`;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (firstError) {
|
|
341
|
+
const fallback = await postSlackJson({
|
|
342
|
+
botToken,
|
|
343
|
+
method: "chat.postMessage",
|
|
344
|
+
payload: {
|
|
345
|
+
channel: record.envelope.channel_conversation_id,
|
|
346
|
+
text: record.envelope.body,
|
|
347
|
+
unfurl_links: false,
|
|
348
|
+
unfurl_media: false,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
if (fallback.response.ok && fallback.payload?.ok) {
|
|
352
|
+
return { kind: "delivered" };
|
|
353
|
+
}
|
|
354
|
+
const status = fallback.response.status;
|
|
355
|
+
const err = fallback.payload?.error ?? "unknown_error";
|
|
356
|
+
if (status === 429 || status >= 500) {
|
|
357
|
+
return {
|
|
358
|
+
kind: "retry",
|
|
359
|
+
error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
|
|
360
|
+
retryDelayMs: parseRetryDelayMs(fallback.response),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
kind: "retry",
|
|
365
|
+
error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return { kind: "delivered" };
|
|
369
|
+
}
|
|
103
370
|
export async function bootstrapControlPlane(opts) {
|
|
104
371
|
const controlPlaneConfig = opts.config ?? DEFAULT_MU_CONFIG.control_plane;
|
|
105
372
|
const detected = detectAdapters(controlPlaneConfig);
|
|
@@ -124,7 +391,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
124
391
|
const paths = getControlPlanePaths(opts.repoRoot);
|
|
125
392
|
const runtime = new ControlPlaneRuntime({ repoRoot: opts.repoRoot });
|
|
126
393
|
let pipeline = null;
|
|
127
|
-
let runSupervisor = null;
|
|
128
394
|
let outboxDrainLoop = null;
|
|
129
395
|
let wakeDeliveryObserver = opts.wakeDeliveryObserver ?? null;
|
|
130
396
|
const outboundDeliveryChannels = new Set();
|
|
@@ -143,29 +409,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
143
409
|
});
|
|
144
410
|
await outbox.load();
|
|
145
411
|
let scheduleOutboxDrainRef = null;
|
|
146
|
-
const runQueue = new DurableRunQueue({ repoRoot: opts.repoRoot });
|
|
147
|
-
const interRootQueuePolicy = normalizeInterRootQueuePolicy(opts.interRootQueuePolicy ?? DEFAULT_INTER_ROOT_QUEUE_POLICY);
|
|
148
|
-
const runQueueCoordinator = new ControlPlaneRunQueueCoordinator({
|
|
149
|
-
runQueue,
|
|
150
|
-
interRootQueuePolicy,
|
|
151
|
-
getRunSupervisor: () => runSupervisor,
|
|
152
|
-
});
|
|
153
|
-
runSupervisor = new ControlPlaneRunSupervisor({
|
|
154
|
-
repoRoot: opts.repoRoot,
|
|
155
|
-
spawnProcess: opts.runSupervisorSpawnProcess,
|
|
156
|
-
onEvent: async (event) => {
|
|
157
|
-
await runQueueCoordinator.onRunEvent(event);
|
|
158
|
-
const outboxRecord = await enqueueRunEventOutbox({
|
|
159
|
-
outbox,
|
|
160
|
-
event,
|
|
161
|
-
nowMs: Math.trunc(Date.now()),
|
|
162
|
-
});
|
|
163
|
-
if (outboxRecord) {
|
|
164
|
-
scheduleOutboxDrainRef?.();
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
await runQueueCoordinator.scheduleReconcile("bootstrap");
|
|
169
412
|
pipeline = new ControlPlaneCommandPipeline({
|
|
170
413
|
runtime,
|
|
171
414
|
operator,
|
|
@@ -177,7 +420,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
177
420
|
errorCode: "cli_validation_failed",
|
|
178
421
|
trace: {
|
|
179
422
|
cliCommandKind: record.target_type,
|
|
180
|
-
runRootId: null,
|
|
181
423
|
},
|
|
182
424
|
mutatingEvents: [
|
|
183
425
|
{
|
|
@@ -201,7 +443,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
201
443
|
errorCode: "session_lifecycle_failed",
|
|
202
444
|
trace: {
|
|
203
445
|
cliCommandKind: action,
|
|
204
|
-
runRootId: null,
|
|
205
446
|
},
|
|
206
447
|
mutatingEvents: [
|
|
207
448
|
{
|
|
@@ -225,7 +466,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
225
466
|
},
|
|
226
467
|
trace: {
|
|
227
468
|
cliCommandKind: action,
|
|
228
|
-
runRootId: null,
|
|
229
469
|
},
|
|
230
470
|
mutatingEvents: [
|
|
231
471
|
{
|
|
@@ -245,7 +485,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
245
485
|
errorCode: err instanceof Error && err.message ? err.message : "session_lifecycle_failed",
|
|
246
486
|
trace: {
|
|
247
487
|
cliCommandKind: action,
|
|
248
|
-
runRootId: null,
|
|
249
488
|
},
|
|
250
489
|
mutatingEvents: [
|
|
251
490
|
{
|
|
@@ -259,96 +498,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
259
498
|
};
|
|
260
499
|
}
|
|
261
500
|
}
|
|
262
|
-
if (record.target_type === "run start" || record.target_type === "run resume") {
|
|
263
|
-
try {
|
|
264
|
-
const launched = await runQueueCoordinator.launchQueuedRunFromCommand(record);
|
|
265
|
-
return {
|
|
266
|
-
terminalState: "completed",
|
|
267
|
-
result: {
|
|
268
|
-
ok: true,
|
|
269
|
-
async_run: true,
|
|
270
|
-
run_job_id: launched.job_id,
|
|
271
|
-
run_root_id: launched.root_issue_id,
|
|
272
|
-
run_status: launched.status,
|
|
273
|
-
run_mode: launched.mode,
|
|
274
|
-
run_source: launched.source,
|
|
275
|
-
},
|
|
276
|
-
trace: {
|
|
277
|
-
cliCommandKind: launched.mode,
|
|
278
|
-
runRootId: launched.root_issue_id,
|
|
279
|
-
},
|
|
280
|
-
mutatingEvents: [
|
|
281
|
-
{
|
|
282
|
-
eventType: "run.supervisor.start",
|
|
283
|
-
payload: {
|
|
284
|
-
run_job_id: launched.job_id,
|
|
285
|
-
run_mode: launched.mode,
|
|
286
|
-
run_root_id: launched.root_issue_id,
|
|
287
|
-
run_source: launched.source,
|
|
288
|
-
queue_id: launched.queue_id ?? null,
|
|
289
|
-
queue_state: launched.queue_state ?? null,
|
|
290
|
-
},
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
return {
|
|
297
|
-
terminalState: "failed",
|
|
298
|
-
errorCode: err instanceof Error && err.message ? err.message : "run_queue_start_failed",
|
|
299
|
-
trace: {
|
|
300
|
-
cliCommandKind: record.target_type.replaceAll(" ", "_"),
|
|
301
|
-
runRootId: record.target_id,
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
if (record.target_type === "run interrupt") {
|
|
307
|
-
const result = await runQueueCoordinator.interruptQueuedRun({
|
|
308
|
-
rootIssueId: record.target_id,
|
|
309
|
-
});
|
|
310
|
-
if (!result.ok) {
|
|
311
|
-
return {
|
|
312
|
-
terminalState: "failed",
|
|
313
|
-
errorCode: result.reason ?? "run_interrupt_failed",
|
|
314
|
-
trace: {
|
|
315
|
-
cliCommandKind: "run_interrupt",
|
|
316
|
-
runRootId: result.run?.root_issue_id ?? record.target_id,
|
|
317
|
-
},
|
|
318
|
-
mutatingEvents: [
|
|
319
|
-
{
|
|
320
|
-
eventType: "run.supervisor.interrupt.failed",
|
|
321
|
-
payload: {
|
|
322
|
-
reason: result.reason,
|
|
323
|
-
target: record.target_id,
|
|
324
|
-
},
|
|
325
|
-
},
|
|
326
|
-
],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
return {
|
|
330
|
-
terminalState: "completed",
|
|
331
|
-
result: {
|
|
332
|
-
ok: true,
|
|
333
|
-
async_run: true,
|
|
334
|
-
interrupted: true,
|
|
335
|
-
run: result.run,
|
|
336
|
-
},
|
|
337
|
-
trace: {
|
|
338
|
-
cliCommandKind: "run_interrupt",
|
|
339
|
-
runRootId: result.run?.root_issue_id ?? record.target_id,
|
|
340
|
-
},
|
|
341
|
-
mutatingEvents: [
|
|
342
|
-
{
|
|
343
|
-
eventType: "run.supervisor.interrupt",
|
|
344
|
-
payload: {
|
|
345
|
-
target: record.target_id,
|
|
346
|
-
run: result.run,
|
|
347
|
-
},
|
|
348
|
-
},
|
|
349
|
-
],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
501
|
return null;
|
|
353
502
|
},
|
|
354
503
|
});
|
|
@@ -366,6 +515,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
366
515
|
await telegramManager.initialize();
|
|
367
516
|
for (const adapter of createStaticAdaptersFromDetected({
|
|
368
517
|
detected,
|
|
518
|
+
config: controlPlaneConfig,
|
|
369
519
|
pipeline,
|
|
370
520
|
outbox,
|
|
371
521
|
})) {
|
|
@@ -416,6 +566,19 @@ export async function bootstrapControlPlane(opts) {
|
|
|
416
566
|
isActive: () => telegramManager.hasActiveGeneration(),
|
|
417
567
|
});
|
|
418
568
|
const deliveryRouter = new OutboundDeliveryRouter([
|
|
569
|
+
{
|
|
570
|
+
channel: "slack",
|
|
571
|
+
deliver: async (record) => {
|
|
572
|
+
const slackBotToken = controlPlaneConfig.adapters.slack.bot_token;
|
|
573
|
+
if (!slackBotToken) {
|
|
574
|
+
return { kind: "retry", error: "slack bot token not configured in mu workspace config" };
|
|
575
|
+
}
|
|
576
|
+
return await deliverSlackOutboxRecord({
|
|
577
|
+
botToken: slackBotToken,
|
|
578
|
+
record,
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
},
|
|
419
582
|
{
|
|
420
583
|
channel: "telegram",
|
|
421
584
|
deliver: async (record) => {
|
|
@@ -423,44 +586,15 @@ export async function bootstrapControlPlane(opts) {
|
|
|
423
586
|
if (!telegramBotToken) {
|
|
424
587
|
return { kind: "retry", error: "telegram bot token not configured in mu workspace config" };
|
|
425
588
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
richFormatting: true,
|
|
589
|
+
return await deliverTelegramOutboxRecord({
|
|
590
|
+
botToken: telegramBotToken,
|
|
591
|
+
record,
|
|
430
592
|
});
|
|
431
|
-
let res = await postTelegramMessage(telegramBotToken, richPayload);
|
|
432
|
-
// Fallback: if Telegram rejects markdown entities, retry as plain text.
|
|
433
|
-
if (!res.ok && res.status === 400 && richPayload.parse_mode) {
|
|
434
|
-
const plainPayload = buildTelegramSendMessagePayload({
|
|
435
|
-
chatId: record.envelope.channel_conversation_id,
|
|
436
|
-
text: record.envelope.body,
|
|
437
|
-
richFormatting: false,
|
|
438
|
-
});
|
|
439
|
-
res = await postTelegramMessage(telegramBotToken, plainPayload);
|
|
440
|
-
}
|
|
441
|
-
if (res.ok) {
|
|
442
|
-
return { kind: "delivered" };
|
|
443
|
-
}
|
|
444
|
-
const responseBody = await res.text().catch(() => "");
|
|
445
|
-
if (res.status === 429 || res.status >= 500) {
|
|
446
|
-
const retryAfter = res.headers.get("retry-after");
|
|
447
|
-
const retryDelayMs = retryAfter ? Number.parseInt(retryAfter, 10) * 1000 : undefined;
|
|
448
|
-
return {
|
|
449
|
-
kind: "retry",
|
|
450
|
-
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
451
|
-
retryDelayMs: retryDelayMs && Number.isFinite(retryDelayMs) ? retryDelayMs : undefined,
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
kind: "retry",
|
|
456
|
-
error: `telegram sendMessage ${res.status}: ${responseBody}`,
|
|
457
|
-
};
|
|
458
593
|
},
|
|
459
594
|
},
|
|
460
595
|
]);
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
596
|
+
outboundDeliveryChannels.add("slack");
|
|
597
|
+
outboundDeliveryChannels.add("telegram");
|
|
464
598
|
const notifyOperators = async (notifyOpts) => {
|
|
465
599
|
if (!pipeline) {
|
|
466
600
|
return emptyNotifyOperatorsResult();
|
|
@@ -491,6 +625,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
491
625
|
sourceTsMs: wakeSourceTsMs,
|
|
492
626
|
};
|
|
493
627
|
const nowMs = Math.trunc(Date.now());
|
|
628
|
+
const slackBotToken = controlPlaneConfig.adapters.slack.bot_token;
|
|
494
629
|
const telegramBotToken = telegramManager.activeBotToken();
|
|
495
630
|
const bindings = pipeline.identities
|
|
496
631
|
.listBindings({ includeInactive: false })
|
|
@@ -505,6 +640,7 @@ export async function bootstrapControlPlane(opts) {
|
|
|
505
640
|
const capability = resolveWakeFanoutCapability({
|
|
506
641
|
binding,
|
|
507
642
|
isChannelDeliverySupported: (channel) => outboundDeliveryChannels.has(channel),
|
|
643
|
+
slackBotToken,
|
|
508
644
|
telegramBotToken,
|
|
509
645
|
});
|
|
510
646
|
if (!capability.ok) {
|
|
@@ -624,76 +760,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
624
760
|
}
|
|
625
761
|
return result;
|
|
626
762
|
},
|
|
627
|
-
async listRuns(opts = {}) {
|
|
628
|
-
const limit = Math.max(1, Math.min(500, Math.trunc(opts.limit ?? 100)));
|
|
629
|
-
const fallbackStatusFilter = queueStatesForRunStatusFilter(opts.status);
|
|
630
|
-
if (Array.isArray(fallbackStatusFilter) && fallbackStatusFilter.length === 0) {
|
|
631
|
-
return [];
|
|
632
|
-
}
|
|
633
|
-
const queued = await runQueue.listRunSnapshots({
|
|
634
|
-
status: opts.status,
|
|
635
|
-
limit,
|
|
636
|
-
runtimeByJobId: runQueueCoordinator.runtimeSnapshotsByJobId(),
|
|
637
|
-
});
|
|
638
|
-
const seen = new Set(queued.map((run) => run.job_id));
|
|
639
|
-
const fallbackRuns = runSupervisor?.list({ limit: 500 }) ?? [];
|
|
640
|
-
for (const run of fallbackRuns) {
|
|
641
|
-
if (seen.has(run.job_id)) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
if (fallbackStatusFilter && fallbackStatusFilter.length > 0) {
|
|
645
|
-
const mapped = run.status === "completed"
|
|
646
|
-
? "done"
|
|
647
|
-
: run.status === "failed"
|
|
648
|
-
? "failed"
|
|
649
|
-
: run.status === "cancelled"
|
|
650
|
-
? "cancelled"
|
|
651
|
-
: "active";
|
|
652
|
-
if (!fallbackStatusFilter.includes(mapped)) {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
queued.push(run);
|
|
657
|
-
seen.add(run.job_id);
|
|
658
|
-
}
|
|
659
|
-
return queued.slice(0, limit);
|
|
660
|
-
},
|
|
661
|
-
async getRun(idOrRoot) {
|
|
662
|
-
const queued = await runQueue.get(idOrRoot);
|
|
663
|
-
if (queued) {
|
|
664
|
-
const runtime = queued.job_id ? (runSupervisor?.get(queued.job_id) ?? null) : null;
|
|
665
|
-
return runSnapshotFromQueueSnapshot(queued, runtime);
|
|
666
|
-
}
|
|
667
|
-
return runSupervisor?.get(idOrRoot) ?? null;
|
|
668
|
-
},
|
|
669
|
-
async startRun(startOpts) {
|
|
670
|
-
return await runQueueCoordinator.launchQueuedRun({
|
|
671
|
-
mode: "run_start",
|
|
672
|
-
prompt: startOpts.prompt,
|
|
673
|
-
maxSteps: startOpts.maxSteps,
|
|
674
|
-
source: "api",
|
|
675
|
-
dedupeKey: `api:run_start:${crypto.randomUUID()}`,
|
|
676
|
-
});
|
|
677
|
-
},
|
|
678
|
-
async resumeRun(resumeOpts) {
|
|
679
|
-
const rootIssueId = normalizeIssueId(resumeOpts.rootIssueId);
|
|
680
|
-
if (!rootIssueId) {
|
|
681
|
-
throw new Error("run_resume_invalid_root_issue_id");
|
|
682
|
-
}
|
|
683
|
-
return await runQueueCoordinator.launchQueuedRun({
|
|
684
|
-
mode: "run_resume",
|
|
685
|
-
rootIssueId,
|
|
686
|
-
maxSteps: resumeOpts.maxSteps,
|
|
687
|
-
source: "api",
|
|
688
|
-
dedupeKey: `api:run_resume:${rootIssueId}:${crypto.randomUUID()}`,
|
|
689
|
-
});
|
|
690
|
-
},
|
|
691
|
-
async interruptRun(interruptOpts) {
|
|
692
|
-
return await runQueueCoordinator.interruptQueuedRun(interruptOpts);
|
|
693
|
-
},
|
|
694
|
-
async traceRun(traceOpts) {
|
|
695
|
-
return (await runSupervisor?.trace(traceOpts.idOrRoot, { limit: traceOpts.limit })) ?? null;
|
|
696
|
-
},
|
|
697
763
|
async submitTerminalCommand(terminalOpts) {
|
|
698
764
|
if (!pipeline) {
|
|
699
765
|
throw new Error("control_plane_pipeline_unavailable");
|
|
@@ -702,7 +768,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
702
768
|
},
|
|
703
769
|
async stop() {
|
|
704
770
|
wakeDeliveryObserver = null;
|
|
705
|
-
runQueueCoordinator.stop();
|
|
706
771
|
if (outboxDrainLoop) {
|
|
707
772
|
outboxDrainLoop.stop();
|
|
708
773
|
outboxDrainLoop = null;
|
|
@@ -715,7 +780,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
715
780
|
// Best effort adapter cleanup.
|
|
716
781
|
}
|
|
717
782
|
}
|
|
718
|
-
await runSupervisor?.stop();
|
|
719
783
|
try {
|
|
720
784
|
await pipeline?.stop();
|
|
721
785
|
}
|
|
@@ -739,12 +803,6 @@ export async function bootstrapControlPlane(opts) {
|
|
|
739
803
|
// Best effort cleanup.
|
|
740
804
|
}
|
|
741
805
|
}
|
|
742
|
-
try {
|
|
743
|
-
await runSupervisor?.stop();
|
|
744
|
-
}
|
|
745
|
-
catch {
|
|
746
|
-
// Best effort cleanup.
|
|
747
|
-
}
|
|
748
806
|
try {
|
|
749
807
|
await pipeline?.stop();
|
|
750
808
|
}
|
|
@@ -14,6 +14,7 @@ export type DetectedAdapter = DetectedStaticAdapter | DetectedTelegramAdapter;
|
|
|
14
14
|
export declare function detectAdapters(config: ControlPlaneConfig): DetectedAdapter[];
|
|
15
15
|
export declare function createStaticAdaptersFromDetected(opts: {
|
|
16
16
|
detected: readonly DetectedAdapter[];
|
|
17
|
+
config: ControlPlaneConfig;
|
|
17
18
|
pipeline: ControlPlaneCommandPipeline;
|
|
18
19
|
outbox: ControlPlaneOutbox;
|
|
19
20
|
}): ControlPlaneAdapter[];
|