mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 06:11:37 +00:00
fix(imessage): prevent duplicate messages after delayed sends (#110853)
* fix(imessage): outlive imsg send fallback * test(imessage): prove delayed fallback resolves once * fix(imessage): preserve the send timeout floor --------- Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
This commit is contained in:
@@ -1,11 +1,8 @@
|
||||
/** Default timeout for iMessage probe/RPC operations (10 seconds). */
|
||||
export const DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS = 10_000;
|
||||
|
||||
// Sends get a much longer default than probes: on macOS 26 (Tahoe) the private
|
||||
// API bridge intermittently stalls up to ~124s before the send completes. The
|
||||
// 10s probe timeout aborts those mid-flight, and non-recoverable shapes
|
||||
// (attachment/reply) are then lost. This must clear the observed upper bound
|
||||
// plus headroom, otherwise the long tail of stalls still loses sends — 150s
|
||||
// covers 124s with margin. Decoupling keeps probes/health checks fast while
|
||||
// letting real sends ride out the stall. Akin to the BlueBubbles fix (#69193).
|
||||
export const DEFAULT_IMESSAGE_SEND_TIMEOUT_MS = 150_000;
|
||||
// imsg waits up to 150s for a private-bridge send, then auto transport can fall
|
||||
// back to AppleScript and spend up to 8s verifying the persisted row. The outer
|
||||
// RPC timeout must outlive both stages; matching imsg's 150s deadline turns a
|
||||
// successful fallback into an ambiguous timeout that callers may retry.
|
||||
export const DEFAULT_IMESSAGE_SEND_TIMEOUT_MS = 180_000;
|
||||
|
||||
@@ -1709,17 +1709,46 @@ describe("sendMessageIMessage receipts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the dedicated send timeout (covers macOS 26 stalls), not the 10s probe default", async () => {
|
||||
const client = createClient({ guid: "p:0/imsg-1" });
|
||||
it("floors a configured probe timeout so one delayed imsg fallback can resolve", async () => {
|
||||
vi.useFakeTimers();
|
||||
const delayedFallbackMs = 158_000;
|
||||
const client = {
|
||||
request: vi.fn(
|
||||
(_method: string, _params: Record<string, unknown>, opts?: { timeoutMs?: number }) =>
|
||||
new Promise<Record<string, unknown>>((resolve, reject) => {
|
||||
const timeout = setTimeout(
|
||||
() => reject(new Error("imsg rpc timeout (send)")),
|
||||
opts?.timeoutMs,
|
||||
);
|
||||
setTimeout(() => {
|
||||
clearTimeout(timeout);
|
||||
resolve({ guid: "p:0/imsg-delayed-fallback" });
|
||||
}, delayedFallbackMs);
|
||||
}),
|
||||
),
|
||||
stop: vi.fn(async () => {}),
|
||||
} as unknown as IMessageRpcClient;
|
||||
|
||||
await sendMessageIMessage("chat_id:42", "hello", {
|
||||
config: IMESSAGE_TEST_CFG,
|
||||
const send = sendMessageIMessage("chat_id:42", "hello", {
|
||||
config: {
|
||||
channels: {
|
||||
imessage: {
|
||||
...IMESSAGE_TEST_CFG.channels.imessage,
|
||||
probeTimeoutMs: 10_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
client,
|
||||
});
|
||||
await vi.advanceTimersByTimeAsync(delayedFallbackMs);
|
||||
|
||||
expect(getClientMocks(client).request).toHaveBeenCalledWith("send", expect.any(Object), {
|
||||
timeoutMs: 150_000,
|
||||
});
|
||||
await expect(send).resolves.toMatchObject({ messageId: "p:0/imsg-delayed-fallback" });
|
||||
expect(getClientMocks(client).request).toHaveBeenCalledTimes(1);
|
||||
expect(getClientMocks(client).request).toHaveBeenCalledWith(
|
||||
"send",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({ timeoutMs: 180_000 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("sends explicit chat media-only payloads through send-attachment auto transport", async () => {
|
||||
|
||||
@@ -768,11 +768,12 @@ export async function sendMessageIMessage(
|
||||
replyToId: opts.replyToId,
|
||||
conversationReadOrigin: opts.conversationReadOrigin,
|
||||
});
|
||||
// Sends use a dedicated longer default (not the 10s probe timeout) so macOS 26
|
||||
// bridge stalls aren't aborted mid-send. Explicit opts/probeTimeoutMs still win
|
||||
// for callers that tuned them. See DEFAULT_IMESSAGE_SEND_TIMEOUT_MS.
|
||||
// Sends use a dedicated longer floor (not the 10s probe timeout) so macOS 26
|
||||
// bridge stalls aren't aborted mid-send. A configured probe timeout may extend
|
||||
// sends, but only an explicit per-call timeout may shorten them.
|
||||
const timeoutMs =
|
||||
opts.timeoutMs ?? account.config.probeTimeoutMs ?? DEFAULT_IMESSAGE_SEND_TIMEOUT_MS;
|
||||
opts.timeoutMs ??
|
||||
Math.max(account.config.probeTimeoutMs ?? 0, DEFAULT_IMESSAGE_SEND_TIMEOUT_MS);
|
||||
const pendingEchoTtlMs = resolvePendingPersistedEchoTtlMs(timeoutMs);
|
||||
const region = opts.region?.trim() || account.config.region?.trim() || "US";
|
||||
const maxBytes =
|
||||
|
||||
Reference in New Issue
Block a user