@openclaw/irc 2026.10.2 → 2026.10.5-beta.2

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.
@@ -10,7 +10,6 @@ import { defineStableChannelIngressIdentity, identityEntryAuthenticationClassifi
10
10
  import { buildMutableAllowEntryDetector, collectStandardAllowlistLists, createAllowlistProviderOpenWarningCollector, createConditionalWarningCollector, createDangerousNameMatchingMutableAllowlistWarningCollector, resolveScopeKeyCaseInsensitive, resolveScopeRequireMention, resolveScopeToolsPolicy } from "openclaw/plugin-sdk/channel-policy";
11
11
  import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-status";
12
12
  import { createChannelDirectoryAdapter, createResolvedDirectoryEntriesLister } from "openclaw/plugin-sdk/directory-runtime";
13
- import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
14
13
  import { buildBaseChannelStatusSummary, createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
15
14
  import { resolveAccountWithDefaultFallback } from "openclaw/plugin-sdk/account-core";
16
15
  import { parseTcpPort } from "openclaw/plugin-sdk/number-runtime";
@@ -19,9 +18,12 @@ import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
19
18
  import { resolveSecretInputString } from "openclaw/plugin-sdk/secret-input";
20
19
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, normalizeStringEntries, normalizeStringifiedOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
21
20
  import { createAccountStatusSink, createMessageReceiptFromOutboundResults, createReplyToFanout, defineChannelMessageAdapter, runPassiveAccountLifecycle, sanitizeForPlainText } from "openclaw/plugin-sdk/channel-outbound";
21
+ import { containsAsciiControlCharacter as hasIrcControlChars } from "openclaw/plugin-sdk/string-normalization-runtime";
22
22
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
23
23
  import { chunkTextForOutbound, convertMarkdownTables, sanitizeAssistantVisibleText, stripMarkdown } from "openclaw/plugin-sdk/text-chunking";
24
24
  import { randomUUID } from "node:crypto";
25
+ import { createChannelPartialDeliveryError, isChannelPartialDeliveryError } from "openclaw/plugin-sdk/channel-inbound";
26
+ import { captureEffectAuthority } from "openclaw/plugin-sdk/fetch-runtime";
25
27
  import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
26
28
  import { requireRuntimeConfig } from "openclaw/plugin-sdk/plugin-config-runtime";
27
29
  import net from "node:net";
@@ -197,7 +199,6 @@ const collectIrcMutableAllowlistWarnings = createDangerousNameMatchingMutableAll
197
199
  });
198
200
  //#endregion
199
201
  //#region extensions/irc/src/gateway.ts
