@wrongstack/telegram 0.293.0 → 0.295.1

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { expectDefined as expectDefined2 } from "@wrongstack/core";
2
+ import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";
3
3
 
4
4
  // src/api-client.ts
5
5
  var TelegramApiClientError = class extends Error {
@@ -163,30 +163,34 @@ var TelegramApiClient = class {
163
163
  });
164
164
  }
165
165
  sendMessage(chatId, text, opts) {
166
+ const body = {
167
+ chat_id: String(chatId),
168
+ text,
169
+ disable_web_page_preview: true
170
+ };
171
+ if (opts?.parseMode) body.parse_mode = opts.parseMode;
166
172
  return this.request("sendMessage", {
167
- body: {
168
- chat_id: String(chatId),
169
- text,
170
- disable_web_page_preview: true
171
- },
173
+ body,
172
174
  signal: composedSignal(opts?.signal)
173
175
  });
174
176
  }
175
177
  sendMessageWithKeyboard(chatId, text, buttons, opts) {
178
+ const body = {
179
+ chat_id: String(chatId),
180
+ text,
181
+ disable_web_page_preview: true,
182
+ reply_markup: {
183
+ inline_keyboard: [
184
+ buttons.map((button) => ({
185
+ text: button.text,
186
+ callback_data: button.callback_data
187
+ }))
188
+ ]
189
+ }
190
+ };
191
+ if (opts?.parseMode) body.parse_mode = opts.parseMode;
176
192
  return this.request("sendMessage", {
177
- body: {
178
- chat_id: String(chatId),
179
- text,
180
- disable_web_page_preview: true,
181
- reply_markup: {
182
- inline_keyboard: [
183
- buttons.map((button) => ({
184
- text: button.text,
185
- callback_data: button.callback_data
186
- }))
187
- ]
188
- }
189
- },
193
+ body,
190
194
  signal: composedSignal(opts?.signal)
191
195
  });
192
196
  }
