@openclaw/imessage 2026.10.1-beta.2 → 2026.10.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/.setup/{accounts-evTpvJMv.mjs → accounts-DacCvDBt.mjs} +23 -45
  2. package/dist/.setup/{actions-DJqbUmbN.mjs → actions-Hxh12myO.mjs} +54 -101
  3. package/dist/.setup/{actions.runtime-DL5i-Y4J.mjs → actions.runtime-DOIX6Fkf.mjs} +29 -37
  4. package/dist/.setup/{api-oq_MryN7.mjs → api-BV8qHfLl.mjs} +7 -7
  5. package/dist/.setup/{api-SAVMfMoY.mjs → api-DC1yHVGj.mjs} +8 -8
  6. package/dist/.setup/{approval-handler.runtime-Bj2NGNXF.mjs → approval-handler.runtime-Dc3KzakG.mjs} +38 -66
  7. package/dist/.setup/{approval-reactions-L9Ul7PAu.mjs → approval-reactions-JvzJCJ4a.mjs} +26 -33
  8. package/dist/.setup/{channel-CL73qdMG.mjs → channel-9TWBUDkP.mjs} +14 -16
  9. package/dist/.setup/{channel.runtime-DY7JJ0_9.mjs → channel.runtime-DrLDEY4R.mjs} +198 -362
  10. package/dist/.setup/{client-12apu4Ip.mjs → client-HixFQQWY.mjs} +34 -38
  11. package/dist/.setup/{media-contract-DjQGZlfl.mjs → media-contract-BwWs5PM7.mjs} +1 -1
  12. package/dist/.setup/{message-tool-api-CqfRxLlT.mjs → message-tool-api-B5NfPnFL.mjs} +2 -2
  13. package/dist/.setup/{question-reactions-DJV-TnA8.mjs → question-reactions-9jrqVZgE.mjs} +20 -25
  14. package/dist/.setup/{reaction-context-By7kUMBC.mjs → reaction-context-CRXpeYAr.mjs} +1 -1
  15. package/dist/.setup/{remote-file-BU7pA-Zi.mjs → remote-file-Dq5Asi_F.mjs} +10 -16
  16. package/dist/.setup/{sanitize-outbound-C0x4BPky.mjs → sanitize-outbound-DpdQERYJ.mjs} +27 -62
  17. package/dist/.setup/{send-pkYwL6Rt.mjs → send-BZZzMbdi.mjs} +186 -251
  18. package/dist/.setup/{setup-core-DqOBe-n0.mjs → setup-core-CaRGbZOL.mjs} +15 -46
  19. package/dist/.setup/{setup-surface-zKhIinwY.mjs → setup-surface-BMWS7VuY.mjs} +2 -2
  20. package/dist/.setup/{targets-BicBiYsM.mjs → targets-B4oU7n-v.mjs} +47 -69
  21. package/dist/api.js +7 -7
  22. package/dist/channel-plugin-api.js +1 -1
  23. package/dist/contract-api.js +1 -1
  24. package/dist/doctor-contract-api.js +21 -45
  25. package/dist/media-contract-api.js +1 -1
  26. package/dist/message-tool-api.js +1 -1
  27. package/dist/runtime-api.js +1 -1
  28. package/dist/setup-entry.js +1 -1
  29. package/dist/src/chat-db.worker.js +1 -1
  30. package/package.json +4 -4
@@ -1,12 +1,12 @@
1
- import { c as parseIMessageTarget, o as normalizeIMessageHandle, t as formatIMessageChatTarget } from "./targets-BicBiYsM.mjs";
2
- import { d as resolveIMessageChatDbLookupPath, l as resolveIMessageRemoteHost, n as hasExclusiveIMessageLocalDatabase, o as resolveIMessageAccount } from "./accounts-evTpvJMv.mjs";
3
- import { C as resolveIMessageDirectChatService, _ as resolveIMessageSentEchoEntryKey, b as chatContextFromIMessageTarget, c as rememberIMessageReplyCache, f as IMESSAGE_SENT_ECHOES_NAMESPACE, g as resolveIMessageEchoMediaKey, n as createIMessageRpcClient, p as IMESSAGE_SENT_ECHOES_TTL_MS, r as runIMessageCliJsonCommand, t as IMessageRpcRequestError } from "./client-12apu4Ip.mjs";
4
- import { a as normalizeIMessageMessageId, i as normalizeIMessageGuid } from "./actions-DJqbUmbN.mjs";
1
+ import { c as parseIMessageTarget, o as normalizeIMessageHandle, t as formatIMessageChatTarget } from "./targets-B4oU7n-v.mjs";
2
+ import { d as resolveIMessageChatDbLookupPath, l as resolveIMessageRemoteHost, n as hasExclusiveIMessageLocalDatabase, o as resolveIMessageAccount } from "./accounts-DacCvDBt.mjs";
3
+ import { C as resolveIMessageDirectChatService, _ as resolveIMessageSentEchoEntryKey, b as chatContextFromIMessageTarget, c as rememberIMessageReplyCache, f as IMESSAGE_SENT_ECHOES_NAMESPACE, g as resolveIMessageEchoMediaKey, n as createIMessageRpcClient, p as IMESSAGE_SENT_ECHOES_TTL_MS, r as runIMessageCliJsonCommand, t as IMessageRpcRequestError } from "./client-HixFQQWY.mjs";
4
+ import { a as normalizeIMessageMessageId, i as normalizeIMessageGuid } from "./actions-Hxh12myO.mjs";
5
5
  import { n as getOptionalIMessageRuntime, t as getIMessageRuntime } from "./runtime-Cza4CY5T.mjs";
