@openclaw/signal 2026.7.2-beta.5 → 2026.8.1-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.
Files changed (29) hide show
  1. package/dist/api.js +7 -7
  2. package/dist/{approval-handler.runtime-C-XYgcxE.js → approval-handler.runtime-Bi5yNhnj.js} +2 -2
  3. package/dist/{approval-reactions-DtL47gSK.js → approval-reactions-BulCzhdB.js} +15 -45
  4. package/dist/{channel-CBIljGCH.js → channel-CzDJMDtX.js} +50 -91
  5. package/dist/channel-config-api.js +1 -1
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/{channel.runtime-B8GXmuol.js → channel.runtime-D12iYzJB.js} +2 -2
  8. package/dist/{client-BazXTH7O.js → client-CvnR8_XA.js} +31 -34
  9. package/dist/{client-adapter-mXGeEHQJ.js → client-adapter-D9SNPaNx.js} +13 -6
  10. package/dist/{config-schema-DLvpQPN5.js → config-schema-CkMIK7zp.js} +4 -7
  11. package/dist/contract-api.js +1 -1
  12. package/dist/{doctor-contract-api-DbUCknbC.js → doctor-contract-api-Dnp5eQQJ.js} +46 -54
  13. package/dist/doctor-contract-api.js +1 -1
  14. package/dist/{install-signal-cli-qJF7iC4h.js → install-signal-cli-Cpiyk0k1.js} +8 -5
  15. package/dist/{monitor-CHBkf7lk.js → monitor-DiN2hKnG.js} +177 -126
  16. package/dist/{probe-Br9abaGp.js → probe-C_THuQzn.js} +23 -7
  17. package/dist/question-reactions-ByjH5qST.js +80 -0
  18. package/dist/{reaction-runtime-api-BjeDyKcz.js → reaction-runtime-api-Cf6QHcE5.js} +8 -19
  19. package/dist/reaction-runtime-api.js +1 -1
  20. package/dist/runtime-api.js +7 -7
  21. package/dist/{send-B_Ks4MZr.js → send-BvGgByMd.js} +12 -3
  22. package/dist/{send.runtime-CbuXycZT.js → send.runtime-CtTBBrjL.js} +1 -1
  23. package/dist/{transport-detection-Da06-LOL.js → transport-detection-BItHawMq.js} +50 -145
  24. package/dist/{transport-detection.runtime-D_190UUL.js → transport-detection.runtime-DoiwPcrG.js} +1 -1
  25. package/dist/{transport-probes.runtime-CV6Qs0Og.js → transport-probes.runtime-BsJdp0oI.js} +1 -1
  26. package/openclaw.plugin.json +4 -1
  27. package/package.json +5 -5
  28. package/dist/approval-resolver-B3m0D2R0.js +0 -16
  29. package/dist/question-reactions-Ds48FtEu.js +0 -109
@@ -2,24 +2,24 @@ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { i as resolveSignalAccount, o as resolveSignalReplyToMode } from "./accounts-DrStJjiK.js";
3
3
  import { a as normalizeSignalAllowRecipient, c as resolveSignalSender, d as normalizeSignalMessagingTarget, i as isSignalSenderAllowed, l as looksLikeUuid, n as formatSignalSenderDisplay, o as resolveSignalPeerId, r as formatSignalSenderId, s as resolveSignalRecipient, t as formatSignalPairingIdLine } from "./identity-BQ4OzXXk.js";
4
4
  import { t as getOptionalSignalRuntime } from "./runtime-JPrGlV7h.js";
