mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 01:57:15 +00:00
fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces
When a subagent completes while the main session is busy, the announce
is queued by the application-level announce queue (subagent-announce-
queue.ts) and drained via sendAnnounce → callGateway({ method: 'agent' }).
However, if the main session is still busy when the drain fires, the
gateway-level message queue also captures this callGateway call, creating
a second copy. Both queues eventually deliver, causing duplicate
announcements.
The root cause is that both sendAnnounce and the direct announce path
use crypto.randomUUID() as the idempotency key, generating a unique key
per call. The gateway's dedup cache (context.dedupe) can never match
because each attempt has a different key.
Replace the random keys with deterministic ones derived from stable
identifiers:
- sendAnnounce: announce:{sessionKey}:{enqueuedAt}
- direct announce: announce:{childSessionKey}:{childRunId}
This allows the gateway dedup cache to recognize the second delivery
attempt as a duplicate and return the cached response instead.
Fixes #17122
This commit is contained in:
committed by
Gustavo Madeira Santana
parent
7ea14a1c87
commit
40e12e81f8
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user