@@ -286,6 +290,7 @@ var TelegramBot = class _TelegramBot {
286
290
  /** Single-poller election across wstack instances sharing this token. */
287
291
  lock;
288
292
  standbyRetryMs;
293
+ getParseMode;
289
294
  standbyTimer = null;
290
295
  standbyAnnounced = false;
291
296
  // Circular buffer for incoming messages
@@ -306,6 +311,7 @@ var TelegramBot = class _TelegramBot {
306
311
  this.offsetStore = opts.offsetStore;
307
312
  this.lock = opts.lock;
308
313
  this.standbyRetryMs = opts.standbyRetryMs ?? 15e3;
314
+ this.getParseMode = opts.getParseMode;
309
315
  if (this.lock) {
310
316
  this.lock.onLost = () => this.handleLockLost();
311
317
  }
@@ -432,7 +438,8 @@ var TelegramBot = class _TelegramBot {
432
438
  try {
433
439
  const timeout = AbortSignal.timeout(1e4);
434
440
  const result = await this.api.sendMessage(chatId, text, {
435
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout
441
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
442
+ parseMode: this.getParseMode?.()
436
443
  });
437
444
  return { ok: true, result };
438
445
  } catch (err) {
@@ -468,7 +475,8 @@ var TelegramBot = class _TelegramBot {
468
475
  try {
469
476
  const timeout = AbortSignal.timeout(1e4);
470
477
  const result = await this.api.sendMessageWithKeyboard(chatId, text, buttons, {
471
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout
478
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
479
+ parseMode: this.getParseMode?.()
472
480
  });
473
481
  return { ok: true, result };
474
482
  } catch (err) {
@@ -529,7 +537,11 @@ var TelegramBot = class _TelegramBot {
529
537
  for (const upd of updates) {
530
538
  this.offset = upd.update_id + 1;
531
539
  if (upd.callback_query) {
532
- void this.dispatchCallback(upd.callback_query);
540
+ void this.dispatchCallback(upd.callback_query).catch(
541
+ (err) => this.log.debug(
542
+ `Callback dispatch failed: ${err instanceof Error ? err.message : String(err)}`
543
+ )
544
+ );
533
545
  continue;
534
546
  }
535
547
  const raw = upd.message ?? upd.edited_message;
@@ -572,7 +584,11 @@ var TelegramBot = class _TelegramBot {
572
584
  const denialReason = this.inboundDenialReason(userId, chatId);
573
585
  if (denialReason === "user") {
574
586
  this.log.debug(`Ignoring message from user ${userId ?? "unknown"} (not in allowedUsers)`);
575
- void this.sendMessage(chatId, "\u26D4 You are not authorized to interact with this bot.");
587
+ void this.sendMessage(chatId, "\u26D4 You are not authorized to interact with this bot.").catch(
588
+ (err) => this.log.debug(
589
+ `Failed to send denial notice: ${err instanceof Error ? err.message : String(err)}`
590
+ )
591
+ );
576
592
  return;
577
593
  }
578
594
  if (denialReason === "chat") {
@@ -736,7 +752,11 @@ var TelegramBot = class _TelegramBot {
736
752
  request.promptMessageId = promptMessageId;
737
753
  const pending = request.pendingCallbacks.splice(0);
738
754
  for (const callback of pending) {
739
- void this.dispatchCallback(callback);
755
+ void this.dispatchCallback(callback).catch(
756
+ (err) => this.log.debug(
757
+ `Callback dispatch failed: ${err instanceof Error ? err.message : String(err)}`
758
+ )
759
+ );
740
760
  }
741
761
  return true;
742
762
  }
@@ -802,7 +822,9 @@ function truncateForTelegram(text, maxLen = 4e3) {
802
822
  }
803
823
 
804
824
  // src/config.ts
825
+ import { resolvePluginConfig } from "@wrongstack/core/plugin";
805
826
  var PLUGIN_NAME = "telegram";
827
+ var PLUGIN_CONFIG_ALIASES = ["@wrongstack/telegram"];
806
828
  var INBOUND_MODES = ["disabled", "paired", "allowlist", "public"];
807
829
  var DEFAULT_CONFIG = {
808
830
  inboundMode: "disabled",
@@ -816,7 +838,32 @@ var DEFAULT_CONFIG = {
816
838
  maxMessageLength: 4e3,
817
839
  singleInstanceLock: true,
818
840
  outboundQueuePerChat: 32,
819
- outboundQueueConcurrency: 4
841
+ outboundQueueConcurrency: 4,
842
+ allowGroupApprovals: false,
843
+ rateLimitTokensPerSecond: 0.33,
844
+ rateLimitBurst: 4,
845
+ parseMode: ""
846
+ };
847
+ var TELEGRAM_CONFIG_FIELDS = {
848
+ botToken: { lifecycle: "restart", secret: true },
849
+ notifyChatId: { lifecycle: "restart" },
850
+ inboundMode: { lifecycle: "hot" },
851
+ allowedUsers: { lifecycle: "hot" },
852
+ allowedChats: { lifecycle: "hot" },
853
+ allowedOutboundChats: { lifecycle: "hot" },
854
+ allowGroupApprovals: { lifecycle: "hot" },
855
+ pollIntervalSec: { lifecycle: "hot" },
856
+ notifyOnSessionEnd: { lifecycle: "hot" },
857
+ longToolThresholdMs: { lifecycle: "hot" },
858
+ notifyOnDelegate: { lifecycle: "hot" },
859
+ maxMessageLength: { lifecycle: "hot" },
860
+ offsetStoragePath: { lifecycle: "immutable" },
861
+ singleInstanceLock: { lifecycle: "restart" },
862
+ outboundQueuePerChat: { lifecycle: "restart" },
863
+ outboundQueueConcurrency: { lifecycle: "restart" },
864
+ rateLimitTokensPerSecond: { lifecycle: "hot", description: "Per-chat rate limit (tokens/sec)" },
865
+ rateLimitBurst: { lifecycle: "hot", description: "Per-chat rate limit burst size" },
866
+ parseMode: { lifecycle: "hot", description: "Telegram parse mode: HTML, MarkdownV2, or empty for plain text" }
820
867
  };
821
868
  var telegramConfigSchema = {
822
869
  type: "object",
@@ -857,6 +904,7 @@ var telegramConfigSchema = {
857
904
  longToolThresholdMs: { type: "integer", minimum: 0 },
858
905
  notifyOnDelegate: { type: "boolean" },
859
906
  maxMessageLength: { type: "integer", minimum: 100, maximum: 4096 },
907
+ offsetStoragePath: { type: "string" },
860
908
  singleInstanceLock: {
861
909
  type: "boolean",
862
910
  description: "Elect a single getUpdates poller per bot token across wstack instances (default true)"
@@ -872,24 +920,37 @@ var telegramConfigSchema = {
872
920
  minimum: 1,
873
921
  maximum: 64,
874
922
  description: "Maximum concurrent outbound sends across all chats (default 4)"
923
+ },
924
+ allowGroupApprovals: { type: "boolean" },
925
+ rateLimitTokensPerSecond: {
926
+ type: "number",
927
+ minimum: 0.1,
928
+ maximum: 100,
929
+ description: "Per-chat rate limit in tokens per second (default: 0.33 \u224820 msg/min)"
930
+ },
931
+ rateLimitBurst: {
932
+ type: "integer",
933
+ minimum: 1,
934
+ maximum: 100,
935
+ description: "Per-chat burst size (default: 1)"
936
+ },
937
+ parseMode: {
938
+ type: "string",
939
+ enum: ["", "HTML", "MarkdownV2"],
940
+ description: "Telegram parse mode: HTML, MarkdownV2, or empty for plain text"
875
941
  }
876
942
  },
877
943
  required: ["botToken"]
878
944
  };
879
945
  function readTelegramConfig(api) {
880
- const config = api.config;
881
- const extensions = config.extensions;
882
- const pluginEntries = config.plugins;
883
- const legacyPlugins = pluginEntries;
884
- const legacyOpts = legacyPlugins && !Array.isArray(legacyPlugins) ? legacyPlugins[PLUGIN_NAME] : void 0;
885
- const entryOpts = pluginOptionsFromEntries(pluginEntries);
886
- const extensionOpts = extensions?.[PLUGIN_NAME];
887
- const opts = {
888
- ...legacyOpts ?? entryOpts,
889
- ...extensionOpts ?? {}
890
- };
946
+ const resolution = resolvePluginConfig({
947
+ name: PLUGIN_NAME,
948
+ aliases: PLUGIN_CONFIG_ALIASES,
949
+ config: api.config
950
+ });
951
+ const opts = resolution.options;
891
952
  const inboundMode = resolveInboundMode(opts, {
892
- configured: legacyOpts !== void 0 || entryOpts !== void 0 || extensionOpts !== void 0,
953
+ configured: resolution.configured,
893
954
  warn: api.log?.warn.bind(api.log)
894
955
  });
895
956
  return {
@@ -898,6 +959,9 @@ function readTelegramConfig(api) {
898
959
  inboundMode
899
960
  };
900
961
  }
962
+ function readTelegramConfigFromConfig(cfg) {
963
+ return readTelegramConfig({ config: cfg });
964
+ }
901
965
  function resolveInboundMode(opts, migration) {
902
966
  if (opts.inboundMode !== void 0) {
903
967
  if (!INBOUND_MODES.includes(opts.inboundMode)) {
@@ -927,13 +991,6 @@ function resolveInboundMode(opts, migration) {
927
991
  function hasEntries(values) {
928
992
  return Array.isArray(values) && values.length > 0;
929
993
  }
930
- function pluginOptionsFromEntries(entries) {
931
- if (!Array.isArray(entries)) return void 0;
932
- const found = entries.find(
933
- (entry) => typeof entry === "object" && entry !== null && "name" in entry && (entry.name === "@wrongstack/telegram" || entry.name === PLUGIN_NAME)
934
- );
935
- return found?.options && typeof found.options === "object" ? found.options : void 0;
936
- }
937
994
 
938
995
  // src/redact.ts
939
996
  var SENSITIVE_FLAG_PATTERNS = [
@@ -965,7 +1022,7 @@ function redactSecrets(text) {
965
1022
  delim = "=";
966
1023
  delimIdx = eq;
967
1024
  } else if (sp !== -1) {
968
- delim = match[sp] ?? null;
1025
+ delim = match[sp];
969
1026
  delimIdx = sp;
970
1027
  }
971
1028
  if (delim !== null && delimIdx >= 0) {
@@ -1143,6 +1200,7 @@ var PollLock = class {
1143
1200
  const raw = readFileSync(this.lockPath, "utf8");
1144
1201
  const parsed = JSON.parse(raw);
1145
1202
  if (typeof parsed.id !== "string" || typeof parsed.pid !== "number") return null;
1203
+ if (!Number.isFinite(parsed.heartbeatAt)) return null;
1146
1204
  return parsed;
1147
1205
  } catch {
1148
1206
  return null;
@@ -1246,7 +1304,8 @@ var OffsetStore = class {
1246
1304
  };
1247
1305
 
1248
1306
  // src/security/outbound.ts
1249
- import { DefaultSecretScrubber, ToolValidationError } from "@wrongstack/core";
1307
+ import { DefaultSecretScrubber } from "@wrongstack/core/security";
1308
+ import { ToolValidationError } from "@wrongstack/core/types";
1250
1309
  var TELEGRAM_APPROVAL_CAPABILITY = "net.outbound.telegram.approval";
1251
1310
  var secretScrubber = new DefaultSecretScrubber();
1252
1311
  var RAW_TELEGRAM_BOT_TOKEN = /(?<![A-Za-z0-9])\d{5,15}:[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])/g;
@@ -1288,7 +1347,7 @@ function scrubTelegramOutboundText(text) {
1288
1347
  }
1289
1348
 
1290
1349
  // src/slash-commands/index.ts
1291
- import { expectDefined } from "@wrongstack/core";
1350
+ import { expectDefined } from "@wrongstack/core/utils";
1292
1351
  function tgHealthCommand(bot, cfg) {
1293
1352
  return {
1294
1353
  name: "telegram-health",
@@ -1427,7 +1486,7 @@ function makeTelegramApproveTool(opts) {
1427
1486
  const timeoutMs = Math.min(Math.max(input.timeout_ms ?? 6e4, 1e3), 6e5);
1428
1487
  const configuredUserIds = opts.getAllowedUserIds?.().map(String) ?? [];
1429
1488
  const isGroup = String(chatId).startsWith("-");
1430
- if (isGroup && (opts.allowGroupApprovals !== true || configuredUserIds.length === 0)) {
1489
+ if (isGroup && (opts.getAllowGroupApprovals?.() !== true || configuredUserIds.length === 0)) {
1431
1490
  throw new Error("Telegram group approvals require explicit per-user configuration.");
1432
1491
  }
1433
1492
  const expectedUserIds = configuredUserIds.length > 0 ? configuredUserIds : [String(chatId)];
@@ -1449,7 +1508,7 @@ _Reply by tapping a button. Auto-denies in ${Math.round(timeoutMs / 1e3)}s._`;
1449
1508
  sessionId: ctx?.session.id ?? "unknown-session",
1450
1509
  expectedChatId: chatId,
1451
1510
  expectedUserIds,
1452
- allowGroup: isGroup && opts.allowGroupApprovals === true,
1511
+ allowGroup: isGroup && opts.getAllowGroupApprovals?.() === true,
1453
1512
  expiresAt: Date.now() + timeoutMs,
1454
1513
  signal: toolOpts?.signal
1455
1514
  });
@@ -1500,8 +1559,8 @@ var OutboundQueue = class {
1500
1559
  #failed = 0;
1501
1560
  #resolvers = /* @__PURE__ */ new Map();
1502
1561
  constructor(opts) {
1503
- const maxPerChat = opts.maxPerChat ?? DEFAULT_MAX_PER_CHAT;
1504
- const maxConcurrency = opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY;
1562
+ const maxPerChat = Math.max(1, opts.maxPerChat ?? DEFAULT_MAX_PER_CHAT);
1563
+ const maxConcurrency = Math.max(1, opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
1505
1564
  this.#opts = {
1506
1565
  maxPerChat,
1507
1566
  maxConcurrency,
@@ -1529,18 +1588,19 @@ var OutboundQueue = class {
1529
1588
  }
1530
1589
  if (entry.kind === "notification") {
1531
1590
  if (lane.pending.length >= this.#opts.maxPerChat) {
1532
- const dropped = lane.pending.shift();
1533
- if (dropped) {
1591
+ const dropIndex = lane.pending.findIndex((e) => e.kind === "notification");
1592
+ if (dropIndex === -1) {
1534
1593
  this.#dropped += 1;
1535
- const droppedResolver = this.#resolvers.get(dropped.id);
1536
- if (droppedResolver) {
1537
- this.#resolvers.delete(dropped.id);
1538
- droppedResolver.resolve(void 0);
1539
- }
1540
1594
  this.#opts.log?.debug(
1541
- `Telegram outbound queue dropped a notification for chat ${dropped.chatId} (per-chat limit ${this.#opts.maxPerChat})`
1595
+ `Telegram outbound queue dropped an incoming notification for chat ${entry.chatId} (per-chat limit ${this.#opts.maxPerChat}; only manual entries pending)`
1542
1596
  );
1597
+ return Promise.resolve(void 0);
1543
1598
  }
1599
+ const dropped = lane.pending.splice(dropIndex, 1)[0];
1600
+ this.#dropped += 1;
1601
+ this.#opts.log?.debug(
1602
+ `Telegram outbound queue dropped a notification for chat ${dropped.chatId} (per-chat limit ${this.#opts.maxPerChat})`
1603
+ );
1544
1604
  }
1545
1605
  } else if (lane.pending.length + (lane.running ? 1 : 0) >= this.#opts.maxPerChat) {
1546
1606
  return Promise.reject(
@@ -1632,15 +1692,6 @@ var OutboundQueue = class {
1632
1692
  async #run(entry) {
1633
1693
  const key = String(entry.chatId);
1634
1694
  const lane = this.#lanes.get(key);
1635
- if (!lane) {
1636
- const resolver = this.#resolvers.get(entry.id);
1637
- if (resolver) {
1638
- this.#resolvers.delete(entry.id);
1639
- resolver.resolve(void 0);
1640
- }
1641
- this.#active -= 1;
1642
- return;
1643
- }
1644
1695
  try {
1645
1696
  const result = await this.#opts.send(entry.chatId, entry.text);
1646
1697
  this.#sent += 1;
@@ -1655,7 +1706,7 @@ var OutboundQueue = class {
1655
1706
  if (resolver) {
1656
1707
  this.#resolvers.delete(entry.id);
1657
1708
  resolver.reject(err);
1658
- } else if (entry.kind === "notification") {
1709
+ } else {
1659
1710
  this.#opts.log?.debug(
1660
1711
  `Telegram outbound queue notification failed for chat ${entry.chatId}: ${err.message}`
1661
1712
  );
@@ -1663,31 +1714,115 @@ var OutboundQueue = class {
1663
1714
  } finally {
1664
1715
  this.#active -= 1;
1665
1716
  lane.running = false;
1717
+ if (lane.pending.length === 0) {
1718
+ this.#lanes.delete(key);
1719
+ }
1666
1720
  this.#schedule();
1667
1721
  }
1668
1722
  }
1669
1723
  };
1670
1724
 
1725
+ // src/rate-limiter.ts
1726
+ function createTokenBucket(opts) {
1727
+ const tokensPerSecond = opts?.tokensPerSecond ?? 0.33;
1728
+ const burst = opts?.burst ?? 4;
1729
+ const refillIntervalMs = 1e3 / tokensPerSecond;
1730
+ let tokens = burst;
1731
+ let lastRefill = Date.now();
1732
+ return { waitForToken, fill, isFull };
1733
+ async function waitForToken(timeoutMs) {
1734
+ const deadline = timeoutMs !== void 0 ? Date.now() + timeoutMs : Infinity;
1735
+ while (true) {
1736
+ refill();
1737
+ if (tokens >= 1) {
1738
+ tokens -= 1;
1739
+ return;
1740
+ }
1741
+ const now = Date.now();
1742
+ if (now >= deadline) {
1743
+ return;
1744
+ }
1745
+ const nextRefill = lastRefill + refillIntervalMs;
1746
+ const delay = Math.min(
1747
+ Math.max(nextRefill - now, 0),
1748
+ deadline - now,
1749
+ 5e3
1750
+ // safety cap: never sleep longer than 5s
1751
+ );
1752
+ await sleep(delay);
1753
+ }
1754
+ }
1755
+ function refill() {
1756
+ const now = Date.now();
1757
+ const elapsed = now - lastRefill;
1758
+ if (elapsed <= 0) return;
1759
+ const newTokens = elapsed / 1e3 * tokensPerSecond;
1760
+ tokens = Math.min(burst, tokens + newTokens);
1761
+ lastRefill = now;
1762
+ }
1763
+ function fill() {
1764
+ refill();
1765
+ return tokens;
1766
+ }
1767
+ function isFull() {
1768
+ refill();
1769
+ return tokens >= burst;
1770
+ }
1771
+ }
1772
+ function sleep(ms) {
1773
+ return new Promise((resolve) => setTimeout(resolve, ms));
1774
+ }
1775
+
1671
1776
  // src/bot-queue.ts
1777
+ var BUCKET_SWEEP_INTERVAL_MS = 6e4;
1672
1778
  var TelegramBotOutbound = class {
1673
1779
  #queue;
1674
1780
  #bot;
1675
1781
  #log;
1782
+ #buckets = /* @__PURE__ */ new Map();
1783
+ #getRateTokensPerSecond;
1784
+ #getRateBurst;
1785
+ #bucketSweepTimer;
1676
1786
  #stopped = false;
1677
1787
  constructor(opts) {
1678
1788
  this.#bot = opts.bot;
1679
1789
  this.#log = opts.log;
1790
+ this.#getRateTokensPerSecond = opts.getRateLimitTokensPerSecond ?? (() => 0.33);
1791
+ this.#getRateBurst = opts.getRateLimitBurst ?? (() => 4);
1680
1792
  this.#queue = new OutboundQueue({
1681
1793
  maxPerChat: opts.maxPerChat,
1682
1794
  maxConcurrency: opts.maxConcurrency,
1683
- send: (chatId, text) => this.#bot.sendMessage(chatId, text).then((res) => {
1684
- if (!res.ok) {
1685
- throw new Error(`Telegram outbound send returned ok=false for chat ${chatId}`);
1686
- }
1687
- return res;
1688
- }),
1795
+ send: (chatId, text) => this.#rateLimitedSend(chatId, text),
1689
1796
  log: opts.log
1690
1797
  });
1798
+ this.#bucketSweepTimer = setInterval(() => this.#sweepIdleBuckets(), BUCKET_SWEEP_INTERVAL_MS);
1799
+ this.#bucketSweepTimer.unref?.();
1800
+ }
1801
+ #sweepIdleBuckets() {
1802
+ for (const [key, bucket] of this.#buckets) {
1803
+ if (bucket.isFull()) this.#buckets.delete(key);
1804
+ }
1805
+ }
1806
+ /** Per-chat rate-limited send: waits for a token, then delegates to the bot.
1807
+ * Rate-limit values are resolved lazily when a new bucket is created via
1808
+ * the constructor getters so a live config change applies to subsequent
1809
+ * chats without rebuilding the queue. */
1810
+ async #rateLimitedSend(chatId, text) {
1811
+ const key = String(chatId);
1812
+ let bucket = this.#buckets.get(key);
1813
+ if (!bucket) {
1814
+ bucket = createTokenBucket({
1815
+ tokensPerSecond: this.#getRateTokensPerSecond(),
1816
+ burst: this.#getRateBurst()
1817
+ });
1818
+ this.#buckets.set(key, bucket);
1819
+ }
1820
+ await bucket.waitForToken(5e3);
1821
+ const res = await this.#bot.sendMessage(chatId, text);
1822
+ if (!res.ok) {
1823
+ throw new Error(`Telegram outbound send returned ok=false for chat ${chatId}`);
1824
+ }
1825
+ return res;
1691
1826
  }
1692
1827
  /** Manual send (telegram_send tool, /telegram:send): never silently dropped. */
1693
1828
  async sendManual(chatId, text) {
@@ -1711,17 +1846,14 @@ var TelegramBotOutbound = class {
1711
1846
  return;
1712
1847
  }
1713
1848
  const entry = { chatId, text, kind: "notification" };
1714
- this.#queue.enqueue(entry).catch((err) => {
1715
- this.#log.debug(
1716
- `Telegram outbound notification enqueue rejected for chat ${chatId}: ${err.message}`
1717
- );
1718
- });
1849
+ void this.#queue.enqueue(entry);
1719
1850
  }
1720
1851
  stats() {
1721
1852
  return this.#queue.stats();
1722
1853
  }
1723
1854
  async stop() {
1724
1855
  this.#stopped = true;
1856
+ clearInterval(this.#bucketSweepTimer);
1725
1857
  await this.#queue.stop();
1726
1858
  }
1727
1859
  };
@@ -1868,7 +2000,7 @@ function makeTelegramReadTool(opts) {
1868
2000
  }
1869
2001
 
1870
2002
  // src/tools/telegram-send.ts
1871
- import { ToolCapabilities } from "@wrongstack/core";
2003
+ import { ToolCapabilities } from "@wrongstack/core/security";
1872
2004
  function makeTelegramSendTool(opts) {
1873
2005
  return {
1874
2006
  name: "telegram_send",
@@ -1912,6 +2044,19 @@ function makeTelegramSendTool(opts) {
1912
2044
  };
1913
2045
  }
1914
2046
 
2047
+ // src/config-classifier.ts
2048
+ import { diffPluginConfig } from "@wrongstack/core/plugin";
2049
+ function diffConfigKeys(previous, next) {
2050
+ return diffPluginConfig(
2051
+ previous,
2052
+ next,
2053
+ TELEGRAM_CONFIG_FIELDS
2054
+ ).map((change) => ({
2055
+ key: change.key,
2056
+ classification: change.lifecycle === "hot" ? "hot" : "restart-required"
2057
+ }));
2058
+ }
2059
+
1915
2060
  // src/index.ts
1916
2061
  var teardownState = null;
1917
2062
  var DENY_ALL_INBOUND = "__wrongstack_telegram_inbound_disabled__";
@@ -1958,22 +2103,25 @@ function registerCommand(api, command, cleanups) {
1958
2103
  api.slashCommands.unregister(`${PLUGIN_NAME}:${command.name}`);
1959
2104
  });
1960
2105
  }
2106
+ function telegramDefaultConfig() {
2107
+ return structuredClone(DEFAULT_CONFIG);
2108
+ }
1961
2109
  function telegramFromConfig(cfg) {
1962
- const ext = cfg.extensions?.[PLUGIN_NAME] ?? {};
2110
+ const tg = readTelegramConfigFromConfig(cfg);
1963
2111
  return {
1964
- notifyChatId: ext.notifyChatId !== void 0 ? String(ext.notifyChatId) : void 0,
1965
- allowedOutboundChats: Array.isArray(ext.allowedOutboundChats) ? ext.allowedOutboundChats.filter(
1966
- (chatId) => typeof chatId === "string" || typeof chatId === "number"
1967
- ) : [],
1968
- allowedUserIds: Array.isArray(ext.allowedUsers) ? ext.allowedUsers.filter(
1969
- (userId) => typeof userId === "string" || typeof userId === "number"
1970
- ) : [],
1971
- allowGroupApprovals: ext.allowGroupApprovals === true,
1972
- notifyOnSessionEnd: ext.notifyOnSessionEnd === true,
1973
- notifyOnDelegate: ext.notifyOnDelegate !== false,
1974
- // default true
1975
- longToolThresholdMs: typeof ext.longToolThresholdMs === "number" ? ext.longToolThresholdMs : 3e4,
1976
- maxMessageLength: typeof ext.maxMessageLength === "number" ? ext.maxMessageLength : 4e3
2112
+ notifyChatId: tg.notifyChatId !== void 0 ? String(tg.notifyChatId) : void 0,
2113
+ allowedOutboundChats: [...tg.allowedOutboundChats ?? []],
2114
+ allowedUserIds: [...tg.allowedUsers ?? []],
2115
+ allowGroupApprovals: tg.allowGroupApprovals ?? false,
2116
+ notifyOnSessionEnd: tg.notifyOnSessionEnd ?? false,
2117
+ notifyOnDelegate: tg.notifyOnDelegate ?? true,
2118
+ longToolThresholdMs: tg.longToolThresholdMs ?? 3e4,
2119
+ maxMessageLength: tg.maxMessageLength ?? 4e3,
2120
+ outboundQueuePerChat: tg.outboundQueuePerChat ?? 32,
2121
+ outboundQueueConcurrency: tg.outboundQueueConcurrency ?? 4,
2122
+ rateLimitTokensPerSecond: tg.rateLimitTokensPerSecond ?? 0.33,
2123
+ rateLimitBurst: tg.rateLimitBurst ?? 1,
2124
+ parseMode: tg.parseMode ?? ""
1977
2125
  };
1978
2126
  }
1979
2127
  var plugin = {
@@ -1986,31 +2134,29 @@ var plugin = {
1986
2134
  slashCommands: true,
1987
2135
  pipelines: []
1988
2136
  },
2137
+ configAliases: [...PLUGIN_CONFIG_ALIASES],
2138
+ configFields: TELEGRAM_CONFIG_FIELDS,
1989
2139
  configSchema: telegramConfigSchema,
1990
- defaultConfig: {
1991
- allowedOutboundChats: [],
1992
- pollIntervalSec: 2,
1993
- notifyOnSessionEnd: false,
1994
- longToolThresholdMs: 3e4,
1995
- maxMessageLength: 4e3
1996
- },
2140
+ defaultConfig: telegramDefaultConfig(),
1997
2141
  async setup(api) {
1998
2142
  const log = api.log;
1999
2143
  disposeRuntime(log);
2000
2144
  const cfg = readTelegramConfig(api);
2001
2145
  log.info("Starting Telegram plugin...");
2002
- const rawCfg = cfg;
2003
2146
  const runtimeCfg = {
2004
2147
  notifyChatId: cfg.notifyChatId,
2005
2148
  allowedOutboundChats: [...cfg.allowedOutboundChats ?? []],
2006
2149
  allowedUserIds: [...cfg.allowedUsers ?? []],
2007
- allowGroupApprovals: rawCfg.allowGroupApprovals === true,
2150
+ allowGroupApprovals: cfg.allowGroupApprovals ?? false,
2008
2151
  notifyOnSessionEnd: cfg.notifyOnSessionEnd ?? false,
2009
2152
  notifyOnDelegate: cfg.notifyOnDelegate ?? true,
2010
2153
  longToolThresholdMs: cfg.longToolThresholdMs ?? 3e4,
2011
2154
  maxMessageLength: cfg.maxMessageLength ?? 4e3,
2012
2155
  outboundQueuePerChat: cfg.outboundQueuePerChat ?? 32,
2013
- outboundQueueConcurrency: cfg.outboundQueueConcurrency ?? 4
2156
+ outboundQueueConcurrency: cfg.outboundQueueConcurrency ?? 4,
2157
+ rateLimitTokensPerSecond: cfg.rateLimitTokensPerSecond ?? 0.33,
2158
+ rateLimitBurst: cfg.rateLimitBurst ?? 1,
2159
+ parseMode: cfg.parseMode ?? ""
2014
2160
  };
2015
2161
  const lock = cfg.singleInstanceLock === false ? void 0 : new PollLock(lockPathForToken(cfg.botToken), { log });
2016
2162
  const offsetStore = cfg.offsetStoragePath === "" ? void 0 : new OffsetStore({ token: cfg.botToken, path: cfg.offsetStoragePath });
@@ -2022,8 +2168,24 @@ var plugin = {
2022
2168
  log,
2023
2169
  offsetStore,
2024
2170
  lock,
2171
+ getParseMode: () => runtimeCfg.parseMode,
2025
2172
  onMessage(msg) {
2026
2173
  api.emitCustom("telegram:message_received", msg);
2174
+ const mailbox = api.mailbox;
2175
+ if (mailbox) {
2176
+ mailbox.send({
2177
+ from: "telegram",
2178
+ to: "leader",
2179
+ type: "note",
2180
+ subject: scrubTelegramOutboundText(
2181
+ `\u{1F4E8} Telegram from ${msg.userName ?? `user_${msg.userId ?? "unknown"}`}`
2182
+ ),
2183
+ body: scrubTelegramOutboundText(msg.text),
2184
+ priority: "low"
2185
+ }).catch((err) => {
2186
+ log.debug(`Telegram\u2192mailbox bridge delivery failed: ${err.message}`);
2187
+ });
2188
+ }
2027
2189
  log.info(`\u{1F4E8} Telegram message received (${Math.min(bot.bufferCount, 50)} unread)`);
2028
2190
  }
2029
2191
  });
@@ -2042,7 +2204,9 @@ var plugin = {
2042
2204
  bot,
2043
2205
  log,
2044
2206
  maxPerChat: runtimeCfg.outboundQueuePerChat,
2045
- maxConcurrency: runtimeCfg.outboundQueueConcurrency
2207
+ maxConcurrency: runtimeCfg.outboundQueueConcurrency,
2208
+ getRateLimitTokensPerSecond: () => runtimeCfg.rateLimitTokensPerSecond,
2209
+ getRateLimitBurst: () => runtimeCfg.rateLimitBurst
2046
2210
  });
2047
2211
  cleanups.push(() => {
2048
2212
  void outbound.stop();
@@ -2060,7 +2224,7 @@ var plugin = {
2060
2224
  getDefaultChatId: () => runtimeCfg.notifyChatId,
2061
2225
  getAllowedOutboundChatIds: () => runtimeCfg.allowedOutboundChats,
2062
2226
  getAllowedUserIds: () => runtimeCfg.allowedUserIds,
2063
- allowGroupApprovals: runtimeCfg.allowGroupApprovals,
2227
+ getAllowGroupApprovals: () => runtimeCfg.allowGroupApprovals,
2064
2228
  maxMessageLength: runtimeCfg.maxMessageLength,
2065
2229
  log
2066
2230
  });
@@ -2128,6 +2292,8 @@ var plugin = {
2128
2292
  source: "session.end"
2129
2293
  }).then((r) => {
2130
2294
  if (!r.ok) log.warn(`session.ended notification delivery failed: ${r.error ?? "unknown"}`);
2295
+ }).catch((err) => {
2296
+ log.debug(`session.ended notification delivery threw: ${err.message}`);
2131
2297
  });
2132
2298
  })
2133
2299
  );
@@ -2148,6 +2314,8 @@ var plugin = {
2148
2314
  source: "tool.exec"
2149
2315
  }).then((r) => {
2150
2316
  if (!r.ok) log.warn(`tool.executed notification delivery failed: ${r.error ?? "unknown"}`);
2317
+ }).catch((err) => {
2318
+ log.debug(`tool.executed notification delivery threw: ${err.message}`);
2151
2319
  });
2152
2320
  })
2153
2321
  );
@@ -2168,33 +2336,81 @@ var plugin = {
2168
2336
  source: "delegate.completed"
2169
2337
  }).then((r) => {
2170
2338
  if (!r.ok) log.warn(`delegate.completed notification delivery failed: ${r.error ?? "unknown"}`);
2339
+ }).catch((err) => {
2340
+ log.debug(`delegate.completed notification delivery threw: ${err.message}`);
2171
2341
  });
2172
2342
  })
2173
2343
  );
2174
2344
  const unlistenConfig = api.onConfigChange((next, prev) => {
2345
+ const nextTg = readTelegramConfigFromConfig(next);
2346
+ const prevTg = readTelegramConfigFromConfig(prev);
2347
+ const changedKeys = diffConfigKeys(prevTg, nextTg);
2348
+ const hotKeys = changedKeys.filter((c) => c.classification === "hot").map((c) => c.key);
2349
+ const restartKeys = changedKeys.filter((c) => c.classification === "restart-required").map((c) => c.key);
2175
2350
  const fresh = telegramFromConfig(next);
2176
2351
  const was = telegramFromConfig(prev);
2177
- runtimeCfg.notifyChatId = fresh.notifyChatId;
2178
- runtimeCfg.allowedOutboundChats = fresh.allowedOutboundChats;
2179
- runtimeCfg.allowedUserIds = fresh.allowedUserIds;
2180
- runtimeCfg.allowGroupApprovals = fresh.allowGroupApprovals;
2181
- runtimeCfg.notifyOnSessionEnd = fresh.notifyOnSessionEnd;
2182
- runtimeCfg.notifyOnDelegate = fresh.notifyOnDelegate;
2183
- runtimeCfg.longToolThresholdMs = fresh.longToolThresholdMs;
2184
- runtimeCfg.maxMessageLength = fresh.maxMessageLength;
2185
- if (fresh.notifyChatId !== was.notifyChatId || fresh.maxMessageLength !== was.maxMessageLength) {
2186
- notifyChannel = fresh.notifyChatId !== void 0 ? new TelegramNotificationChannel({
2352
+ const hotSet = new Set(hotKeys);
2353
+ const HOT_APPLIERS = /* @__PURE__ */ new Map([
2354
+ ["allowedOutboundChats", (r, f) => {
2355
+ r.allowedOutboundChats = f.allowedOutboundChats;
2356
+ }],
2357
+ ["allowedUsers", (r, f) => {
2358
+ r.allowedUserIds = f.allowedUserIds;
2359
+ }],
2360
+ ["notifyOnSessionEnd", (r, f) => {
2361
+ r.notifyOnSessionEnd = f.notifyOnSessionEnd;
2362
+ }],
2363
+ ["notifyOnDelegate", (r, f) => {
2364
+ r.notifyOnDelegate = f.notifyOnDelegate;
2365
+ }],
2366
+ ["longToolThresholdMs", (r, f) => {
2367
+ r.longToolThresholdMs = f.longToolThresholdMs;
2368
+ }],
2369
+ ["maxMessageLength", (r, f) => {
2370
+ r.maxMessageLength = f.maxMessageLength;
2371
+ }],
2372
+ ["allowGroupApprovals", (r, f) => {
2373
+ r.allowGroupApprovals = f.allowGroupApprovals;
2374
+ }],
2375
+ ["rateLimitTokensPerSecond", (r, f) => {
2376
+ r.rateLimitTokensPerSecond = f.rateLimitTokensPerSecond;
2377
+ }],
2378
+ ["rateLimitBurst", (r, f) => {
2379
+ r.rateLimitBurst = f.rateLimitBurst;
2380
+ }],
2381
+ ["parseMode", (r, f) => {
2382
+ r.parseMode = f.parseMode;
2383
+ }]
2384
+ ]);
2385
+ for (const [key, apply] of HOT_APPLIERS) {
2386
+ if (hotSet.has(key)) apply(runtimeCfg, fresh);
2387
+ }
2388
+ if (hotSet.has("maxMessageLength") && fresh.maxMessageLength !== was.maxMessageLength) {
2389
+ notifyChannel = runtimeCfg.notifyChatId !== void 0 ? new TelegramNotificationChannel({
2187
2390
  bot,
2188
- chatId: fresh.notifyChatId,
2391
+ chatId: runtimeCfg.notifyChatId,
2189
2392
  maxMessageLength: fresh.maxMessageLength,
2190
2393
  enqueueNotification: (chatId, text) => outbound.enqueueNotification(chatId, text),
2191
2394
  log
2192
2395
  }) : void 0;
2193
2396
  }
2194
- log.debug("Telegram notification settings updated from config", {
2397
+ if (restartKeys.length > 0) {
2398
+ log.warn(
2399
+ "Telegram config changed restart-required keys \u2014 restart the plugin for these to take effect",
2400
+ { restartKeys, hotKeys }
2401
+ );
2402
+ api.emitCustom("telegram:restart_required", {
2403
+ keys: restartKeys,
2404
+ message: `Restart required for: ${restartKeys.join(", ")}`
2405
+ });
2406
+ }
2407
+ log.debug("Telegram config updated", {
2408
+ hotApplied: hotKeys,
2409
+ restartRequired: restartKeys,
2195
2410
  notifyOnSessionEnd: runtimeCfg.notifyOnSessionEnd,
2196
2411
  notifyOnDelegate: runtimeCfg.notifyOnDelegate,
2197
2412
  longToolThresholdMs: runtimeCfg.longToolThresholdMs,
2413
+ parseMode: runtimeCfg.parseMode,
2198
2414
  notifyChatId: runtimeCfg.notifyChatId ?? "not set"
2199
2415
  });
2200
2416
  });