200
- const loadIrcChannelRuntime$1 = createLazyRuntimeModule(() => import("./channel-runtime-BvC0hwF1.mjs"));
201
202
  async function startIrcGatewayAccount(ctx) {
202
203
  const account = ctx.account;
203
204
  const statusSink = createAccountStatusSink({
@@ -206,7 +207,7 @@ async function startIrcGatewayAccount(ctx) {
206
207
  });
207
208
  if (!account.configured) throw new Error(`IRC is not configured for account "${account.accountId}" (need host and nick in channels.irc).`);
208
209
  ctx.log?.info?.(`[${account.accountId}] starting IRC provider (${account.host}:${account.port}${account.tls ? " tls" : ""})`);
209
- const { monitorIrcProvider } = await loadIrcChannelRuntime$1();
210
+ const { monitorIrcProvider } = await import("./channel-runtime-FNBqToAo.mjs");
210
211
  await runPassiveAccountLifecycle({
211
212
  abortSignal: ctx.abortSignal,
212
213
  start: async () => await monitorIrcProvider({
@@ -226,10 +227,6 @@ async function startIrcGatewayAccount(ctx) {
226
227
  function isIrcControlChar(charCode) {
227
228
  return charCode <= 31 || charCode === 127;
228
229
  }
229
- function hasIrcControlChars(value) {
230
- for (const char of value) if (isIrcControlChar(char.charCodeAt(0))) return true;
231
- return false;
232
- }
233
230
  function stripIrcControlChars(value) {
234
231
  let out = "";
235
232
  for (const char of value) if (!isIrcControlChar(char.charCodeAt(0))) out += char;
@@ -352,7 +349,7 @@ function normalizeIrcNickHostEntry(value) {
352
349
  }
353
350
  function normalizeIrcNickUserEntry(value) {
354
351
  const normalized = normalizeIrcAllowEntry(value);
355
- if (!normalized || normalized === "*" || !isHostlessNickUser(normalized)) return null;
352
+ if (!isHostlessNickUser(normalized)) return null;
356
353
  return normalized;
357
354
  }
358
355
  function normalizeIrcNickEntry(value) {
@@ -368,7 +365,7 @@ function createIrcIngressSubject(message) {
368
365
  stableId: stableCandidates[stableCandidates.length - 1] ?? nick,
369
366
  aliases: {
370
367
  "irc-id-nick-user": candidates.find((candidate) => isHostlessNickUser(candidate)),
371
- "irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!") && candidate.includes("@")),
368
+ "irc-id-nick-host": stableCandidates.find((candidate) => !candidate.includes("!")),
372
369
  "irc-nick": nick
373
370
  }
374
371
  };
@@ -386,6 +383,51 @@ const ircOutboundBaseAdapter = {
386
383
  sanitizeText: ({ text }) => sanitizeIrcAssistantText(text)
387
384
  };
388
385
  //#endregion
386
+ //#region packages/normalization-core/src/utf16-slice.ts
387
+ function isHighSurrogate(codeUnit) {
388
+ return codeUnit >= 55296 && codeUnit <= 56319;
389
+ }
390
+ function isLowSurrogate(codeUnit) {
391
+ return codeUnit >= 56320 && codeUnit <= 57343;
392
+ }
393
+ /** Moves a chunk boundary away from the middle of a UTF-16 surrogate pair. */
394
+ function avoidTrailingHighSurrogateBreak(text, start, end) {
395
+ if (end <= start || end >= text.length || !isHighSurrogate(text.charCodeAt(end - 1)) || !isLowSurrogate(text.charCodeAt(end))) return end;
396
+ const adjusted = end - 1;
397
+ return adjusted > start ? adjusted : end + 1;
398
+ }
399
+ //#endregion
400
+ //#region packages/normalization-core/src/grapheme.ts
401
+ let graphemeSegmenter;
402
+ function getGraphemeSegmenter() {
403
+ graphemeSegmenter ??= new Intl.Segmenter(void 0, { granularity: "grapheme" });
404
+ return graphemeSegmenter;
405
+ }
406
+ /** Uses the same source text that created segments. */
407
+ function containingSegment(segments, text, index) {
408
+ const position = Number.isNaN(index) ? 0 : Math.trunc(index);
409
+ const codePoint = text.codePointAt(position);
410
+ return segments.containing(codePoint !== void 0 && codePoint > 65535 ? position + 1 : position);
411
+ }
412
+ /**
413
+ * Chooses a whole-grapheme cut within the hard budget, honoring a usable preference.
414
+ * If no whole grapheme fits, allowPartial permits a surrogate-safe progress cut;
415
+ * a leading surrogate pair can exceed maxEnd by one code unit.
416
+ */
417
+ function findGraphemeChunkEnd(text, start, maxEnd, preferredEnd = maxEnd, allowPartial = true) {
418
+ const hardEnd = Math.min(maxEnd, text.length);
419
+ if (hardEnd <= start) return start;
420
+ const preferred = Number.isInteger(preferredEnd) && preferredEnd > start && preferredEnd <= hardEnd ? preferredEnd : hardEnd;
421
+ if (preferred === text.length) return preferred;
422
+ const before = text.charCodeAt(preferred - 1);
423
+ const after = text.charCodeAt(preferred);
424
+ if (Number.isInteger(preferred) && before <= 127 && after <= 127 && !(before === 13 && after === 10)) return preferred;
425
+ const segments = getGraphemeSegmenter().segment(text);
426
+ let end = containingSegment(segments, text, preferred)?.index ?? preferred;
427
+ if (end <= start && preferred < hardEnd) end = hardEnd === text.length ? hardEnd : containingSegment(segments, text, hardEnd)?.index ?? hardEnd;
428
+ return end > start ? end : allowPartial ? avoidTrailingHighSurrogateBreak(text, start, hardEnd) : start;
429
+ }
430
+ //#endregion
389
431
  //#region extensions/irc/src/protocol.ts
390
432
  const IRC_TARGET_PATTERN = /^[^\s:]+$/u;
391
433
  function parseIrcLine(line) {
@@ -451,20 +493,13 @@ function parseIrcPrefix(prefix) {
451
493
  if (prefix.includes(".")) return { server: prefix };
452
494
  return { nick: prefix };
453
495
  }
454
- function decodeLiteralEscapes(input) {
455
- return input.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\0/g, "\0").replace(/\\x([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16))).replace(/\\u([dD][89abAB][0-9a-fA-F]{2})\\u([dD][c-fC-F][0-9a-fA-F]{2})/g, (_match, h, l) => String.fromCodePoint(65536 + (Number.parseInt(h, 16) - 55296 << 10) + (Number.parseInt(l, 16) - 56320))).replace(/\\u([0-9a-fA-F]{4})/g, (match, hex) => {
456
- const codePoint = Number.parseInt(hex, 16);
457
- return codePoint >= 55296 && codePoint <= 57343 ? match : String.fromCharCode(codePoint);
458
- });
459
- }
460
496
  function sanitizeIrcOutboundText(text) {
461
- return stripIrcControlChars(decodeLiteralEscapes(text).replace(/\r?\n/g, " ")).trim();
497
+ return stripIrcControlChars(text.replace(/\r?\n/g, " ")).trim();
462
498
  }
463
499
  function sanitizeIrcTarget(raw) {
464
- const decoded = decodeLiteralEscapes(raw);
465
- if (!decoded) throw new Error("IRC target is required");
466
- if (decoded !== decoded.trim() || hasIrcControlChars(decoded) || !IRC_TARGET_PATTERN.test(decoded)) throw new Error(`Invalid IRC target: ${raw}`);
467
- return decoded;
500
+ if (!raw) throw new Error("IRC target is required");
501
+ if (raw !== raw.trim() || hasIrcControlChars(raw) || !IRC_TARGET_PATTERN.test(raw)) throw new Error(`Invalid IRC target: ${raw}`);
502
+ return raw;
468
503
  }
469
504
  //#endregion
470
505
  //#region extensions/irc/src/client.ts
@@ -475,6 +510,9 @@ const IRC_ERROR_CODES = /* @__PURE__ */ new Set([
475
510
  ]);
476
511
  const IRC_NICK_COLLISION_CODES = /* @__PURE__ */ new Set(["433", "436"]);
477
512
  const IRC_MAX_LINE_BYTES = 512;
513
+ const IRC_MAX_RELAY_USER_BYTES = 11;
514
+ const IRC_MAX_RELAY_HOST_BYTES = 63;
515
+ const IRC_MAX_INBOUND_LINE_LENGTH = 16384;
478
516
  function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
479
517
  let end = 0;
480
518
  let bytes = 0;
@@ -486,11 +524,9 @@ function takeIrcPrivmsgChunk(text, maxChars, maxBytes) {
486
524
  }
487
525
  if (end === 0) throw new Error("IRC target leaves no room for message text within the 512-byte line limit");
488
526
  if (end === text.length) return text;
489
- const fitted = text.slice(0, end);
490
- if (text[end] === " ") return fitted;
491
- const splitAt = fitted.lastIndexOf(" ");
492
- if (splitAt >= Math.floor(fitted.length / 2)) return fitted.slice(0, splitAt);
493
- return fitted;
527
+ const splitAt = text.lastIndexOf(" ", end);
528
+ const preferredEnd = splitAt >= Math.floor(end / 2) ? splitAt : end;
529
+ return text.slice(0, findGraphemeChunkEnd(text, 0, end, preferredEnd));
494
530
  }
495
531
  function toIrcError(err) {
496
532
  if (err instanceof Error) return err;
@@ -498,7 +534,7 @@ function toIrcError(err) {
498
534
  }
499
535
  let nickCollisionFallbackSeq = 0;
500
536
  function buildFallbackNick(nick) {
501
- const base = nick.replace(/\s+/g, "").replace(/[^A-Za-z0-9_\-[\]\\`^{}|]/g, "") || "openclaw";
537
+ const base = nick.replace(/[^A-Za-z0-9_\-[\]\\`^{}|]/g, "") || "openclaw";
502
538
  const seq = ++nickCollisionFallbackSeq;
503
539
  const suffix = seq === 1 ? "_" : `_${seq}`;
504
540
  const maxNickLen = 30;
@@ -586,18 +622,35 @@ async function connectIrcClient(options) {
586
622
  if (!target.startsWith("#") && !target.startsWith("&")) throw new Error(`IRC JOIN target must be a channel: ${channel}`);
587
623
  sendRaw(`JOIN ${target}`);
588
624
  };
589
- const sendPrivmsg = (target, text, replyTo) => {
625
+ const sendPrivmsg = async (target, text, replyTo) => {
626
+ const effect = captureEffectAuthority();
590
627
  const normalizedTarget = sanitizeIrcTarget(target);
591
628
  const cleaned = sanitizeIrcOutboundText(text);
592
629
  if (!cleaned) throw new Error("Message must be non-empty for IRC sends");
593
- const lineOverheadBytes = Buffer.byteLength(`PRIVMSG ${normalizedTarget} :\r\n`, "utf8");
630
+ const lineOverheadBytes = Buffer.byteLength(`:${currentNick}!@ `, "utf8") + IRC_MAX_RELAY_USER_BYTES + IRC_MAX_RELAY_HOST_BYTES + Buffer.byteLength(`PRIVMSG ${normalizedTarget} :\r\n`, "utf8");
594
631
  const maxChunkBytes = IRC_MAX_LINE_BYTES - lineOverheadBytes;
595
632
  let remaining = replyTo ? sanitizeIrcOutboundText(`${text}\n\n[reply:${replyTo}]`) : cleaned;
633
+ const chunks = [];
596
634
  while (remaining.length > 0) {
597
635
  const chunk = takeIrcPrivmsgChunk(remaining, messageChunkMaxChars, maxChunkBytes).trim();
598
- sendRaw(`PRIVMSG ${normalizedTarget} :${chunk}`);
636
+ chunks.push(chunk);
599
637
  remaining = remaining.slice(chunk.length).trimStart();
600
638
  }
639
+ let sent = false;
640
+ await effect.initiate(() => {
641
+ for (const chunk of chunks) {
642
+ options.abortSignal?.throwIfAborted();
643
+ if (!ready || closed) throw new Error("IRC connection closed before send");
644
+ sendRaw(`PRIVMSG ${normalizedTarget} :${chunk}`);
645
+ sent = true;
646
+ }
647
+ }).catch((error) => {
648
+ if (sent) throw createChannelPartialDeliveryError(error, {
649
+ messageIds: [],
650
+ visibleReplySent: true
651
+ });
652
+ throw error;
653
+ });
601
654
  };
602
655
  const quit = (reason) => {
603
656
  if (closed) return;
@@ -619,10 +672,22 @@ async function connectIrcClient(options) {
619
672
  socket.destroy();
620
673
  };
621
674
  let buffer = "";
675
+ let receiveOverflowed = false;
676
+ const abortOversizedInput = () => {
677
+ receiveOverflowed = true;
678
+ buffer = "";
679
+ fail(/* @__PURE__ */ new Error(`IRC server sent a line longer than ${IRC_MAX_INBOUND_LINE_LENGTH} characters`));
680
+ socket.destroy();
681
+ };
622
682
  socket.on("data", (chunk) => {
683
+ if (receiveOverflowed) return;
623
684
  buffer += chunk;
624
685
  let idx = buffer.indexOf("\n");
625
686
  while (idx !== -1) {
687
+ if (idx > IRC_MAX_INBOUND_LINE_LENGTH) {
688
+ abortOversizedInput();
689
+ return;
690
+ }
626
691
  const rawLine = buffer.slice(0, idx).replace(/\r$/, "");
627
692
  buffer = buffer.slice(idx + 1);
628
693
  idx = buffer.indexOf("\n");
@@ -687,10 +752,12 @@ async function connectIrcClient(options) {
687
752
  });
688
753
  }
689
754
  }
755
+ if (buffer.length > IRC_MAX_INBOUND_LINE_LENGTH) abortOversizedInput();
690
756
  });
691
757
  socket.once("connect", () => {
692
758
  try {
693
- if (options.password && options.password.trim()) sendRaw(`PASS ${options.password.trim()}`);
759
+ const password = options.password?.trim();
760
+ if (password) sendRaw(password.includes(" ") || password.startsWith(":") ? `PASS :${password}` : `PASS ${password}`);
694
761
  sendRaw(`NICK ${options.nick.trim()}`);
695
762
  sendRaw(`USER ${options.username.trim()} 0 * :${sanitizeIrcOutboundText(options.realname)}`);
696
763
  } catch (err) {
@@ -770,6 +837,7 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
770
837
  text: preparedText,
771
838
  replyTo: opts.replyTo
772
839
  }], onDeliveryResult) {
840
+ const effect = captureEffectAuthority();
773
841
  const cfg = requireRuntimeConfig(opts.cfg, "IRC send");
774
842
  const account = resolveIrcAccount({
775
843
  cfg,
@@ -796,19 +864,18 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
796
864
  const results = [];
797
865
  try {
798
866
  opts.abortSignal?.throwIfAborted();
799
- if (transient && (target.startsWith("#") || target.startsWith("&"))) client.join(target);
867
+ if (transient && (target.startsWith("#") || target.startsWith("&"))) await effect.initiate(() => {
868
+ opts.abortSignal?.throwIfAborted();
869
+ if (!client.isReady()) throw new Error("IRC connection closed before join");
870
+ client.join(target);
871
+ });
800
872
  for (const message of messages) {
801
873
  opts.abortSignal?.throwIfAborted();
802
874
  if (!client.isReady()) throw new Error("IRC connection closed before send");
803
875
  await opts.onPlatformSendDispatch?.();
804
876
  opts.abortSignal?.throwIfAborted();
805
877
  if (!client.isReady()) throw new Error("IRC connection closed before send");
806
- client.sendPrivmsg(target, message.text, message.replyTo);
807
- getOptionalIrcRuntime()?.channel.activity.record({
808
- channel: "irc",
809
- accountId: account.accountId,
810
- direction: "outbound"
811
- });
878
+ await client.sendPrivmsg(target, message.text, message.replyTo);
812
879
  const messageId = randomUUID();
813
880
  const result = {
814
881
  messageId,
@@ -824,9 +891,35 @@ async function sendIrcMessages(to, text, opts, planMessages = (preparedText) =>
824
891
  })
825
892
  };
826
893
  results.push(result);
894
+ getOptionalIrcRuntime()?.channel.activity.record({
895
+ channel: "irc",
896
+ accountId: account.accountId,
897
+ direction: "outbound"
898
+ });
827
899
  await onDeliveryResult?.(result);
828
900
  }
829
901
  return results;
902
+ } catch (error) {
903
+ if (results.length === 0) throw error;
904
+ const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : void 0;
905
+ const receipt = createMessageReceiptFromOutboundResults({
906
+ results: [
907
+ ...results.map((result) => ({ receipt: result.receipt })),
908
+ ...partial?.receipt ? [{ receipt: partial.receipt }] : [],
909
+ ...(partial?.messageIds ?? []).filter((messageId) => !partial?.receipt?.platformMessageIds.includes(messageId)).map((messageId) => ({
910
+ channel: "irc",
911
+ messageId,
912
+ conversationId: target
913
+ }))
914
+ ],
915
+ kind: "text"
916
+ });
917
+ throw createChannelPartialDeliveryError(error, {
918
+ ...partial,
919
+ messageIds: receipt.platformMessageIds,
920
+ receipt,
921
+ visibleReplySent: true
922
+ });
830
923
  } finally {
831
924
  transient?.quit("sent");
832
925
  }
@@ -972,8 +1065,8 @@ async function probeIrc(cfg, opts) {
972
1065
  //#endregion
973
1066
  //#region extensions/irc/src/setup-core.ts
974
1067
  const channel$1 = "irc";
975
- const setIrcTopLevelDmPolicy = createTopLevelChannelDmPolicySetter({ channel: channel$1 });
976
- const setIrcTopLevelAllowFrom = createTopLevelChannelAllowFromSetter({ channel: channel$1 });
1068
+ const setIrcDmPolicy = createTopLevelChannelDmPolicySetter({ channel: channel$1 });
1069
+ const setIrcAllowFrom = createTopLevelChannelAllowFromSetter({ channel: channel$1 });
977
1070
  const validateIrcRequiredSetupInput = createSetupInputPresenceValidator({ whenNotUseEnv: [{
978
1071
  someOf: ["host"],
979
1072
  message: "IRC requires host."
@@ -996,12 +1089,6 @@ function updateIrcAccountConfig(cfg, accountId, patch) {
996
1089
  ensureAccountEnabled: false
997
1090
  });
998
1091
  }
999
- function setIrcDmPolicy(cfg, dmPolicy) {
1000
- return setIrcTopLevelDmPolicy(cfg, dmPolicy);
1001
- }
1002
- function setIrcAllowFrom(cfg, allowFrom) {
1003
- return setIrcTopLevelAllowFrom(cfg, allowFrom);
1004
- }
1005
1092
  function setIrcNickServ(cfg, accountId, nickserv) {
1006
1093
  return updateIrcAccountConfig(cfg, accountId, { nickserv });
1007
1094
  }
@@ -1156,7 +1243,7 @@ function normalizeGroupEntry(raw) {
1156
1243
  return `#${normalized.replace(/^#+/, "")}`;
1157
1244
  }
1158
1245
  const promptIrcAllowFrom = createPromptParsedAllowFromForAccount({
1159
- defaultAccountId: (cfg) => resolveDefaultIrcAccountId(cfg),
1246
+ defaultAccountId: resolveDefaultIrcAccountId,
1160
1247
  noteTitle: t("wizard.irc.allowlistTitle"),
1161
1248
  noteLines: [
1162
1249
  t("wizard.irc.allowlistIntro"),
@@ -1222,11 +1309,7 @@ const ircDmPolicy = {
1222
1309
  allowFromKey: "channels.irc.allowFrom",
1223
1310
  getCurrent: (cfg) => cfg.channels?.irc?.dmPolicy ?? "pairing",
1224
1311
  setPolicy: (cfg, policy) => setIrcDmPolicy(cfg, policy),
1225
- promptAllowFrom: async ({ cfg, prompter, accountId }) => await promptIrcAllowFrom({
1226
- cfg,
1227
- prompter,
1228
- accountId
1229
- })
1312
+ promptAllowFrom: promptIrcAllowFrom
1230
1313
  };
1231
1314
  const ircSetupWizard = {
1232
1315
  channel,
@@ -1459,7 +1542,6 @@ const meta = {
1459
1542
  systemImage: "number",
1460
1543
  markdownCapable: true
1461
1544
  };
1462
- const loadIrcChannelRuntime = createLazyRuntimeModule(() => import("./channel-runtime-BvC0hwF1.mjs"));
1463
1545
  function normalizePairingTarget(raw) {
1464
1546
  const normalized = normalizeIrcAllowEntry(raw);
1465
1547
  if (!normalized) return "";
@@ -1697,7 +1779,7 @@ const ircPlugin = createChatChannelPlugin({
1697
1779
  notify: async ({ cfg, id, message, accountId }) => {
1698
1780
  const target = normalizePairingTarget(id);
1699
1781
  if (!target) throw new Error(`invalid IRC pairing id: ${id}`);
1700
- const { sendMessageIrc } = await loadIrcChannelRuntime();
1782
+ const { sendMessageIrc } = await import("./channel-runtime-FNBqToAo.mjs");
1701
1783
  await sendMessageIrc(target, message, {
1702
1784
  cfg,
1703
1785
  accountId
@@ -1,3 +1,3 @@
1
- import { t as ircPlugin } from "./channel-DP5YY5l3.mjs";
1
+ import { t as ircPlugin } from "./channel-BXrFfvr6.mjs";
2
2
  import "../channel-plugin-api.js";
3
3
  export { ircPlugin };
@@ -1,12 +1,14 @@
1
- import { a as resolveIrcGroupRequireMention, c as connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-DP5YY5l3.mjs";
1
+ import { a as resolveIrcGroupRequireMention, c as connectIrcClient, d as sanitizeIrcAssistantText, f as createIrcIngressSubject, h as normalizeIrcAllowEntry, i as resolveIrcGroupMatch, l as parseIrcLine, m as isChannelTarget, o as sendMessageIrc, p as ircIngressIdentity, s as buildIrcConnectOptions, u as parseIrcPrefix, y as resolveIrcAccount } from "./channel-BXrFfvr6.mjs";
2
2
  import { t as getIrcRuntime } from "./runtime-C_V9tmpk.mjs";
3
3
  import { channelIngressRoutes } from "openclaw/plugin-sdk/channel-ingress-runtime";
4
+ import { computeBackoff } from "openclaw/plugin-sdk/runtime-env";
4
5
  import { normalizeLowercaseStringOrEmpty, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
5
6
  import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
6
7
  import { randomUUID } from "node:crypto";
8
+ import { createChannelInboundEnvelopeBuilderAsync, logInboundDrop } from "openclaw/plugin-sdk/channel-inbound";
7
9
  import { resolveLoggerBackedRuntime } from "openclaw/plugin-sdk/extension-shared";
10
+ import { resolveAgentRoute } from "openclaw/plugin-sdk/routing";
8
11
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
9
- import { logInboundDrop, resolveChannelInboundRouteEnvelope } from "openclaw/plugin-sdk/channel-inbound";
10
12
  import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pairing";
11
13
  import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
12
14
  import { deliverFormattedTextWithAttachments } from "openclaw/plugin-sdk/reply-payload";
@@ -34,26 +36,6 @@ function buildIrcNickMentionPattern(value) {
34
36
  function hasEntries(entries) {
35
37
  return normalizeStringEntries(entries).some((entry) => normalizeIrcAllowEntry(entry));
36
38
  }
37
- function routeDescriptorsForIrcGroup(params) {
38
- if (!params.isGroup) return [];
39
- return channelIngressRoutes(params.groupPolicy === "allowlist" && {
40
- id: "irc:channel",
41
- allowed: params.hasConfiguredGroups && params.groupAllowed,
42
- precedence: 0,
43
- matchId: "irc-channel",
44
- blockReason: "channel_not_allowlisted"
45
- }, !params.groupEnabled && {
46
- id: "irc:channel-enabled",
47
- enabled: false,
48
- precedence: 10,
49
- blockReason: "channel_disabled"
50
- }, hasEntries(params.routeGroupAllowFrom) && {
51
- id: "irc:channel-sender",
52
- precedence: 20,
53
- senderPolicy: "replace",
54
- senderAllowFrom: params.routeGroupAllowFrom
55
- });
56
- }
57
39
  async function deliverIrcReply(params) {
58
40
  await deliverFormattedTextWithAttachments({
59
41
  payload: {
@@ -123,7 +105,7 @@ async function handleIrcInbound(params) {
123
105
  const accessGroupPolicy = groupPolicy === "open" && (hasEntries(account.config.groupAllowFrom) || hasEntries(routeGroupAllowFrom)) ? "allowlist" : groupPolicy;
124
106
  const channelTarget = message.target.startsWith("#") || message.target.startsWith("&") ? message.target : `#${message.target}`;
125
107
  const peerId = message.isGroup ? channelTarget : message.senderNick;
126
- const { route, buildEnvelope } = resolveChannelInboundRouteEnvelope({
108
+ const route = resolveAgentRoute({
127
109
  cfg: config,
128
110
  channel: CHANNEL_ID,
129
111
  accountId: account.accountId,
@@ -150,14 +132,23 @@ async function handleIrcInbound(params) {
150
132
  ...message.messageId ? { messageId: message.messageId } : {},
151
133
  inboundEventKind: "user_request"
152
134
  },
153
- route: routeDescriptorsForIrcGroup({
154
- isGroup: message.isGroup,
155
- groupPolicy,
156
- groupAllowed: groupMatch.allowed,
157
- hasConfiguredGroups: groupMatch.hasConfiguredGroups,
158
- groupEnabled: groupMatch.groupConfig?.enabled !== false && groupMatch.wildcardConfig?.enabled !== false,
159
- routeGroupAllowFrom
160
- }),
135
+ route: message.isGroup ? channelIngressRoutes(groupPolicy === "allowlist" && {
136
+ id: "irc:channel",
137
+ allowed: groupMatch.hasConfiguredGroups && groupMatch.allowed,
138
+ precedence: 0,
139
+ matchId: "irc-channel",
140
+ blockReason: "channel_not_allowlisted"
141
+ }, (groupMatch.groupConfig?.enabled === false || groupMatch.wildcardConfig?.enabled === false) && {
142
+ id: "irc:channel-enabled",
143
+ enabled: false,
144
+ precedence: 10,
145
+ blockReason: "channel_disabled"
146
+ }, hasEntries(routeGroupAllowFrom) && {
147
+ id: "irc:channel-sender",
148
+ precedence: 20,
149
+ senderPolicy: "replace",
150
+ senderAllowFrom: routeGroupAllowFrom
151
+ }) : [],
161
152
  mentionFacts: message.isGroup ? {
162
153
  canDetectMention: true,
163
154
  wasMentioned,
@@ -229,7 +220,10 @@ async function handleIrcInbound(params) {
229
220
  error: turnAdoptionLifecycle.abortSignal.reason
230
221
  };
231
222
  const fromLabel = message.isGroup ? message.target : senderDisplay;
232
- const body = buildEnvelope({
223
+ const body = (await createChannelInboundEnvelopeBuilderAsync({
224
+ cfg: config,
225
+ route
226
+ }))({
233
227
  channel: "IRC",
234
228
  from: fromLabel,
235
229
  timestamp: message.timestamp,
@@ -476,8 +470,31 @@ function createIrcIngressMonitor(options) {
476
470
  };
477
471
  }
478
472
  //#endregion
473
+ //#region extensions/irc/src/reconnect-backoff.ts
474
+ const IRC_RECONNECT_BACKOFF = {
475
+ initialMs: 1e3,
476
+ maxMs: 3e4,
477
+ factor: 2,
478
+ jitter: .2
479
+ };
480
+ const IRC_RECONNECT_STABLE_MS = 6e4;
481
+ function createIrcReconnectBackoff(now = Date.now) {
482
+ let attempt = 0;
483
+ let connectedAt;
484
+ return {
485
+ markConnected: () => {
486
+ connectedAt = now();
487
+ },
488
+ nextDelayMs: () => {
489
+ if (connectedAt !== void 0 && now() - connectedAt >= IRC_RECONNECT_STABLE_MS) attempt = 0;
490
+ connectedAt = void 0;
491
+ attempt += 1;
492
+ return computeBackoff(IRC_RECONNECT_BACKOFF, attempt);
493
+ }
494
+ };
495
+ }
496
+ //#endregion
479
497
  //#region extensions/irc/src/monitor.ts
480
- const IRC_MONITOR_RECONNECT_DELAY_MS = 1e3;
481
498
  async function monitorIrcProvider(opts) {
482
499
  const core = getIrcRuntime();
483
500
  const cfg = opts.config ?? core.config.current();
@@ -495,6 +512,7 @@ async function monitorIrcProvider(opts) {
495
512
  let activeConnectionEpoch = null;
496
513
  let ingressPause = Promise.resolve();
497
514
  let reconnectTimer = null;
515
+ const reconnectBackoff = createIrcReconnectBackoff();
498
516
  let stopped = false;
499
517
  const monitorAbort = new AbortController();
500
518
  const abortSignal = opts.abortSignal ? AbortSignal.any([opts.abortSignal, monitorAbort.signal]) : monitorAbort.signal;
@@ -528,7 +546,7 @@ async function monitorIrcProvider(opts) {
528
546
  const replyClient = client;
529
547
  if (!replyClient || !replyClient.isReady() || stopped || abortSignal.aborted) throw new Error("IRC transport disconnected before reply send.");
530
548
  if (!message.isGroup && context.connectionEpoch !== activeConnectionEpoch) throw new Error("IRC connection changed before private reply send.");
531
- replyClient.sendPrivmsg(target, text);
549
+ await replyClient.sendPrivmsg(target, text);
532
550
  opts.statusSink?.({ lastOutboundAt: Date.now() });
533
551
  core.channel.activity.record({
534
552
  channel: "irc",
@@ -543,6 +561,7 @@ async function monitorIrcProvider(opts) {
543
561
  function scheduleReconnect() {
544
562
  if (stopped || abortSignal.aborted || reconnectTimer) return;
545
563
  opts.statusSink?.({ lifecycle: "recovering" });
564
+ const delayMs = reconnectBackoff.nextDelayMs();
546
565
  reconnectTimer = setTimeout(() => {
547
566
  reconnectTimer = null;
548
567
  connect().catch((error) => {
@@ -551,7 +570,8 @@ async function monitorIrcProvider(opts) {
551
570
  logger.error(`[${account.accountId}] IRC reconnect failed: ${message}`);
552
571
  scheduleReconnect();
553
572
  });
554
- }, IRC_MONITOR_RECONNECT_DELAY_MS);
573
+ }, delayMs);
574
+ return delayMs;
555
575
  }
556
576
  async function connect() {
557
577
  if (stopped || abortSignal.aborted) return;
@@ -573,8 +593,8 @@ async function monitorIrcProvider(opts) {
573
593
  ingressPause = ingress.pause();
574
594
  if (activeConnectionEpoch === ingressConnection.connectionEpoch) activeConnectionEpoch = null;
575
595
  client = null;
576
- logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${IRC_MONITOR_RECONNECT_DELAY_MS}ms`);
577
- scheduleReconnect();
596
+ const delayMs = scheduleReconnect();
597
+ if (delayMs !== void 0) logger.warn?.(`[${account.accountId}] IRC connection closed; reconnecting in ${delayMs}ms`);
578
598
  },
579
599
  onPrivmsg: async (event) => {
580
600
  await ingressConnection.accept(event.rawLine, event.connectedNick);
@@ -599,6 +619,7 @@ async function monitorIrcProvider(opts) {
599
619
  return;
600
620
  }
601
621
  ingress.start();
622
+ reconnectBackoff.markConnected();
602
623
  opts.statusSink?.(channelReadyPatch());
603
624
  logger.info(`[${account.accountId}] connected to ${account.host}:${account.port}${account.tls ? " (tls)" : ""} as ${nextClient.nick}`);
604
625
  }
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-DP5YY5l3.mjs";
1
+ import { _ as listIrcAccountIds, g as listEnabledIrcAccounts, n as ircSetupWizard, r as ircSetupAdapter, t as ircPlugin, v as resolveDefaultIrcAccountId, y as resolveIrcAccount } from "./.setup/channel-BXrFfvr6.mjs";
2
2
  import { r as setIrcRuntime } from "./.setup/runtime-C_V9tmpk.mjs";
3
3
  export { ircPlugin, ircSetupAdapter, ircSetupWizard, listEnabledIrcAccounts, listIrcAccountIds, resolveDefaultIrcAccountId, resolveIrcAccount, setIrcRuntime };
@@ -1,2 +1,2 @@
1
- import { t as ircPlugin } from "./.setup/channel-DP5YY5l3.mjs";
1
+ import { t as ircPlugin } from "./.setup/channel-BXrFfvr6.mjs";
2
2
  export { ircPlugin };
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  var setup_entry_default = defineBundledChannelSetupEntry({
5
5
  importMetaUrl: import.meta.url,
6
6
  plugin: {
7
- specifier: fileURLToPath(new URL(".setup/channel-plugin-api-tjqFvfGT.mjs", import.meta.url).href),
7
+ specifier: fileURLToPath(new URL(".setup/channel-plugin-api-tb7ep4Ml.mjs", import.meta.url).href),
8
8
  exportName: "ircPlugin"
9
9
  },
10
10
  secrets: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/irc",
3
- "version": "2026.10.2",
3
+ "version": "2026.10.5-beta.2",
4
4
  "description": "OpenClaw IRC channel plugin",
5
5
  "type": "module",
6
6
  "openclaw": {
@@ -121,10 +121,10 @@
121
121
  }
122
122
  },
123
123
  "compat": {
124
- "pluginApi": ">=2026.10.2"
124
+ "pluginApi": ">=2026.10.5-beta.2"
125
125
  },
126
126
  "build": {
127
- "openclawVersion": "2026.10.2",
127
+ "openclawVersion": "2026.10.5-beta.2",
128
128
  "bundledDist": false
129
129
  },
130
130
  "release": {
@@ -151,7 +151,7 @@
151
151
  "assets/activity.svg"
152
152
  ],
153
153
  "peerDependencies": {
154
- "openclaw": ">=2026.10.2"
154
+ "openclaw": ">=2026.10.5-beta.2"
155
155
  },
156
156
  "peerDependenciesMeta": {
157
157
  "openclaw": {