@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.
Files changed (33) hide show
  1. package/dist/{accounts-DrStJjiK.js → accounts-DO4HMqaK.js} +2 -2
  2. package/dist/api.js +9 -9
  3. package/dist/{runtime-JPrGlV7h.js → approval-auth-BY6uuApE.js} +2 -9
  4. package/dist/{approval-handler.runtime-C-XYgcxE.js → approval-handler.runtime-Co6EVTqY.js} +4 -4
  5. package/dist/{approval-reactions-DtL47gSK.js → approval-reactions-cs3Ox9Ow.js} +67 -189
  6. package/dist/channel-config-api.js +1 -1
  7. package/dist/channel-plugin-api.js +1 -1
  8. package/dist/{channel-CmbWaYlm.js → channel-zIopSllO.js} +64 -99
  9. package/dist/{channel.runtime-BRDaqr5-.js → channel.runtime-CpDBElG8.js} +3 -3
  10. package/dist/{client-BazXTH7O.js → client-CvnR8_XA.js} +31 -34
  11. package/dist/{client-adapter-mXGeEHQJ.js → client-adapter-D9SNPaNx.js} +13 -6
  12. package/dist/{config-schema-Y3dFGwdL.js → config-schema-CkMIK7zp.js} +4 -5
  13. package/dist/contract-api.js +1 -1
  14. package/dist/{doctor-contract-api-Dcte-QYB.js → doctor-contract-api-Dnp5eQQJ.js} +19 -22
  15. package/dist/doctor-contract-api.js +1 -1
  16. package/dist/{install-signal-cli-qJF7iC4h.js → install-signal-cli-Cpiyk0k1.js} +8 -5
  17. package/dist/{monitor-BvIzkrYW.js → monitor-BLFIkpH0.js} +305 -142
  18. package/dist/{probe-Br9abaGp.js → probe-B0E_cXQw.js} +23 -7
  19. package/dist/question-reactions-BaatFqJ7.js +80 -0
  20. package/dist/{reaction-runtime-api-BjeDyKcz.js → reaction-runtime-api-YGD6hhDh.js} +10 -21
  21. package/dist/reaction-runtime-api.js +1 -1
  22. package/dist/rpc-context-dXSy4NtF.js +15 -0
  23. package/dist/runtime-api.js +8 -9
  24. package/dist/{send-B_Ks4MZr.js → send-BNl5folj.js} +14 -23
  25. package/dist/{send.runtime-CbuXycZT.js → send.runtime-DnMoKk27.js} +1 -1
  26. package/dist/{transport-detection-Da06-LOL.js → transport-detection-D_M79R5f.js} +60 -148
  27. package/dist/{transport-detection.runtime-D_190UUL.js → transport-detection.runtime-DKuuIHvf.js} +1 -1
  28. package/dist/{transport-probes.runtime-CV6Qs0Og.js → transport-probes.runtime-BsJdp0oI.js} +1 -1
  29. package/openclaw.plugin.json +4 -1
  30. package/package.json +5 -5
  31. package/dist/approval-resolver-B3m0D2R0.js +0 -16
  32. package/dist/question-reactions-Ds48FtEu.js +0 -109
  33. 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 { i as resolveSignalAccount, o as resolveSignalReplyToMode } from "./accounts-DrStJjiK.js";
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 { 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";
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 nodePath.join(os.homedir(), value.slice(2));
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
- const failedCounts = result.failedCounts;
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 Promise.all(entries.map((entry) => Promise.resolve(entry.turnAdoptionLifecycle?.onAbandoned())));
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 { senderAccess, commandAccess } = await resolveSignalAccessState({
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: envelope.timestamp ?? void 0,
1222
- messageId: typeof envelope.timestamp === "number" ? String(envelope.timestamp) : void 0
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 typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
1575
+ return asPositiveSafeInteger(value) ?? null;
1378
1576
  }
1379
- function parseReceiveEnvelope(event) {
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$1(parsed)) throw new SignalIngressPermanentError("parse-error", "Signal receive event must contain a JSON object");
1388
- return isRecord$1(parsed.envelope) ? parsed.envelope : null;
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$1(envelope.dataMessage)) return envelope.dataMessage;
1392
- return isRecord$1(envelope.editMessage?.dataMessage) ? envelope.editMessage.dataMessage : null;
1589
+ if (isRecord(envelope.dataMessage)) return envelope.dataMessage;
1590
+ return isRecord(envelope.editMessage?.dataMessage) ? envelope.editMessage.dataMessage : null;
1393
1591
  }
1394
- function inspectSignalIngressEvent(event) {
1395
- const envelope = parseReceiveEnvelope(event);
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$1(envelope.reactionMessage) ? envelope.reactionMessage : null;
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$1(dataMessage?.groupInfo) ? dataMessage.groupInfo : null;
1407
- const reactionGroup = isRecord$1(reactionMessage?.groupInfo) ? reactionMessage.groupInfo : null;
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: (event) => inspectSignalIngressEvent(event),
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: 1e3,
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
- async function runSignalSseLoop({ baseUrl, account, abortSignal, runtime, onEvent, timeoutMs, transportKind, policy }) {
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?.trim();
1611
- if (!accountId || !targets || targets.length === 0) return false;
1612
- const normalizedAccount = normalizeE164(accountId);
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 || accountId === `uuid:${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 nativeReply = resolveSignalNativeReplyOptions(params);
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 = planner.use();
1793
- if (params.replyContext?.state && !isStatusNotice) params.replyContext.state.hasReplied = planner.hasReplied();
1794
- return replyToId ? {
1795
- ...nativeReply,
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
- timeoutMs: startupTimeoutMs,
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();