6
- import { d as getIMessageApprovalApprovers, f as imessageApprovalAuth } from "./channel-CL73qdMG.mjs";
7
- import { n as sanitizeIMessageFinalOutboundText, t as protectIMessageFencedRoleMarkers } from "./sanitize-outbound-C0x4BPky.mjs";
8
- import { a as withIMessageReceiptGuidReader, r as resolveAuthorizedIMessageReplyReference, t as withIMessageRemoteFile } from "./remote-file-BU7pA-Zi.mjs";
9
- import { d as normalizeConversationKey, i as registerIMessageApprovalReactionTarget, l as enumerateApprovalTargetKeys, s as buildIMessageApprovalConversationKeyForInbound, u as enumerateConversationKeyForms } from "./approval-reactions-L9Ul7PAu.mjs";
6
+ import { d as getIMessageApprovalApprovers, f as imessageApprovalAuth } from "./channel-9TWBUDkP.mjs";
7
+ import { n as sanitizeIMessageFinalOutboundText, t as protectIMessageFencedRoleMarkers } from "./sanitize-outbound-DpdQERYJ.mjs";
8
+ import { a as withIMessageReceiptGuidReader, r as resolveAuthorizedIMessageReplyReference, t as withIMessageRemoteFile } from "./remote-file-Dq5Asi_F.mjs";
9
+ import { d as normalizeConversationKey, f as resolveIMessageApprovalControlActor, i as registerIMessageApprovalReactionTarget, l as enumerateApprovalTargetKeys, s as buildIMessageApprovalConversationKeyForInbound, u as enumerateConversationKeyForms } from "./approval-reactions-JvzJCJ4a.mjs";
10
10
  import { asOptionalRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
11
11
  import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
12
12
  import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
@@ -17,6 +17,7 @@ import { extractOriginalFilename, kindFromMime, resolveOutboundAttachmentFromUrl
17
17
  import { logVerbose, sleep } from "openclaw/plugin-sdk/runtime-env";
18
18
  import { asDateTimestampMs } from "openclaw/plugin-sdk/number-runtime";
19
19
  import { PlatformMessageNotDispatchedError, isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime";
20
+ import { captureChannelReadAuthority, captureEffectAuthority } from "openclaw/plugin-sdk/fetch-runtime";
20
21
  import { racePromiseWithAbortSignal } from "openclaw/plugin-sdk/time-runtime";
21
22
  import { addApprovalReactionHintToText, createApprovalReactionTargetStore, listApprovalReactionBindings } from "openclaw/plugin-sdk/approval-reaction-runtime";
22
23
  import { convertMarkdownTables, stripInlineDirectiveTagsForDelivery } from "openclaw/plugin-sdk/text-chunking";
@@ -39,12 +40,9 @@ function bindingKeys(accountId, conversation) {
39
40
  function beginIMessageApprovalControlBinding(params) {
40
41
  const keys = bindingKeys(params.accountId, params.conversation);
41
42
  const { promise, resolve } = createDeferred();
42
- let closed = false;
43
43
  const window = {
44
44
  done: promise,
45
45
  close: () => {
46
- if (closed) return;
47
- closed = true;
48
46
  for (const key of keys) {
49
47
  const windows = pendingByConversation.get(key);
50
48
  windows?.delete(window);
@@ -224,9 +222,7 @@ function readPollVoteEvent(message) {
224
222
  const poll = message.poll;
225
223
  if (!poll || poll.kind !== "vote") return null;
226
224
  const pollGuid = normalizeIMessageGuid(typeof poll.original_guid === "string" && poll.original_guid || typeof poll.poll_guid === "string" && poll.poll_guid || "");
227
- const sender = normalizeIMessageHandle((message.sender ?? "").trim());
228
- const destinationCallerId = normalizeIMessageHandle((message.destination_caller_id ?? "").trim());
229
- const actorHandle = (message.is_from_me !== true && Boolean(sender) && Boolean(destinationCallerId) && sender === destinationCallerId ? "" : sender) || (message.is_from_me === true ? destinationCallerId : "");
225
+ const actorHandle = resolveIMessageApprovalControlActor(message);
230
226
  if (!pollGuid || !actorHandle) return null;
231
227
  const rawVotes = Array.isArray(poll.votes) ? poll.votes : poll.vote ? [poll.vote] : [];
232
228
  let malformedVotes = false;
@@ -246,13 +242,7 @@ function readPollVoteEvent(message) {
246
242
  selected: vote.event_type === "selected"
247
243
  }];
248
244
  });
249
- const conversation = buildIMessageApprovalConversationKeyForInbound({
250
- chatGuid: message.chat_guid,
251
- chatIdentifier: message.chat_identifier,
252
- chatId: message.chat_id,
253
- isGroup: message.is_group,
254
- actorHandle
255
- });
245
+ const conversation = buildIMessageApprovalConversationKeyForInbound(message, actorHandle);
256
246
  if (!normalizeConversationKey(conversation)) return null;
257
247
  return {
258
248
  conversation,
@@ -279,20 +269,12 @@ async function lookupPollRecord(store, params) {
279
269
  * decision is security-relevant, and diagnosing "the tap did nothing" must not
280
270
  * require re-running the gateway in debug.
281
271
  */
282
- function info(message, fields) {
272
+ function logPollVote(level, message, fields) {
283
273
  try {
284
- getOptionalIMessageRuntime()?.logging.getChildLogger({
274
+ (getOptionalIMessageRuntime()?.logging.getChildLogger({
285
275
  plugin: "imessage",
286
276
  feature: "approval-polls"
287
- }).info(message, fields);
288
- } catch {}
289
- }
290
- function warn$1(message, fields) {
291
- try {
292
- getOptionalIMessageRuntime()?.logging.getChildLogger({
293
- plugin: "imessage",
294
- feature: "approval-polls"
295
- }).warn(message, fields);
277
+ }))?.[level](message, fields);
296
278
  } catch {}
297
279
  }
298
280
  /**
@@ -312,7 +294,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
312
294
  const target = await lookupPollRecord(pollTargets, lookupKey);
313
295
  if (!target) return Boolean(await lookupPollRecord(pollTombstones, lookupKey));
314
296
  if (event.malformedVotes) {
315
- warn$1("approval poll vote ignored: malformed complete vote set", {
297
+ logPollVote("warn", "approval poll vote ignored: malformed complete vote set", {
316
298
  approvalId: target.approvalId,
317
299
  actorHandle: event.actorHandle
318
300
  });
@@ -322,7 +304,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
322
304
  const participantKeys = new Set(event.votes.map((vote) => vote.participantKey).filter(Boolean));
323
305
  const actorVotes = directlyAttributedVotes.length > 0 ? directlyAttributedVotes : event.votes.length === 1 || participantKeys.size === 1 ? event.votes : [];
324
306
  if (actorVotes.length === 0) {
325
- warn$1("approval poll vote participants did not identify the transport actor", {
307
+ logPollVote("warn", "approval poll vote participants did not identify the transport actor", {
326
308
  approvalId: target.approvalId,
327
309
  actorHandle: event.actorHandle
328
310
  });
@@ -330,7 +312,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
330
312
  }
331
313
  const selectedVotes = actorVotes.filter((vote) => vote.selected);
332
314
  if (selectedVotes.length === 0) {
333
- info("approval poll deselect ignored; first selection decides", { approvalId: target.approvalId });
315
+ logPollVote("info", "approval poll deselect ignored; first selection decides", { approvalId: target.approvalId });
334
316
  return true;
335
317
  }
336
318
  const selectedDecisions = selectedVotes.map((vote) => ({
@@ -338,7 +320,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
338
320
  decision: target.optionDecisions.find(([optionId]) => optionId === vote.optionId)?.[1]
339
321
  }));
340
322
  if (selectedDecisions.some((entry) => !entry.decision)) {
341
- warn$1("approval poll vote ignored: selected option not bound to a decision", {
323
+ logPollVote("warn", "approval poll vote ignored: selected option not bound to a decision", {
342
324
  approvalId: target.approvalId,
343
325
  optionIds: selectedDecisions.map((entry) => entry.optionId)
344
326
  });
@@ -346,7 +328,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
346
328
  }
347
329
  const decisions = [...new Set(selectedDecisions.map((entry) => entry.decision))];
348
330
  if (decisions.length !== 1) {
349
- warn$1("approval poll vote ignored: ambiguous selected decisions", {
331
+ logPollVote("warn", "approval poll vote ignored: ambiguous selected decisions", {
350
332
  approvalId: target.approvalId,
351
333
  decisions
352
334
  });
@@ -358,7 +340,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
358
340
  cfg: params.cfg,
359
341
  accountId: params.accountId
360
342
  }).length === 0) {
361
- info("approval poll vote denied: no explicit approvers configured", { approvalId: target.approvalId });
343
+ logPollVote("info", "approval poll vote denied: no explicit approvers configured", { approvalId: target.approvalId });
362
344
  return true;
363
345
  }
364
346
  if (!imessageApprovalAuth.authorizeActorAction({
@@ -368,7 +350,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
368
350
  action: "approve",
369
351
  approvalKind: target.approvalKind
370
352
  }).authorized) {
371
- info("approval poll vote denied: sender not an approver", {
353
+ logPollVote("info", "approval poll vote denied: sender not an approver", {
372
354
  approvalId: target.approvalId,
373
355
  actorHandle: event.actorHandle
374
356
  });
@@ -392,7 +374,7 @@ async function maybeResolveIMessageApprovalPollVote(params) {
392
374
  optionDecisions: target.optionDecisions,
393
375
  approvalId: target.approvalId
394
376
  });
395
- info(`approval poll vote ${result.applied ? "resolved" : "already resolved"}`, {
377
+ logPollVote("info", `approval poll vote ${result.applied ? "resolved" : "already resolved"}`, {
396
378
  approvalId: target.approvalId,
397
379
  actorHandle: event.actorHandle,
398
380
  decision
@@ -405,10 +387,10 @@ async function maybeResolveIMessageApprovalPollVote(params) {
405
387
  optionDecisions: target.optionDecisions,
406
388
  approvalId: target.approvalId
407
389
  });
408
- info("approval poll vote ignored: approval already gone", { approvalId: target.approvalId });
390
+ logPollVote("info", "approval poll vote ignored: approval already gone", { approvalId: target.approvalId });
409
391
  return true;
410
392
  }
411
- warn$1("approval poll vote failed", {
393
+ logPollVote("warn", "approval poll vote failed", {
412
394
  approvalId: target.approvalId,
413
395
  senderId: event.actorHandle,
414
396
  error: String(error)
@@ -534,6 +516,19 @@ async function hasPersistedIMessageEcho(params) {
534
516
  }
535
517
  //#endregion
536
518
  //#region extensions/imessage/src/send-transport.ts
519
+ function bindIMessageCliSend(handoff, options) {
520
+ const effect = captureEffectAuthority();
521
+ const assertReadAuthority = captureChannelReadAuthority();
522
+ const runCli = handoff.runCliJson ?? ((args) => runIMessageCliJsonCommand({
523
+ ...options,
524
+ args
525
+ }));
526
+ return (args) => effect.initiate(() => {
527
+ assertReadAuthority?.();
528
+ handoff.assertDirectAdapterHandoff?.();
529
+ return runCli(args);
530
+ });
531
+ }
537
532
  function normalizeIMessageRpcSendError(error) {
538
533
  if (!(error instanceof IMessageRpcRequestError)) return error;
539
534
  const data = asOptionalRecord(error.data);
@@ -544,7 +539,10 @@ async function requestIMessageRpcSend(client, method, params, timeoutMs, handoff
544
539
  await handoff.onPlatformSendDispatch?.();
545
540
  handoff.assertDirectAdapterHandoff?.();
546
541
  try {
547
- return await client.request(method, params, { timeoutMs });
542
+ return await client.request(method, params, {
543
+ timeoutMs,
544
+ assertCurrent: handoff.assertDirectAdapterHandoff
545
+ });
548
546
  } catch (error) {
549
547
  throw normalizeIMessageRpcSendError(error);
550
548
  }
@@ -558,20 +556,6 @@ function resolveMessageId(result) {
558
556
  const raw = typeof result.messageGuid === "string" && result.messageGuid.trim() || typeof result.messageId === "string" && result.messageId.trim() || typeof result.message_id === "string" && result.message_id.trim() || typeof result.id === "string" && result.id.trim() || typeof result.guid === "string" && result.guid.trim() || (typeof result.message_id === "number" ? String(result.message_id) : null) || (typeof result.id === "number" ? String(result.id) : null);
559
557
  return raw ? raw.trim() : null;
560
558
  }
561
- function resolveOutboundMessageGuid(result) {
562
- if (!result) return null;
563
- for (const key of [
564
- "messageGuid",
565
- "guid",
566
- "messageId",
567
- "message_id",
568
- "id"
569
- ]) {
570
- const guid = normalizeResolvedMessageGuid(result[key]);
571
- if (guid) return guid;
572
- }
573
- return null;
574
- }
575
559
  function isNumericMessageRowId(value) {
576
560
  return typeof value === "string" && /^\d+$/.test(value.trim());
577
561
  }
@@ -580,15 +564,6 @@ function normalizeResolvedMessageGuid(value) {
580
564
  const trimmed = value.trim();
581
565
  return normalizeIMessageMessageId(trimmed) && !isNumericMessageRowId(trimmed) ? trimmed : null;
582
566
  }
583
- async function resolveMessageGuidFromChatDb(params) {
584
- const dbPath = params.dbPath?.trim();
585
- const messageId = params.messageId.trim();
586
- if (!dbPath || !isNumericMessageRowId(messageId)) return null;
587
- return normalizeResolvedMessageGuid(await withIMessageReceiptGuidReader(dbPath, (read) => read({
588
- type: "messageGuid",
589
- input: { messageId }
590
- })));
591
- }
592
567
  function canResolveLatestSentMessageGuidFromChatDb(dbPath) {
593
568
  const normalizedDbPath = dbPath?.trim();
594
569
  if (!normalizedDbPath) return false;
@@ -600,14 +575,27 @@ function canResolveLatestSentMessageGuidFromChatDb(dbPath) {
600
575
  }
601
576
  }
602
577
  async function resolveApprovalBindingMessageGuid(params) {
603
- const immediateGuid = resolveOutboundMessageGuid(params.result);
604
- if (immediateGuid) return immediateGuid;
578
+ for (const key of [
579
+ "messageGuid",
580
+ "guid",
581
+ "messageId",
582
+ "message_id",
583
+ "id"
584
+ ]) {
585
+ const guid = normalizeResolvedMessageGuid(params.result?.[key]);
586
+ if (guid) return guid;
587
+ }
605
588
  const messageId = params.messageId?.trim();
606
589
  if (!messageId || !isNumericMessageRowId(messageId)) return null;
607
- return normalizeResolvedMessageGuid(await (params.resolveMessageGuidImpl ?? resolveMessageGuidFromChatDb)({
590
+ if (params.resolveMessageGuidImpl) return normalizeResolvedMessageGuid(await params.resolveMessageGuidImpl({
608
591
  dbPath: params.dbPath,
609
592
  messageId
610
593
  }));
594
+ const dbPath = params.dbPath?.trim();
595
+ return dbPath ? normalizeResolvedMessageGuid(await withIMessageReceiptGuidReader(dbPath, (read) => read({
596
+ type: "messageGuid",
597
+ input: { messageId }
598
+ }))) : null;
611
599
  }
612
600
  async function resolveFallbackSentMessageGuid(params) {
613
601
  const dbPath = params.dbPath?.trim();
@@ -638,13 +626,6 @@ async function resolveFallbackSentMessageGuid(params) {
638
626
  }
639
627
  })));
640
628
  }
641
- function resolveOutboundEchoMedia(mediaContentType) {
642
- if (!mediaContentType) return;
643
- return {
644
- contentType: mediaContentType,
645
- kind: kindFromMime(mediaContentType) ?? "unknown"
646
- };
647
- }
648
629
  function createIMessageSendReceipt(params) {
649
630
  const messageId = params.messageId.trim();
650
631
  const results = normalizeIMessageMessageId(messageId) ? [{
@@ -671,10 +652,6 @@ async function withOriginalIMessageAttachmentPath(filePath, send) {
671
652
  prefix: "openclaw-imessage-outbound-"
672
653
  }, async (workspace) => await send(await workspace.copyIn(filename, filePath)));
673
654
  }
674
- function canSynthesizeAttachmentChatHandle(raw) {
675
- const trimmed = raw.trim();
676
- return trimmed.includes("@") || trimmed.startsWith("+");
677
- }
678
655
  function resolveOutboundEchoScope(params) {
679
656
  if (params.target.kind === "chat_id") return `${params.accountId}:${formatIMessageChatTarget(params.target.chatId)}`;
680
657
  if (params.target.kind === "chat_guid") return `${params.accountId}:chat_guid:${params.target.chatGuid}`;
@@ -693,9 +670,6 @@ function resultService(value) {
693
670
  const normalized = normalizeOptionalString(value)?.toLowerCase();
694
671
  return normalized === "imessage" || normalized === "sms" ? normalized : void 0;
695
672
  }
696
- function resolvePendingPersistedEchoTtlMs(timeoutMs) {
697
- return Math.max(MIN_PENDING_PERSISTED_ECHO_TTL_MS, Math.max(0, timeoutMs) + PENDING_PERSISTED_ECHO_GRACE_MS);
698
- }
699
673
  function isAttachmentCommandFallbackError(error) {
700
674
  const message = error instanceof Error ? error.message : String(error);
701
675
  return /(?:unknown|unrecognized|invalid|unsupported)\s+(?:command|subcommand)|not a recognized command|send-attachment.*(?:not found|unsupported|unavailable)|private api bridge.*unavailable|requires the imsg private api bridge|run imsg launch/iu.test(message);
@@ -704,146 +678,6 @@ function isThreadedReplyUnsupportedError(error) {
704
678
  const message = error instanceof Error ? error.message : String(error);
705
679
  return /reply_to requires bridge transport|cannot send threaded repl|threaded repl(?:y|ies)\b.*(?:unsupported|not supported|requires|unavailable)|requires bridge transport/iu.test(message);
706
680
  }
707
- async function resolveAttachmentChatTarget(params) {
708
- if (params.target.kind === "chat_guid") return params.target.chatGuid;
709
- if (params.target.kind === "handle") {
710
- if (!canSynthesizeAttachmentChatHandle(params.target.to)) return null;
711
- const normalizedHandle = normalizeIMessageHandle(params.target.to);
712
- if (!normalizedHandle) return null;
713
- const service = params.target.service !== "auto" ? params.target.service : params.service;
714
- if (service === "sms") return `SMS;-;${normalizedHandle}`;
715
- if (service === "imessage") return `iMessage;-;${normalizedHandle}`;
716
- return `any;-;${normalizedHandle}`;
717
- }
718
- if (params.target.kind !== "chat_id") return null;
719
- const result = await params.runCliJson([
720
- "group",
721
- "--chat-id",
722
- String(params.target.chatId)
723
- ]);
724
- return normalizeOptionalString(result.guid) ?? normalizeOptionalString(result.chat_guid) ?? null;
725
- }
726
- async function trySendAttachmentForTarget(params) {
727
- if (params.audioAsVoice && params.sendTransport === "applescript") throw new Error("iMessage voice messages require bridge transport; AppleScript cannot send native voice notes. Set sendTransport to bridge or auto.");
728
- if (params.target.kind === "handle" && !params.audioAsVoice && params.sendTransport !== "bridge" && (params.service === "sms" || params.service === "imessage")) return null;
729
- if (params.remoteHost && params.sendTransport === "applescript") return null;
730
- let attachmentChatTarget = null;
731
- if (params.remoteHost) {
732
- if (params.target.kind === "chat_guid") attachmentChatTarget = params.target.chatGuid;
733
- else if (params.target.kind === "chat_identifier") attachmentChatTarget = params.target.chatIdentifier;
734
- else if (params.target.kind === "handle") {
735
- const normalizedHandle = normalizeIMessageHandle(params.target.to);
736
- if (normalizedHandle) {
737
- const service = params.target.service !== "auto" ? params.target.service : params.service;
738
- attachmentChatTarget = `${service === "sms" ? "SMS" : service === "imessage" ? "iMessage" : "any"};-;${normalizedHandle}`;
739
- }
740
- } else attachmentChatTarget = formatIMessageChatTarget(params.target.chatId);
741
- } else try {
742
- attachmentChatTarget = await resolveAttachmentChatTarget({
743
- target: params.target,
744
- service: params.service,
745
- runCliJson: params.runCliJson
746
- });
747
- } catch (error) {
748
- if (!params.audioAsVoice && isAttachmentCommandFallbackError(error)) return null;
749
- throw error;
750
- }
751
- if (!attachmentChatTarget) {
752
- if (params.audioAsVoice) throw new Error("iMessage voice messages require an existing chat and bridge transport.");
753
- return null;
754
- }
755
- params.assertDirectAdapterHandoff?.();
756
- const echoScope = resolveOutboundEchoScope({
757
- accountId: params.accountId,
758
- target: params.target
759
- });
760
- let result;
761
- let pendingEchoKey;
762
- try {
763
- pendingEchoKey = await rememberPersistedIMessageEcho({
764
- scope: echoScope,
765
- text: params.echoText,
766
- media: params.echoMedia,
767
- ttlMs: params.pendingEchoTtlMs,
768
- pending: true
769
- });
770
- result = await withOriginalIMessageAttachmentPath(params.filePath, async (attachmentPath) => {
771
- if (params.remoteHost) return await params.withRemoteFile({
772
- remoteHost: params.remoteHost,
773
- localPath: attachmentPath,
774
- timeoutMs: params.timeoutMs,
775
- assertDirectAdapterHandoff: params.assertDirectAdapterHandoff,
776
- use: async (remotePath) => {
777
- const rpcParams = {
778
- file: remotePath,
779
- ...params.audioAsVoice ? { audio: true } : {},
780
- ...params.replyToId ? { reply_to: params.replyToId } : {}
781
- };
782
- if (params.target.kind === "chat_id") rpcParams.chat_id = params.target.chatId;
783
- else if (params.target.kind === "chat_guid") rpcParams.chat_guid = params.target.chatGuid;
784
- else rpcParams.chat_identifier = attachmentChatTarget;
785
- return await params.requestRpc("send.attachment", rpcParams);
786
- }
787
- });
788
- params.assertDirectAdapterHandoff?.();
789
- await params.onPlatformSendDispatch?.();
790
- params.assertDirectAdapterHandoff?.();
791
- return await params.runCliJson([
792
- "send-attachment",
793
- "--chat",
794
- attachmentChatTarget,
795
- "--file",
796
- attachmentPath,
797
- ...params.audioAsVoice ? ["--audio"] : [],
798
- ...params.replyToId ? ["--reply-to", params.replyToId] : [],
799
- "--transport",
800
- params.sendTransport === "bridge" ? "dylib" : params.sendTransport
801
- ]);
802
- });
803
- const failure = resolveIMessageSendFailure(result);
804
- if (failure) throw new Error(failure);
805
- } catch (error) {
806
- await forgetPersistedIMessageEchoKey(pendingEchoKey);
807
- if (!params.audioAsVoice && isAttachmentCommandFallbackError(error)) return null;
808
- throw error;
809
- }
810
- const resolvedId = resolveMessageId(result);
811
- const approvalBindingMessageId = await resolveApprovalBindingMessageGuid({
812
- dbPath: params.dbPath,
813
- messageId: resolvedId,
814
- result,
815
- resolveMessageGuidImpl: params.resolveMessageGuidImpl
816
- });
817
- const messageId = resolvedId ?? (result.ok || result.success ? "ok" : "unknown");
818
- await rememberPersistedIMessageEcho({
819
- scope: echoScope,
820
- text: params.echoText,
821
- media: params.echoMedia,
822
- messageId: resolvedId ?? void 0
823
- });
824
- if (resolvedId && normalizeIMessageMessageId(resolvedId)) await rememberIMessageReplyCache({
825
- accountId: params.accountId,
826
- messageId: resolvedId,
827
- chatGuid: params.target.kind === "chat_guid" ? params.target.chatGuid : params.target.kind === "chat_id" ? attachmentChatTarget : void 0,
828
- chatIdentifier: params.target.kind === "chat_identifier" || params.target.kind === "handle" ? attachmentChatTarget : void 0,
829
- chatId: params.target.kind === "chat_id" ? params.target.chatId : void 0,
830
- timestamp: Date.now(),
831
- isFromMe: true
832
- });
833
- return {
834
- messageId,
835
- ...approvalBindingMessageId ? { guid: approvalBindingMessageId } : {},
836
- sentText: "",
837
- ...params.echoText ? { echoText: params.echoText } : {},
838
- ...params.echoMedia ? { echoMedia: params.echoMedia } : {},
839
- receipt: createIMessageSendReceipt({
840
- messageId,
841
- target: params.target,
842
- kind: params.audioAsVoice ? "voice" : "media",
843
- ...params.replyToId ? { replyToId: params.replyToId } : {}
844
- })
845
- };
846
- }
847
681
  async function sendMessageIMessage(to, text, opts) {
848
682
  const cfg = requireRuntimeConfig(opts.config, "iMessage send");
849
683
  opts.assertDirectAdapterHandoff?.();
@@ -885,7 +719,7 @@ async function sendMessageIMessage(to, text, opts) {
885
719
  });
886
720
  opts.assertDirectAdapterHandoff?.();
887
721
  const timeoutMs = opts.timeoutMs ?? Math.max(account.config.probeTimeoutMs ?? 0, 18e4);
888
- const pendingEchoTtlMs = resolvePendingPersistedEchoTtlMs(timeoutMs);
722
+ const pendingEchoTtlMs = Math.max(MIN_PENDING_PERSISTED_ECHO_TTL_MS, Math.max(0, timeoutMs) + PENDING_PERSISTED_ECHO_GRACE_MS);
889
723
  const region = opts.region?.trim() || account.config.region?.trim() || "US";
890
724
  const maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : typeof account.config.mediaMaxMb === "number" ? account.config.mediaMaxMb * 1024 * 1024 : 16777216;
891
725
  let message = opts.approvalPrompt ? addApprovalReactionHintToText({
@@ -928,18 +762,16 @@ async function sendMessageIMessage(to, text, opts) {
928
762
  message = formatted.text;
929
763
  if (!message.trim() && !filePath) throw new Error("iMessage send requires text or media");
930
764
  const echoText = message.trim() || void 0;
931
- const echoMedia = filePath ? resolveOutboundEchoMedia(mediaContentType) : void 0;
765
+ const echoMedia = filePath && mediaContentType ? {
766
+ contentType: mediaContentType,
767
+ kind: kindFromMime(mediaContentType) ?? "unknown"
768
+ } : void 0;
932
769
  let effectiveReplyToId = resolvedReplyToId;
933
- const runCli = opts.runCliJson ?? ((args) => runIMessageCliJsonCommand({
934
- args,
770
+ const runCliJson = bindIMessageCliSend(opts, {
935
771
  cliPath,
936
772
  dbPath,
937
773
  timeoutMs
938
- }));
939
- const runCliJson = async (args) => {
940
- opts.assertDirectAdapterHandoff?.();
941
- return await runCli(args);
942
- };
774
+ });
943
775
  const requestOwnedRpc = async (method, rpcParams) => {
944
776
  opts.assertDirectAdapterHandoff?.();
945
777
  const rpcClient = await (opts.createClient ?? createIMessageRpcClient)({
@@ -954,27 +786,130 @@ async function sendMessageIMessage(to, text, opts) {
954
786
  }
955
787
  };
956
788
  const withRemoteFile = opts.withRemoteFile ?? withIMessageRemoteFile;
957
- if (filePath && (!resolvedReplyToId || opts.audioAsVoice)) {
958
- const attachmentResult = await trySendAttachmentForTarget({
959
- accountId: account.accountId,
789
+ async function trySendAttachment(attachmentFilePath) {
790
+ const { audioAsVoice, resolveMessageGuidImpl, assertDirectAdapterHandoff, onPlatformSendDispatch } = opts;
791
+ const accountId = account.accountId;
792
+ if (audioAsVoice && sendTransport === "applescript") throw new Error("iMessage voice messages require bridge transport; AppleScript cannot send native voice notes. Set sendTransport to bridge or auto.");
793
+ if (target.kind === "handle" && !audioAsVoice && sendTransport !== "bridge" && (service === "sms" || service === "imessage")) return null;
794
+ if (remoteHost && sendTransport === "applescript") return null;
795
+ let attachmentChatTarget = null;
796
+ if (target.kind === "chat_guid") attachmentChatTarget = target.chatGuid;
797
+ else if (target.kind === "handle") {
798
+ const rawHandle = target.to.trim();
799
+ if (remoteHost || rawHandle.includes("@") || rawHandle.startsWith("+")) {
800
+ const normalizedHandle = normalizeIMessageHandle(target.to);
801
+ if (normalizedHandle) {
802
+ const attachmentService = target.service !== "auto" ? target.service : service;
803
+ attachmentChatTarget = `${attachmentService === "sms" ? "SMS" : attachmentService === "imessage" ? "iMessage" : "any"};-;${normalizedHandle}`;
804
+ }
805
+ }
806
+ } else if (remoteHost) attachmentChatTarget = target.kind === "chat_identifier" ? target.chatIdentifier : formatIMessageChatTarget(target.chatId);
807
+ else if (target.kind === "chat_id") try {
808
+ const result = await runCliJson([
809
+ "group",
810
+ "--chat-id",
811
+ String(target.chatId)
812
+ ]);
813
+ attachmentChatTarget = normalizeOptionalString(result.guid) ?? normalizeOptionalString(result.chat_guid) ?? null;
814
+ } catch (error) {
815
+ if (!audioAsVoice && isAttachmentCommandFallbackError(error)) return null;
816
+ throw error;
817
+ }
818
+ if (!attachmentChatTarget) {
819
+ if (audioAsVoice) throw new Error("iMessage voice messages require an existing chat and bridge transport.");
820
+ return null;
821
+ }
822
+ assertDirectAdapterHandoff?.();
823
+ const echoScope = resolveOutboundEchoScope({
824
+ accountId,
825
+ target
826
+ });
827
+ let result;
828
+ let pendingEchoKey;
829
+ try {
830
+ pendingEchoKey = await rememberPersistedIMessageEcho({
831
+ scope: echoScope,
832
+ media: echoMedia,
833
+ ttlMs: pendingEchoTtlMs,
834
+ pending: true
835
+ });
836
+ result = await withOriginalIMessageAttachmentPath(attachmentFilePath, async (attachmentPath) => {
837
+ if (remoteHost) return await withRemoteFile({
838
+ remoteHost,
839
+ localPath: attachmentPath,
840
+ timeoutMs,
841
+ assertDirectAdapterHandoff,
842
+ use: async (remotePath) => {
843
+ const rpcParams = {
844
+ file: remotePath,
845
+ ...audioAsVoice ? { audio: true } : {},
846
+ ...resolvedReplyToId ? { reply_to: resolvedReplyToId } : {}
847
+ };
848
+ if (target.kind === "chat_id") rpcParams.chat_id = target.chatId;
849
+ else if (target.kind === "chat_guid") rpcParams.chat_guid = target.chatGuid;
850
+ else rpcParams.chat_identifier = attachmentChatTarget;
851
+ return await requestOwnedRpc("send.attachment", rpcParams);
852
+ }
853
+ });
854
+ assertDirectAdapterHandoff?.();
855
+ await onPlatformSendDispatch?.();
856
+ assertDirectAdapterHandoff?.();
857
+ return await runCliJson([
858
+ "send-attachment",
859
+ "--chat",
860
+ attachmentChatTarget,
861
+ "--file",
862
+ attachmentPath,
863
+ ...audioAsVoice ? ["--audio"] : [],
864
+ ...resolvedReplyToId ? ["--reply-to", resolvedReplyToId] : [],
865
+ "--transport",
866
+ sendTransport === "bridge" ? "dylib" : sendTransport
867
+ ]);
868
+ });
869
+ const failure = resolveIMessageSendFailure(result);
870
+ if (failure) throw new Error(failure);
871
+ } catch (error) {
872
+ await forgetPersistedIMessageEchoKey(pendingEchoKey);
873
+ if (!audioAsVoice && isAttachmentCommandFallbackError(error)) return null;
874
+ throw error;
875
+ }
876
+ const resolvedId = resolveMessageId(result);
877
+ const approvalBindingMessageId = await resolveApprovalBindingMessageGuid({
960
878
  dbPath: chatDbLookupPath,
961
- target,
962
- service,
963
- sendTransport,
964
- filePath,
965
- audioAsVoice: opts.audioAsVoice,
966
- ...resolvedReplyToId ? { replyToId: resolvedReplyToId } : {},
967
- echoMedia,
968
- pendingEchoTtlMs,
969
- timeoutMs,
970
- remoteHost,
971
- runCliJson,
972
- requestRpc: requestOwnedRpc,
973
- withRemoteFile,
974
- resolveMessageGuidImpl: opts.resolveMessageGuidImpl,
975
- assertDirectAdapterHandoff: opts.assertDirectAdapterHandoff,
976
- onPlatformSendDispatch: opts.onPlatformSendDispatch
879
+ messageId: resolvedId,
880
+ result,
881
+ resolveMessageGuidImpl
977
882
  });
883
+ const messageId = resolvedId ?? (result.ok || result.success ? "ok" : "unknown");
884
+ await rememberPersistedIMessageEcho({
885
+ scope: echoScope,
886
+ media: echoMedia,
887
+ messageId: resolvedId ?? void 0
888
+ });
889
+ if (resolvedId && normalizeIMessageMessageId(resolvedId)) await rememberIMessageReplyCache({
890
+ accountId,
891
+ messageId: resolvedId,
892
+ chatGuid: target.kind === "chat_guid" ? target.chatGuid : target.kind === "chat_id" ? attachmentChatTarget : void 0,
893
+ chatIdentifier: target.kind === "chat_identifier" || target.kind === "handle" ? attachmentChatTarget : void 0,
894
+ chatId: target.kind === "chat_id" ? target.chatId : void 0,
895
+ timestamp: Date.now(),
896
+ isFromMe: true
897
+ });
898
+ return {
899
+ messageId,
900
+ ...approvalBindingMessageId ? { guid: approvalBindingMessageId } : {},
901
+ sentText: "",
902
+ ...echoMedia ? { echoMedia } : {},
903
+ receipt: createIMessageSendReceipt({
904
+ messageId,
905
+ target,
906
+ kind: audioAsVoice ? "voice" : "media",
907
+ ...resolvedReplyToId ? { replyToId: resolvedReplyToId } : {}
908
+ })
909
+ };
910
+ }
911
+ if (filePath && (!resolvedReplyToId || opts.audioAsVoice)) {
912
+ const attachmentResult = await trySendAttachment(filePath);
978
913
  if (attachmentResult) {
979
914
  if (!message.trim()) return attachmentResult;
980
915
  await opts.onDeliveryResult?.({