@femtomc/mu-server 26.2.94 → 26.2.96

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 CHANGED
@@ -189,6 +189,13 @@ When validating attachment support end-to-end, use this sequence:
189
189
  Operational fallback expectations:
190
190
 
191
191
  - If media upload fails, Telegram delivery falls back to text `sendMessage`.
192
+ - Telegram text delivery chunks long messages into deterministic in-order `sendMessage` calls to stay below API size limits.
193
+ - When outbound metadata includes `telegram_reply_to_message_id`, Telegram delivery anchors replies to the originating chat message.
194
+ - Invalid/non-integer `telegram_reply_to_message_id` metadata is ignored so delivery degrades gracefully to non-anchored sends.
195
+ - Awaiting-confirmation envelopes include Telegram inline `Confirm`/`Cancel` callback buttons when interaction metadata provides confirmation actions.
196
+ - Telegram callback payloads are contract-limited to `confirm:<command_id>` and `cancel:<command_id>`; unsupported payloads are explicitly rejected.
197
+ - Callback buttons keep parity with command fallback: `/mu confirm <id>` and `/mu cancel <id>` remain valid.
198
+ - Group/supergroup Telegram freeform text is deterministic no-op with guidance; explicit `/mu ...` commands stay actionable.
192
199
  - If Slack/Telegram bot token is missing, channel capability reason codes should report `*_bot_token_missing` and outbound delivery retries rather than hard-crashing runtime.
193
200
 
194
201
  ## Running the Server
