fix(imessage): settle native inbound reply delivery (#117282)

Co-authored-by: Peter Steinberger <steipete@macos.shared>
This commit is contained in:
Peter Steinberger
2026-08-01 01:24:05 -07:00
committed by GitHub
parent 0cec56df26
commit 338a7ccb2f
4 changed files with 432 additions and 68 deletions
@@ -1,8 +1,22 @@
// Imessage tests cover monitor.plugin payload plugin behavior.
import path from "node:path";
import * as channelInbound from "openclaw/plugin-sdk/channel-inbound";
import {
addTestHook,
createEmptyPluginRegistry,
createTestInboundDebounceFlush,
initializeGlobalHookRunner,
resetGlobalHookRunner,
} from "openclaw/plugin-sdk/channel-test-helpers";
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
import type { dispatchReplyWithBufferedBlockDispatcher } from "openclaw/plugin-sdk/reply-runtime";
import { resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
import type { waitForTransportReady } from "openclaw/plugin-sdk/transport-ready-runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { createIMessageRpcClient } from "./client.js";
import { monitorIMessageProvider } from "./monitor.js";
import { setCachedIMessagePrivateApiStatus } from "./private-api-status.js";
import { getIMessageRuntime } from "./runtime.js";
import { installIMessageStateRuntimeForTest } from "./test-support/runtime.js";
const waitForTransportReadyMock = vi.hoisted(() =>
@@ -10,6 +24,7 @@ const waitForTransportReadyMock = vi.hoisted(() =>
);
const createIMessageRpcClientMock = vi.hoisted(() => vi.fn<typeof createIMessageRpcClient>());
const shouldDebounceTextInboundMock = vi.hoisted(() => vi.fn(() => false));
const directDeliveryProof = vi.hoisted(() => ({ flush: false }));
vi.mock("openclaw/plugin-sdk/transport-ready-runtime", () => ({
waitForTransportReady: waitForTransportReadyMock,
@@ -20,10 +35,19 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async (importOriginal) => {
return {
...actual,
createChannelInboundDebouncer: vi.fn(
(opts: { shouldDebounce: (entry: unknown) => boolean }) => ({
(opts: {
shouldDebounce: (entry: unknown) => boolean;
onFlush: (
entries: unknown[],
createFlush: typeof createTestInboundDebounceFlush,
) => { completion: Promise<void> };
}) => ({
debouncer: {
enqueue: async (entry: unknown) => {
opts.shouldDebounce(entry);
if (directDeliveryProof.flush) {
await opts.onFlush([entry], createTestInboundDebounceFlush).completion;
}
},
},
}),
@@ -46,6 +70,12 @@ describe("iMessage plugin payload attachments", () => {
waitForTransportReadyMock.mockReset().mockResolvedValue(undefined);
createIMessageRpcClientMock.mockReset();
shouldDebounceTextInboundMock.mockReset().mockReturnValue(false);
directDeliveryProof.flush = false;
});
afterEach(() => {
resetGlobalHookRunner();
vi.restoreAllMocks();
});
it("does not count Apple rich-link plugin payloads as user media", async () => {
@@ -103,4 +133,161 @@ describe("iMessage plugin payload attachments", () => {
}),
);
});
it.each([
{ kind: "tool", text: "provider-visible tool result", visible: true },
{ kind: "block", text: "provider-visible streamed block", visible: true },
{ kind: "tool", text: "<thinking>private reasoning</thinking>", visible: false },
] as const)(
"settles direct $kind delivery through actual provider, hooks, and SQLite ($visible)",
async ({ kind, text, visible }) => {
directDeliveryProof.flush = true;
const messageSent = vi.fn();
const registry = createEmptyPluginRegistry();
addTestHook({
registry,
pluginId: "imessage-monitor-proof",
hookName: "message_sent",
handler: messageSent,
});
initializeGlobalHookRunner(registry);
setCachedIMessagePrivateApiStatus("imsg", {
available: true,
v2Ready: true,
selectors: {},
rpcMethods: ["watch.subscribe", "send"],
});
const dispatch = vi.fn<typeof dispatchReplyWithBufferedBlockDispatcher>(async (params) => {
const settled = await params.dispatcherOptions.deliver({ text }, { kind });
expect(settled).toMatchObject(
visible
? {
visibleReplySent: true,
messageIds: [`native-${kind}-guid`],
receipt: { platformMessageIds: [`native-${kind}-guid`] },
content: text,
}
: { visibleReplySent: false, suppression: { reason: "no_visible_result" } },
);
return {
queuedFinal: false,
counts: { tool: kind === "tool" ? 1 : 0, block: kind === "block" ? 1 : 0, final: 0 },
};
});
const runActual = channelInbound.runChannelInboundEvent;
vi.spyOn(channelInbound, "runChannelInboundEvent").mockImplementation(async (params) =>
runActual({
...params,
adapter: {
...params.adapter,
resolveTurn: async (input, eventClass, preflight) => {
const turn = await params.adapter.resolveTurn(input, eventClass, preflight);
if (!("route" in turn) || !("delivery" in turn)) {
throw new Error("expected assembled iMessage delivery turn");
}
const { route, ...resolvedTurn } = turn;
return {
...resolvedTurn,
agentId: route.agentId,
routeSessionKey: route.sessionKey,
storePath: resolveStorePath(turn.cfg.session?.store, { agentId: route.agentId }),
recordInboundSession,
dispatchReplyWithBufferedBlockDispatcher: dispatch,
};
},
},
}),
);
const nativeClient = {
request: vi.fn(async (method: string) => {
if (method !== "send") {
throw new Error(`unexpected native iMessage method ${method}`);
}
return { guid: `native-${kind}-guid`, status: "sent" };
}),
stop: vi.fn(async () => {}),
};
let onNotification: ((message: { method: string; params: unknown }) => void) | undefined;
const watchClient = {
request: vi.fn(async () => ({ subscription: 1 })),
waitForClose: vi.fn(async () => {
onNotification?.({
method: "message",
params: {
message: {
id: 91,
guid: `monitor-${kind}-${visible}-guid`,
chat_id: 123,
sender: "+15550001111",
is_from_me: false,
text: "exercise actual direct delivery",
is_group: false,
created_at: new Date().toISOString(),
},
},
});
await Promise.resolve();
await Promise.resolve();
}),
stop: vi.fn(async () => {}),
};
createIMessageRpcClientMock.mockImplementation(async (params) => {
if (params?.onNotification) {
onNotification = params.onNotification;
return watchClient as never;
}
return nativeClient as never;
});
await monitorIMessageProvider({
config: {
channels: {
imessage: {
dmPolicy: "allowlist",
allowFrom: ["+15550001111"],
sendReadReceipts: false,
},
},
messages: { inbound: { debounceMs: 0 } },
session: { mainKey: "main" },
} as never,
runtime: { error: vi.fn(), exit: vi.fn(), log: vi.fn() },
});
if (!visible) {
expect(nativeClient.request).not.toHaveBeenCalled();
expect(messageSent).not.toHaveBeenCalled();
return;
}
await vi.waitFor(() => {
expect(messageSent).toHaveBeenCalledOnce();
});
expect(messageSent).toHaveBeenCalledWith(
expect.objectContaining({ content: text, success: true, messageId: `native-${kind}-guid` }),
expect.objectContaining({ channelId: "imessage" }),
);
const { DatabaseSync } = await import("node:sqlite");
const database = new DatabaseSync(
path.join(getIMessageRuntime().state.resolveStateDir(), "state", "openclaw.sqlite"),
{ readOnly: true },
);
try {
const persisted = database
.prepare(
"SELECT value_json FROM plugin_state_entries WHERE plugin_id = ? AND namespace = ?",
)
.all("imessage", "imessage.sent-echoes");
expect(persisted).toEqual(
expect.arrayContaining([
expect.objectContaining({ value_json: expect.stringContaining(`native-${kind}-guid`) }),
]),
);
} finally {
database.close();
}
},
);
});
+155 -21
View File
@@ -1,4 +1,9 @@
// Imessage tests cover deliver plugin behavior.
import {
createChannelPartialDeliveryError,
isChannelPartialDeliveryError,
} from "openclaw/plugin-sdk/channel-inbound";
import { createMessageReceiptFromOutboundResults } from "openclaw/plugin-sdk/channel-outbound";
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
@@ -6,6 +11,10 @@ const sendMessageIMessageMock = vi.hoisted(() =>
vi.fn().mockImplementation(async (_to: string, message: string) => ({
messageId: "imsg-1",
sentText: message,
receipt: createMessageReceiptFromOutboundResults({
results: [{ channel: "imessage", messageId: "imsg-1" }],
kind: "text",
}),
})),
);
const chunkTextWithModeMock = vi.hoisted(() => vi.fn((text: string) => [text]));
@@ -13,6 +22,13 @@ const resolveChunkModeMock = vi.hoisted(() => vi.fn(() => "length"));
const convertMarkdownTablesMock = vi.hoisted(() => vi.fn((text: string) => text));
const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "code"));
function createTestIMessageReceipt(messageId: string, kind: "text" | "media" = "text") {
return createMessageReceiptFromOutboundResults({
results: [{ channel: "imessage", messageId }],
kind,
});
}
vi.mock("../send.js", () => ({
sendMessageIMessage: (to: string, message: string, opts?: unknown) =>
sendMessageIMessageMock(to, message, opts),
@@ -25,15 +41,15 @@ vi.mock("./deliver.runtime.js", () => ({
convertMarkdownTables: (text: string) => convertMarkdownTablesMock(text),
}));
let deliverReplies: typeof import("./deliver.js").deliverReplies;
let deliverIMessageReply: typeof import("./deliver.js").deliverIMessageReply;
let createIMessageEchoCachingSend: typeof import("./deliver.js").createIMessageEchoCachingSend;
describe("deliverReplies", () => {
describe("deliverIMessageReply", () => {
const IMESSAGE_TEST_CFG = { channels: { imessage: { accounts: { default: {} } } } };
const runtime = { log: vi.fn(), error: vi.fn() } as unknown as RuntimeEnv;
beforeAll(async () => {
({ createIMessageEchoCachingSend, deliverReplies } = await import("./deliver.js"));
({ createIMessageEchoCachingSend, deliverIMessageReply } = await import("./deliver.js"));
});
beforeEach(() => {
@@ -50,9 +66,9 @@ describe("deliverReplies", () => {
it("sends monitor text chunks without reusing the watch rpc client", async () => {
chunkTextWithModeMock.mockImplementation((text: string) => text.split("|"));
await deliverReplies({
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
replies: [{ text: "first|second", replyToId: "reply-1" }],
payload: { text: "first|second", replyToId: "reply-1" },
target: "chat_id:10",
accountId: "default",
runtime,
@@ -86,15 +102,13 @@ describe("deliverReplies", () => {
});
it("propagates payload replyToId through media sends", async () => {
await deliverReplies({
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
replies: [
{
text: "caption",
mediaUrls: ["https://example.com/a.jpg", "https://example.com/b.jpg"],
replyToId: "reply-2",
},
],
payload: {
text: "caption",
mediaUrls: ["https://example.com/a.jpg", "https://example.com/b.jpg"],
replyToId: "reply-2",
},
target: "chat_id:20",
accountId: "acct-2",
runtime,
@@ -130,9 +144,9 @@ describe("deliverReplies", () => {
});
it("forwards voice-note payloads to the canonical iMessage media sender", async () => {
await deliverReplies({
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
replies: [{ mediaUrl: "https://example.com/voice.caf", audioAsVoice: true }],
payload: { mediaUrl: "https://example.com/voice.caf", audioAsVoice: true },
target: "chat_id:20",
accountId: "acct-2",
runtime,
@@ -160,6 +174,7 @@ describe("deliverReplies", () => {
sendMessageIMessageMock.mockResolvedValueOnce({
messageId: "imsg-durable-1",
sentText: "durable hello",
receipt: createTestIMessageReceipt("imsg-durable-1"),
});
await send("chat_id:50", "durable hello", {
@@ -192,6 +207,7 @@ describe("deliverReplies", () => {
sendMessageIMessageMock.mockResolvedValueOnce({
messageId: "imsg-durable-2",
sentText: "Visible reply",
receipt: createTestIMessageReceipt("imsg-durable-2"),
});
await send("chat_id:60", "<thinking>hidden</thinking>\nVisible reply\nassistant:", {
@@ -221,12 +237,20 @@ describe("deliverReplies", () => {
const remember = vi.fn();
chunkTextWithModeMock.mockImplementation((text: string) => text.split("|"));
sendMessageIMessageMock
.mockResolvedValueOnce({ messageId: "imsg-1", sentText: "first" })
.mockResolvedValueOnce({ messageId: "imsg-2", sentText: "second" });
.mockResolvedValueOnce({
messageId: "imsg-1",
sentText: "first",
receipt: createTestIMessageReceipt("imsg-1"),
})
.mockResolvedValueOnce({
messageId: "imsg-2",
sentText: "second",
receipt: createTestIMessageReceipt("imsg-2"),
});
await deliverReplies({
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
replies: [{ text: "first|second" }],
payload: { text: "first|second" },
target: "chat_id:30",
accountId: "acct-3",
runtime,
@@ -251,11 +275,12 @@ describe("deliverReplies", () => {
messageId: "imsg-media-1",
sentText: "",
echoMedia: { contentType: "image/jpeg", kind: "image" },
receipt: createTestIMessageReceipt("imsg-media-1", "media"),
});
await deliverReplies({
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
replies: [{ mediaUrls: ["https://example.com/a.jpg"] }],
payload: { mediaUrls: ["https://example.com/a.jpg"] },
target: "chat_id:40",
accountId: "acct-4",
runtime,
@@ -269,4 +294,113 @@ describe("deliverReplies", () => {
messageId: "imsg-media-1",
});
});
it("returns every accepted native chunk without inventing failed thread metadata", async () => {
chunkTextWithModeMock.mockImplementation((text: string) => text.split("|"));
sendMessageIMessageMock
.mockResolvedValueOnce({
messageId: "accepted-first",
sentText: "first",
receipt: createTestIMessageReceipt("accepted-first"),
})
.mockResolvedValueOnce({
messageId: "accepted-second",
sentText: "second",
receipt: createTestIMessageReceipt("accepted-second"),
});
const delivered = await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
payload: { text: "first|second", replyToId: "unsupported-thread" },
target: "chat_id:70",
accountId: "default",
runtime,
maxBytes: 4096,
textLimit: 4000,
});
expect(delivered).toMatchObject({
visibleReplySent: true,
messageIds: ["accepted-first", "accepted-second"],
content: "first\nsecond",
receipt: { platformMessageIds: ["accepted-first", "accepted-second"] },
});
expect(delivered?.receipt).not.toHaveProperty("replyToId");
});
it("preserves earlier media receipts when a later native caption fails", async () => {
const firstReceipt = createMessageReceiptFromOutboundResults({
results: [
{ channel: "imessage", messageId: "first-attachment" },
{ channel: "imessage", messageId: "first-caption" },
],
kind: "media",
});
const lastReceipt = createTestIMessageReceipt("second-attachment", "media");
sendMessageIMessageMock
.mockResolvedValueOnce({
messageId: "first-attachment",
sentText: "visible caption",
receipt: firstReceipt,
})
.mockRejectedValueOnce(
createChannelPartialDeliveryError(new Error("caption rejected"), {
messageIds: ["second-attachment"],
receipt: lastReceipt,
visibleReplySent: true,
content: "",
}),
);
let observed: unknown;
try {
await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
payload: {
text: "visible caption",
mediaUrls: ["https://example.com/first.jpg", "https://example.com/second.jpg"],
replyToId: "unsupported-thread",
},
target: "chat_id:80",
accountId: "default",
runtime,
maxBytes: 4096,
textLimit: 4000,
});
} catch (error: unknown) {
observed = error;
}
expect(isChannelPartialDeliveryError(observed)).toBe(true);
if (!isChannelPartialDeliveryError(observed)) {
throw new Error("expected canonical partial delivery error");
}
expect(observed.deliveryResult).toMatchObject({
visibleReplySent: true,
messageIds: ["first-attachment", "first-caption", "second-attachment"],
content: "visible caption",
receipt: {
platformMessageIds: ["first-attachment", "first-caption", "second-attachment"],
},
});
expect(observed.deliveryResult.receipt).not.toHaveProperty("replyToId");
});
it("returns a recorded non-visible outcome when sanitization removes the complete reply", async () => {
const delivered = await deliverIMessageReply({
cfg: IMESSAGE_TEST_CFG,
payload: { text: "<thinking>private reasoning</thinking>" },
target: "chat_id:90",
accountId: "default",
runtime,
maxBytes: 4096,
textLimit: 4000,
});
expect(sendMessageIMessageMock).not.toHaveBeenCalled();
expect(delivered).toEqual({
visibleReplySent: false,
suppression: { reason: "no_visible_result" },
});
});
});
+79 -40
View File
@@ -1,4 +1,12 @@
// Imessage plugin module implements deliver behavior.
import {
createChannelPartialDeliveryError,
isChannelPartialDeliveryError,
} from "openclaw/plugin-sdk/channel-inbound";
import {
createMessageReceiptFromOutboundResults,
listMessageReceiptPlatformIds,
} from "openclaw/plugin-sdk/channel-outbound";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
deliverTextOrMediaReply,
@@ -16,9 +24,9 @@ import {
import type { SentMessageCache } from "./echo-cache.js";
import { sanitizeOutboundText } from "./sanitize-outbound.js";
export async function deliverReplies(params: {
export async function deliverIMessageReply(params: {
cfg: OpenClawConfig;
replies: ReplyPayload[];
payload: ReplyPayload;
target: string;
accountId?: string;
runtime: RuntimeEnv;
@@ -26,7 +34,7 @@ export async function deliverReplies(params: {
textLimit: number;
sentMessageCache?: Pick<SentMessageCache, "remember">;
}) {
const { replies, target, runtime, maxBytes, textLimit, accountId, sentMessageCache } = params;
const { payload, target, runtime, maxBytes, textLimit, accountId, sentMessageCache } = params;
const scope = `${accountId ?? ""}:${target}`;
const { cfg } = params;
const tableMode = resolveMarkdownTableMode({
@@ -35,50 +43,81 @@ export async function deliverReplies(params: {
accountId,
});
const chunkMode = resolveChunkMode(cfg, "imessage", accountId);
for (const payload of replies) {
const rawText = sanitizeOutboundText(payload.text ?? "");
const reply = resolveSendableOutboundReplyParts(payload, {
text: convertMarkdownTables(rawText, tableMode),
const rawText = sanitizeOutboundText(payload.text ?? "");
const reply = resolveSendableOutboundReplyParts(payload, {
text: convertMarkdownTables(rawText, tableMode),
});
const accepted: Awaited<ReturnType<typeof sendMessageIMessage>>[] = [];
const sendAccepted = async (text: string, mediaUrl?: string) => {
const sent = await sendMessageIMessage(target, text, {
config: cfg,
...(mediaUrl ? { mediaUrl, ...(payload.audioAsVoice ? { audioAsVoice: true } : {}) } : {}),
maxBytes,
accountId,
replyToId: payload.replyToId,
});
const delivered = await deliverTextOrMediaReply({
accepted.push(sent);
const echoText = sent.echoText ?? (sent.sentText || undefined);
sentMessageCache?.remember(scope, {
...(echoText ? { text: echoText } : {}),
...(sent.echoMedia ? { media: sent.echoMedia } : {}),
messageId: sent.messageId,
});
};
let delivered: Awaited<ReturnType<typeof deliverTextOrMediaReply>>;
try {
delivered = await deliverTextOrMediaReply({
payload,
text: reply.text,
chunkText: (value) => chunkTextWithMode(value, textLimit, chunkMode),
sendText: async (chunk) => {
const sent = await sendMessageIMessage(target, chunk, {
config: params.cfg,
maxBytes,
accountId,
replyToId: payload.replyToId,
});
const echoText = sent.echoText ?? sent.sentText;
sentMessageCache?.remember(scope, {
...(echoText ? { text: echoText } : {}),
...(sent.echoMedia ? { media: sent.echoMedia } : {}),
messageId: sent.messageId,
});
},
sendMedia: async ({ mediaUrl, caption }) => {
const sent = await sendMessageIMessage(target, caption ?? "", {
config: params.cfg,
mediaUrl,
...(payload.audioAsVoice ? { audioAsVoice: true } : {}),
maxBytes,
accountId,
replyToId: payload.replyToId,
});
const echoText = sent.echoText ?? (sent.sentText || undefined);
sentMessageCache?.remember(scope, {
...(echoText ? { text: echoText } : {}),
...(sent.echoMedia ? { media: sent.echoMedia } : {}),
messageId: sent.messageId,
});
},
sendText: sendAccepted,
sendMedia: ({ mediaUrl, caption }) => sendAccepted(caption ?? "", mediaUrl),
});
if (delivered !== "empty") {
runtime.log?.(`imessage: delivered reply to ${target}`);
} catch (error: unknown) {
const partial = isChannelPartialDeliveryError(error) ? error.deliveryResult : undefined;
if (accepted.length === 0 && partial?.visibleReplySent !== true) {
throw error;
}
// A native attachment can settle before its caption rejects; preserve every
// previously accepted receipt plus that nested provider-visible subset.
const receipt = createMessageReceiptFromOutboundResults({
results: [
...accepted.map((result) => ({ receipt: result.receipt })),
...(partial?.receipt
? [{ receipt: partial.receipt }]
: (partial?.messageIds ?? []).map((messageId) => ({ messageId }))),
],
kind: reply.mediaUrls.length > 0 ? "media" : "text",
});
throw createChannelPartialDeliveryError(error, {
messageIds: listMessageReceiptPlatformIds(receipt),
receipt,
visibleReplySent: true,
content: [...accepted.map((result) => result.sentText), partial?.content]
.filter(Boolean)
.join("\n"),
});
}
if (delivered === "empty") {
return {
visibleReplySent: false as const,
suppression: { reason: "no_visible_result" as const },
};
}
const receipt = createMessageReceiptFromOutboundResults({
results: accepted.map((result) => ({ receipt: result.receipt })),
kind: delivered,
});
runtime.log?.(`imessage: delivered reply to ${target}`);
return {
messageIds: listMessageReceiptPlatformIds(receipt),
receipt,
visibleReplySent: true as const,
content: accepted
.map((result) => result.sentText)
.filter(Boolean)
.join("\n"),
};
}
export function createIMessageEchoCachingSend(params: {
@@ -78,7 +78,7 @@ import { runIMessageCatchup } from "./catchup-bridge.js";
import { advanceIMessageCatchupCursor, resolveCatchupConfig } from "./catchup.js";
import { combineIMessagePayloads } from "./coalesce.js";
import { repairIMessageConversationAnchor } from "./conversation-repair.js";
import { createIMessageEchoCachingSend, deliverReplies } from "./deliver.js";
import { createIMessageEchoCachingSend, deliverIMessageReply } from "./deliver.js";
import { resolveIMessageDmHistoryContext, resolveIMessageDmHistoryLimit } from "./dm-history.js";
import { createIMessageThrottledDropDiagnosticCache } from "./drop-diagnostic-cache.js";
import { createSentMessageCache } from "./echo-cache.js";
@@ -1163,15 +1163,19 @@ export async function monitorIMessageProvider(opts: MonitorIMessageOpts = {}): P
},
}
: false,
deliver: async (payload: Parameters<typeof deliverReplies>[0]["replies"][number]) => {
observeMessageSent: true,
deliver: async (payload: Parameters<typeof deliverIMessageReply>[0]["payload"]) => {
const target = ctxPayload.To;
if (!target) {
runtime.error?.(danger("imessage: missing delivery target"));
return;
return {
visibleReplySent: false,
suppression: { reason: "no_visible_result" },
} as const;
}
await deliverReplies({
return await deliverIMessageReply({
cfg,
replies: [payload],
payload,
target,
accountId: accountInfo.accountId,
runtime,