@delt/claude-alarm 1.2.0 → 1.4.0

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/dist/index.d.ts CHANGED
@@ -49,6 +49,7 @@ type ChannelMessage = {
49
49
  title: string;
50
50
  message: string;
51
51
  level?: NotifyLevel;
52
+ to?: MessageSource;
52
53
  } | {
53
54
  type: 'reply';
54
55
  sessionId: string;
@@ -73,6 +74,15 @@ type ChannelMessage = {
73
74
  originalName?: string;
74
75
  content?: string;
75
76
  source?: MessageSource;
77
+ } | {
78
+ type: 'upload_rejected';
79
+ sessionId: string;
80
+ reason: string;
81
+ withText: boolean;
82
+ } | {
83
+ type: 'message_rejected';
84
+ sessionId: string;
85
+ reason: string;
76
86
  } | {
77
87
  type: 'sessions_list';
78
88
  sessions: SessionInfo[];
@@ -316,18 +326,20 @@ declare class TelegramBot {
316
326
  private polling;
317
327
  private pollTimer;
318
328
  private messageSessionMap;
319
- onMessageToSession?: (sessionId: string, content: string) => void;
320
- onImageToSession?: (sessionId: string, imagePath: string, mimeType: string, caption?: string) => void;
329
+ onMessageToSession?: (sessionId: string, content: string) => boolean;
330
+ onImageToSession?: (sessionId: string, imagePath: string, mimeType: string, caption?: string) => boolean;
321
331
  onPermissionVerdict?: (sessionId: string, requestId: string, behavior: 'allow' | 'deny') => void;
322
332
  onChoiceVerdict?: (sessionId: string, requestId: string, choiceId: string) => void;
323
333
  private choiceTokens;
324
334
  private choiceMessages;
325
335
  getSessions?: () => SessionInfo[];
326
- private pendingMessages;
336
+ private latestSelection;
337
+ private selections;
327
338
  constructor(config: TelegramConfig);
328
339
  private get apiUrl();
329
340
  /** Send a notification message to Telegram */
330
341
  sendNotification(sessionId: string, _sessionLabel: string, title: string, message: string): Promise<void>;
342
+ private fitNotification;
331
343
  /** Send a text message to the configured chat */
332
344
  private sendMessage;
333
345
  /** Start long polling for incoming messages */
@@ -338,8 +350,10 @@ declare class TelegramBot {
338
350
  private scheduleNextPoll;
339
351
  private handleIncomingMessage;
340
352
  private deliverToSession;
353
+ private deliverPending;
341
354
  private deliverPhotoToSession;
342
355
  private deliverPhotoToSessionByFileId;
356
+ private photoNotDelivered;
343
357
  private pendingSession;
344
358
  private getLabel;
345
359
  /** Send a permission request with inline buttons */
@@ -352,6 +366,8 @@ declare class TelegramBot {
352
366
  private handleCallbackQuery;
353
367
  private handleSessionSelectCallback;
354
368
  private answerCallbackQuery;
369
+ private expire;
370
+ private removeButtons;
355
371
  private editMessageText;
356
372
  private escHtml;
357
373
  private static readonly LANG_BY_EXT;
package/dist/index.js CHANGED
@@ -245,6 +245,14 @@ ${requestId}`;
245
245
  // src/hub/telegram.ts
246
246
  var TELEGRAM_API = "https://api.telegram.org/bot";
247
247
  var MAX_CHOICE_MESSAGES = 200;
248
+ var MAX_SELECTIONS = 20;
249
+ var MAX_VISIBLE_CHARS = 4e3;
250
+ var TRUNCATED = "\u2026(truncated)";
251
+ var MAX_PHOTO_BYTES = 10 * 1024 * 1024;
252
+ var NO_LONGER_CONNECTED = "the session is no longer connected";
253
+ function visibleLength(html) {
254
+ return html.replace(/<[^>]*>/g, "").replace(/&(?:amp|lt|gt);/g, "&").length;
255
+ }
248
256
  var TelegramBot = class _TelegramBot {
249
257
  config;
250
258
  offset = 0;
@@ -265,9 +273,9 @@ var TelegramBot = class _TelegramBot {
265
273
  choiceMessages = /* @__PURE__ */ new Map();
266
274
  // Callback: get current sessions list
267
275
  getSessions;
268
- // Pending messages for session selection
269
- pendingMessages = /* @__PURE__ */ new Map();
270
- // chatId -> pending
276
+ latestSelection;
277
+ // Keyed by prompt, not by chat: buttons from an older prompt must still send that prompt's message.
278
+ selections = /* @__PURE__ */ new Map();
271
279
  constructor(config) {
272
280
  this.config = config;
273
281
  }
@@ -276,8 +284,7 @@ var TelegramBot = class _TelegramBot {
276
284
  }
277
285
  /** Send a notification message to Telegram */
278
286
  async sendNotification(sessionId, _sessionLabel, title, message) {
279
- const text = `<b>${this.escHtml(title)}</b>
280
- ${this.mdToHtml(message)}`;
287
+ const text = this.fitNotification(title, message);
281
288
  const result = await this.sendMessage(text);
282
289
  if (result?.message_id) {
283
290
  this.messageSessionMap.set(result.message_id, sessionId);
@@ -289,6 +296,25 @@ ${this.mdToHtml(message)}`;
289
296
  }
290
297
  }
291
298
  }
299
+ fitNotification(title, message) {
300
+ const render = (body) => `<b>${this.escHtml(title)}</b>
301
+ ${this.mdToHtml(body)}`;
302
+ const full = render(message);
303
+ if (visibleLength(full) <= MAX_VISIBLE_CHARS) return full;
304
+ const cut = (n) => {
305
+ const end = n > 0 && /[\uD800-\uDBFF]/.test(message[n - 1]) ? n - 1 : n;
306
+ return render(`${message.slice(0, end)}
307
+ ${TRUNCATED}`);
308
+ };
309
+ let lo = 0;
310
+ let hi = message.length;
311
+ while (lo < hi) {
312
+ const mid = Math.ceil((lo + hi) / 2);
313
+ if (visibleLength(cut(mid)) <= MAX_VISIBLE_CHARS) lo = mid;
314
+ else hi = mid - 1;
315
+ }
316
+ return cut(lo);
317
+ }
292
318
  /** Send a text message to the configured chat */
293
319
  async sendMessage(text, replyToMessageId, replyMarkup) {
294
320
  try {
@@ -370,6 +396,13 @@ ${this.mdToHtml(message)}`;
370
396
  const hasPhoto = msg.photo && msg.photo.length > 0;
371
397
  const text = (msg.text || msg.caption || "").trim();
372
398
  if (!text && !hasPhoto) return;
399
+ if (hasPhoto) {
400
+ const size = msg.photo[msg.photo.length - 1].file_size;
401
+ if (size !== void 0 && size > MAX_PHOTO_BYTES) {
402
+ this.photoNotDelivered("it is larger than 10 MB");
403
+ return;
404
+ }
405
+ }
373
406
  if (msg.reply_to_message) {
374
407
  const sessionId = this.messageSessionMap.get(msg.reply_to_message.message_id);
375
408
  if (sessionId) {
@@ -383,18 +416,14 @@ ${this.mdToHtml(message)}`;
383
416
  }
384
417
  if (text) {
385
418
  const selectMatch = text.match(/^\/s_(\d+)$/);
386
- if (selectMatch) {
387
- const pending = this.pendingMessages.get(msg.chat.id);
419
+ const latest = this.latestSelection;
420
+ if (selectMatch && latest !== void 0) {
421
+ const pending = this.selections.get(latest);
388
422
  if (pending) {
389
- this.pendingMessages.delete(msg.chat.id);
423
+ this.selections.delete(latest);
390
424
  const session = this.pendingSession(pending, parseInt(selectMatch[1], 10) - 1);
391
425
  if (session) {
392
- if (pending.photoFileId) {
393
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
394
- } else if (pending.text) {
395
- this.deliverToSession(session.id, pending.text);
396
- }
397
- this.sendMessage(`Sent to [${this.getLabel(session)}]`);
426
+ if (await this.deliverPending(session.id, pending)) this.sendMessage(`Sent to [${this.getLabel(session)}]`);
398
427
  } else {
399
428
  this.sendMessage("Invalid session number.");
400
429
  }
@@ -415,16 +444,19 @@ ${this.mdToHtml(message)}`;
415
444
  }
416
445
  return;
417
446
  }
447
+ const selectionId = randomUUID().replace(/-/g, "").slice(0, 8);
448
+ this.latestSelection = selectionId;
418
449
  const sessionIds = sessions.map((s) => s.id);
419
450
  if (hasPhoto) {
420
451
  const largest = msg.photo[msg.photo.length - 1];
421
- this.pendingMessages.set(msg.chat.id, { photoFileId: largest.file_id, caption: text, sessionIds });
452
+ this.selections.set(selectionId, { photoFileId: largest.file_id, caption: text, sessionIds });
422
453
  } else {
423
- this.pendingMessages.set(msg.chat.id, { text, sessionIds });
454
+ this.selections.set(selectionId, { text, sessionIds });
424
455
  }
456
+ while (this.selections.size > MAX_SELECTIONS) this.selections.delete(this.selections.keys().next().value);
425
457
  const buttons = sessions.map((s, i) => ({
426
458
  text: this.getLabel(s),
427
- callback_data: `sess:${i}:${msg.chat.id}`
459
+ callback_data: `sel:${selectionId}:${i}`
428
460
  }));
429
461
  const rows = [];
430
462
  for (let i = 0; i < buttons.length; i += 2) {
@@ -433,30 +465,37 @@ ${this.mdToHtml(message)}`;
433
465
  this.sendMessage("Multiple sessions active. Select one:", void 0, { inline_keyboard: rows });
434
466
  }
435
467
  deliverToSession(sessionId, content) {
436
- if (this.onMessageToSession) {
437
- this.onMessageToSession(sessionId, content);
438
- }
468
+ const delivered = this.onMessageToSession?.(sessionId, content) === true;
469
+ if (!delivered) void this.sendMessage(`Not delivered: ${NO_LONGER_CONNECTED}`);
470
+ return delivered;
471
+ }
472
+ async deliverPending(sessionId, pending) {
473
+ if (pending.photoFileId) return this.deliverPhotoToSessionByFileId(sessionId, pending.photoFileId, pending.caption);
474
+ return pending.text ? this.deliverToSession(sessionId, pending.text) : false;
439
475
  }
440
476
  async deliverPhotoToSession(sessionId, photos, caption) {
441
477
  const largest = photos[photos.length - 1];
442
- await this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
478
+ return this.deliverPhotoToSessionByFileId(sessionId, largest.file_id, caption);
443
479
  }
444
480
  async deliverPhotoToSessionByFileId(sessionId, fileId, caption) {
445
481
  try {
446
482
  const fileRes = await fetch(`${this.apiUrl}/getFile?file_id=${fileId}`);
447
- if (!fileRes.ok) {
448
- logger.warn("Failed to get Telegram file info");
449
- return;
483
+ const fileData = fileRes.ok ? await fileRes.json() : void 0;
484
+ if (!fileData?.ok || !fileData.result) {
485
+ this.photoNotDelivered("Telegram did not return the file");
486
+ return false;
450
487
  }
451
- const fileData = await fileRes.json();
452
- if (!fileData.ok) return;
453
488
  const downloadUrl = `https://api.telegram.org/file/bot${this.config.botToken}/${fileData.result.file_path}`;
454
489
  const imgRes = await fetch(downloadUrl);
455
490
  if (!imgRes.ok) {
456
- logger.warn("Failed to download Telegram photo");
457
- return;
491
+ this.photoNotDelivered("the download failed");
492
+ return false;
458
493
  }
459
494
  const buffer = Buffer.from(await imgRes.arrayBuffer());
495
+ if (buffer.length > MAX_PHOTO_BYTES) {
496
+ this.photoNotDelivered("it is larger than 10 MB");
497
+ return false;
498
+ }
460
499
  const ext = fileData.result.file_path.split(".").pop() || "jpg";
461
500
  const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : ext === "webp" ? "image/webp" : "image/jpeg";
462
501
  fs.mkdirSync(UPLOADS_DIR, { recursive: true });
@@ -464,19 +503,37 @@ ${this.mdToHtml(message)}`;
464
503
  const filePath = path2.join(UPLOADS_DIR, filename);
465
504
  fs.writeFileSync(filePath, buffer);
466
505
  logger.info(`Telegram photo saved: ${filename} (${buffer.length} bytes)`);
467
- if (this.onImageToSession) {
468
- this.onImageToSession(sessionId, filePath, mimeType, caption);
506
+ let accepted = false;
507
+ try {
508
+ accepted = this.onImageToSession?.(sessionId, filePath, mimeType, caption) === true;
509
+ } catch (err) {
510
+ logger.warn(`Telegram photo hand-off failed: ${err.message}`);
511
+ }
512
+ if (!accepted) {
513
+ try {
514
+ fs.unlinkSync(filePath);
515
+ } catch {
516
+ }
517
+ this.photoNotDelivered(NO_LONGER_CONNECTED);
518
+ return false;
469
519
  }
470
520
  setTimeout(() => {
471
521
  try {
472
522
  fs.unlinkSync(filePath);
473
523
  } catch {
474
524
  }
475
- }, 5 * 60 * 1e3);
525
+ }, 5 * 60 * 1e3).unref();
526
+ return true;
476
527
  } catch (err) {
477
528
  logger.warn(`Telegram photo download failed: ${err.message}`);
529
+ this.photoNotDelivered("the download failed");
530
+ return false;
478
531
  }
479
532
  }
533
+ photoNotDelivered(reason) {
534
+ logger.warn(`Telegram photo not delivered: ${reason}`);
535
+ void this.sendMessage(`Photo not delivered: ${reason}`);
536
+ }
480
537
  // Buttons are numbered against the list shown when they were sent; Codex sessions come and go, so the live list may have shifted.
481
538
  pendingSession(pending, idx) {
482
539
  const id = pending.sessionIds[idx];
@@ -590,7 +647,7 @@ ${this.escHtml(description)}` : "") + (slice ? `
590
647
  async handleChoiceCallback(query) {
591
648
  const choice = this.choiceTokens.get(query.data.slice("pc:".length));
592
649
  if (!choice) {
593
- await this.answerCallbackQuery(query.id, "Expired");
650
+ await this.expire(query);
594
651
  return;
595
652
  }
596
653
  this.dropChoiceTokens(choice.sessionId, choice.requestId);
@@ -619,10 +676,14 @@ ${this.escHtml(description)}` : "") + (slice ? `
619
676
  async handleCallbackQuery(query) {
620
677
  if (!query.data) return;
621
678
  if (String(query.message?.chat.id) !== String(this.config.chatId)) return;
622
- if (query.data.startsWith("sess:")) {
679
+ if (query.data.startsWith("sel:")) {
623
680
  await this.handleSessionSelectCallback(query);
624
681
  return;
625
682
  }
683
+ if (query.data.startsWith("sess:")) {
684
+ await this.expire(query);
685
+ return;
686
+ }
626
687
  if (query.data.startsWith("pc:")) {
627
688
  await this.handleChoiceCallback(query);
628
689
  return;
@@ -645,21 +706,22 @@ ${label}`);
645
706
  }
646
707
  }
647
708
  async handleSessionSelectCallback(query) {
648
- const parts = query.data.split(":");
649
- if (parts.length < 3) return;
650
- const [, idxStr, chatIdStr] = parts;
651
- const chatId = parseInt(chatIdStr, 10);
652
- const pending = this.pendingMessages.get(chatId);
653
- const session = pending ? this.pendingSession(pending, parseInt(idxStr, 10)) : void 0;
654
- if (!pending || !session) {
709
+ const [, selectionId, idxStr] = query.data.split(":");
710
+ const pending = this.selections.get(selectionId);
711
+ if (!pending) {
712
+ await this.expire(query);
713
+ return;
714
+ }
715
+ const session = this.pendingSession(pending, parseInt(idxStr, 10));
716
+ if (!session) {
655
717
  await this.answerCallbackQuery(query.id, "Session not found");
656
718
  return;
657
719
  }
658
- this.pendingMessages.delete(chatId);
659
- if (pending.photoFileId) {
660
- await this.deliverPhotoToSessionByFileId(session.id, pending.photoFileId, pending.caption);
661
- } else if (pending.text) {
662
- this.deliverToSession(session.id, pending.text);
720
+ this.selections.delete(selectionId);
721
+ if (!await this.deliverPending(session.id, pending)) {
722
+ await this.answerCallbackQuery(query.id, pending.photoFileId ? "Photo not delivered" : "Not delivered");
723
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
724
+ return;
663
725
  }
664
726
  await this.answerCallbackQuery(query.id, `Sent to ${this.getLabel(session)}`);
665
727
  if (query.message) {
@@ -677,6 +739,21 @@ ${label}`);
677
739
  logger.warn(`Telegram answerCallbackQuery error: ${err.message}`);
678
740
  }
679
741
  }
742
+ async expire(query) {
743
+ await this.answerCallbackQuery(query.id, "Expired");
744
+ if (query.message) await this.removeButtons(query.message.chat.id, query.message.message_id);
745
+ }
746
+ async removeButtons(chatId, messageId) {
747
+ try {
748
+ await fetch(`${this.apiUrl}/editMessageReplyMarkup`, {
749
+ method: "POST",
750
+ headers: { "Content-Type": "application/json" },
751
+ body: JSON.stringify({ chat_id: chatId, message_id: messageId, reply_markup: { inline_keyboard: [] } })
752
+ });
753
+ } catch (err) {
754
+ logger.warn(`Telegram editMessageReplyMarkup error: ${err.message}`);
755
+ }
756
+ }
680
757
  async editMessageText(chatId, messageId, text) {
681
758
  try {
682
759
  await fetch(`${this.apiUrl}/editMessageText`, {
@@ -773,19 +850,30 @@ var HEALTHY_RUN_MS = 6e4;
773
850
  function resolveAdapterScript(baseDir) {
774
851
  return [path3.join(baseDir, "..", "codex", "main.js"), path3.join(baseDir, "codex", "main.js")].find((p) => fs2.existsSync(p));
775
852
  }
853
+ function adapterEnv(base, hub) {
854
+ const env = { ...base, CLAUDE_ALARM_HUB_HOST: hub.host, CLAUDE_ALARM_HUB_PORT: String(hub.port) };
855
+ delete env.CLAUDE_ALARM_HUB_TOKEN;
856
+ return env;
857
+ }
776
858
  var CodexSupervisor = class {
777
- constructor(script, spawnFn = spawn, minDelayMs = 2e3, maxDelayMs = 6e4, stopGraceMs = 3e3) {
859
+ constructor(script, opts = {}) {
778
860
  this.script = script;
779
- this.spawnFn = spawnFn;
780
- this.minDelayMs = minDelayMs;
781
- this.maxDelayMs = maxDelayMs;
782
- this.stopGraceMs = stopGraceMs;
783
- this.delay = minDelayMs;
861
+ this.spawnFn = opts.spawnFn ?? spawn;
862
+ this.minDelayMs = opts.minDelayMs ?? 2e3;
863
+ this.maxDelayMs = opts.maxDelayMs ?? 6e4;
864
+ this.stopGraceMs = opts.stopGraceMs ?? 3e3;
865
+ this.env = opts.env;
866
+ this.delay = this.minDelayMs;
784
867
  }
785
868
  child;
786
869
  timer;
787
870
  stopped = true;
788
871
  delay;
872
+ spawnFn;
873
+ minDelayMs;
874
+ maxDelayMs;
875
+ stopGraceMs;
876
+ env;
789
877
  start() {
790
878
  this.stopped = false;
791
879
  this.launch();
@@ -807,7 +895,8 @@ var CodexSupervisor = class {
807
895
  const child = this.spawnFn(process.execPath, [this.script, "--watch-stdin"], {
808
896
  detached: true,
809
897
  stdio: ["pipe", "pipe", "pipe"],
810
- windowsHide: true
898
+ windowsHide: true,
899
+ ...this.env ? { env: this.env } : {}
811
900
  });
812
901
  this.child = child;
813
902
  child.stdout?.on("data", (d) => process.stdout.write(d));
@@ -1411,7 +1500,16 @@ var HubServer = class {
1411
1500
  this.sessions.updateActivity(msg.sessionId);
1412
1501
  const notifySession = this.sessions.get(msg.sessionId);
1413
1502
  const notifyLabel = this.getSessionLabel(notifySession);
1414
- this.notifier.notifyWithSession(msg.sessionId, notifyLabel, `[${notifyLabel}] ${msg.title}`, msg.message, msg.level ?? "info");
1503
+ const notifyTitle = `[${notifyLabel}] ${msg.title}`;
1504
+ if (msg.to === "api") {
1505
+ logger.info(`${notifyTitle}: ${msg.message}`);
1506
+ break;
1507
+ }
1508
+ if (msg.to === "telegram") {
1509
+ void this.telegramBot?.sendNotification(msg.sessionId, notifyLabel, notifyTitle, msg.message);
1510
+ break;
1511
+ }
1512
+ if (msg.to !== "dashboard") this.notifier.notifyWithSession(msg.sessionId, notifyLabel, notifyTitle, msg.message, msg.level ?? "info");
1415
1513
  this.broadcastToDashboards({
1416
1514
  type: "notification",
1417
1515
  sessionId: msg.sessionId,
@@ -1613,9 +1711,14 @@ var HubServer = class {
1613
1711
  const channelWs = this.channelSockets.get(msg.sessionId);
1614
1712
  if (channelWs?.readyState === WebSocket.OPEN) {
1615
1713
  channelWs.send(JSON.stringify({ ...msg, source: "dashboard" }));
1714
+ } else {
1715
+ const reason = "the session is not connected";
1716
+ logger.warn(`Message rejected for ${msg.sessionId}: ${reason}`);
1717
+ const rejected = { type: "message_rejected", sessionId: msg.sessionId, reason };
1718
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
1616
1719
  }
1617
1720
  } else if (msg.type === "image_upload") {
1618
- this.handleImageUpload(msg);
1721
+ this.handleImageUpload(ws, msg);
1619
1722
  } else if (msg.type === "permission_response") {
1620
1723
  if (this.forwardPermissionResponse(msg)) {
1621
1724
  const verdict = msg.choiceId !== void 0 ? `choice ${msg.choiceId}` : msg.behavior;
@@ -1666,20 +1769,31 @@ var HubServer = class {
1666
1769
  }
1667
1770
  }
1668
1771
  }
1669
- handleImageUpload(msg) {
1772
+ handleImageUpload(ws, msg) {
1670
1773
  const { sessionId, imageData, mimeType, originalName, content } = msg;
1774
+ const reject = (reason) => {
1775
+ logger.warn(`Image upload rejected for ${sessionId}: ${reason}`);
1776
+ const rejected = { type: "upload_rejected", sessionId, reason, withText: typeof content === "string" && content.trim().length > 0 };
1777
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(rejected));
1778
+ };
1779
+ const channelWs = this.channelSockets.get(sessionId);
1780
+ if (!channelWs || channelWs.readyState !== WebSocket.OPEN) {
1781
+ reject("the session is not connected");
1782
+ return;
1783
+ }
1671
1784
  if (!this.localChannels.has(sessionId)) {
1672
- logger.warn(`Image upload rejected: session ${sessionId} is not local`);
1785
+ reject("this session is on another PC; images can only go to sessions on the hub's PC");
1673
1786
  return;
1674
1787
  }
1675
- const channelWs = this.channelSockets.get(sessionId);
1676
- if (!channelWs || channelWs.readyState !== WebSocket.OPEN) return;
1677
1788
  const allowedTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
1678
- if (!allowedTypes.includes(mimeType)) return;
1789
+ if (!allowedTypes.includes(mimeType)) {
1790
+ reject("only PNG, JPEG, GIF and WebP images are supported");
1791
+ return;
1792
+ }
1679
1793
  const base64Data = imageData.replace(/^data:image\/\w+;base64,/, "");
1680
1794
  const buffer = Buffer.from(base64Data, "base64");
1681
1795
  if (buffer.length > 10 * 1024 * 1024) {
1682
- logger.warn("Image upload rejected: exceeds 10MB");
1796
+ reject("the image is larger than 10 MB");
1683
1797
  return;
1684
1798
  }
1685
1799
  fs5.mkdirSync(UPLOADS_DIR, { recursive: true });
@@ -1727,19 +1841,25 @@ var HubServer = class {
1727
1841
  this.telegramBot.getSessions = () => this.sessions.getAll();
1728
1842
  this.telegramBot.onMessageToSession = (sessionId, content) => {
1729
1843
  const channelWs = this.channelSockets.get(sessionId);
1730
- if (channelWs?.readyState === WebSocket.OPEN) {
1731
- const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1732
- channelWs.send(JSON.stringify(msg));
1733
- logger.info(`Telegram message forwarded to session: ${sessionId}`);
1844
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1845
+ logger.warn(`Telegram message not delivered to ${sessionId}: the session is not connected`);
1846
+ return false;
1734
1847
  }
1848
+ const msg = { type: "message_to_session", sessionId, content, source: "telegram" };
1849
+ channelWs.send(JSON.stringify(msg));
1850
+ logger.info(`Telegram message forwarded to session: ${sessionId}`);
1851
+ return true;
1735
1852
  };
1736
1853
  this.telegramBot.onImageToSession = (sessionId, imagePath, mimeType, caption) => {
1737
1854
  const channelWs = this.channelSockets.get(sessionId);
1738
- if (channelWs?.readyState === WebSocket.OPEN) {
1739
- const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1740
- channelWs.send(JSON.stringify(msg));
1741
- logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1855
+ if (channelWs?.readyState !== WebSocket.OPEN) {
1856
+ logger.warn(`Telegram photo not delivered to ${sessionId}: the session is not connected`);
1857
+ return false;
1742
1858
  }
1859
+ const msg = { type: "image_to_session", sessionId, imagePath, mimeType, content: caption, source: "telegram" };
1860
+ channelWs.send(JSON.stringify(msg));
1861
+ logger.info(`Telegram photo forwarded to session: ${sessionId}`);
1862
+ return true;
1743
1863
  };
1744
1864
  this.telegramBot.onPermissionVerdict = (sessionId, requestId, behavior) => {
1745
1865
  if (this.forwardPermissionResponse({ sessionId, requestId, behavior })) {
@@ -1864,7 +1984,7 @@ var HubServer = class {
1864
1984
  logger.warn("Codex adapter is enabled but codex/main.js was not found next to the hub");
1865
1985
  return;
1866
1986
  }
1867
- this.codexSupervisor = new CodexSupervisor(script);
1987
+ this.codexSupervisor = new CodexSupervisor(script, { env: adapterEnv(process.env, { host: this.host, port: this.port }) });
1868
1988
  this.codexSupervisor.start();
1869
1989
  logger.info("Codex adapter started");
1870
1990
  }