@@ -4,21 +4,35 @@ import { type ControlPlaneConfig, type ControlPlaneGenerationContext, type Contr
4
4
  import { detectAdapters } from "./control_plane_adapter_registry.js";
5
5
  export type { ActiveAdapter, ControlPlaneConfig, ControlPlaneGenerationContext, ControlPlaneHandle, ControlPlaneSessionLifecycle, ControlPlaneSessionMutationAction, ControlPlaneSessionMutationResult, NotifyOperatorsOpts, NotifyOperatorsResult, TelegramGenerationReloadResult, TelegramGenerationRollbackTrigger, TelegramGenerationSwapHooks, WakeDeliveryEvent, WakeDeliveryObserver, WakeNotifyContext, WakeNotifyDecision, } from "./control_plane_contract.js";
6
6
  export { detectAdapters };
7
+ type TelegramInlineKeyboardButton = {
8
+ text: string;
9
+ callback_data: string;
10
+ };
11
+ type TelegramInlineKeyboardMarkup = {
12
+ inline_keyboard: TelegramInlineKeyboardButton[][];
13
+ };
7
14
  export type TelegramSendMessagePayload = {
8
15
  chat_id: string;
9
16
  text: string;
10
17
  parse_mode?: "Markdown";
11
18
  disable_web_page_preview?: boolean;
19
+ reply_markup?: TelegramInlineKeyboardMarkup;
20
+ reply_to_message_id?: number;
21
+ allow_sending_without_reply?: boolean;
12
22
  };
13
23
  export type TelegramSendPhotoPayload = {
14
24
  chat_id: string;
15
25
  photo: string;
16
26
  caption?: string;
27
+ reply_to_message_id?: number;
28
+ allow_sending_without_reply?: boolean;
17
29
  };
18
30
  export type TelegramSendDocumentPayload = {
19
31
  chat_id: string;
20
32
  document: string;
21
33
  caption?: string;
34
+ reply_to_message_id?: number;
35
+ allow_sending_without_reply?: boolean;
22
36
  };
23
37
  /**
24
38
  * Telegram supports a markdown dialect that uses single markers for emphasis.
@@ -32,6 +46,8 @@ export declare function buildTelegramSendMessagePayload(opts: {
32
46
  text: string;
33
47
  richFormatting: boolean;
34
48
  }): TelegramSendMessagePayload;
49
+ export declare function splitTelegramMessageText(text: string, maxLen?: number): string[];
50
+ export declare function splitSlackMessageText(text: string, maxLen?: number): string[];
35
51
  export declare function deliverTelegramOutboxRecord(opts: {
36
52
  botToken: string;
37
53
  record: OutboxRecord;
@@ -79,6 +79,147 @@ export function buildTelegramSendMessagePayload(opts) {
79
79
  disable_web_page_preview: true,
80
80
  };
81
81
  }
82
+ const TELEGRAM_MESSAGE_MAX_LEN = 4_096;
83
+ function maybeParseTelegramMessageId(value) {
84
+ if (typeof value === "number" && Number.isFinite(value)) {
85
+ return Math.trunc(value);
86
+ }
87
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
88
+ const parsed = Number.parseInt(value.trim(), 10);
89
+ if (Number.isFinite(parsed)) {
90
+ return parsed;
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ export function splitTelegramMessageText(text, maxLen = TELEGRAM_MESSAGE_MAX_LEN) {
96
+ if (text.length <= maxLen) {
97
+ return [text];
98
+ }
99
+ const chunks = [];
100
+ let cursor = 0;
101
+ while (cursor < text.length) {
102
+ const end = Math.min(text.length, cursor + maxLen);
103
+ if (end === text.length) {
104
+ chunks.push(text.slice(cursor));
105
+ break;
106
+ }
107
+ const window = text.slice(cursor, end);
108
+ const splitAtNewline = window.lastIndexOf("\n");
109
+ const splitPoint = splitAtNewline >= Math.floor(maxLen * 0.5) ? cursor + splitAtNewline + 1 : end;
110
+ chunks.push(text.slice(cursor, splitPoint));
111
+ cursor = splitPoint;
112
+ }
113
+ return chunks;
114
+ }
115
+ const SLACK_MESSAGE_MAX_LEN = 3_500;
116
+ export function splitSlackMessageText(text, maxLen = SLACK_MESSAGE_MAX_LEN) {
117
+ if (text.length <= maxLen) {
118
+ return [text];
119
+ }
120
+ const chunks = [];
121
+ let cursor = 0;
122
+ while (cursor < text.length) {
123
+ const end = Math.min(text.length, cursor + maxLen);
124
+ if (end === text.length) {
125
+ chunks.push(text.slice(cursor));
126
+ break;
127
+ }
128
+ const window = text.slice(cursor, end);
129
+ const splitAtNewline = window.lastIndexOf("\n");
130
+ const splitPoint = splitAtNewline >= Math.floor(maxLen * 0.5) ? cursor + splitAtNewline + 1 : end;
131
+ chunks.push(text.slice(cursor, splitPoint));
132
+ cursor = splitPoint;
133
+ }
134
+ return chunks;
135
+ }
136
+ function slackBlocksForOutboxRecord(record) {
137
+ const interactionMessage = record.envelope.metadata?.interaction_message;
138
+ if (!interactionMessage || typeof interactionMessage !== "object") {
139
+ return undefined;
140
+ }
141
+ const rawActions = interactionMessage.actions;
142
+ if (!Array.isArray(rawActions) || rawActions.length === 0) {
143
+ return undefined;
144
+ }
145
+ const buttons = [];
146
+ for (const action of rawActions) {
147
+ if (!action || typeof action !== "object") {
148
+ continue;
149
+ }
150
+ const candidate = action;
151
+ if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
152
+ continue;
153
+ }
154
+ const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
155
+ const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
156
+ if (confirm?.[1]) {
157
+ buttons.push({
158
+ type: "button",
159
+ text: { type: "plain_text", text: candidate.label },
160
+ value: `confirm:${confirm[1]}`,
161
+ action_id: `confirm_${confirm[1]}`,
162
+ });
163
+ continue;
164
+ }
165
+ const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
166
+ if (cancel?.[1]) {
167
+ buttons.push({
168
+ type: "button",
169
+ text: { type: "plain_text", text: candidate.label },
170
+ value: `cancel:${cancel[1]}`,
171
+ action_id: `cancel_${cancel[1]}`,
172
+ });
173
+ }
174
+ }
175
+ if (buttons.length === 0) {
176
+ return undefined;
177
+ }
178
+ return [
179
+ { type: "section", text: { type: "mrkdwn", text: record.envelope.body } },
180
+ { type: "actions", elements: buttons },
181
+ ];
182
+ }
183
+ function slackThreadTsFromMetadata(metadata) {
184
+ const candidates = [metadata?.slack_thread_ts, metadata?.slack_message_ts, metadata?.thread_ts];
185
+ for (const value of candidates) {
186
+ if (typeof value === "string" && value.trim().length > 0) {
187
+ return value.trim();
188
+ }
189
+ }
190
+ return undefined;
191
+ }
192
+ function telegramReplyMarkupForOutboxRecord(record) {
193
+ const interactionMessage = record.envelope.metadata?.interaction_message;
194
+ if (!interactionMessage || typeof interactionMessage !== "object") {
195
+ return undefined;
196
+ }
197
+ const rawActions = interactionMessage.actions;
198
+ if (!Array.isArray(rawActions) || rawActions.length === 0) {
199
+ return undefined;
200
+ }
201
+ const row = [];
202
+ for (const action of rawActions) {
203
+ if (!action || typeof action !== "object") {
204
+ continue;
205
+ }
206
+ const candidate = action;
207
+ if (typeof candidate.label !== "string" || typeof candidate.command !== "string") {
208
+ continue;
209
+ }
210
+ const normalized = candidate.command.trim().replace(/^\/mu\s+/i, "");
211
+ const confirm = /^confirm\s+([^\s]+)$/i.exec(normalized);
212
+ if (confirm?.[1]) {
213
+ row.push({ text: candidate.label, callback_data: `confirm:${confirm[1]}` });
214
+ continue;
215
+ }
216
+ const cancel = /^cancel\s+([^\s]+)$/i.exec(normalized);
217
+ if (cancel?.[1]) {
218
+ row.push({ text: candidate.label, callback_data: `cancel:${cancel[1]}` });
219
+ }
220
+ }
221
+ return row.length > 0 ? { inline_keyboard: [row] } : undefined;
222
+ }
82
223
  async function postTelegramMessage(botToken, payload) {
83
224
  return await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
84
225
  method: "POST",
@@ -133,37 +274,76 @@ function parseRetryDelayMs(res) {
133
274
  }
134
275
  return parsed * 1000;
135
276
  }
277
+ async function sendTelegramTextChunks(opts) {
278
+ const chunks = splitTelegramMessageText(opts.basePayload.text);
279
+ for (const [index, chunk] of chunks.entries()) {
280
+ const payload = {
281
+ ...opts.basePayload,
282
+ text: chunk,
283
+ ...(index === 0 ? {} : { reply_markup: undefined, reply_to_message_id: undefined, allow_sending_without_reply: undefined }),
284
+ };
285
+ let res = await postTelegramMessage(opts.botToken, payload);
286
+ if (!res.ok && res.status === 400 && payload.parse_mode && opts.fallbackToPlainOnMarkdownError) {
287
+ const plainPayload = buildTelegramSendMessagePayload({
288
+ chatId: payload.chat_id,
289
+ text: payload.text,
290
+ richFormatting: false,
291
+ });
292
+ res = await postTelegramMessage(opts.botToken, {
293
+ ...plainPayload,
294
+ ...(payload.reply_markup ? { reply_markup: payload.reply_markup } : {}),
295
+ ...(payload.reply_to_message_id != null
296
+ ? {
297
+ reply_to_message_id: payload.reply_to_message_id,
298
+ allow_sending_without_reply: payload.allow_sending_without_reply,
299
+ }
300
+ : {}),
301
+ });
302
+ }
303
+ if (!res.ok) {
304
+ return { ok: false, response: res, body: await res.text().catch(() => "") };
305
+ }
306
+ }
307
+ return { ok: true };
308
+ }
136
309
  export async function deliverTelegramOutboxRecord(opts) {
137
310
  const { botToken, record } = opts;
138
- const fallbackMessagePayload = buildTelegramSendMessagePayload({
139
- chatId: record.envelope.channel_conversation_id,
140
- text: record.envelope.body,
141
- richFormatting: true,
142
- });
311
+ const replyMarkup = telegramReplyMarkupForOutboxRecord(record);
312
+ const replyToMessageId = maybeParseTelegramMessageId(record.envelope.metadata?.telegram_reply_to_message_id);
313
+ const fallbackMessagePayload = {
314
+ ...buildTelegramSendMessagePayload({
315
+ chatId: record.envelope.channel_conversation_id,
316
+ text: record.envelope.body,
317
+ richFormatting: true,
318
+ }),
319
+ ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
320
+ ...(replyToMessageId != null
321
+ ? {
322
+ reply_to_message_id: replyToMessageId,
323
+ allow_sending_without_reply: true,
324
+ }
325
+ : {}),
326
+ };
143
327
  const firstAttachment = record.envelope.attachments?.[0] ?? null;
144
328
  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) {
329
+ const sent = await sendTelegramTextChunks({
330
+ botToken,
331
+ basePayload: fallbackMessagePayload,
332
+ fallbackToPlainOnMarkdownError: true,
333
+ });
334
+ if (sent.ok) {
154
335
  return { kind: "delivered" };
155
336
  }
156
- const responseBody = await res.text().catch(() => "");
157
- if (res.status === 429 || res.status >= 500) {
337
+ if (sent.response.status === 429 || sent.response.status >= 500) {
158
338
  return {
159
339
  kind: "retry",
160
- error: `telegram sendMessage ${res.status}: ${responseBody}`,
161
- retryDelayMs: parseRetryDelayMs(res),
340
+ error: `telegram sendMessage ${sent.response.status}: ${sent.body}`,
341
+ retryDelayMs: parseRetryDelayMs(sent.response),
162
342
  };
163
343
  }
164
344
  return {
165
345
  kind: "retry",
166
- error: `telegram sendMessage ${res.status}: ${responseBody}`,
346
+ error: `telegram sendMessage ${sent.response.status}: ${sent.body}`,
167
347
  };
168
348
  }
169
349
  const mediaMethod = chooseTelegramMediaMethod(firstAttachment);
@@ -180,11 +360,23 @@ export async function deliverTelegramOutboxRecord(opts) {
180
360
  chat_id: record.envelope.channel_conversation_id,
181
361
  photo: firstAttachment.reference.file_id,
182
362
  caption: mediaCaption,
363
+ ...(replyToMessageId != null
364
+ ? {
365
+ reply_to_message_id: replyToMessageId,
366
+ allow_sending_without_reply: true,
367
+ }
368
+ : {}),
183
369
  }
184
370
  : {
185
371
  chat_id: record.envelope.channel_conversation_id,
186
372
  document: firstAttachment.reference.file_id,
187
373
  caption: mediaCaption,
374
+ ...(replyToMessageId != null
375
+ ? {
376
+ reply_to_message_id: replyToMessageId,
377
+ allow_sending_without_reply: true,
378
+ }
379
+ : {}),
188
380
  });
189
381
  }
190
382
  else {
@@ -206,6 +398,10 @@ export async function deliverTelegramOutboxRecord(opts) {
206
398
  if (mediaCaption.length > 0) {
207
399
  form.append("caption", mediaCaption);
208
400
  }
401
+ if (replyToMessageId != null) {
402
+ form.append("reply_to_message_id", String(replyToMessageId));
403
+ form.append("allow_sending_without_reply", "true");
404
+ }
209
405
  form.append(mediaField, new Blob([body], { type: contentType }), filename);
210
406
  mediaResponse = await postTelegramApiMultipart(botToken, mediaMethod, form);
211
407
  }
@@ -220,26 +416,38 @@ export async function deliverTelegramOutboxRecord(opts) {
220
416
  retryDelayMs: parseRetryDelayMs(mediaResponse),
221
417
  };
222
418
  }
223
- const fallbackPlainPayload = buildTelegramSendMessagePayload({
224
- chatId: record.envelope.channel_conversation_id,
225
- text: record.envelope.body,
226
- richFormatting: false,
419
+ const fallbackPlainPayload = {
420
+ ...buildTelegramSendMessagePayload({
421
+ chatId: record.envelope.channel_conversation_id,
422
+ text: record.envelope.body,
423
+ richFormatting: false,
424
+ }),
425
+ ...(replyMarkup ? { reply_markup: replyMarkup } : {}),
426
+ ...(replyToMessageId != null
427
+ ? {
428
+ reply_to_message_id: replyToMessageId,
429
+ allow_sending_without_reply: true,
430
+ }
431
+ : {}),
432
+ };
433
+ const fallbackSent = await sendTelegramTextChunks({
434
+ botToken,
435
+ basePayload: fallbackPlainPayload,
436
+ fallbackToPlainOnMarkdownError: false,
227
437
  });
228
- const fallbackRes = await postTelegramMessage(botToken, fallbackPlainPayload);
229
- if (fallbackRes.ok) {
438
+ if (fallbackSent.ok) {
230
439
  return { kind: "delivered" };
231
440
  }
232
- const fallbackBody = await fallbackRes.text().catch(() => "");
233
- if (fallbackRes.status === 429 || fallbackRes.status >= 500) {
441
+ if (fallbackSent.response.status === 429 || fallbackSent.response.status >= 500) {
234
442
  return {
235
443
  kind: "retry",
236
- error: `telegram media fallback sendMessage ${fallbackRes.status}: ${fallbackBody}`,
237
- retryDelayMs: parseRetryDelayMs(fallbackRes),
444
+ error: `telegram media fallback sendMessage ${fallbackSent.response.status}: ${fallbackSent.body}`,
445
+ retryDelayMs: parseRetryDelayMs(fallbackSent.response),
238
446
  };
239
447
  }
240
448
  return {
241
449
  kind: "retry",
242
- error: `telegram media fallback sendMessage ${fallbackRes.status}: ${fallbackBody} (media_error=${mediaMethod} ${mediaResponse.status}: ${mediaBody})`,
450
+ error: `telegram media fallback sendMessage ${fallbackSent.response.status}: ${fallbackSent.body} (media_error=${mediaMethod} ${mediaResponse.status}: ${mediaBody})`,
243
451
  };
244
452
  }
245
453
  async function postSlackJson(opts) {
@@ -257,30 +465,38 @@ async function postSlackJson(opts) {
257
465
  export async function deliverSlackOutboxRecord(opts) {
258
466
  const { botToken, record } = opts;
259
467
  const attachments = record.envelope.attachments ?? [];
468
+ const textChunks = splitSlackMessageText(record.envelope.body);
469
+ const blocks = slackBlocksForOutboxRecord(record);
470
+ const threadTs = slackThreadTsFromMetadata(record.envelope.metadata);
260
471
  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
- };
472
+ for (const [index, chunk] of textChunks.entries()) {
473
+ const delivered = await postSlackJson({
474
+ botToken,
475
+ method: "chat.postMessage",
476
+ payload: {
477
+ channel: record.envelope.channel_conversation_id,
478
+ text: chunk,
479
+ unfurl_links: false,
480
+ unfurl_media: false,
481
+ ...(index === 0 && blocks ? { blocks } : {}),
482
+ ...(threadTs ? { thread_ts: threadTs } : {}),
483
+ },
484
+ });
485
+ if (delivered.response.ok && delivered.payload?.ok) {
486
+ continue;
487
+ }
488
+ const status = delivered.response.status;
489
+ const err = delivered.payload?.error ?? "unknown_error";
490
+ if (status === 429 || status >= 500) {
491
+ return {
492
+ kind: "retry",
493
+ error: `slack chat.postMessage ${status}: ${err}`,
494
+ retryDelayMs: parseRetryDelayMs(delivered.response),
495
+ };
496
+ }
497
+ return { kind: "retry", error: `slack chat.postMessage ${status}: ${err}` };
282
498
  }
283
- return { kind: "retry", error: `slack chat.postMessage ${status}: ${err}` };
499
+ return { kind: "delivered" };
284
500
  }
285
501
  let firstError = null;
286
502
  for (const [index, attachment] of attachments.entries()) {
@@ -311,7 +527,10 @@ export async function deliverSlackOutboxRecord(opts) {
311
527
  form.set("filename", filename);
312
528
  form.set("title", filename);
313
529
  if (index === 0 && record.envelope.body.trim().length > 0) {
314
- form.set("initial_comment", record.envelope.body);
530
+ form.set("initial_comment", textChunks[0] ?? record.envelope.body);
531
+ }
532
+ if (threadTs) {
533
+ form.set("thread_ts", threadTs);
315
534
  }
316
535
  form.set("file", new Blob([bytes], { type: contentType }), filename);
317
536
  const uploaded = await fetch("https://slack.com/api/files.upload", {
@@ -337,33 +556,39 @@ export async function deliverSlackOutboxRecord(opts) {
337
556
  }
338
557
  }
339
558
  }
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) {
559
+ if (firstError || textChunks.length > 1) {
560
+ for (const [index, chunk] of textChunks.entries()) {
561
+ if (index === 0 && !firstError) {
562
+ continue;
563
+ }
564
+ const fallback = await postSlackJson({
565
+ botToken,
566
+ method: "chat.postMessage",
567
+ payload: {
568
+ channel: record.envelope.channel_conversation_id,
569
+ text: chunk,
570
+ unfurl_links: false,
571
+ unfurl_media: false,
572
+ ...(threadTs ? { thread_ts: threadTs } : {}),
573
+ },
574
+ });
575
+ if (fallback.response.ok && fallback.payload?.ok) {
576
+ continue;
577
+ }
578
+ const status = fallback.response.status;
579
+ const err = fallback.payload?.error ?? "unknown_error";
580
+ if (status === 429 || status >= 500) {
581
+ return {
582
+ kind: "retry",
583
+ error: `slack chat.postMessage fallback ${status}: ${err}${firstError ? ` (upload_error=${firstError})` : ""}`,
584
+ retryDelayMs: parseRetryDelayMs(fallback.response),
585
+ };
586
+ }
357
587
  return {
358
588
  kind: "retry",
359
- error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
360
- retryDelayMs: parseRetryDelayMs(fallback.response),
589
+ error: `slack chat.postMessage fallback ${status}: ${err}${firstError ? ` (upload_error=${firstError})` : ""}`,
361
590
  };
362
591
  }
363
- return {
364
- kind: "retry",
365
- error: `slack chat.postMessage fallback ${status}: ${err} (upload_error=${firstError})`,
366
- };
367
592
  }
368
593
  return { kind: "delivered" };
369
594
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@femtomc/mu-server",
3
- "version": "26.2.94",
3
+ "version": "26.2.96",
4
4
  "description": "HTTP API server for mu control-plane transport/session plus run/activity scheduling coordination.",
5
5
  "keywords": [
6
6
  "mu",
@@ -30,8 +30,8 @@
30
30
  "start": "bun run dist/cli.js"
31
31
  },
32
32
  "dependencies": {
33
- "@femtomc/mu-agent": "26.2.94",
34
- "@femtomc/mu-control-plane": "26.2.94",
35
- "@femtomc/mu-core": "26.2.94"
33
+ "@femtomc/mu-agent": "26.2.96",
34
+ "@femtomc/mu-control-plane": "26.2.96",
35
+ "@femtomc/mu-core": "26.2.96"
36
36
  }
37
37
  }