@openclaw/signal 2026.7.2-beta.7 → 2026.8.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{accounts-DrStJjiK.js → accounts-DO4HMqaK.js} +2 -2
- package/dist/api.js +9 -9
- package/dist/{runtime-JPrGlV7h.js → approval-auth-BY6uuApE.js} +2 -9
- package/dist/{approval-handler.runtime-C-XYgcxE.js → approval-handler.runtime-Co6EVTqY.js} +4 -4
- package/dist/{approval-reactions-DtL47gSK.js → approval-reactions-cs3Ox9Ow.js} +67 -189
- package/dist/channel-config-api.js +1 -1
- package/dist/channel-plugin-api.js +1 -1
- package/dist/{channel-CmbWaYlm.js → channel-zIopSllO.js} +64 -99
- package/dist/{channel.runtime-BRDaqr5-.js → channel.runtime-CpDBElG8.js} +3 -3
- package/dist/{client-BazXTH7O.js → client-CvnR8_XA.js} +31 -34
- package/dist/{client-adapter-mXGeEHQJ.js → client-adapter-D9SNPaNx.js} +13 -6
- package/dist/{config-schema-Y3dFGwdL.js → config-schema-CkMIK7zp.js} +4 -5
- package/dist/contract-api.js +1 -1
- package/dist/{doctor-contract-api-Dcte-QYB.js → doctor-contract-api-Dnp5eQQJ.js} +19 -22
- package/dist/doctor-contract-api.js +1 -1
- package/dist/{install-signal-cli-qJF7iC4h.js → install-signal-cli-Cpiyk0k1.js} +8 -5
- package/dist/{monitor-BvIzkrYW.js → monitor-BLFIkpH0.js} +305 -142
- package/dist/{probe-Br9abaGp.js → probe-B0E_cXQw.js} +23 -7
- package/dist/question-reactions-BaatFqJ7.js +80 -0
- package/dist/{reaction-runtime-api-BjeDyKcz.js → reaction-runtime-api-YGD6hhDh.js} +10 -21
- package/dist/reaction-runtime-api.js +1 -1
- package/dist/rpc-context-dXSy4NtF.js +15 -0
- package/dist/runtime-api.js +8 -9
- package/dist/{send-B_Ks4MZr.js → send-BNl5folj.js} +14 -23
- package/dist/{send.runtime-CbuXycZT.js → send.runtime-DnMoKk27.js} +1 -1
- package/dist/{transport-detection-Da06-LOL.js → transport-detection-D_M79R5f.js} +60 -148
- package/dist/{transport-detection.runtime-D_190UUL.js → transport-detection.runtime-DKuuIHvf.js} +1 -1
- package/dist/{transport-probes.runtime-CV6Qs0Og.js → transport-probes.runtime-BsJdp0oI.js} +1 -1
- package/openclaw.plugin.json +4 -1
- package/package.json +5 -5
- package/dist/approval-resolver-B3m0D2R0.js +0 -16
- package/dist/question-reactions-Ds48FtEu.js +0 -109
- package/dist/rpc-context-CZUoE0-A.js +0 -17
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
|
|
2
|
-
import {
|
|
2
|
+
import { l as normalizeSignalTransportHost } from "./transport-policy-DxvSMHp9.js";
|
|
3
|
+
import { i as resolveSignalAccount, o as resolveSignalReplyToMode } from "./accounts-DO4HMqaK.js";
|
|
3
4
|
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
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { r as
|
|
10
|
-
import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-B_Ks4MZr.js";
|
|
5
|
+
import { c as resolveSignalReactionLevel, f as isSignalNativeApprovalHandlerConfigured, i as getOptionalSignalRuntime, l as formatSignalMediaText, n as registerSignalReplyContext, o as materializeSignalPresentationFallback } from "./transport-detection-D_M79R5f.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-YGD6hhDh.js";
|
|
8
|
+
import { i as maybeResolveSignalApprovalReaction, o as registerSignalApprovalReactionTargetForDeliveredPayload, s as resolveSignalApprovalConversationKey, t as addSignalApprovalReactionHintToStructuredPayload } from "./approval-reactions-cs3Ox9Ow.js";
|
|
9
|
+
import { r as registerSignalQuestionReactionTargetForDeliveredPayload, t as maybeResolveSignalQuestionReaction } from "./question-reactions-BaatFqJ7.js";
|
|
10
|
+
import { n as sendReadReceiptSignal, r as sendTypingSignal, t as sendMessageSignal } from "./send-BNl5folj.js";
|
|
11
11
|
import { bindIngressLifecycleToReplyOptions, createChannelIngressError, createChannelIngressMonitor, createChannelMessageReplyPipeline, resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-outbound";
|
|
12
12
|
import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
|
|
13
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 { normalizeNullableString, 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, toInboundMediaFactsWithMetadata } 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
|
-
import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
|
22
|
-
import { isRecord as isRecord$1 } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
|
20
|
+
import { ensurePortAvailable, extractErrorCode, formatErrorMessage as formatErrorMessage$1, resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/security-runtime";
|
|
23
21
|
import { resolveChannelGroupPolicy, resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy";
|
|
22
|
+
import path from "node:path";
|
|
24
23
|
import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context";
|
|
25
24
|
import { DEFAULT_GROUP_HISTORY_LIMIT, createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
26
25
|
import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
|
@@ -45,11 +44,53 @@ import { upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-ru
|
|
|
45
44
|
import { listChatCommands, maybeResolveTextAlias, normalizeCommandBody } from "openclaw/plugin-sdk/command-auth-native";
|
|
46
45
|
import { isAbortRequestText } from "openclaw/plugin-sdk/command-primitives-runtime";
|
|
47
46
|
import { resolveChannelContextVisibilityMode } from "openclaw/plugin-sdk/context-visibility-runtime";
|
|
47
|
+
import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
|
|
48
48
|
//#region extensions/signal/src/daemon.ts
|
|
49
49
|
const SIGNAL_DAEMON_STOP_KILL_TIMEOUT_MS = 1500;
|
|
50
50
|
function formatSignalDaemonExit(exit) {
|
|
51
51
|
return `signal daemon exited (source=${exit.source} code=${exit.code ?? "null"} signal=${exit.signal ?? "null"})`;
|
|
52
52
|
}
|
|
53
|
+
function formatSignalDaemonEndpoint(httpHost, httpPort) {
|
|
54
|
+
return `${httpHost.includes(":") ? `[${httpHost}]` : httpHost}:${httpPort}`;
|
|
55
|
+
}
|
|
56
|
+
async function assertSignalDaemonEndpointAvailable(params) {
|
|
57
|
+
try {
|
|
58
|
+
await ensurePortAvailable(params.httpPort, params.httpHost, params.abortSignal);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (params.abortSignal?.aborted) throw params.abortSignal.reason;
|
|
61
|
+
if (!(extractErrorCode(error) === "EADDRINUSE" || error instanceof Error && error.name === "PortInUseError")) return;
|
|
62
|
+
const endpoint = formatSignalDaemonEndpoint(params.httpHost, params.httpPort);
|
|
63
|
+
throw new Error(`Signal managed native endpoint ${endpoint} is unavailable: ${formatErrorMessage$1(error)} Stop the conflicting service, configure this Signal account with a different transport.httpPort, or use external-native for an intentionally operator-managed daemon.`, { cause: error });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function waitForSignalDaemonReady(params) {
|
|
67
|
+
await (params.waitForTransportReadyFn ?? waitForTransportReady)({
|
|
68
|
+
label: "signal daemon",
|
|
69
|
+
timeoutMs: Math.max(0, params.startupDeadlineMs - Date.now()),
|
|
70
|
+
logAfterMs: params.logAfterMs,
|
|
71
|
+
logIntervalMs: params.logIntervalMs,
|
|
72
|
+
pollIntervalMs: 150,
|
|
73
|
+
abortSignal: params.abortSignal,
|
|
74
|
+
runtime: params.runtime,
|
|
75
|
+
check: async () => {
|
|
76
|
+
const remainingMs = params.startupDeadlineMs - Date.now();
|
|
77
|
+
if (remainingMs <= 0) return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: "startup deadline exceeded"
|
|
80
|
+
};
|
|
81
|
+
const res = await signalCheck(params.baseUrl, Math.min(1e3, remainingMs));
|
|
82
|
+
if (Date.now() >= params.startupDeadlineMs) return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: "startup deadline exceeded"
|
|
85
|
+
};
|
|
86
|
+
if (res.ok) return { ok: true };
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable")
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
53
94
|
function isRecoverableSignalCliReceiveException(line) {
|
|
54
95
|
return /\breceive exception:\s+.*\binvalid PreKey message:\s+decryption failed\b/i.test(line);
|
|
55
96
|
}
|
|
@@ -72,7 +113,7 @@ function bindSignalCliOutput(params) {
|
|
|
72
113
|
function resolveSignalCliConfigPath(raw) {
|
|
73
114
|
const value = raw.trim();
|
|
74
115
|
if (value === "~") return os.homedir();
|
|
75
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) return
|
|
116
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) return path.join(os.homedir(), value.slice(2));
|
|
76
117
|
return value;
|
|
77
118
|
}
|
|
78
119
|
function buildDaemonArgs(opts) {
|
|
@@ -180,6 +221,99 @@ function spawnSignalDaemon(opts) {
|
|
|
180
221
|
};
|
|
181
222
|
}
|
|
182
223
|
//#endregion
|
|
224
|
+
//#region extensions/signal/src/daemon-lifecycle.ts
|
|
225
|
+
function createSignalDaemonLifecycle(params) {
|
|
226
|
+
let daemonHandle = null;
|
|
227
|
+
let daemonStopRequested = false;
|
|
228
|
+
let daemonStopPromise;
|
|
229
|
+
let daemonExitError;
|
|
230
|
+
const daemonAbortController = new AbortController();
|
|
231
|
+
const abortSignal = params.abortSignal ? AbortSignal.any([params.abortSignal, daemonAbortController.signal]) : daemonAbortController.signal;
|
|
232
|
+
const stop = () => {
|
|
233
|
+
if (daemonStopPromise) return daemonStopPromise;
|
|
234
|
+
daemonStopRequested = true;
|
|
235
|
+
if (!daemonAbortController.signal.aborted) daemonAbortController.abort(params.abortSignal?.reason ?? /* @__PURE__ */ new Error("Signal monitor stopped"));
|
|
236
|
+
daemonStopPromise = daemonHandle?.stop() ?? Promise.resolve();
|
|
237
|
+
return daemonStopPromise;
|
|
238
|
+
};
|
|
239
|
+
const attach = (handle) => {
|
|
240
|
+
daemonHandle = handle;
|
|
241
|
+
handle.exited.then((exit) => {
|
|
242
|
+
if (daemonStopRequested || params.abortSignal?.aborted) return;
|
|
243
|
+
daemonExitError = new Error(formatSignalDaemonExit(exit));
|
|
244
|
+
if (!daemonAbortController.signal.aborted) daemonAbortController.abort(daemonExitError);
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
return {
|
|
248
|
+
attach,
|
|
249
|
+
stop,
|
|
250
|
+
getExitError: () => daemonExitError,
|
|
251
|
+
abortSignal
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region extensions/signal/src/native-reply.ts
|
|
256
|
+
function resolveSignalNativeReplyId(params) {
|
|
257
|
+
if (params.payload.replyToCurrent === false) return;
|
|
258
|
+
const payloadReplyToId = normalizeOptionalString(params.payload.replyToId);
|
|
259
|
+
const isExplicitCurrentReply = params.payload.replyToTag === true || params.payload.replyToCurrent === true;
|
|
260
|
+
if (!payloadReplyToId && !isExplicitCurrentReply && params.replyContext?.allowImplicitCurrentMessage === false) return;
|
|
261
|
+
const contextReplyToId = normalizeOptionalString(params.replyContext?.replyToId);
|
|
262
|
+
if (!contextReplyToId || payloadReplyToId && payloadReplyToId !== contextReplyToId) return;
|
|
263
|
+
return payloadReplyToId ?? contextReplyToId;
|
|
264
|
+
}
|
|
265
|
+
function isSignalStatusNoticePayload(payload) {
|
|
266
|
+
return Boolean(payload.isCompactionNotice || payload.isFallbackNotice || payload.isStatusNotice);
|
|
267
|
+
}
|
|
268
|
+
function createSignalNativeReplyIdPlan(params) {
|
|
269
|
+
const replyToId = resolveSignalNativeReplyId(params);
|
|
270
|
+
if (!replyToId) return {
|
|
271
|
+
peek: () => void 0,
|
|
272
|
+
use: () => void 0,
|
|
273
|
+
markSent: () => void 0
|
|
274
|
+
};
|
|
275
|
+
const isExplicitReply = params.payload.replyToTag === true || params.payload.replyToCurrent === true;
|
|
276
|
+
if (isSignalStatusNoticePayload(params.payload)) {
|
|
277
|
+
const resolve = params.replyToMode === "off" ? () => void 0 : () => replyToId;
|
|
278
|
+
return {
|
|
279
|
+
peek: resolve,
|
|
280
|
+
use: resolve,
|
|
281
|
+
markSent: () => void 0
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (isExplicitReply) {
|
|
285
|
+
const resolve = () => replyToId;
|
|
286
|
+
return {
|
|
287
|
+
peek: resolve,
|
|
288
|
+
use: resolve,
|
|
289
|
+
markSent: () => void 0
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const planner = createReplyReferencePlanner({
|
|
293
|
+
replyToMode: params.replyToMode,
|
|
294
|
+
existingId: replyToId,
|
|
295
|
+
hasReplied: params.replyContext?.state?.hasReplied
|
|
296
|
+
});
|
|
297
|
+
const syncState = () => {
|
|
298
|
+
if (params.replyContext?.state) params.replyContext.state.hasReplied = planner.hasReplied();
|
|
299
|
+
};
|
|
300
|
+
return {
|
|
301
|
+
peek: () => planner.peek(),
|
|
302
|
+
use: () => {
|
|
303
|
+
const nextReplyToId = planner.use();
|
|
304
|
+
syncState();
|
|
305
|
+
return nextReplyToId;
|
|
306
|
+
},
|
|
307
|
+
markSent: () => {
|
|
308
|
+
planner.markSent();
|
|
309
|
+
syncState();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function createSignalNativeReplyIdResolver(params) {
|
|
314
|
+
return createSignalNativeReplyIdPlan(params).use;
|
|
315
|
+
}
|
|
316
|
+
//#endregion
|
|
183
317
|
//#region extensions/signal/src/monitor/access-policy.ts
|
|
184
318
|
const SIGNAL_UUID_KIND = "plugin:signal-uuid";
|
|
185
319
|
const SIGNAL_GROUP_KIND = "plugin:signal-group";
|
|
@@ -274,6 +408,7 @@ async function resolveSignalAccessState(params) {
|
|
|
274
408
|
kind: isGroup ? "group" : "direct",
|
|
275
409
|
id: isGroup ? params.groupId ?? "unknown" : params.sender.raw
|
|
276
410
|
},
|
|
411
|
+
contextBinding: params.contextBinding,
|
|
277
412
|
...isGroup ? { event: { mayPair: false } } : {},
|
|
278
413
|
dmPolicy: params.dmPolicy,
|
|
279
414
|
groupPolicy: params.groupPolicy,
|
|
@@ -524,8 +659,7 @@ function resolveSignalStatusReactionTimestamp(params) {
|
|
|
524
659
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
525
660
|
}
|
|
526
661
|
function hasSignalStatusReplyDeliveryFailure(result) {
|
|
527
|
-
|
|
528
|
-
return (failedCounts?.tool ?? 0) > 0 || (failedCounts?.block ?? 0) > 0 || (failedCounts?.final ?? 0) > 0;
|
|
662
|
+
return Object.values(result.settledReceipt?.counts ?? {}).some((counts) => counts.failedBeforeSend > 0 || counts.failedAfterSend > 0);
|
|
529
663
|
}
|
|
530
664
|
function resolveSignalStatusReactionEmojis(emojis) {
|
|
531
665
|
if (emojis?.stallHard !== void 0) return emojis;
|
|
@@ -606,7 +740,7 @@ function createSignalEventHandler(deps) {
|
|
|
606
740
|
});
|
|
607
741
|
const replyThreading = resolveBatchedReplyThreadingPolicy(replyToMode, entry.isBatched === true);
|
|
608
742
|
const media = await toInboundMediaFactsWithMetadata(entry.media);
|
|
609
|
-
const ctxPayload = buildChannelInboundEventContext({
|
|
743
|
+
const ctxPayload = (deps.channelRuntime?.inbound.buildContext ?? buildChannelInboundEventContext)({
|
|
610
744
|
channel: "signal",
|
|
611
745
|
supplemental: { quote: entry.replyToBody ? {
|
|
612
746
|
body: entry.replyToBody,
|
|
@@ -650,6 +784,7 @@ function createSignalEventHandler(deps) {
|
|
|
650
784
|
} } : {},
|
|
651
785
|
commands: { authorized: entry.commandAuthorized }
|
|
652
786
|
},
|
|
787
|
+
channelIngress: entry.channelIngress,
|
|
653
788
|
media,
|
|
654
789
|
extra: {
|
|
655
790
|
GroupSubject: entry.isGroup ? entry.groupName ?? void 0 : void 0,
|
|
@@ -742,6 +877,7 @@ function createSignalEventHandler(deps) {
|
|
|
742
877
|
};
|
|
743
878
|
const dispatcherOptions = {
|
|
744
879
|
...replyPipeline,
|
|
880
|
+
propagateRetryableNoSendFailure: true,
|
|
745
881
|
humanDelay: resolveHumanDelayConfig(deps.cfg, route.agentId),
|
|
746
882
|
typingCallbacks
|
|
747
883
|
};
|
|
@@ -762,6 +898,32 @@ function createSignalEventHandler(deps) {
|
|
|
762
898
|
chatType: entry.isGroup ? "group" : "direct"
|
|
763
899
|
});
|
|
764
900
|
},
|
|
901
|
+
durable: (payload, info) => {
|
|
902
|
+
if (info.kind !== "final") return false;
|
|
903
|
+
const replyPlan = createSignalNativeReplyIdPlan({
|
|
904
|
+
payload,
|
|
905
|
+
replyContext: nativeReplyContext,
|
|
906
|
+
replyToMode
|
|
907
|
+
});
|
|
908
|
+
const send = async (to, text, options) => {
|
|
909
|
+
entry.turnAdoptionLifecycle?.abortSignal.throwIfAborted();
|
|
910
|
+
deps.abortSignal?.throwIfAborted();
|
|
911
|
+
const result = await sendMessageSignal(to, text, {
|
|
912
|
+
...options,
|
|
913
|
+
baseUrl: deps.baseUrl,
|
|
914
|
+
account: deps.account,
|
|
915
|
+
maxBytes: deps.mediaMaxBytes,
|
|
916
|
+
accountId: deps.accountId
|
|
917
|
+
});
|
|
918
|
+
replyPlan.markSent();
|
|
919
|
+
return result;
|
|
920
|
+
};
|
|
921
|
+
return {
|
|
922
|
+
deps: { signal: send },
|
|
923
|
+
replyToId: replyPlan.peek() ?? null,
|
|
924
|
+
replyToMode
|
|
925
|
+
};
|
|
926
|
+
},
|
|
765
927
|
onError: (err, info) => {
|
|
766
928
|
deps.runtime.error?.(danger(`signal ${info.kind} reply failed: ${String(err)}`));
|
|
767
929
|
}
|
|
@@ -790,6 +952,7 @@ function createSignalEventHandler(deps) {
|
|
|
790
952
|
sessionKey: route.sessionKey
|
|
791
953
|
},
|
|
792
954
|
ctxPayload,
|
|
955
|
+
dispatchReplyFromConfig: deps.channelRuntime?.reply?.dispatchReplyFromConfig,
|
|
793
956
|
record: {
|
|
794
957
|
updateLastRoute: !entry.isGroup ? {
|
|
795
958
|
sessionKey: inboundLastRouteSessionKey,
|
|
@@ -838,13 +1001,16 @@ function createSignalEventHandler(deps) {
|
|
|
838
1001
|
onToolStart: async (payload) => {
|
|
839
1002
|
const toolName = payload.name?.trim();
|
|
840
1003
|
if (toolName) await statusReactionController.setTool(toolName);
|
|
1004
|
+
return false;
|
|
841
1005
|
},
|
|
842
1006
|
onCompactionStart: async () => {
|
|
843
1007
|
await statusReactionController.setCompacting();
|
|
1008
|
+
return false;
|
|
844
1009
|
},
|
|
845
1010
|
onCompactionEnd: async () => {
|
|
846
1011
|
statusReactionController.cancelPending();
|
|
847
1012
|
await statusReactionController.setThinking();
|
|
1013
|
+
return false;
|
|
848
1014
|
}
|
|
849
1015
|
} : {},
|
|
850
1016
|
onModelSelected
|
|
@@ -854,9 +1020,10 @@ function createSignalEventHandler(deps) {
|
|
|
854
1020
|
if (!statusReactionController) return;
|
|
855
1021
|
const hasFinalResponse = result.dispatched && hasVisibleInboundReplyDispatch(result.dispatchResult);
|
|
856
1022
|
const hasDeliveryFailure = result.dispatched && hasSignalStatusReplyDeliveryFailure(result.dispatchResult);
|
|
1023
|
+
const hasAgentRunFailure = result.dispatched && readAgentRunTerminalOutcome(result.dispatchResult) === "failed";
|
|
857
1024
|
finalizeSignalStatusReaction({
|
|
858
1025
|
controller: statusReactionController,
|
|
859
|
-
outcome: hasFinalResponse && !hasDeliveryFailure ? "done" : "error"
|
|
1026
|
+
outcome: hasFinalResponse && !hasDeliveryFailure && !hasAgentRunFailure ? "done" : "error"
|
|
860
1027
|
}).catch((err) => {
|
|
861
1028
|
logVerbose(`signal: status reaction finalize failed: ${String(err)}`);
|
|
862
1029
|
});
|
|
@@ -867,9 +1034,16 @@ function createSignalEventHandler(deps) {
|
|
|
867
1034
|
async function flushSignalInboundEntries(entries, lifecycle, settle) {
|
|
868
1035
|
const last = entries.at(-1);
|
|
869
1036
|
if (!last) return;
|
|
1037
|
+
const channelIngress = await resolveSignalBatchChannelIngress(entries, last);
|
|
1038
|
+
if (channelIngress.some((entry) => !entry.senderAccess.allowed)) {
|
|
1039
|
+
logVerbose("signal: authorization changed before dispatch");
|
|
1040
|
+
await settle();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
870
1043
|
if (entries.length === 1) {
|
|
871
1044
|
await handleSignalInboundMessage({
|
|
872
1045
|
...last,
|
|
1046
|
+
channelIngress,
|
|
873
1047
|
turnAdoptionLifecycle: lifecycle
|
|
874
1048
|
});
|
|
875
1049
|
await settle();
|
|
@@ -888,10 +1062,30 @@ function createSignalEventHandler(deps) {
|
|
|
888
1062
|
turnAdoptionLifecycle: lifecycle,
|
|
889
1063
|
isBatched: true,
|
|
890
1064
|
nativeReplyBody: last.nativeReplyBody ?? last.bodyText,
|
|
891
|
-
media: entries.flatMap((entry) => entry.media ?? [])
|
|
1065
|
+
media: entries.flatMap((entry) => entry.media ?? []),
|
|
1066
|
+
channelIngress
|
|
892
1067
|
});
|
|
893
1068
|
await settle();
|
|
894
1069
|
}
|
|
1070
|
+
async function resolveSignalBatchChannelIngress(entries, last) {
|
|
1071
|
+
if (last.boundChannelIngress) return last.boundChannelIngress;
|
|
1072
|
+
const route = resolveSignalInboundRoute({
|
|
1073
|
+
cfg: deps.cfg,
|
|
1074
|
+
accountId: deps.accountId,
|
|
1075
|
+
isGroup: last.isGroup,
|
|
1076
|
+
groupId: last.groupId,
|
|
1077
|
+
senderPeerId: last.senderPeerId
|
|
1078
|
+
});
|
|
1079
|
+
const contextBinding = {
|
|
1080
|
+
agentId: route.agentId,
|
|
1081
|
+
sessionKey: route.sessionKey,
|
|
1082
|
+
...last.messageId ? { messageId: last.messageId } : {},
|
|
1083
|
+
inboundEventKind: "user_request"
|
|
1084
|
+
};
|
|
1085
|
+
const resolved = await Promise.all(entries.flatMap((entry) => entry.resolveChannelIngress ? [entry.resolveChannelIngress(contextBinding)] : (entry.channelIngress ?? []).map(async (ingress) => ingress)));
|
|
1086
|
+
last.boundChannelIngress = resolved;
|
|
1087
|
+
return resolved;
|
|
1088
|
+
}
|
|
895
1089
|
async function retrySignalInboundFlush(entries, lifecycle, settle, initialError) {
|
|
896
1090
|
let lastError = initialError;
|
|
897
1091
|
for (const [attemptIndex, delayMs] of RETRYABLE_FLUSH_RETRY_DELAYS_MS.entries()) {
|
|
@@ -930,7 +1124,7 @@ function createSignalEventHandler(deps) {
|
|
|
930
1124
|
if (!isSignalReplySessionInitConflictError(err)) throw err;
|
|
931
1125
|
if (deps.abortSignal?.aborted) return;
|
|
932
1126
|
await retrySignalInboundFlush(entries, admissionLifecycle, settle, err).catch(async (terminalError) => {
|
|
933
|
-
await
|
|
1127
|
+
await lifecycle?.onAbandoned();
|
|
934
1128
|
throw terminalError;
|
|
935
1129
|
});
|
|
936
1130
|
}
|
|
@@ -1017,6 +1211,7 @@ function createSignalEventHandler(deps) {
|
|
|
1017
1211
|
if (!deps.shouldEmitSignalReactionNotification({
|
|
1018
1212
|
mode: deps.reactionMode,
|
|
1019
1213
|
account: deps.account,
|
|
1214
|
+
accountUuid: deps.accountUuid,
|
|
1020
1215
|
targets,
|
|
1021
1216
|
sender: params.sender,
|
|
1022
1217
|
allowlist: deps.reactionAllowlist
|
|
@@ -1052,10 +1247,10 @@ function createSignalEventHandler(deps) {
|
|
|
1052
1247
|
});
|
|
1053
1248
|
return true;
|
|
1054
1249
|
}
|
|
1055
|
-
return async (event, turnAdoptionLifecycle) => {
|
|
1250
|
+
return async (event, turnAdoptionLifecycle, preparedPayload) => {
|
|
1056
1251
|
if (event.event !== "receive" || !event.data) return;
|
|
1057
|
-
let payload;
|
|
1058
|
-
try {
|
|
1252
|
+
let payload = preparedPayload ?? null;
|
|
1253
|
+
if (!preparedPayload) try {
|
|
1059
1254
|
payload = JSON.parse(event.data);
|
|
1060
1255
|
} catch (err) {
|
|
1061
1256
|
deps.runtime.error?.(`failed to parse event: ${String(err)}`);
|
|
@@ -1077,7 +1272,7 @@ function createSignalEventHandler(deps) {
|
|
|
1077
1272
|
const isGroup = Boolean(groupId);
|
|
1078
1273
|
const hasControlCommandInMessage = isControlCommandMessage(messageText, deps.cfg);
|
|
1079
1274
|
const senderDisplay = formatSignalSenderDisplay(sender);
|
|
1080
|
-
const
|
|
1275
|
+
const resolveChannelIngress = async (contextBinding) => await resolveSignalAccessState({
|
|
1081
1276
|
accountId: deps.accountId,
|
|
1082
1277
|
dmPolicy: deps.dmPolicy,
|
|
1083
1278
|
groupPolicy: deps.groupPolicy,
|
|
@@ -1087,8 +1282,10 @@ function createSignalEventHandler(deps) {
|
|
|
1087
1282
|
groupId,
|
|
1088
1283
|
isGroup,
|
|
1089
1284
|
cfg: deps.cfg,
|
|
1090
|
-
hasControlCommand: hasControlCommandInMessage
|
|
1285
|
+
hasControlCommand: hasControlCommandInMessage,
|
|
1286
|
+
contextBinding
|
|
1091
1287
|
});
|
|
1288
|
+
const { senderAccess, commandAccess } = await resolveChannelIngress();
|
|
1092
1289
|
const quoteText = normalizeOptionalString(dataMessage?.quote?.text) ?? "";
|
|
1093
1290
|
const { contextVisibilityMode, quoteSenderAllowed, visibleQuoteText, visibleQuoteSender } = resolveSignalQuoteContext({
|
|
1094
1291
|
cfg: deps.cfg,
|
|
@@ -1218,8 +1415,8 @@ function createSignalEventHandler(deps) {
|
|
|
1218
1415
|
sender: envelope.sourceName ?? senderDisplay,
|
|
1219
1416
|
body: messageText || visibleQuoteText,
|
|
1220
1417
|
media: toHistoryMediaEntries(pendingMedia),
|
|
1221
|
-
timestamp:
|
|
1222
|
-
messageId
|
|
1418
|
+
timestamp: inboundTimestamp,
|
|
1419
|
+
messageId
|
|
1223
1420
|
}
|
|
1224
1421
|
});
|
|
1225
1422
|
await registerSignalReplyContext({
|
|
@@ -1349,7 +1546,8 @@ function createSignalEventHandler(deps) {
|
|
|
1349
1546
|
replyToBody: visibleQuoteText || void 0,
|
|
1350
1547
|
replyToSender: visibleQuoteSender,
|
|
1351
1548
|
replyToIsQuote: visibleQuoteText ? true : void 0,
|
|
1352
|
-
turnAdoptionLifecycle
|
|
1549
|
+
turnAdoptionLifecycle,
|
|
1550
|
+
resolveChannelIngress
|
|
1353
1551
|
};
|
|
1354
1552
|
pendingInboundRegistry.cancelPendingOnAbort(entry, debouncer.cancelKey);
|
|
1355
1553
|
const inboundLane = resolveSignalControlLaneKey(deps.accountId, entry) ? controlDebouncer : debouncer;
|
|
@@ -1374,9 +1572,9 @@ function registerSignalReactionTargetsForDeliveredPayload(params) {
|
|
|
1374
1572
|
const SIGNAL_INGRESS_DRAIN_INTERVAL_MS = 1e3;
|
|
1375
1573
|
const SignalIngressPermanentError = createChannelIngressError("SignalIngressPermanentError", { withReason: true });
|
|
1376
1574
|
function normalizeTimestamp(value) {
|
|
1377
|
-
return
|
|
1575
|
+
return asPositiveSafeInteger(value) ?? null;
|
|
1378
1576
|
}
|
|
1379
|
-
function
|
|
1577
|
+
function parseReceivePayload(event) {
|
|
1380
1578
|
if (event.event !== "receive" || !event.data) return null;
|
|
1381
1579
|
let parsed;
|
|
1382
1580
|
try {
|
|
@@ -1384,18 +1582,19 @@ function parseReceiveEnvelope(event) {
|
|
|
1384
1582
|
} catch (error) {
|
|
1385
1583
|
throw new SignalIngressPermanentError("parse-error", "Signal receive event contains invalid JSON", { cause: error });
|
|
1386
1584
|
}
|
|
1387
|
-
if (!isRecord
|
|
1388
|
-
return
|
|
1585
|
+
if (!isRecord(parsed)) throw new SignalIngressPermanentError("parse-error", "Signal receive event must contain a JSON object");
|
|
1586
|
+
return parsed;
|
|
1389
1587
|
}
|
|
1390
1588
|
function resolveDataMessage(envelope) {
|
|
1391
|
-
if (isRecord
|
|
1392
|
-
return isRecord
|
|
1589
|
+
if (isRecord(envelope.dataMessage)) return envelope.dataMessage;
|
|
1590
|
+
return isRecord(envelope.editMessage?.dataMessage) ? envelope.editMessage.dataMessage : null;
|
|
1393
1591
|
}
|
|
1394
|
-
function inspectSignalIngressEvent(
|
|
1395
|
-
const
|
|
1592
|
+
function inspectSignalIngressEvent(prepared) {
|
|
1593
|
+
const payload = prepared[1] ??= parseReceivePayload(prepared[0]);
|
|
1594
|
+
const envelope = isRecord(payload?.envelope) ? payload.envelope : null;
|
|
1396
1595
|
if (!envelope || "syncMessage" in envelope) return null;
|
|
1397
1596
|
const dataMessage = resolveDataMessage(envelope);
|
|
1398
|
-
const reactionMessage = isRecord
|
|
1597
|
+
const reactionMessage = isRecord(envelope.reactionMessage) ? envelope.reactionMessage : null;
|
|
1399
1598
|
if (!dataMessage && !reactionMessage) return null;
|
|
1400
1599
|
const senderUuid = normalizeNullableString(envelope.sourceUuid);
|
|
1401
1600
|
const senderNumber = normalizeNullableString(envelope.sourceNumber);
|
|
@@ -1403,12 +1602,13 @@ function inspectSignalIngressEvent(event) {
|
|
|
1403
1602
|
if (!senderKey) throw new SignalIngressPermanentError("missing-sender", "Signal dispatchable envelope is missing sourceUuid/sourceNumber");
|
|
1404
1603
|
const timestamp = normalizeTimestamp(envelope.timestamp) ?? normalizeTimestamp(dataMessage?.timestamp);
|
|
1405
1604
|
if (timestamp === null) throw new SignalIngressPermanentError("missing-timestamp", "Signal dispatchable envelope is missing a stable timestamp");
|
|
1406
|
-
const dataGroup = isRecord
|
|
1407
|
-
const reactionGroup = isRecord
|
|
1605
|
+
const dataGroup = isRecord(dataMessage?.groupInfo) ? dataMessage.groupInfo : null;
|
|
1606
|
+
const reactionGroup = isRecord(reactionMessage?.groupInfo) ? reactionMessage.groupInfo : null;
|
|
1408
1607
|
const groupId = normalizeNullableString(dataGroup?.groupId) ?? normalizeNullableString(reactionGroup?.groupId);
|
|
1409
1608
|
return {
|
|
1410
1609
|
eventId: JSON.stringify([senderKey, timestamp]),
|
|
1411
|
-
laneKey: groupId ? `group:${groupId}` : `direct:${senderKey}
|
|
1610
|
+
laneKey: groupId ? `group:${groupId}` : `direct:${senderKey}`,
|
|
1611
|
+
...senderUuid && senderNumber ? { numberAliasEventId: JSON.stringify([`number:${senderNumber}`, timestamp]) } : {}
|
|
1412
1612
|
};
|
|
1413
1613
|
}
|
|
1414
1614
|
function resolveSignalIngressNonRetryableFailure(error) {
|
|
@@ -1425,16 +1625,17 @@ async function startSignalIngressMonitor(params) {
|
|
|
1425
1625
|
if (!pluginRuntime) throw new Error("Signal runtime not initialized for durable ingress");
|
|
1426
1626
|
queue = pluginRuntime.state.openChannelIngressQueue({ accountId: params.accountId });
|
|
1427
1627
|
}
|
|
1628
|
+
const ingressQueue = queue;
|
|
1428
1629
|
const monitor = createChannelIngressMonitor({
|
|
1429
|
-
queue,
|
|
1430
|
-
inspect: (
|
|
1630
|
+
queue: ingressQueue,
|
|
1631
|
+
inspect: (prepared) => inspectSignalIngressEvent(prepared),
|
|
1431
1632
|
payload: {
|
|
1432
1633
|
version: 1,
|
|
1433
|
-
serialize: (event, { receivedAt }) => ({
|
|
1634
|
+
serialize: ([event], { receivedAt }) => ({
|
|
1434
1635
|
receivedAt,
|
|
1435
1636
|
event
|
|
1436
1637
|
}),
|
|
1437
|
-
deserialize: (body) => body.event,
|
|
1638
|
+
deserialize: (body) => [body.event, void 0],
|
|
1438
1639
|
encode: ({ body }) => ({
|
|
1439
1640
|
version: 1,
|
|
1440
1641
|
...body
|
|
@@ -1445,11 +1646,16 @@ async function startSignalIngressMonitor(params) {
|
|
|
1445
1646
|
}),
|
|
1446
1647
|
createClaimError: (_kind, claim) => new SignalIngressPermanentError("unsupported-event", `Signal ingress row ${claim.id} has an invalid payload`)
|
|
1447
1648
|
},
|
|
1448
|
-
deliver: (event, lifecycle) => params.dispatch(event, lifecycle),
|
|
1649
|
+
deliver: ([event, parsedPayload], lifecycle) => parsedPayload ? params.dispatch(event, lifecycle, parsedPayload) : void 0,
|
|
1650
|
+
onDurableAdmission: async (_event, { facts }) => {
|
|
1651
|
+
const { numberAliasEventId } = facts;
|
|
1652
|
+
if (!numberAliasEventId) return;
|
|
1653
|
+
if (!await ingressQueue.complete(numberAliasEventId)) await ingressQueue.complete(facts.eventId);
|
|
1654
|
+
},
|
|
1449
1655
|
pollIntervalMs: SIGNAL_INGRESS_DRAIN_INTERVAL_MS,
|
|
1450
1656
|
retention: {
|
|
1451
1657
|
pruneIntervalMs: 0,
|
|
1452
|
-
completedMaxEntries:
|
|
1658
|
+
completedMaxEntries: 2e3,
|
|
1453
1659
|
failedMaxEntries: 1e3
|
|
1454
1660
|
},
|
|
1455
1661
|
appendRetryDelaysMs: [0],
|
|
@@ -1462,7 +1668,7 @@ async function startSignalIngressMonitor(params) {
|
|
|
1462
1668
|
monitor.start();
|
|
1463
1669
|
return {
|
|
1464
1670
|
receive: async (event) => {
|
|
1465
|
-
await monitor.admit(event);
|
|
1671
|
+
await monitor.admit([event, void 0]);
|
|
1466
1672
|
await monitor.waitForPumpIdle();
|
|
1467
1673
|
},
|
|
1468
1674
|
stop: monitor.stop,
|
|
@@ -1477,7 +1683,14 @@ const DEFAULT_RECONNECT_POLICY = {
|
|
|
1477
1683
|
factor: 2,
|
|
1478
1684
|
jitter: .2
|
|
1479
1685
|
};
|
|
1480
|
-
|
|
1686
|
+
function publishSignalRecovering(statusSink, lastError) {
|
|
1687
|
+
statusSink?.({
|
|
1688
|
+
connected: false,
|
|
1689
|
+
lifecycle: "recovering",
|
|
1690
|
+
...lastError ? { lastError } : {}
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEvent, timeoutMs, transportKind, policy, statusSink }) {
|
|
1481
1694
|
const reconnectPolicy = {
|
|
1482
1695
|
...DEFAULT_RECONNECT_POLICY,
|
|
1483
1696
|
...policy
|
|
@@ -1496,6 +1709,9 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
|
|
|
1496
1709
|
abortSignal,
|
|
1497
1710
|
timeoutMs,
|
|
1498
1711
|
transportKind,
|
|
1712
|
+
onStreamOpen: () => {
|
|
1713
|
+
statusSink?.(channelReadyPatch());
|
|
1714
|
+
},
|
|
1499
1715
|
onEvent: async (event) => {
|
|
1500
1716
|
reconnectAttempts = 0;
|
|
1501
1717
|
await onEvent(event);
|
|
@@ -1506,6 +1722,7 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
|
|
|
1506
1722
|
}
|
|
1507
1723
|
});
|
|
1508
1724
|
if (abortSignal?.aborted) return;
|
|
1725
|
+
publishSignalRecovering(statusSink);
|
|
1509
1726
|
reconnectAttempts += 1;
|
|
1510
1727
|
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
|
|
1511
1728
|
logReconnectVerbose(`Signal stream ended, reconnecting in ${delayMs / 1e3}s...`);
|
|
@@ -1513,6 +1730,7 @@ async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEven
|
|
|
1513
1730
|
} catch (err) {
|
|
1514
1731
|
if (abortSignal?.aborted) return;
|
|
1515
1732
|
runtime.error?.(`Signal stream error: ${String(err)}`);
|
|
1733
|
+
publishSignalRecovering(statusSink, String(err));
|
|
1516
1734
|
reconnectAttempts += 1;
|
|
1517
1735
|
const delayMs = computeBackoff(reconnectPolicy, reconnectAttempts);
|
|
1518
1736
|
runtime.log?.(`Signal connection lost, reconnecting in ${delayMs / 1e3}s...`);
|
|
@@ -1546,36 +1764,6 @@ function createSignalMonitorTaskRunner(runtime) {
|
|
|
1546
1764
|
}
|
|
1547
1765
|
};
|
|
1548
1766
|
}
|
|
1549
|
-
function createSignalDaemonLifecycle(params) {
|
|
1550
|
-
let daemonHandle = null;
|
|
1551
|
-
let daemonStopRequested = false;
|
|
1552
|
-
let daemonStopPromise;
|
|
1553
|
-
let daemonExitError;
|
|
1554
|
-
const daemonAbortController = new AbortController();
|
|
1555
|
-
const abortSignal = params.abortSignal ? AbortSignal.any([params.abortSignal, daemonAbortController.signal]) : daemonAbortController.signal;
|
|
1556
|
-
const stop = () => {
|
|
1557
|
-
if (daemonStopPromise) return daemonStopPromise;
|
|
1558
|
-
daemonStopRequested = true;
|
|
1559
|
-
if (!daemonAbortController.signal.aborted) daemonAbortController.abort(params.abortSignal?.reason ?? /* @__PURE__ */ new Error("Signal monitor stopped"));
|
|
1560
|
-
daemonStopPromise = daemonHandle?.stop() ?? Promise.resolve();
|
|
1561
|
-
return daemonStopPromise;
|
|
1562
|
-
};
|
|
1563
|
-
const attach = (handle) => {
|
|
1564
|
-
daemonHandle = handle;
|
|
1565
|
-
handle.exited.then((exit) => {
|
|
1566
|
-
if (daemonStopRequested || params.abortSignal?.aborted) return;
|
|
1567
|
-
daemonExitError = new Error(formatSignalDaemonExit(exit));
|
|
1568
|
-
if (!daemonAbortController.signal.aborted) daemonAbortController.abort(daemonExitError);
|
|
1569
|
-
});
|
|
1570
|
-
};
|
|
1571
|
-
const getExitError = () => daemonExitError;
|
|
1572
|
-
return {
|
|
1573
|
-
attach,
|
|
1574
|
-
stop,
|
|
1575
|
-
getExitError,
|
|
1576
|
-
abortSignal
|
|
1577
|
-
};
|
|
1578
|
-
}
|
|
1579
1767
|
function resolveSignalReactionTargets(reaction) {
|
|
1580
1768
|
const targets = [];
|
|
1581
1769
|
const uuid = reaction.targetAuthorUuid?.trim();
|
|
@@ -1603,16 +1791,17 @@ function isSignalReactionMessage(reaction) {
|
|
|
1603
1791
|
return Boolean(emoji && typeof timestamp === "number" && timestamp > 0 && hasTarget);
|
|
1604
1792
|
}
|
|
1605
1793
|
function shouldEmitSignalReactionNotification(params) {
|
|
1606
|
-
const { mode, account, targets, sender, allowlist } = params;
|
|
1794
|
+
const { mode, account, accountUuid, targets, sender, allowlist } = params;
|
|
1607
1795
|
const effectiveMode = mode ?? "own";
|
|
1608
1796
|
if (effectiveMode === "off") return false;
|
|
1609
1797
|
if (effectiveMode === "own") {
|
|
1610
|
-
const accountId = account
|
|
1611
|
-
|
|
1612
|
-
|
|
1798
|
+
const accountId = normalizeOptionalString(account);
|
|
1799
|
+
const normalizedAccountUuid = normalizeOptionalString(accountUuid);
|
|
1800
|
+
if (!accountId && !normalizedAccountUuid || !targets || targets.length === 0) return false;
|
|
1801
|
+
const normalizedAccount = accountId ? normalizeE164(accountId) : void 0;
|
|
1613
1802
|
return targets.some((target) => {
|
|
1614
|
-
if (target.kind === "uuid") return accountId === target.id ||
|
|
1615
|
-
return normalizedAccount === target.id;
|
|
1803
|
+
if (target.kind === "uuid") return [accountId, normalizedAccountUuid].some((candidate) => candidate === target.id || candidate === `uuid:${target.id}`);
|
|
1804
|
+
return Boolean(normalizedAccount) && normalizedAccount === target.id;
|
|
1616
1805
|
});
|
|
1617
1806
|
}
|
|
1618
1807
|
if (effectiveMode === "allowlist") {
|
|
@@ -1626,25 +1815,6 @@ function buildSignalReactionSystemEventText(params) {
|
|
|
1626
1815
|
const withTarget = params.targetLabel ? `${base} from ${params.targetLabel}` : base;
|
|
1627
1816
|
return params.groupLabel ? `${withTarget} in ${params.groupLabel}` : withTarget;
|
|
1628
1817
|
}
|
|
1629
|
-
async function waitForSignalDaemonReady(params) {
|
|
1630
|
-
await (params.waitForTransportReadyFn ?? waitForTransportReady)({
|
|
1631
|
-
label: "signal daemon",
|
|
1632
|
-
timeoutMs: params.timeoutMs,
|
|
1633
|
-
logAfterMs: params.logAfterMs,
|
|
1634
|
-
logIntervalMs: params.logIntervalMs,
|
|
1635
|
-
pollIntervalMs: 150,
|
|
1636
|
-
abortSignal: params.abortSignal,
|
|
1637
|
-
runtime: params.runtime,
|
|
1638
|
-
check: async () => {
|
|
1639
|
-
const res = await signalCheck(params.baseUrl, 1e3);
|
|
1640
|
-
if (res.ok) return { ok: true };
|
|
1641
|
-
return {
|
|
1642
|
-
ok: false,
|
|
1643
|
-
error: res.error ?? (res.status ? `HTTP ${res.status}` : "unreachable")
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
});
|
|
1647
|
-
}
|
|
1648
1818
|
const SIGNAL_ATTACHMENT_RPC_RESPONSE_HEADROOM_BYTES = 64 * 1024;
|
|
1649
1819
|
const SIGNAL_BASE64_OVERHEAD_NUMERATOR = 4;
|
|
1650
1820
|
const SIGNAL_BASE64_OVERHEAD_DENOMINATOR = 3;
|
|
@@ -1756,45 +1926,19 @@ async function deliverReplies(params) {
|
|
|
1756
1926
|
}
|
|
1757
1927
|
}
|
|
1758
1928
|
}
|
|
1759
|
-
function resolveSignalNativeReplyOptions(params) {
|
|
1760
|
-
if (params.payload.replyToCurrent === false) return {};
|
|
1761
|
-
const payloadReplyToId = normalizeOptionalString(params.payload.replyToId);
|
|
1762
|
-
const isExplicitCurrentReply = params.payload.replyToTag === true || params.payload.replyToCurrent === true;
|
|
1763
|
-
if (!payloadReplyToId && !isExplicitCurrentReply && params.replyContext?.allowImplicitCurrentMessage === false) return {};
|
|
1764
|
-
const contextReplyToId = normalizeOptionalString(params.replyContext?.replyToId);
|
|
1765
|
-
if (!contextReplyToId || payloadReplyToId && payloadReplyToId !== contextReplyToId) return {};
|
|
1766
|
-
const replyToId = payloadReplyToId ?? contextReplyToId;
|
|
1767
|
-
const replyToAuthor = normalizeOptionalString(params.replyContext?.author);
|
|
1768
|
-
if (!replyToAuthor) return { replyToId };
|
|
1769
|
-
return {
|
|
1770
|
-
replyToId,
|
|
1771
|
-
replyToAuthor,
|
|
1772
|
-
replyToBody: params.replyContext?.body ?? ""
|
|
1773
|
-
};
|
|
1774
|
-
}
|
|
1775
|
-
function isSignalStatusNoticePayload(payload) {
|
|
1776
|
-
return Boolean(payload.isCompactionNotice || payload.isFallbackNotice || payload.isStatusNotice);
|
|
1777
|
-
}
|
|
1778
1929
|
function createSignalNativeReplyResolver(params) {
|
|
1779
|
-
const
|
|
1780
|
-
if (!nativeReply.replyToId) return () => ({});
|
|
1781
|
-
const isExplicitReply = params.payload.replyToTag === true || params.payload.replyToCurrent === true;
|
|
1782
|
-
const isStatusNotice = isSignalStatusNoticePayload(params.payload);
|
|
1783
|
-
if (isStatusNotice && params.replyToMode === "off") return () => ({});
|
|
1784
|
-
if (isExplicitReply) return () => nativeReply;
|
|
1785
|
-
if (isStatusNotice) return () => nativeReply;
|
|
1786
|
-
const planner = createReplyReferencePlanner({
|
|
1787
|
-
replyToMode: params.replyToMode,
|
|
1788
|
-
existingId: nativeReply.replyToId,
|
|
1789
|
-
hasReplied: params.replyContext?.state?.hasReplied
|
|
1790
|
-
});
|
|
1930
|
+
const nextReplyToId = createSignalNativeReplyIdResolver(params);
|
|
1791
1931
|
return () => {
|
|
1792
|
-
const replyToId =
|
|
1793
|
-
if (
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
replyToId
|
|
1797
|
-
|
|
1932
|
+
const replyToId = nextReplyToId();
|
|
1933
|
+
if (!replyToId) return {};
|
|
1934
|
+
const replyToAuthor = normalizeOptionalString(params.replyContext?.author);
|
|
1935
|
+
return {
|
|
1936
|
+
replyToId,
|
|
1937
|
+
...replyToAuthor ? {
|
|
1938
|
+
replyToAuthor,
|
|
1939
|
+
replyToBody: params.replyContext?.body ?? ""
|
|
1940
|
+
} : {}
|
|
1941
|
+
};
|
|
1798
1942
|
};
|
|
1799
1943
|
}
|
|
1800
1944
|
async function monitorSignalProvider(opts = {}) {
|
|
@@ -1840,11 +1984,27 @@ async function monitorSignalProvider(opts = {}) {
|
|
|
1840
1984
|
const monitorTaskRunner = createSignalMonitorTaskRunner(runtime);
|
|
1841
1985
|
let daemonHandle = null;
|
|
1842
1986
|
let ingressMonitor;
|
|
1987
|
+
const startupDeadline = Date.now() + startupTimeoutMs;
|
|
1843
1988
|
if (autoStart) {
|
|
1844
1989
|
const cliPath = opts.cliPath ?? managedTransport?.cliPath ?? "signal-cli";
|
|
1845
1990
|
const configPath = normalizeOptionalString(opts.configPath) ?? normalizeOptionalString(managedTransport?.configPath);
|
|
1846
|
-
const httpHost = opts.httpHost ?? managedTransport?.httpHost ?? "127.0.0.1";
|
|
1991
|
+
const httpHost = normalizeSignalTransportHost(opts.httpHost ?? managedTransport?.httpHost ?? "127.0.0.1");
|
|
1847
1992
|
const httpPort = opts.httpPort ?? managedTransport?.httpPort ?? 8080;
|
|
1993
|
+
const startupTimeoutSignal = AbortSignal.timeout(startupTimeoutMs);
|
|
1994
|
+
const endpointProbeSignal = opts.abortSignal ? AbortSignal.any([opts.abortSignal, startupTimeoutSignal]) : startupTimeoutSignal;
|
|
1995
|
+
try {
|
|
1996
|
+
await assertSignalDaemonEndpointAvailable({
|
|
1997
|
+
httpHost,
|
|
1998
|
+
httpPort,
|
|
1999
|
+
abortSignal: endpointProbeSignal
|
|
2000
|
+
});
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
if (opts.abortSignal?.aborted) return;
|
|
2003
|
+
if (startupTimeoutSignal.aborted || Date.now() >= startupDeadline) throw new Error(`signal daemon startup timed out after ${startupTimeoutMs}ms while checking its endpoint`, { cause: error });
|
|
2004
|
+
throw error;
|
|
2005
|
+
}
|
|
2006
|
+
if (opts.abortSignal?.aborted) return;
|
|
2007
|
+
if (Date.now() >= startupDeadline) throw new Error(`signal daemon startup timed out after ${startupTimeoutMs}ms before starting`);
|
|
1848
2008
|
daemonHandle = spawnSignalDaemon({
|
|
1849
2009
|
cliPath,
|
|
1850
2010
|
...configPath ? { configPath } : {},
|
|
@@ -1866,7 +2026,7 @@ async function monitorSignalProvider(opts = {}) {
|
|
|
1866
2026
|
await waitForSignalDaemonReady({
|
|
1867
2027
|
baseUrl,
|
|
1868
2028
|
abortSignal: daemonLifecycle.abortSignal,
|
|
1869
|
-
|
|
2029
|
+
startupDeadlineMs: startupDeadline,
|
|
1870
2030
|
logAfterMs: 1e4,
|
|
1871
2031
|
logIntervalMs: 1e4,
|
|
1872
2032
|
runtime,
|
|
@@ -1893,6 +2053,7 @@ async function monitorSignalProvider(opts = {}) {
|
|
|
1893
2053
|
});
|
|
1894
2054
|
const handleEvent = createSignalEventHandler({
|
|
1895
2055
|
runtime,
|
|
2056
|
+
channelRuntime: opts.channelRuntime,
|
|
1896
2057
|
abortSignal: daemonLifecycle.abortSignal,
|
|
1897
2058
|
runTrackedTask: (task) => {
|
|
1898
2059
|
monitorTaskRunner.runTask(task);
|
|
@@ -1943,6 +2104,7 @@ async function monitorSignalProvider(opts = {}) {
|
|
|
1943
2104
|
timeoutMs: 0,
|
|
1944
2105
|
transportKind,
|
|
1945
2106
|
policy: opts.reconnectPolicy,
|
|
2107
|
+
statusSink: opts.statusSink,
|
|
1946
2108
|
onEvent: (event) => monitorTaskRunner.runTask(async () => await ingressMonitor?.receive(event))
|
|
1947
2109
|
});
|
|
1948
2110
|
const daemonExitError = daemonLifecycle.getExitError();
|
|
@@ -1950,6 +2112,7 @@ async function monitorSignalProvider(opts = {}) {
|
|
|
1950
2112
|
} catch (err) {
|
|
1951
2113
|
const daemonExitError = daemonLifecycle.getExitError();
|
|
1952
2114
|
if (opts.abortSignal?.aborted && !daemonExitError) return;
|
|
2115
|
+
if (daemonExitError) publishSignalRecovering(opts.statusSink, daemonExitError.message);
|
|
1953
2116
|
throw err;
|
|
1954
2117
|
} finally {
|
|
1955
2118
|
await ingressMonitor?.stop();
|