diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index 4d454a04bb7..b966af0c5e6 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -1,4 +1,4 @@ -import crypto from "node:crypto"; +import path from "node:path"; import { resolveQueueSettings } from "../auto-reply/reply/queue.js"; import { loadConfig } from "../config/config.js"; import { @@ -113,6 +113,13 @@ async function sendAnnounce(item: AnnounceQueueItem) { const origin = item.origin; const threadId = origin?.threadId != null && origin.threadId !== "" ? String(origin.threadId) : undefined; + // Use a deterministic idempotency key derived from the session key and + // enqueue timestamp so the gateway dedup cache catches duplicates when + // the announce is delivered via both the announce queue drain *and* the + // gateway-level message queue (which re-queues the callGateway "agent" + // call if the main session is still busy). + // See: https://github.com/openclaw/openclaw/issues/17122 + const idempotencyKey = `announce:${item.sessionKey}:${item.enqueuedAt}`; await callGateway({ method: "agent", params: { @@ -123,7 +130,7 @@ async function sendAnnounce(item: AnnounceQueueItem) { to: requesterIsSubagent ? undefined : origin?.to, threadId: requesterIsSubagent ? undefined : threadId, deliver: !requesterIsSubagent, - idempotencyKey: crypto.randomUUID(), + idempotencyKey, }, timeoutMs: 15_000, }); @@ -565,6 +572,10 @@ export async function runSubagentAnnounceFlow(params: { const { entry } = loadRequesterSessionEntry(targetRequesterSessionKey); directOrigin = deliveryContextFromSession(entry); } + // Use a deterministic idempotency key so the gateway dedup cache + // catches duplicates if this announce is also queued by the gateway- + // level message queue while the main session is busy (#17122). + const directIdempotencyKey = `announce:${params.childSessionKey}:${params.childRunId}`; await callGateway({ method: "agent", params: { @@ -578,7 +589,7 @@ export async function runSubagentAnnounceFlow(params: { !requesterIsSubagent && directOrigin?.threadId != null && directOrigin.threadId !== "" ? String(directOrigin.threadId) : undefined, - idempotencyKey: crypto.randomUUID(), + idempotencyKey: directIdempotencyKey, }, expectFinal: true, timeoutMs: 15_000,