5
- import { i as materializeSignalPresentationFallback, n as registerSignalReplyContext, o as resolveSignalReactionLevel, s as formatSignalMediaText, u as isSignalNativeApprovalHandlerConfigured } from "./transport-detection-Da06-LOL.js";
6
- import { n as signalRpcRequest, r as streamSignalEvents, t as signalCheck } from "./client-adapter-mXGeEHQJ.js";
7
- import { n as sendReactionSignal } from "./reaction-runtime-api-BjeDyKcz.js";
8
- import { a as maybeResolveSignalApprovalReaction, l as resolveSignalApprovalConversationKey, s as registerSignalApprovalReactionTargetForDeliveredPayload, t as addSignalApprovalReactionHintToStructuredPayload } from "./approval-reactions-DtL47gSK.js";
9
- import { r as registerSignalQuestionReactionTargetForDeliveredPayload, t as maybeResolveSignalQuestionReaction } from "./question-reactions-Ds48FtEu.js";
10
- import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-B_Ks4MZr.js";
11
- import { bindIngressLifecycleToReplyOptions, createChannelIngressMonitor, createChannelMessageReplyPipeline, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
5
+ import { i as materializeSignalPresentationFallback, n as registerSignalReplyContext, o as resolveSignalReactionLevel, s as formatSignalMediaText, u as isSignalNativeApprovalHandlerConfigured } from "./transport-detection-BItHawMq.js";
6
+ import { n as signalRpcRequest, r as streamSignalEvents, t as signalCheck } from "./client-adapter-D9SNPaNx.js";
7
+ import { n as sendReactionSignal } from "./reaction-runtime-api-Cf6QHcE5.js";
8
+ import { a as maybeResolveSignalApprovalReaction, l as resolveSignalApprovalConversationKey, s as registerSignalApprovalReactionTargetForDeliveredPayload, t as addSignalApprovalReactionHintToStructuredPayload } from "./approval-reactions-BulCzhdB.js";
9
+ import { r as registerSignalQuestionReactionTargetForDeliveredPayload, t as maybeResolveSignalQuestionReaction } from "./question-reactions-ByjH5qST.js";
10
+ import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-BvGgByMd.js";
11
+ import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageReplyPipeline, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
12
12
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
13
- import { detectMime, estimateBase64DecodedBytes, kindFromMime, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
13
+ import { canonicalizeBase64, detectMime, estimateBase64DecodedBytes, kindFromMime, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
14
14
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
15
- import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
15
+ import { asPositiveSafeInteger, isRecord, normalizeNullableString, normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
16
16
  import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime";
17
17
  import { normalizeE164, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
18
- import { buildChannelInboundEventContext, buildMentionRegexes, createChannelInboundDebouncer, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundFromLabel, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, logInboundDrop, matchesMentionPatterns, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toHistoryMediaEntries, toInboundMediaFacts } from "openclaw/plugin-sdk/channel-inbound";
19
- import nodePath from "node:path";
18
+ import { buildChannelInboundEventContext, buildMentionRegexes, createChannelInboundDebouncer, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundFromLabel, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, logInboundDrop, matchesMentionPatterns, readAgentRunTerminalOutcome, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, resolveInboundSupplementalSenderAllowed, runChannelInboundEvent, shouldDebounceTextInbound, toHistoryMediaEntries, toInboundMediaFactsWithMetadata } from "openclaw/plugin-sdk/channel-inbound";
20
19
  import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
21
20
  import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
22
21
  import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
22
+ import path from "node:path";
23
23
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
24
24
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
25
25
  import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
@@ -44,6 +44,7 @@ import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-ru
44
44
  import { listChatCommands, maybeResolveTextAlias, normalizeCommandBody } from "openclaw/plugin-sdk/command-auth-native";
45
45
  import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
46
46
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
47
+ import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
47
48
  //#region extensions/signal/src/daemon.ts
48
49
  const SIGNAL_DAEMON_STOP_KILL_TIMEOUT_MS = 1500;
49
50
  function formatSignalDaemonExit(exit) {
@@ -71,7 +72,7 @@ function bindSignalCliOutput(params) {
71
72
  function resolveSignalCliConfigPath(raw) {
72
73
  const value = raw.trim();
73
74
  if (value === "~") return os.homedir();
74
- if (value.startsWith("~/") || value.startsWith("~\\")) return nodePath.join(os.homedir(), value.slice(2));
75
+ if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
75
76
  return value;
76
77
  }
77
78
  function buildDaemonArgs(opts) {
@@ -179,6 +180,37 @@ function spawnSignalDaemon(opts) {
179
180
  };
180
181
  }
181
182
  //#endregion
183
+ //#region extensions/signal/src/daemon-lifecycle.ts
184
+ function createSignalDaemonLifecycle(params) {
185
+ let daemonHandle = null;
186
+ let daemonStopRequested = false;
187
+ let daemonStopPromise;
188
+ let daemonExitError;
189
+ const daemonAbortController = new AbortController();
190
+ const abortSignal = params.abortSignal ? AbortSignal.any([params.abortSignal, daemonAbortController.signal]) : daemonAbortController.signal;
191
+ const stop = () => {
192
+ if (daemonStopPromise) return daemonStopPromise;
193
+ daemonStopRequested = true;
194
+ if (!daemonAbortController.signal.aborted) daemonAbortController.abort(params.abortSignal?.reason ?? /* @__PURE__ */ new Error("Signal monitor stopped"));
195
+ daemonStopPromise = daemonHandle?.stop() ?? Promise.resolve();
196
+ return daemonStopPromise;
197
+ };
198
+ const attach = (handle) => {
199
+ daemonHandle = handle;
200
+ handle.exited.then((exit) => {
201
+ if (daemonStopRequested || params.abortSignal?.aborted) return;
202
+ daemonExitError = new Error(formatSignalDaemonExit(exit));
203
+ if (!daemonAbortController.signal.aborted) daemonAbortController.abort(daemonExitError);
204
+ });
205
+ };
206
+ return {
207
+ attach,
208
+ stop,
209
+ getExitError: () => daemonExitError,
210
+ abortSignal
211
+ };
212
+ }
213
+ //#endregion
182
214
  //#region extensions/signal/src/monitor/access-policy.ts
183
215
  const SIGNAL_UUID_KIND = "plugin:signal-uuid";
184
216
  const SIGNAL_GROUP_KIND = "plugin:signal-group";
@@ -273,6 +305,7 @@ async function resolveSignalAccessState(params) {
273
305
  kind: isGroup ? "group" : "direct",
274
306
  id: isGroup ? params.groupId ?? "unknown" : params.sender.raw
275
307
  },
308
+ contextBinding: params.contextBinding,
276
309
  ...isGroup ? { event: { mayPair: false } } : {},
277
310
  dmPolicy: params.dmPolicy,
278
311
  groupPolicy: params.groupPolicy,
@@ -387,17 +420,9 @@ function createSignalPendingInboundRegistry(accountId) {
387
420
  if (!conversationKey) return;
388
421
  for (const inboundKey of countsByConversation.get(conversationKey)?.keys() ?? []) cancelKey(inboundKey);
389
422
  };
390
- const completeAfter = (flush) => async (entries) => {
391
- try {
392
- await flush(entries);
393
- } finally {
394
- complete(entries);
395
- }
396
- };
397
423
  return {
398
424
  track,
399
425
  complete,
400
- completeAfter,
401
426
  cancelPendingOnAbort
402
427
  };
403
428
  }
@@ -612,8 +637,8 @@ function createSignalEventHandler(deps) {
612
637
  chatType: entry.isGroup ? "group" : "direct"
613
638
  });
614
639
  const replyThreading = resolveBatchedReplyThreadingPolicy(replyToMode, entry.isBatched === true);
615
- const media = toInboundMediaFacts(entry.media);
616
- const ctxPayload = buildChannelInboundEventContext({
640
+ const media = await toInboundMediaFactsWithMetadata(entry.media);
641
+ const ctxPayload = (deps.channelRuntime?.inbound.buildContext ?? buildChannelInboundEventContext)({
617
642
  channel: "signal",
618
643
  supplemental: { quote: entry.replyToBody ? {
619
644
  body: entry.replyToBody,
@@ -657,6 +682,7 @@ function createSignalEventHandler(deps) {
657
682
  } } : {},
658
683
  commands: { authorized: entry.commandAuthorized }
659
684
  },
685
+ channelIngress: entry.channelIngress,
660
686
  media,
661
687
  extra: {
662
688
  GroupSubject: entry.isGroup ? entry.groupName ?? void 0 : void 0,
@@ -664,7 +690,7 @@ function createSignalEventHandler(deps) {
664
690
  }
665
691
  });
666
692
  if (shouldLogVerbose()) {
667
- const preview = truncateUtf16Safe(body, 200).replace(/\\n/g, "\\\\n");
693
+ const preview = truncateUtf16Safe(body, 200).replace(/\r/g, "\\r").replace(/\n/g, "\\n");
668
694
  logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
669
695
  }
670
696
  const statusReactionTimestamp = resolveSignalStatusReactionTimestamp(entry);
@@ -682,7 +708,6 @@ function createSignalEventHandler(deps) {
682
708
  isDirect: !entry.isGroup,
683
709
  isGroup: entry.isGroup,
684
710
  isMentionableGroup: entry.isGroup,
685
- requireMention: entry.requireMention === true,
686
711
  canDetectMention: entry.canDetectMention === true,
687
712
  effectiveWasMentioned: entry.wasMentioned === true
688
713
  }));
@@ -846,13 +871,16 @@ function createSignalEventHandler(deps) {
846
871
  onToolStart: async (payload) => {
847
872
  const toolName = payload.name?.trim();
848
873
  if (toolName) await statusReactionController.setTool(toolName);
874
+ return false;
849
875
  },
850
876
  onCompactionStart: async () => {
851
877
  await statusReactionController.setCompacting();
878
+ return false;
852
879
  },
853
880
  onCompactionEnd: async () => {
854
881
  statusReactionController.cancelPending();
855
882
  await statusReactionController.setThinking();
883
+ return false;
856
884
  }
857
885
  } : {},
858
886
  onModelSelected
@@ -862,9 +890,10 @@ function createSignalEventHandler(deps) {
862
890
  if (!statusReactionController) return;
863
891
  const hasFinalResponse = result.dispatched && hasVisibleInboundReplyDispatch(result.dispatchResult);
864
892
  const hasDeliveryFailure = result.dispatched && hasSignalStatusReplyDeliveryFailure(result.dispatchResult);
893
+ const hasAgentRunFailure = result.dispatched && readAgentRunTerminalOutcome(result.dispatchResult) === "failed";
865
894
  finalizeSignalStatusReaction({
866
895
  controller: statusReactionController,
867
- outcome: hasFinalResponse && !hasDeliveryFailure ? "done" : "error"
896
+ outcome: hasFinalResponse && !hasDeliveryFailure && !hasAgentRunFailure ? "done" : "error"
868
897
  }).catch((err) => {
869
898
  logVerbose(`signal: status reaction finalize failed: ${String(err)}`);
870
899
  });
@@ -872,15 +901,21 @@ function createSignalEventHandler(deps) {
872
901
  }
873
902
  });
874
903
  }
875
- async function flushSignalInboundEntries(entries) {
904
+ async function flushSignalInboundEntries(entries, lifecycle, settle) {
876
905
  const last = entries.at(-1);
877
906
  if (!last) return;
878
- const { lifecycle, settle } = fanInChannelIngressLifecycles(entries.map((entry) => entry.turnAdoptionLifecycle));
907
+ const channelIngress = await resolveSignalBatchChannelIngress(entries, last);
908
+ if (channelIngress.some((entry) => !entry.senderAccess.allowed)) {
909
+ logVerbose("signal: authorization changed before dispatch");
910
+ await settle();
911
+ return;
912
+ }
879
913
  if (entries.length === 1) {
880
- await handleSignalInboundMessage(lifecycle ? {
914
+ await handleSignalInboundMessage({
881
915
  ...last,
916
+ channelIngress,
882
917
  turnAdoptionLifecycle: lifecycle
883
- } : last);
918
+ });
884
919
  await settle();
885
920
  return;
886
921
  }
@@ -894,14 +929,34 @@ function createSignalEventHandler(deps) {
894
929
  ...last,
895
930
  bodyText: combinedText,
896
931
  commandBody: combinedCommandBody,
897
- ...lifecycle ? { turnAdoptionLifecycle: lifecycle } : {},
932
+ turnAdoptionLifecycle: lifecycle,
898
933
  isBatched: true,
899
934
  nativeReplyBody: last.nativeReplyBody ?? last.bodyText,
900
- media: entries.flatMap((entry) => entry.media ?? [])
935
+ media: entries.flatMap((entry) => entry.media ?? []),
936
+ channelIngress
901
937
  });
902
938
  await settle();
903
939
  }
904
- async function retrySignalInboundFlush(entries, initialError) {
940
+ async function resolveSignalBatchChannelIngress(entries, last) {
941
+ if (last.boundChannelIngress) return last.boundChannelIngress;
942
+ const route = resolveSignalInboundRoute({
943
+ cfg: deps.cfg,
944
+ accountId: deps.accountId,
945
+ isGroup: last.isGroup,
946
+ groupId: last.groupId,
947
+ senderPeerId: last.senderPeerId
948
+ });
949
+ const contextBinding = {
950
+ agentId: route.agentId,
951
+ sessionKey: route.sessionKey,
952
+ ...last.messageId ? { messageId: last.messageId } : {},
953
+ inboundEventKind: "user_request"
954
+ };
955
+ const resolved = await Promise.all(entries.flatMap((entry) => entry.resolveChannelIngress ? [entry.resolveChannelIngress(contextBinding)] : (entry.channelIngress ?? []).map(async (ingress) => ingress)));
956
+ last.boundChannelIngress = resolved;
957
+ return resolved;
958
+ }
959
+ async function retrySignalInboundFlush(entries, lifecycle, settle, initialError) {
905
960
  let lastError = initialError;
906
961
  for (const [attemptIndex, delayMs] of RETRYABLE_FLUSH_RETRY_DELAYS_MS.entries()) {
907
962
  const attempt = attemptIndex + 1;
@@ -917,7 +972,7 @@ function createSignalEventHandler(deps) {
917
972
  }
918
973
  if (deps.abortSignal?.aborted) return;
919
974
  try {
920
- await flushSignalInboundEntries(entries);
975
+ await flushSignalInboundEntries(entries, lifecycle, settle);
921
976
  return;
922
977
  } catch (err) {
923
978
  if (deps.abortSignal?.aborted) return;
@@ -927,26 +982,41 @@ function createSignalEventHandler(deps) {
927
982
  }
928
983
  throw lastError;
929
984
  }
930
- const flushDebouncedSignalInboundEntries = async (entries) => {
931
- if (!entries.some((entry) => activeEnqueueEntries.has(entry)) && deps.abortSignal?.aborted) return;
932
- try {
933
- await flushSignalInboundEntries(entries);
934
- } catch (err) {
935
- if (!isSignalReplySessionInitConflictError(err)) throw err;
936
- if (deps.abortSignal?.aborted) return;
937
- const retryTask = retrySignalInboundFlush(entries, err).catch(async (terminalError) => {
938
- await Promise.all(entries.map((entry) => Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned())));
939
- throw terminalError;
940
- });
941
- deps.runTrackedTask?.(() => retryTask.catch(() => void 0));
942
- await retryTask;
943
- }
985
+ const flushDebouncedSignalInboundEntries = (entries, createFlush) => {
986
+ const { lifecycle, settle } = fanInChannelIngressLifecycles(entries.map((entry) => entry.turnAdoptionLifecycle));
987
+ return createFlush({
988
+ lifecycle,
989
+ dispatch: async (admissionLifecycle) => {
990
+ if (!entries.some((entry) => activeEnqueueEntries.has(entry)) && deps.abortSignal?.aborted) return;
991
+ try {
992
+ await flushSignalInboundEntries(entries, admissionLifecycle, settle);
993
+ } catch (err) {
994
+ if (!isSignalReplySessionInitConflictError(err)) throw err;
995
+ if (deps.abortSignal?.aborted) return;
996
+ await retrySignalInboundFlush(entries, admissionLifecycle, settle, err).catch(async (terminalError) => {
997
+ await Promise.all(entries.map((entry) => Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned())));
998
+ throw terminalError;
999
+ });
1000
+ }
1001
+ }
1002
+ });
944
1003
  };
945
1004
  const reportSignalInboundFlushError = (err) => {
946
1005
  deps.runtime.error?.(`signal debounce flush failed: ${String(err)}`);
947
1006
  };
948
1007
  const pendingInboundRegistry = createSignalPendingInboundRegistry(deps.accountId);
949
- const flushNormalSignalInboundEntries = pendingInboundRegistry.completeAfter(flushDebouncedSignalInboundEntries);
1008
+ const trackSignalInboundFlush = (flush) => {
1009
+ deps.runTrackedTask?.(() => flush.completion.catch(() => void 0));
1010
+ return flush;
1011
+ };
1012
+ const flushNormalSignalInboundEntries = (entries, createFlush) => {
1013
+ const flush = flushDebouncedSignalInboundEntries(entries, createFlush);
1014
+ const completion = flush.completion.finally(() => pendingInboundRegistry.complete(entries));
1015
+ return trackSignalInboundFlush({
1016
+ admission: flush.admission,
1017
+ completion
1018
+ });
1019
+ };
950
1020
  const { debouncer } = createChannelInboundDebouncer({
951
1021
  cfg: deps.cfg,
952
1022
  channel: "signal",
@@ -966,7 +1036,7 @@ function createSignalEventHandler(deps) {
966
1036
  serializeImmediate: true,
967
1037
  buildKey: (entry) => resolveSignalControlLaneKey(deps.accountId, entry),
968
1038
  shouldDebounce: () => false,
969
- onFlush: flushDebouncedSignalInboundEntries,
1039
+ onFlush: (entries, createFlush) => trackSignalInboundFlush(flushDebouncedSignalInboundEntries(entries, createFlush)),
970
1040
  onError: reportSignalInboundFlushError
971
1041
  });
972
1042
  async function handleReactionOnlyInbound(params) {
@@ -1011,6 +1081,7 @@ function createSignalEventHandler(deps) {
1011
1081
  if (!deps.shouldEmitSignalReactionNotification({
1012
1082
  mode: deps.reactionMode,
1013
1083
  account: deps.account,
1084
+ accountUuid: deps.accountUuid,
1014
1085
  targets,
1015
1086
  sender: params.sender,
1016
1087
  allowlist: deps.reactionAllowlist
@@ -1071,7 +1142,7 @@ function createSignalEventHandler(deps) {
1071
1142
  const isGroup = Boolean(groupId);
1072
1143
  const hasControlCommandInMessage = isControlCommandMessage(messageText, deps.cfg);
1073
1144
  const senderDisplay = formatSignalSenderDisplay(sender);
1074
- const { senderAccess, commandAccess } = await resolveSignalAccessState({
1145
+ const resolveChannelIngress = async (contextBinding) => await resolveSignalAccessState({
1075
1146
  accountId: deps.accountId,
1076
1147
  dmPolicy: deps.dmPolicy,
1077
1148
  groupPolicy: deps.groupPolicy,
@@ -1081,8 +1152,10 @@ function createSignalEventHandler(deps) {
1081
1152
  groupId,
1082
1153
  isGroup,
1083
1154
  cfg: deps.cfg,
1084
- hasControlCommand: hasControlCommandInMessage
1155
+ hasControlCommand: hasControlCommandInMessage,
1156
+ contextBinding
1085
1157
  });
1158
+ const { senderAccess, commandAccess } = await resolveChannelIngress();
1086
1159
  const quoteText = normalizeOptionalString(dataMessage?.quote?.text) ?? "";
1087
1160
  const { contextVisibilityMode, quoteSenderAllowed, visibleQuoteText, visibleQuoteSender } = resolveSignalQuoteContext({
1088
1161
  cfg: deps.cfg,
@@ -1212,8 +1285,8 @@ function createSignalEventHandler(deps) {
1212
1285
  sender: envelope.sourceName ?? senderDisplay,
1213
1286
  body: messageText || visibleQuoteText,
1214
1287
  media: toHistoryMediaEntries(pendingMedia),
1215
- timestamp: envelope.timestamp ?? void 0,
1216
- messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0
1288
+ timestamp: inboundTimestamp,
1289
+ messageId
1217
1290
  }
1218
1291
  });
1219
1292
  await registerSignalReplyContext({
@@ -1343,7 +1416,8 @@ function createSignalEventHandler(deps) {
1343
1416
  replyToBody: visibleQuoteText || void 0,
1344
1417
  replyToSender: visibleQuoteSender,
1345
1418
  replyToIsQuote: visibleQuoteText ? true : void 0,
1346
- turnAdoptionLifecycle
1419
+ turnAdoptionLifecycle,
1420
+ resolveChannelIngress
1347
1421
  };
1348
1422
  pendingInboundRegistry.cancelPendingOnAbort(entry, debouncer.cancelKey);
1349
1423
  const inboundLane = resolveSignalControlLaneKey(deps.accountId, entry) ? controlDebouncer : debouncer;
@@ -1365,26 +1439,10 @@ function registerSignalReactionTargetsForDeliveredPayload(params) {
1365
1439
  }
1366
1440
  //#endregion
1367
1441
  //#region extensions/signal/src/signal-ingress.ts
1368
- const SIGNAL_INGRESS_COMPLETED_TTL_MS = 720 * 60 * 60 * 1e3;
1369
- const SIGNAL_INGRESS_COMPLETED_MAX_ENTRIES = 1e3;
1370
- const SIGNAL_INGRESS_FAILED_TTL_MS = 720 * 60 * 60 * 1e3;
1371
- const SIGNAL_INGRESS_FAILED_MAX_ENTRIES = 1e3;
1372
1442
  const SIGNAL_INGRESS_DRAIN_INTERVAL_MS = 1e3;
1373
- var SignalIngressPermanentError = class extends Error {
1374
- constructor(reason, message, options) {
1375
- super(message, options);
1376
- this.reason = reason;
1377
- this.name = "SignalIngressPermanentError";
1378
- }
1379
- };
1380
- function isRecord$1(value) {
1381
- return typeof value === "object" && value !== null && !Array.isArray(value);
1382
- }
1383
- function normalizeRawString(value) {
1384
- return typeof value === "string" && value.trim() ? value.trim() : null;
1385
- }
1443
+ const SignalIngressPermanentError = createChannelIngressError("SignalIngressPermanentError", { withReason: true });
1386
1444
  function normalizeTimestamp(value) {
1387
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1445
+ return asPositiveSafeInteger(value) ?? null;
1388
1446
  }
1389
1447
  function parseReceiveEnvelope(event) {
1390
1448
  if (event.event !== "receive" || !event.data) return null;
@@ -1394,31 +1452,32 @@ function parseReceiveEnvelope(event) {
1394
1452
  } catch (error) {
1395
1453
  throw new SignalIngressPermanentError("parse-error", "Signal receive event contains invalid JSON", { cause: error });
1396
1454
  }
1397
- if (!isRecord$1(parsed)) throw new SignalIngressPermanentError("parse-error", "Signal receive event must contain a JSON object");
1398
- return isRecord$1(parsed.envelope) ? parsed.envelope : null;
1455
+ if (!isRecord(parsed)) throw new SignalIngressPermanentError("parse-error", "Signal receive event must contain a JSON object");
1456
+ return isRecord(parsed.envelope) ? parsed.envelope : null;
1399
1457
  }
1400
1458
  function resolveDataMessage(envelope) {
1401
- if (isRecord$1(envelope.dataMessage)) return envelope.dataMessage;
1402
- return isRecord$1(envelope.editMessage?.dataMessage) ? envelope.editMessage.dataMessage : null;
1459
+ if (isRecord(envelope.dataMessage)) return envelope.dataMessage;
1460
+ return isRecord(envelope.editMessage?.dataMessage) ? envelope.editMessage.dataMessage : null;
1403
1461
  }
1404
1462
  function inspectSignalIngressEvent(event) {
1405
1463
  const envelope = parseReceiveEnvelope(event);
1406
1464
  if (!envelope || "syncMessage" in envelope) return null;
1407
1465
  const dataMessage = resolveDataMessage(envelope);
1408
- const reactionMessage = isRecord$1(envelope.reactionMessage) ? envelope.reactionMessage : null;
1466
+ const reactionMessage = isRecord(envelope.reactionMessage) ? envelope.reactionMessage : null;
1409
1467
  if (!dataMessage && !reactionMessage) return null;
1410
- const senderUuid = normalizeRawString(envelope.sourceUuid);
1411
- const senderNumber = normalizeRawString(envelope.sourceNumber);
1468
+ const senderUuid = normalizeNullableString(envelope.sourceUuid);
1469
+ const senderNumber = normalizeNullableString(envelope.sourceNumber);
1412
1470
  const senderKey = senderUuid ? `uuid:${senderUuid}` : senderNumber ? `number:${senderNumber}` : null;
1413
1471
  if (!senderKey) throw new SignalIngressPermanentError("missing-sender", "Signal dispatchable envelope is missing sourceUuid/sourceNumber");
1414
1472
  const timestamp = normalizeTimestamp(envelope.timestamp) ?? normalizeTimestamp(dataMessage?.timestamp);
1415
1473
  if (timestamp === null) throw new SignalIngressPermanentError("missing-timestamp", "Signal dispatchable envelope is missing a stable timestamp");
1416
- const dataGroup = isRecord$1(dataMessage?.groupInfo) ? dataMessage.groupInfo : null;
1417
- const reactionGroup = isRecord$1(reactionMessage?.groupInfo) ? reactionMessage.groupInfo : null;
1418
- const groupId = normalizeRawString(dataGroup?.groupId) ?? normalizeRawString(reactionGroup?.groupId);
1474
+ const dataGroup = isRecord(dataMessage?.groupInfo) ? dataMessage.groupInfo : null;
1475
+ const reactionGroup = isRecord(reactionMessage?.groupInfo) ? reactionMessage.groupInfo : null;
1476
+ const groupId = normalizeNullableString(dataGroup?.groupId) ?? normalizeNullableString(reactionGroup?.groupId);
1419
1477
  return {
1420
1478
  eventId: JSON.stringify([senderKey, timestamp]),
1421
- laneKey: groupId ? `group:${groupId}` : `direct:${senderKey}`
1479
+ laneKey: groupId ? `group:${groupId}` : `direct:${senderKey}`,
1480
+ ...senderUuid && senderNumber ? { numberAliasEventId: JSON.stringify([`number:${senderNumber}`, timestamp]) } : {}
1422
1481
  };
1423
1482
  }
1424
1483
  function resolveSignalIngressNonRetryableFailure(error) {
@@ -1435,8 +1494,9 @@ async function startSignalIngressMonitor(params) {
1435
1494
  if (!pluginRuntime) throw new Error("Signal runtime not initialized for durable ingress");
1436
1495
  queue = pluginRuntime.state.openChannelIngressQueue({ accountId: params.accountId });
1437
1496
  }
1497
+ const ingressQueue = queue;
1438
1498
  const monitor = createChannelIngressMonitor({
1439
- queue,
1499
+ queue: ingressQueue,
1440
1500
  inspect: (event) => inspectSignalIngressEvent(event),
1441
1501
  payload: {
1442
1502
  version: 1,
@@ -1456,13 +1516,16 @@ async function startSignalIngressMonitor(params) {
1456
1516
  createClaimError: (_kind, claim) => new SignalIngressPermanentError("unsupported-event", `Signal ingress row ${claim.id} has an invalid payload`)
1457
1517
  },
1458
1518
  deliver: (event, lifecycle) => params.dispatch(event, lifecycle),
1519
+ onDurableAdmission: async (_event, { facts }) => {
1520
+ const { numberAliasEventId } = facts;
1521
+ if (!numberAliasEventId) return;
1522
+ if (!await ingressQueue.complete(numberAliasEventId)) await ingressQueue.complete(facts.eventId);
1523
+ },
1459
1524
  pollIntervalMs: SIGNAL_INGRESS_DRAIN_INTERVAL_MS,
1460
1525
  retention: {
1461
1526
  pruneIntervalMs: 0,
1462
- completedTtlMs: SIGNAL_INGRESS_COMPLETED_TTL_MS,
1463
- completedMaxEntries: SIGNAL_INGRESS_COMPLETED_MAX_ENTRIES,
1464
- failedTtlMs: SIGNAL_INGRESS_FAILED_TTL_MS,
1465
- failedMaxEntries: SIGNAL_INGRESS_FAILED_MAX_ENTRIES
1527
+ completedMaxEntries: 2e3,
1528
+ failedMaxEntries: 1e3
1466
1529
  },
1467
1530
  appendRetryDelaysMs: [0],
1468
1531
  drain: {
@@ -1489,7 +1552,14 @@ const DEFAULT_RECONNECT_POLICY = {
1489
1552
  factor: 2,
1490
1553
  jitter: .2
1491
1554
  };
1492
- async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEvent, timeoutMs, transportKind, policy }) {
1555
+ function publishSignalRecovering(statusSink, lastError) {
1556
+ statusSink?.({
1557
+ connected: false,
1558
+ lifecycle: "recovering",
1559
+ ...lastError ? { lastError } : {}
1560
+ });
1561
+ }
1562
+ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEvent, timeoutMs, transportKind, policy, statusSink }) {
1493
1563
  const reconnectPolicy = {
1494
1564
  ...DEFAULT_RECONNECT_POLICY,
1495
1565
  ...policy
@@ -1508,6 +1578,9 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
1508
1578
  abortSignal,
1509
1579
  timeoutMs,
1510
1580
  transportKind,
1581
+ onStreamOpen: () => {
1582
+ statusSink?.(channelReadyPatch());
1583
+ },
1511
1584
  onEvent: async (event) => {
1512
1585
  reconnectAttempts = 0;
1513
1586
  await onEvent(event);
@@ -1518,6 +1591,7 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
1518
1591
  }
1519
1592
  });
1520
1593
  if (abortSignal?.aborted) return;
1594
+ publishSignalRecovering(statusSink);
1521
1595
  reconnectAttempts += 1;
1522
1596
  const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
1523
1597
  logReconnectVerbose(`Signal stream ended, reconnecting in ${delayMs / 1e3}s...`);
@@ -1525,6 +1599,7 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
1525
1599
  } catch (err) {
1526
1600
  if (abortSignal?.aborted) return;
1527
1601
  runtime.error?.(`Signal stream error: ${String(err)}`);
1602
+ publishSignalRecovering(statusSink, String(err));
1528
1603
  reconnectAttempts += 1;
1529
1604
  const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
1530
1605
  runtime.log?.(`Signal connection lost, reconnecting in ${delayMs / 1e3}s...`);
@@ -1558,36 +1633,6 @@ function createSignalMonitorTaskRunner(runtime) {
1558
1633
  }
1559
1634
  };
1560
1635
  }
1561
- function createSignalDaemonLifecycle(params) {
1562
- let daemonHandle = null;
1563
- let daemonStopRequested = false;
1564
- let daemonStopPromise;
1565
- let daemonExitError;
1566
- const daemonAbortController = new AbortController();
1567
- const abortSignal = params.abortSignal ? AbortSignal.any([params.abortSignal, daemonAbortController.signal]) : daemonAbortController.signal;
1568
- const stop = () => {
1569
- if (daemonStopPromise) return daemonStopPromise;
1570
- daemonStopRequested = true;
1571
- if (!daemonAbortController.signal.aborted) daemonAbortController.abort(params.abortSignal?.reason ?? /* @__PURE__ */ new Error("Signal monitor stopped"));
1572
- daemonStopPromise = daemonHandle?.stop() ?? Promise.resolve();
1573
- return daemonStopPromise;
1574
- };
1575
- const attach = (handle) => {
1576
- daemonHandle = handle;
1577
- handle.exited.then((exit) => {
1578
- if (daemonStopRequested || params.abortSignal?.aborted) return;
1579
- daemonExitError = new Error(formatSignalDaemonExit(exit));
1580
- if (!daemonAbortController.signal.aborted) daemonAbortController.abort(daemonExitError);
1581
- });
1582
- };
1583
- const getExitError = () => daemonExitError;
1584
- return {
1585
- attach,
1586
- stop,
1587
- getExitError,
1588
- abortSignal
1589
- };
1590
- }
1591
1636
  function resolveSignalReactionTargets(reaction) {
1592
1637
  const targets = [];
1593
1638
  const uuid = reaction.targetAuthorUuid?.trim();
@@ -1615,16 +1660,17 @@ function isSignalReactionMessage(reaction) {
1615
1660
  return Boolean(emoji && typeof timestamp === "number" && timestamp > 0 && hasTarget);
1616
1661
  }
1617
1662
  function shouldEmitSignalReactionNotification(params) {
1618
- const { mode, account, targets, sender, allowlist } = params;
1663
+ const { mode, account, accountUuid, targets, sender, allowlist } = params;
1619
1664
  const effectiveMode = mode ?? "own";
1620
1665
  if (effectiveMode === "off") return false;
1621
1666
  if (effectiveMode === "own") {
1622
- const accountId = account?.trim();
1623
- if (!accountId || !targets || targets.length === 0) return false;
1624
- const normalizedAccount = normalizeE164(accountId);
1667
+ const accountId = normalizeOptionalString(account);
1668
+ const normalizedAccountUuid = normalizeOptionalString(accountUuid);
1669
+ if (!accountId && !normalizedAccountUuid || !targets || targets.length === 0) return false;
1670
+ const normalizedAccount = accountId ? normalizeE164(accountId) : void 0;
1625
1671
  return targets.some((target) => {
1626
- if (target.kind === "uuid") return accountId === target.id || accountId === `uuid:${target.id}`;
1627
- return normalizedAccount === target.id;
1672
+ if (target.kind === "uuid") return [accountId, normalizedAccountUuid].some((candidate) => candidate === target.id || candidate === `uuid:${target.id}`);
1673
+ return Boolean(normalizedAccount) && normalizedAccount === target.id;
1628
1674
  });
1629
1675
  }
1630
1676
  if (effectiveMode === "allowlist") {
@@ -1680,7 +1726,9 @@ async function fetchAttachment(params) {
1680
1726
  });
1681
1727
  if (!result?.data) return null;
1682
1728
  if (estimateBase64DecodedBytes(result.data) > params.maxBytes) throw new Error(`Signal attachment ${attachment.id} exceeds ${(params.maxBytes / (1024 * 1024)).toFixed(0)}MB limit`);
1683
- const buffer = Buffer.from(result.data, "base64");
1729
+ const canonicalData = canonicalizeBase64(result.data);
1730
+ if (!canonicalData) throw new Error(`Signal attachment ${attachment.id} returned malformed base64 data`);
1731
+ const buffer = Buffer.from(canonicalData, "base64");
1684
1732
  const originalFilename = normalizeOptionalString(attachment.filename ?? void 0);
1685
1733
  const saved = await saveMediaBuffer(buffer, normalizeOptionalString(attachment.contentType ?? void 0) ?? await detectMime({
1686
1734
  buffer,
@@ -1903,6 +1951,7 @@ async function monitorSignalProvider(opts = {}) {
1903
1951
  });
1904
1952
  const handleEvent = createSignalEventHandler({
1905
1953
  runtime,
1954
+ channelRuntime: opts.channelRuntime,
1906
1955
  abortSignal: daemonLifecycle.abortSignal,
1907
1956
  runTrackedTask: (task) => {
1908
1957
  monitorTaskRunner.runTask(task);
@@ -1953,6 +2002,7 @@ async function monitorSignalProvider(opts = {}) {
1953
2002
  timeoutMs: 0,
1954
2003
  transportKind,
1955
2004
  policy: opts.reconnectPolicy,
2005
+ statusSink: opts.statusSink,
1956
2006
  onEvent: (event) => monitorTaskRunner.runTask(async () => await ingressMonitor?.receive(event))
1957
2007
  });
1958
2008
  const daemonExitError = daemonLifecycle.getExitError();
@@ -1960,6 +2010,7 @@ async function monitorSignalProvider(opts = {}) {
1960
2010
  } catch (err) {
1961
2011
  const daemonExitError = daemonLifecycle.getExitError();
1962
2012
  if (opts.abortSignal?.aborted && !daemonExitError) return;
2013
+ if (daemonExitError) publishSignalRecovering(opts.statusSink, daemonExitError.message);
1963
2014
  throw err;
1964
2015
  } finally {
1965
2016
  await ingressMonitor?.stop();
@@ -1,19 +1,26 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { t as detectSignalTransport } from "./transport-detection-Da06-LOL.js";
3
- import { n as signalRpcRequest, t as signalCheck } from "./client-adapter-mXGeEHQJ.js";
2
+ import { t as detectSignalTransport } from "./transport-detection-BItHawMq.js";
3
+ import { n as signalRpcRequest, t as signalCheck } from "./client-adapter-D9SNPaNx.js";
4
4
  import { runChannelProbe } from "openclaw/plugin-sdk/text-utility-runtime";
5
5
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
6
6
  //#region extensions/signal/src/probe.ts
7
- var probe_exports = /* @__PURE__ */ __exportAll({ probeSignal: () => probeSignal });
7
+ var probe_exports = /* @__PURE__ */ __exportAll({
8
+ probeSignal: () => probeSignal,
9
+ probeSignalAccount: () => probeSignalAccount
10
+ });
8
11
  function parseSignalVersion(value) {
9
12
  if (typeof value === "string" && value.trim()) return value.trim();
10
13
  if (typeof value === "object" && value !== null) {
11
- const version = value.version;
14
+ const { version, versions } = value;
12
15
  if (typeof version === "string" && version.trim()) return version.trim();
16
+ if (Array.isArray(versions)) {
17
+ const normalizedVersions = versions.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
18
+ if (normalizedVersions.length > 0) return normalizedVersions.join(", ");
19
+ }
13
20
  }
14
21
  return null;
15
22
  }
16
- async function probeSignal(baseUrl, timeoutMs, options = {}) {
23
+ async function probeSignalTransport(baseUrl, timeoutMs, options, account) {
17
24
  return await runChannelProbe(void 0, async () => {
18
25
  const result = {
19
26
  ok: false,
@@ -30,7 +37,10 @@ async function probeSignal(baseUrl, timeoutMs, options = {}) {
30
37
  error: formatErrorMessage(error)
31
38
  };
32
39
  }
33
- const check = await signalCheck(baseUrl, timeoutMs, { transportKind });
40
+ const check = await signalCheck(baseUrl, timeoutMs, {
41
+ transportKind,
42
+ ...account && transportKind === "container" ? { account } : {}
43
+ });
34
44
  if (!check.ok) return {
35
45
  ...result,
36
46
  status: check.status ?? null,
@@ -47,11 +57,17 @@ async function probeSignal(baseUrl, timeoutMs, options = {}) {
47
57
  }
48
58
  return {
49
59
  ...result,
50
- ok: true,
60
+ ok: result.error === null,
51
61
  status: check.status ?? null
52
62
  };
53
63
  });
54
64
  }
65
+ async function probeSignal(baseUrl, timeoutMs, options = {}) {
66
+ return await probeSignalTransport(baseUrl, timeoutMs, options);
67
+ }
68
+ async function probeSignalAccount(params) {
69
+ return await probeSignalTransport(params.baseUrl, params.timeoutMs, { transportKind: params.transportKind }, params.account);
70
+ }
55
71
  async function resolveProbeTransportKind(baseUrl, timeoutMs, options) {
56
72
  if (options.transportKind) return options.transportKind;
57
73
  if (options.apiMode === "container") return "container";