@openclaw/signal 2026.7.1 → 2026.7.2-beta.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.
Files changed (28) hide show
  1. package/dist/api.js +8 -8
  2. package/dist/{approval-handler.runtime-DDQ4ZRyc.js → approval-handler.runtime-I1SMFwwR.js} +4 -4
  3. package/dist/{approval-reactions-UFmUPD0A.js → approval-reactions-ChcS2rQN.js} +34 -25
  4. package/dist/{approval-resolver-BR0MioAA.js → approval-resolver-B3m0D2R0.js} +2 -1
  5. package/dist/{channel-Cnhy1RwG.js → channel-DK8XZHPe.js} +132 -42
  6. package/dist/channel-config-api.js +1 -1
  7. package/dist/channel-plugin-api.js +1 -1
  8. package/dist/{channel.runtime-CQN0RaFx.js → channel.runtime-BbDh5UIr.js} +3 -2
  9. package/dist/{client-adapter-C9mB2Jz8.js → client-adapter-aEM0kGhC.js} +52 -18
  10. package/dist/config-api-DIg1lInT.js +3 -0
  11. package/dist/{config-schema-BiojLEsX.js → config-schema-CfVNH0DB.js} +7 -9
  12. package/dist/contract-api.js +3 -3
  13. package/dist/doctor-contract-api.js +16 -0
  14. package/dist/{identity-C8-yk4J9.js → identity-BF8taj7g.js} +7 -4
  15. package/dist/{install-signal-cli-ik8VPaGg.js → install-signal-cli-CtusY3tN.js} +43 -32
  16. package/dist/{monitor-Bee8O5Ay.js → monitor-BdzH3U64.js} +331 -126
  17. package/dist/{probe-Cnht8Ihg.js → probe-CbWkI24J.js} +1 -1
  18. package/dist/{reaction-runtime-api-EQ7cc7-Y.js → reaction-runtime-api-DifyinAU.js} +1 -1
  19. package/dist/reaction-runtime-api.js +1 -1
  20. package/dist/{message-actions-3OLDgjis.js → reply-authors-osU_SY4J.js} +212 -9
  21. package/dist/{approval-auth-CsHNcAiy.js → runtime-BZ9hnS3a.js} +34 -24
  22. package/dist/runtime-api.js +9 -9
  23. package/dist/{send-CUJy6WPt.js → send-BWtA6h3J.js} +14 -9
  24. package/dist/{send.runtime-CEi5oyCG.js → send.runtime-D4WBNNgn.js} +1 -1
  25. package/npm-shrinkwrap.json +2 -2
  26. package/openclaw.plugin.json +72 -48
  27. package/package.json +4 -4
  28. package/dist/config-api-KS-qhQvD.js +0 -2
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { a as resolveSignalReplyToMode, i as resolveSignalAccount } from "./accounts-Bz4HtP0g.js";
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-C8-yk4J9.js";
4
- import { a as isSignalNativeApprovalHandlerConfigured, n as resolveSignalReactionLevel } from "./message-actions-3OLDgjis.js";
5
- import { n as signalRpcRequest, r as streamSignalEvents, t as signalCheck } from "./client-adapter-C9mB2Jz8.js";
6
- import { n as sendReactionSignal, t as removeReactionSignal } from "./reaction-runtime-api-EQ7cc7-Y.js";
7
- import { a as maybeResolveSignalApprovalReaction, l as resolveSignalApprovalConversationKey, s as registerSignalApprovalReactionTargetForDeliveredPayload, t as addSignalApprovalReactionHintToStructuredPayload } from "./approval-reactions-UFmUPD0A.js";
8
- import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-CUJy6WPt.js";
9
- import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
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-BF8taj7g.js";
4
+ import { a as resolveSignalReactionLevel, c as isSignalNativeApprovalHandlerConfigured, r as materializeSignalPresentationFallback, t as registerSignalReplyContext } from "./reply-authors-osU_SY4J.js";
5
+ import { n as signalRpcRequest, r as streamSignalEvents, t as signalCheck } from "./client-adapter-aEM0kGhC.js";
6
+ import { n as sendReactionSignal, t as removeReactionSignal } from "./reaction-runtime-api-DifyinAU.js";
7
+ import { a as maybeResolveSignalApprovalReaction, l as resolveSignalApprovalConversationKey, s as registerSignalApprovalReactionTargetForDeliveredPayload, t as addSignalApprovalReactionHintToStructuredPayload } from "./approval-reactions-ChcS2rQN.js";
8
+ import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-BWtA6h3J.js";
9
+ import { createChannelMessageReplyPipeline, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
10
10
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
11
11
  import { detectMime, estimateBase64DecodedBytes, kindFromMime, saveMediaBuffer } from "openclaw/plugin-sdk/media-runtime";
12
12
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
@@ -15,6 +15,7 @@ import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plu
15
15
  import { normalizeE164, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
16
16
  import nodePath from "node:path";
17
17
  import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
18
+ import { collectErrorGraphCandidates, formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
18
19
  import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
19
20
  import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
20
21
  import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
@@ -26,18 +27,23 @@ import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy,
26
27
  import { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime";
27
28
  import { spawn } from "node:child_process";
28
29
  import os from "node:os";
30
+ import { createInterface } from "node:readline";
31
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
29
32
  import { resolveHumanDelayConfig } from "openclaw/plugin-sdk/agent-runtime";
30
33
  import { DEFAULT_EMOJIS, DEFAULT_TIMING, createStatusReactionController, logAckFailure, logTypingFailure, resolveAckReaction, shouldAckReaction } from "openclaw/plugin-sdk/channel-feedback";
31
34
  import { buildChannelInboundEventContext, buildMentionRegexes, createChannelInboundDebouncer, filterChannelInboundQuoteContext, formatInboundEnvelope, formatInboundFromLabel, formatInboundMediaUnavailableText, hasVisibleInboundReplyDispatch, logInboundDrop, matchesMentionPatterns, resolveEnvelopeFormatOptions, resolveInboundMentionDecision, runChannelInboundEvent, shouldDebounceTextInbound } from "openclaw/plugin-sdk/channel-inbound";
32
- import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
35
+ import { isControlCommandMessage } from "openclaw/plugin-sdk/command-detection";
33
36
  import { recordInboundSession, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
34
37
  import { createInternalHookEvent, fireAndForgetHook, toInternalMessageReceivedContext, triggerInternalHook } from "openclaw/plugin-sdk/hook-runtime";
35
38
  import { resolveBatchedReplyThreadingPolicy } from "openclaw/plugin-sdk/reply-reference";
36
39
  import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
37
40
  import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
38
41
  import { createChannelIngressResolver, defineStableChannelIngressIdentity } from "openclaw/plugin-sdk/channel-ingress-runtime";
42
+ import { listChatCommands, maybeResolveTextAlias, normalizeCommandBody } from "openclaw/plugin-sdk/command-auth-native";
43
+ import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
39
44
  import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
40
45
  //#region extensions/signal/src/daemon.ts
46
+ const SIGNAL_DAEMON_STOP_KILL_TIMEOUT_MS = 1500;
41
47
  function formatSignalDaemonExit(exit) {
42
48
  return `signal daemon exited (source=${exit.source} code=${exit.code ?? "null"} signal=${exit.signal ?? "null"})`;
43
49
  }
@@ -53,12 +59,11 @@ function classifySignalCliLogLine(line) {
53
59
  return "log";
54
60
  }
55
61
  function bindSignalCliOutput(params) {
56
- params.stream?.on("data", (data) => {
57
- for (const line of data.toString().split(/\r?\n/)) {
58
- const kind = classifySignalCliLogLine(line);
59
- if (kind === "log") params.log(`signal-cli: ${line.trim()}`);
60
- else if (kind === "error") params.error(`signal-cli: ${line.trim()}`);
61
- }
62
+ if (!params.stream) return;
63
+ createInterface({ input: params.stream }).on("line", (line) => {
64
+ const kind = classifySignalCliLogLine(line);
65
+ if (kind === "log") params.log(`signal-cli: ${line.trim()}`);
66
+ else if (kind === "error") params.error(`signal-cli: ${line.trim()}`);
62
67
  });
63
68
  }
64
69
  function resolveSignalCliConfigPath(raw) {
@@ -91,6 +96,7 @@ function spawnSignalDaemon(opts) {
91
96
  const error = opts.runtime?.error ?? (() => {});
92
97
  let exited = false;
93
98
  let settledExit = false;
99
+ let stopPromise;
94
100
  let resolveExit;
95
101
  const exitedPromise = new Promise((resolve) => {
96
102
  resolveExit = resolve;
@@ -131,19 +137,42 @@ function spawnSignalDaemon(opts) {
131
137
  });
132
138
  });
133
139
  child.on("error", (err) => {
134
- error(`signal-cli spawn error: ${String(err)}`);
135
- settleExit({
136
- source: "spawn-error",
137
- code: null,
138
- signal: null
139
- });
140
+ if (child.pid === void 0) {
141
+ error(`signal-cli spawn error: ${String(err)}`);
142
+ settleExit({
143
+ source: "spawn-error",
144
+ code: null,
145
+ signal: null
146
+ });
147
+ } else error(`signal-cli process error: ${String(err)}`);
140
148
  });
141
149
  return {
142
150
  pid: child.pid ?? void 0,
143
151
  exited: exitedPromise,
144
152
  isExited: () => exited,
145
153
  stop: () => {
146
- if (!child.killed && !exited) child.kill("SIGTERM");
154
+ if (exited) return Promise.resolve();
155
+ if (stopPromise) return stopPromise;
156
+ if (!child.killed) try {
157
+ child.kill("SIGTERM");
158
+ } catch (err) {
159
+ error(`signal-cli stop error: ${String(err)}`);
160
+ }
161
+ stopPromise = new Promise((resolve) => {
162
+ const timeout = setTimeout(() => {
163
+ if (!exited) try {
164
+ child.kill("SIGKILL");
165
+ } catch (err) {
166
+ error(`signal-cli force-stop error: ${String(err)}`);
167
+ }
168
+ }, SIGNAL_DAEMON_STOP_KILL_TIMEOUT_MS);
169
+ timeout.unref?.();
170
+ exitedPromise.then(() => {
171
+ clearTimeout(timeout);
172
+ resolve();
173
+ });
174
+ });
175
+ return stopPromise;
147
176
  }
148
177
  };
149
178
  }
@@ -214,8 +243,8 @@ function signalSubjectInput(params) {
214
243
  return {
215
244
  stableId: formatSignalSenderId(params.sender),
216
245
  aliases: {
217
- phone: params.sender.kind === "phone" ? params.sender.e164 : void 0,
218
- uuid: params.sender.kind === "uuid" ? params.sender.raw : void 0,
246
+ phone: params.sender.kind === "phone" ? params.sender.e164 : params.sender.aliases?.e164,
247
+ uuid: params.sender.kind === "uuid" ? params.sender.raw : params.sender.aliases?.uuid,
219
248
  group: params.groupId
220
249
  }
221
250
  };
@@ -281,6 +310,96 @@ async function handleSignalDirectMessageAccess(params) {
281
310
  return false;
282
311
  }
283
312
  //#endregion
313
+ //#region extensions/signal/src/monitor/event-handler.control-lane.ts
314
+ const SIGNAL_ACTIVE_RUN_CONTROL_COMMAND_KEYS = /* @__PURE__ */ new Set([
315
+ "approve",
316
+ "commands",
317
+ "context",
318
+ "help",
319
+ "status",
320
+ "steer",
321
+ "tasks",
322
+ "tools",
323
+ "whoami"
324
+ ]);
325
+ function resolveSignalConversationId(entry) {
326
+ return (entry.isGroup ? entry.groupId : entry.senderPeerId)?.trim() || null;
327
+ }
328
+ function resolveSignalInboundDebounceKey(accountId, entry) {
329
+ const conversationId = resolveSignalConversationId(entry);
330
+ if (!conversationId || !entry.senderPeerId) return null;
331
+ return `signal:${accountId}:${conversationId}:${entry.senderPeerId}`;
332
+ }
333
+ function resolveSignalInboundConversationKey(accountId, entry) {
334
+ const conversationId = resolveSignalConversationId(entry);
335
+ return conversationId ? `signal:${accountId}:${conversationId}` : null;
336
+ }
337
+ function isSignalActiveRunControlText(text) {
338
+ if (isAbortRequestText(text)) return true;
339
+ const normalizedBody = normalizeCommandBody(text.trim());
340
+ const alias = maybeResolveTextAlias(normalizedBody);
341
+ if (!alias) return false;
342
+ const command = listChatCommands().find((entry) => entry.textAliases.some((candidate) => candidate.trim().toLowerCase() === alias));
343
+ if (command?.key === "queue") return normalizedBody.slice(alias.length).trim() === "";
344
+ return command ? SIGNAL_ACTIVE_RUN_CONTROL_COMMAND_KEYS.has(command.key) : false;
345
+ }
346
+ function resolveSignalControlLaneKey(accountId, entry) {
347
+ if (!entry.commandAuthorized || !isSignalActiveRunControlText(entry.commandBody)) return null;
348
+ const conversationId = resolveSignalConversationId(entry);
349
+ return conversationId ? `signal:${accountId}:${conversationId}:control` : null;
350
+ }
351
+ function createSignalPendingInboundRegistry(accountId) {
352
+ const trackedEntries = /* @__PURE__ */ new WeakMap();
353
+ const countsByConversation = /* @__PURE__ */ new Map();
354
+ const track = (entry) => {
355
+ if (trackedEntries.has(entry)) return;
356
+ const conversationKey = resolveSignalInboundConversationKey(accountId, entry);
357
+ const inboundKey = resolveSignalInboundDebounceKey(accountId, entry);
358
+ if (!conversationKey || !inboundKey) return;
359
+ const counts = countsByConversation.get(conversationKey) ?? /* @__PURE__ */ new Map();
360
+ counts.set(inboundKey, (counts.get(inboundKey) ?? 0) + 1);
361
+ countsByConversation.set(conversationKey, counts);
362
+ trackedEntries.set(entry, {
363
+ conversationKey,
364
+ inboundKey
365
+ });
366
+ };
367
+ const complete = (entries) => {
368
+ for (const entry of entries) {
369
+ const tracked = trackedEntries.get(entry);
370
+ if (!tracked) continue;
371
+ trackedEntries.delete(entry);
372
+ const counts = countsByConversation.get(tracked.conversationKey);
373
+ const nextCount = (counts?.get(tracked.inboundKey) ?? 0) - 1;
374
+ if (nextCount > 0) {
375
+ counts?.set(tracked.inboundKey, nextCount);
376
+ continue;
377
+ }
378
+ counts?.delete(tracked.inboundKey);
379
+ if (counts?.size === 0) countsByConversation.delete(tracked.conversationKey);
380
+ }
381
+ };
382
+ const cancelPendingOnAbort = (entry, cancelKey) => {
383
+ if (!entry.commandAuthorized || !isAbortRequestText(entry.commandBody)) return;
384
+ const conversationKey = resolveSignalInboundConversationKey(accountId, entry);
385
+ if (!conversationKey) return;
386
+ for (const inboundKey of countsByConversation.get(conversationKey)?.keys() ?? []) cancelKey(inboundKey);
387
+ };
388
+ const completeAfter = (flush) => async (entries) => {
389
+ try {
390
+ await flush(entries);
391
+ } finally {
392
+ complete(entries);
393
+ }
394
+ };
395
+ return {
396
+ track,
397
+ complete,
398
+ completeAfter,
399
+ cancelPendingOnAbort
400
+ };
401
+ }
402
+ //#endregion
284
403
  //#region extensions/signal/src/monitor/inbound-context.ts
285
404
  function resolveSignalQuoteContext(params) {
286
405
  const contextVisibilityMode = resolveChannelContextVisibilityMode({
@@ -328,6 +447,41 @@ function clampBounds(start, length, textLength) {
328
447
  end: Math.min(textLength, safeStart + Math.max(0, Math.trunc(length)))
329
448
  };
330
449
  }
450
+ function isValidStructuredMention(message, mention) {
451
+ if (!mention || !(mention.uuid || mention.number)) return false;
452
+ const { start, length } = mention;
453
+ if (typeof start !== "number" || typeof length !== "number") return false;
454
+ return Number.isInteger(start) && Number.isInteger(length) && start >= 0 && length > 0 && start + length <= message.length;
455
+ }
456
+ function normalizeAccountPhone(account) {
457
+ const trimmed = account?.trim();
458
+ return trimmed ? normalizeE164(trimmed) : void 0;
459
+ }
460
+ function resolveSignalNativeMentionFacts(params) {
461
+ const validMentions = (params.mentions ?? []).filter((mention) => isValidStructuredMention(params.message, mention));
462
+ const botUuid = params.accountUuid?.trim();
463
+ const botPhone = normalizeAccountPhone(params.account);
464
+ const canDetectBotMention = Boolean(botUuid || botPhone);
465
+ const mentionsBot = validMentions.some((mention) => {
466
+ const mentionUuid = mention.uuid?.trim();
467
+ if (botUuid && mentionUuid === botUuid) return true;
468
+ const mentionNumber = mention.number?.trim();
469
+ return Boolean(botPhone && mentionNumber && normalizeE164(mentionNumber) === botPhone);
470
+ });
471
+ return {
472
+ canDetectBotMention,
473
+ hasAnyMention: validMentions.length > 0,
474
+ mentionsBot
475
+ };
476
+ }
477
+ function resolveSignalMentionFacts(identity, message, mentions) {
478
+ return resolveSignalNativeMentionFacts({
479
+ message,
480
+ mentions,
481
+ account: identity.account,
482
+ accountUuid: identity.accountUuid
483
+ });
484
+ }
331
485
  function renderSignalMentions(message, mentions) {
332
486
  if (!message || !mentions?.length) return message;
333
487
  let normalized = message;
@@ -344,6 +498,15 @@ function renderSignalMentions(message, mentions) {
344
498
  }
345
499
  //#endregion
346
500
  //#region extensions/signal/src/monitor/event-handler.ts
501
+ const REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE = /reply session initialization conflicted for \S+/u;
502
+ const RETRYABLE_FLUSH_RETRY_DELAYS_MS = [
503
+ 1e3,
504
+ 2e3,
505
+ 4e3
506
+ ];
507
+ function isSignalReplySessionInitConflictError(error) {
508
+ return collectErrorGraphCandidates(error, (current) => [current.cause, current.error]).some((candidate) => REPLY_SESSION_INIT_CONFLICT_MESSAGE_RE.test(formatErrorMessage(candidate)));
509
+ }
347
510
  function formatAttachmentKindCount(kind, count) {
348
511
  if (kind === "attachment") return `${count} file${count > 1 ? "s" : ""}`;
349
512
  return `${count} ${kind}${count > 1 ? "s" : ""}`;
@@ -404,6 +567,7 @@ async function finalizeSignalStatusReaction(params) {
404
567
  await params.controller.restoreInitial();
405
568
  }
406
569
  function createSignalEventHandler(deps) {
570
+ const activeEnqueueEntries = /* @__PURE__ */ new WeakSet();
407
571
  async function handleSignalInboundMessage(entry) {
408
572
  const fromLabel = formatInboundFromLabel({
409
573
  isGroup: entry.isGroup,
@@ -461,11 +625,12 @@ function createSignalEventHandler(deps) {
461
625
  historyKey,
462
626
  limit: deps.historyLimit
463
627
  }) : void 0;
464
- const replyThreading = resolveBatchedReplyThreadingPolicy(resolveSignalReplyToMode({
628
+ const replyToMode = resolveSignalReplyToMode({
465
629
  cfg: deps.cfg,
466
630
  accountId: deps.accountId,
467
631
  chatType: entry.isGroup ? "group" : "direct"
468
- }), entry.isBatched === true);
632
+ });
633
+ const replyThreading = resolveBatchedReplyThreadingPolicy(replyToMode, entry.isBatched === true);
469
634
  const media = entry.mediaPaths && entry.mediaPaths.length > 0 ? entry.mediaPaths.map((path, index) => ({
470
635
  path,
471
636
  url: path,
@@ -618,6 +783,7 @@ function createSignalEventHandler(deps) {
618
783
  replyToId: ctxPayload.ReplyToId,
619
784
  author: entry.senderRecipient,
620
785
  body: entry.nativeReplyBody ?? entry.bodyText,
786
+ allowImplicitCurrentMessage: replyToMode !== "off" && replyThreading?.implicitCurrentMessage !== "deny",
621
787
  state: { hasReplied: false }
622
788
  };
623
789
  const { dispatcher, replyOptions, markDispatchIdle } = createReplyDispatcherWithTyping({
@@ -753,14 +919,75 @@ function createSignalEventHandler(deps) {
753
919
  }
754
920
  });
755
921
  }
756
- const { debouncer: inboundDebouncer } = createChannelInboundDebouncer({
922
+ async function flushSignalInboundEntries(entries) {
923
+ const last = entries.at(-1);
924
+ if (!last) return;
925
+ if (entries.length === 1) {
926
+ await handleSignalInboundMessage(last);
927
+ return;
928
+ }
929
+ const combinedText = entries.map((entry) => entry.bodyText).filter(Boolean).join("\\n");
930
+ const combinedCommandBody = entries.map((entry) => entry.commandBody).filter(Boolean).join("\\n");
931
+ if (!combinedText.trim()) return;
932
+ await handleSignalInboundMessage({
933
+ ...last,
934
+ bodyText: combinedText,
935
+ commandBody: combinedCommandBody,
936
+ isBatched: true,
937
+ nativeReplyBody: last.nativeReplyBody ?? last.bodyText,
938
+ mediaPath: void 0,
939
+ mediaType: void 0,
940
+ mediaPaths: void 0,
941
+ mediaTypes: void 0
942
+ });
943
+ }
944
+ async function retrySignalInboundFlush(entries, initialError) {
945
+ let lastError = initialError;
946
+ for (const [attemptIndex, delayMs] of RETRYABLE_FLUSH_RETRY_DELAYS_MS.entries()) {
947
+ const attempt = attemptIndex + 1;
948
+ logVerbose(`signal: reply session init conflict, retrying ${entries.length} inbound message(s) in ${delayMs}ms (attempt ${attempt}/${RETRYABLE_FLUSH_RETRY_DELAYS_MS.length})`);
949
+ try {
950
+ await setTimeout$1(delayMs, void 0, {
951
+ ref: false,
952
+ signal: deps.abortSignal
953
+ });
954
+ } catch (err) {
955
+ if (deps.abortSignal?.aborted) return;
956
+ throw err;
957
+ }
958
+ if (deps.abortSignal?.aborted) return;
959
+ try {
960
+ await flushSignalInboundEntries(entries);
961
+ return;
962
+ } catch (err) {
963
+ if (deps.abortSignal?.aborted) return;
964
+ lastError = err;
965
+ if (!isSignalReplySessionInitConflictError(err)) throw err;
966
+ }
967
+ }
968
+ throw lastError;
969
+ }
970
+ const flushDebouncedSignalInboundEntries = async (entries) => {
971
+ if (!entries.some((entry) => activeEnqueueEntries.has(entry)) && deps.abortSignal?.aborted) return;
972
+ try {
973
+ await flushSignalInboundEntries(entries);
974
+ } catch (err) {
975
+ if (!isSignalReplySessionInitConflictError(err)) throw err;
976
+ if (deps.abortSignal?.aborted) return;
977
+ const retryTask = retrySignalInboundFlush(entries, err);
978
+ deps.runTrackedTask?.(() => retryTask.catch(() => void 0));
979
+ await retryTask;
980
+ }
981
+ };
982
+ const reportSignalInboundFlushError = (err) => {
983
+ deps.runtime.error?.(`signal debounce flush failed: ${String(err)}`);
984
+ };
985
+ const pendingInboundRegistry = createSignalPendingInboundRegistry(deps.accountId);
986
+ const flushNormalSignalInboundEntries = pendingInboundRegistry.completeAfter(flushDebouncedSignalInboundEntries);
987
+ const { debouncer } = createChannelInboundDebouncer({
757
988
  cfg: deps.cfg,
758
989
  channel: "signal",
759
- buildKey: (entry) => {
760
- const conversationId = entry.isGroup ? entry.groupId ?? "unknown" : entry.senderPeerId;
761
- if (!conversationId || !entry.senderPeerId) return null;
762
- return `signal:${deps.accountId}:${conversationId}:${entry.senderPeerId}`;
763
- },
990
+ buildKey: (entry) => resolveSignalInboundDebounceKey(deps.accountId, entry),
764
991
  shouldDebounce: (entry) => {
765
992
  return shouldDebounceTextInbound({
766
993
  text: entry.commandBody,
@@ -768,31 +995,18 @@ function createSignalEventHandler(deps) {
768
995
  hasMedia: Boolean(entry.mediaPath || entry.mediaType || entry.mediaPaths?.length)
769
996
  });
770
997
  },
771
- onFlush: async (entries) => {
772
- const last = entries.at(-1);
773
- if (!last) return;
774
- if (entries.length === 1) {
775
- await handleSignalInboundMessage(last);
776
- return;
777
- }
778
- const combinedText = entries.map((entry) => entry.bodyText).filter(Boolean).join("\\n");
779
- const combinedCommandBody = entries.map((entry) => entry.commandBody).filter(Boolean).join("\\n");
780
- if (!combinedText.trim()) return;
781
- await handleSignalInboundMessage({
782
- ...last,
783
- bodyText: combinedText,
784
- commandBody: combinedCommandBody,
785
- isBatched: true,
786
- nativeReplyBody: last.nativeReplyBody ?? last.bodyText,
787
- mediaPath: void 0,
788
- mediaType: void 0,
789
- mediaPaths: void 0,
790
- mediaTypes: void 0
791
- });
792
- },
793
- onError: (err) => {
794
- deps.runtime.error?.(`signal debounce flush failed: ${String(err)}`);
795
- }
998
+ onFlush: flushNormalSignalInboundEntries,
999
+ onError: reportSignalInboundFlushError,
1000
+ onCancel: pendingInboundRegistry.complete
1001
+ });
1002
+ const { debouncer: controlDebouncer } = createChannelInboundDebouncer({
1003
+ cfg: deps.cfg,
1004
+ channel: "signal",
1005
+ serializeImmediate: true,
1006
+ buildKey: (entry) => resolveSignalControlLaneKey(deps.accountId, entry),
1007
+ shouldDebounce: () => false,
1008
+ onFlush: flushDebouncedSignalInboundEntries,
1009
+ onError: reportSignalInboundFlushError
796
1010
  });
797
1011
  async function handleReactionOnlyInbound(params) {
798
1012
  if (params.hasBodyContent) return false;
@@ -878,10 +1092,11 @@ function createSignalEventHandler(deps) {
878
1092
  if ("syncMessage" in envelope) return;
879
1093
  const dataMessage = envelope.dataMessage ?? envelope.editMessage?.dataMessage;
880
1094
  const reaction = deps.isSignalReactionMessage(envelope.reactionMessage) ? envelope.reactionMessage : deps.isSignalReactionMessage(dataMessage?.reaction) ? dataMessage?.reaction : null;
881
- const messageText = renderSignalMentions(dataMessage?.message ?? "", dataMessage?.mentions).trim();
1095
+ const rawMessage = dataMessage?.message ?? "";
1096
+ const messageText = renderSignalMentions(rawMessage, dataMessage?.mentions).trim();
882
1097
  const groupId = dataMessage?.groupInfo?.groupId ?? reaction?.groupInfo?.groupId ?? void 0;
883
1098
  const isGroup = Boolean(groupId);
884
- const hasControlCommandInMessage = hasControlCommand(messageText, deps.cfg);
1099
+ const hasControlCommandInMessage = isControlCommandMessage(messageText, deps.cfg);
885
1100
  const senderDisplay = formatSignalSenderDisplay(sender);
886
1101
  const { senderAccess, commandAccess } = await resolveSignalAccessState({
887
1102
  accountId: deps.accountId,
@@ -966,8 +1181,16 @@ function createSignalEventHandler(deps) {
966
1181
  groupId,
967
1182
  senderPeerId
968
1183
  });
1184
+ const inboundTimestamp = typeof envelope.timestamp === "number" ? envelope.timestamp : typeof dataMessage.timestamp === "number" ? dataMessage.timestamp : void 0;
1185
+ const nativeReplyTargetTimestamp = typeof envelope.editMessage?.targetSentTimestamp === "number" ? envelope.editMessage.targetSentTimestamp : inboundTimestamp;
1186
+ const messageId = typeof inboundTimestamp === "number" ? String(inboundTimestamp) : void 0;
1187
+ const replyToId = typeof nativeReplyTargetTimestamp === "number" ? String(nativeReplyTargetTimestamp) : void 0;
1188
+ const signalToRaw = isGroup ? `group:${groupId}` : `signal:${senderRecipient}`;
1189
+ const signalTo = normalizeSignalMessagingTarget(signalToRaw) ?? signalToRaw;
969
1190
  const mentionRegexes = buildMentionRegexes(deps.cfg, route.agentId);
970
- const wasMentioned = isGroup && matchesMentionPatterns(messageText, mentionRegexes);
1191
+ const textWasMentioned = isGroup && matchesMentionPatterns(messageText, mentionRegexes);
1192
+ const nativeMentionFacts = resolveSignalMentionFacts(deps, rawMessage, dataMessage?.mentions);
1193
+ const wasMentioned = isGroup && (textWasMentioned || nativeMentionFacts.mentionsBot);
971
1194
  const requireMention = isGroup && resolveChannelGroupRequireMention({
972
1195
  cfg: deps.cfg,
973
1196
  channel: "signal",
@@ -975,12 +1198,12 @@ function createSignalEventHandler(deps) {
975
1198
  accountId: deps.accountId,
976
1199
  configuredGroupDefaultsToNoMention: true
977
1200
  });
978
- const canDetectMention = mentionRegexes.length > 0;
1201
+ const canDetectMention = mentionRegexes.length > 0 || nativeMentionFacts.canDetectBotMention;
979
1202
  const mentionDecision = resolveInboundMentionDecision({
980
1203
  facts: {
981
1204
  canDetectMention,
982
1205
  wasMentioned,
983
- hasAnyMention: false,
1206
+ hasAnyMention: nativeMentionFacts.hasAnyMention,
984
1207
  implicitMentionKinds: []
985
1208
  },
986
1209
  policy: {
@@ -1020,6 +1243,14 @@ function createSignalEventHandler(deps) {
1020
1243
  messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0
1021
1244
  }
1022
1245
  });
1246
+ await registerSignalReplyContext({
1247
+ accountId: deps.accountId,
1248
+ to: signalTo,
1249
+ replyToId,
1250
+ author: senderRecipient,
1251
+ body: pendingBodyText,
1252
+ sourceTimestamp: inboundTimestamp
1253
+ });
1023
1254
  const signalGroupPolicy = resolveChannelGroupPolicy({
1024
1255
  cfg: deps.cfg,
1025
1256
  channel: "signal",
@@ -1098,8 +1329,6 @@ function createSignalEventHandler(deps) {
1098
1329
  });
1099
1330
  }
1100
1331
  if (!bodyText) return;
1101
- const inboundTimestamp = typeof envelope.timestamp === "number" ? envelope.timestamp : typeof dataMessage.timestamp === "number" ? dataMessage.timestamp : void 0;
1102
- const nativeReplyTargetTimestamp = typeof envelope.editMessage?.targetSentTimestamp === "number" ? envelope.editMessage.targetSentTimestamp : inboundTimestamp;
1103
1332
  if (deps.sendReadReceipts && !deps.readReceiptsViaDaemon && !isGroup && inboundTimestamp) try {
1104
1333
  await sendReadReceiptSignal(`signal:${senderRecipient}`, inboundTimestamp, {
1105
1334
  cfg: deps.cfg,
@@ -1112,9 +1341,15 @@ function createSignalEventHandler(deps) {
1112
1341
  }
1113
1342
  else if (deps.sendReadReceipts && !deps.readReceiptsViaDaemon && !isGroup && !inboundTimestamp) logVerbose(`signal read receipt skipped (missing timestamp) for ${senderDisplay}`);
1114
1343
  const senderName = envelope.sourceName ?? senderDisplay;
1115
- const messageId = typeof inboundTimestamp === "number" ? String(inboundTimestamp) : void 0;
1116
- const replyToId = typeof nativeReplyTargetTimestamp === "number" ? String(nativeReplyTargetTimestamp) : void 0;
1117
- await inboundDebouncer.enqueue({
1344
+ await registerSignalReplyContext({
1345
+ accountId: deps.accountId,
1346
+ to: signalTo,
1347
+ replyToId,
1348
+ author: senderRecipient,
1349
+ body: bodyText,
1350
+ sourceTimestamp: inboundTimestamp
1351
+ });
1352
+ const entry = {
1118
1353
  senderName,
1119
1354
  senderDisplay,
1120
1355
  senderRecipient,
@@ -1138,7 +1373,16 @@ function createSignalEventHandler(deps) {
1138
1373
  replyToBody: visibleQuoteText || void 0,
1139
1374
  replyToSender: visibleQuoteSender,
1140
1375
  replyToIsQuote: visibleQuoteText ? true : void 0
1141
- });
1376
+ };
1377
+ pendingInboundRegistry.cancelPendingOnAbort(entry, debouncer.cancelKey);
1378
+ const inboundLane = resolveSignalControlLaneKey(deps.accountId, entry) ? controlDebouncer : debouncer;
1379
+ if (inboundLane === debouncer) pendingInboundRegistry.track(entry);
1380
+ activeEnqueueEntries.add(entry);
1381
+ try {
1382
+ await inboundLane.enqueue(entry);
1383
+ } finally {
1384
+ activeEnqueueEntries.delete(entry);
1385
+ }
1142
1386
  };
1143
1387
  }
1144
1388
  //#endregion
@@ -1209,8 +1453,8 @@ function resolveRuntime(opts) {
1209
1453
  function createSignalMonitorTaskRunner(runtime) {
1210
1454
  const inFlight = /* @__PURE__ */ new Set();
1211
1455
  return {
1212
- runEventTask(task) {
1213
- const trackedTask = Promise.resolve().then(task).catch((err) => runtime.error?.(`event handler failed: ${String(err)}`)).finally(() => inFlight.delete(trackedTask));
1456
+ runTask(task) {
1457
+ const trackedTask = Promise.resolve().then(task).catch((err) => runtime.error?.(`signal monitor task failed: ${String(err)}`)).finally(() => inFlight.delete(trackedTask));
1214
1458
  inFlight.add(trackedTask);
1215
1459
  },
1216
1460
  async waitForIdle() {
@@ -1218,58 +1462,16 @@ function createSignalMonitorTaskRunner(runtime) {
1218
1462
  }
1219
1463
  };
1220
1464
  }
1221
- function mergeAbortSignals(a, b) {
1222
- if (!a && !b) return {
1223
- signal: void 0,
1224
- dispose: () => {}
1225
- };
1226
- if (!a) return {
1227
- signal: b,
1228
- dispose: () => {}
1229
- };
1230
- if (!b) return {
1231
- signal: a,
1232
- dispose: () => {}
1233
- };
1234
- const controller = new AbortController();
1235
- const abortFrom = (source) => {
1236
- if (!controller.signal.aborted) controller.abort(source.reason);
1237
- };
1238
- if (a.aborted) {
1239
- abortFrom(a);
1240
- return {
1241
- signal: controller.signal,
1242
- dispose: () => {}
1243
- };
1244
- }
1245
- if (b.aborted) {
1246
- abortFrom(b);
1247
- return {
1248
- signal: controller.signal,
1249
- dispose: () => {}
1250
- };
1251
- }
1252
- const onAbortA = () => abortFrom(a);
1253
- const onAbortB = () => abortFrom(b);
1254
- a.addEventListener("abort", onAbortA, { once: true });
1255
- b.addEventListener("abort", onAbortB, { once: true });
1256
- return {
1257
- signal: controller.signal,
1258
- dispose: () => {
1259
- a.removeEventListener("abort", onAbortA);
1260
- b.removeEventListener("abort", onAbortB);
1261
- }
1262
- };
1263
- }
1264
1465
  function createSignalDaemonLifecycle(params) {
1265
1466
  let daemonHandle = null;
1266
1467
  let daemonStopRequested = false;
1267
1468
  let daemonExitError;
1268
1469
  const daemonAbortController = new AbortController();
1269
- const mergedAbort = mergeAbortSignals(params.abortSignal, daemonAbortController.signal);
1470
+ const abortSignal = params.abortSignal ? AbortSignal.any([params.abortSignal, daemonAbortController.signal]) : daemonAbortController.signal;
1270
1471
  const stop = () => {
1271
1472
  daemonStopRequested = true;
1272
- daemonHandle?.stop();
1473
+ if (!daemonAbortController.signal.aborted) daemonAbortController.abort(params.abortSignal?.reason ?? /* @__PURE__ */ new Error("Signal monitor stopped"));
1474
+ return daemonHandle?.stop() ?? Promise.resolve();
1273
1475
  };
1274
1476
  const attach = (handle) => {
1275
1477
  daemonHandle = handle;
@@ -1284,8 +1486,7 @@ function createSignalDaemonLifecycle(params) {
1284
1486
  attach,
1285
1487
  stop,
1286
1488
  getExitError,
1287
- abortSignal: mergedAbort.signal,
1288
- dispose: mergedAbort.dispose
1489
+ abortSignal
1289
1490
  };
1290
1491
  }
1291
1492
  function normalizeAllowList(raw) {
@@ -1403,14 +1604,15 @@ async function deliverReplies(params) {
1403
1604
  });
1404
1605
  for (const payload of replies) {
1405
1606
  const deliveryResults = [];
1607
+ const presentationPayload = materializeSignalPresentationFallback(payload);
1406
1608
  const deliveredPayload = addSignalApprovalReactionHintToStructuredPayload({
1407
1609
  cfg: params.cfg,
1408
1610
  accountId,
1409
1611
  to: target,
1410
- payload,
1612
+ payload: presentationPayload,
1411
1613
  targetAuthor: account,
1412
1614
  targetAuthorUuid: accountUuid
1413
- }) ?? payload;
1615
+ }) ?? presentationPayload;
1414
1616
  const reply = resolveSendableOutboundReplyParts(deliveredPayload);
1415
1617
  const nextNativeReply = createSignalNativeReplyResolver({
1416
1618
  payload: deliveredPayload,
@@ -1471,12 +1673,15 @@ async function deliverReplies(params) {
1471
1673
  function resolveSignalNativeReplyOptions(params) {
1472
1674
  if (params.payload.replyToCurrent === false) return {};
1473
1675
  const payloadReplyToId = normalizeOptionalString(params.payload.replyToId);
1676
+ const isExplicitCurrentReply = params.payload.replyToTag === true || params.payload.replyToCurrent === true;
1677
+ if (!payloadReplyToId && !isExplicitCurrentReply && params.replyContext?.allowImplicitCurrentMessage === false) return {};
1474
1678
  const contextReplyToId = normalizeOptionalString(params.replyContext?.replyToId);
1475
- if (!payloadReplyToId || !contextReplyToId || payloadReplyToId !== contextReplyToId) return {};
1679
+ if (!contextReplyToId || payloadReplyToId && payloadReplyToId !== contextReplyToId) return {};
1680
+ const replyToId = payloadReplyToId ?? contextReplyToId;
1476
1681
  const replyToAuthor = normalizeOptionalString(params.replyContext?.author);
1477
- if (!replyToAuthor) return { replyToId: payloadReplyToId };
1682
+ if (!replyToAuthor) return { replyToId };
1478
1683
  return {
1479
- replyToId: payloadReplyToId,
1684
+ replyToId,
1480
1685
  replyToAuthor,
1481
1686
  replyToBody: params.replyContext?.body ?? ""
1482
1687
  };
@@ -1603,12 +1808,14 @@ async function monitorSignalProvider(opts = {}) {
1603
1808
  });
1604
1809
  const handleEvent = createSignalEventHandler({
1605
1810
  runtime,
1811
+ abortSignal: daemonLifecycle.abortSignal,
1812
+ runTrackedTask: (task) => monitorTaskRunner.runTask(task),
1606
1813
  cfg,
1607
1814
  baseUrl,
1608
1815
  account,
1609
1816
  accountUuid: accountInfo.config.accountUuid,
1610
1817
  accountId: accountInfo.accountId,
1611
- blockStreaming: accountInfo.config.blockStreaming,
1818
+ blockStreaming: resolveChannelStreamingBlockEnabled(accountInfo.config),
1612
1819
  historyLimit,
1613
1820
  groupHistories,
1614
1821
  textLimit,
@@ -1645,7 +1852,7 @@ async function monitorSignalProvider(opts = {}) {
1645
1852
  apiMode: configuredApiMode,
1646
1853
  policy: opts.reconnectPolicy,
1647
1854
  onEvent: (event) => {
1648
- monitorTaskRunner.runEventTask(() => handleEvent(event));
1855
+ monitorTaskRunner.runTask(() => handleEvent(event));
1649
1856
  }
1650
1857
  });
1651
1858
  const daemonExitError = daemonLifecycle.getExitError();
@@ -1655,10 +1862,8 @@ async function monitorSignalProvider(opts = {}) {
1655
1862
  if (opts.abortSignal?.aborted && !daemonExitError) return;
1656
1863
  throw err;
1657
1864
  } finally {
1658
- await monitorTaskRunner.waitForIdle();
1659
- daemonLifecycle.dispose();
1865
+ await Promise.all([daemonLifecycle.stop(), monitorTaskRunner.waitForIdle()]);
1660
1866
  opts.abortSignal?.removeEventListener("abort", onAbort);
1661
- daemonLifecycle.stop();
1662
1867
  }
1663
1868
  }
1664
1869
  //#endregion