Skip to content

fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces#17150

Merged
gumadeiras merged 4 commits intoopenclaw:mainfrom
widingmarcus-cyber:fix/subagent-announce-dedup-17122
Feb 15, 2026
Merged

fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces#17150
gumadeiras merged 4 commits intoopenclaw:mainfrom
widingmarcus-cyber:fix/subagent-announce-dedup-17122

Conversation

@widingmarcus-cyber
Copy link
Contributor

@widingmarcus-cyber widingmarcus-cyber commented Feb 15, 2026

fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces

Fixes #17122

Problem

When a subagent completes while the main session is busy, the completion announcement is delivered twice:

  1. The application-level announce queue (subagent-announce-queue.ts) drains and calls sendAnnouncecallGateway({ method: "agent" })
  2. If the main session is still busy, the gateway-level message queue also captures this callGateway call
  3. Both queues eventually deliver → duplicate announcement

Root Cause

Both sendAnnounce (queue drain path) and the direct announce path in runSubagentAnnounceFlow use crypto.randomUUID() as the idempotency key:

// Before fix:
idempotencyKey: crypto.randomUUID(),  // unique per call → dedup never matches

The gateway's dedup cache (context.dedupe.get(\agent:${idem}`)`) can never detect the duplicate because each delivery attempt has a different key.

Fix

Replace random idempotency keys with deterministic ones derived from stable identifiers:

// Queue drain path:
idempotencyKey: \`announce:${item.sessionKey}:${item.enqueuedAt}\`

// Direct announce path:
idempotencyKey: \`announce:${params.childSessionKey}:${params.childRunId}\`

This allows the gateway dedup cache to recognize the second delivery attempt and return the cached response instead of processing it again.

Changed Files

File Change
src/agents/subagent-announce.ts Replace crypto.randomUUID() with deterministic keys in both sendAnnounce and direct announce path; remove unused crypto import

Testing

  • All 79 agent tests pass
  • All 101 cron tests pass
  • Lint + format pass (oxlint + oxfmt)

Related

Greptile Summary

Replaced random UUIDs with deterministic idempotency keys derived from stable identifiers to prevent duplicate subagent announcements when both the application-level announce queue and gateway-level message queue deliver the same announcement.

Key changes:

  • Queue drain path in sendAnnounce: uses template with session key and enqueue timestamp instead of random UUID
  • Direct announce path in runSubagentAnnounceFlow: uses template with child session key and run ID instead of random UUID
  • Removed unused crypto import

The fix allows the gateway's deduplication cache to properly recognize and reject duplicate delivery attempts by using the same idempotency key for both delivery paths.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The fix correctly addresses the root cause of duplicate announcements by replacing non-deterministic random UUIDs with stable, deterministic idempotency keys. The values used (sessionKey, enqueuedAt, childRunId) are all stable identifiers that remain constant across duplicate delivery attempts. The change is minimal, well-tested (79 agent tests + 101 cron tests passing), and follows the same pattern used for cron duplicate delivery in fix(cron): prevent duplicate delivery for isolated jobs with announce mode #15739.
  • No files require special attention

Last reviewed commit: 8f695c3

(5/5) You can turn off certain types of comments like style here!

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: XS labels Feb 15, 2026
@gumadeiras gumadeiras self-assigned this Feb 15, 2026
@gumadeiras gumadeiras force-pushed the fix/subagent-announce-dedup-17122 branch 4 times, most recently from b992e27 to 41ae6a0 Compare February 15, 2026 15:31
widingmarcus-cyber and others added 4 commits February 15, 2026 10:33
…e 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 openclaw#17122
@gumadeiras gumadeiras force-pushed the fix/subagent-announce-dedup-17122 branch from 41ae6a0 to 54bba3c Compare February 15, 2026 15:34
@gumadeiras gumadeiras merged commit ade11ec into openclaw:main Feb 15, 2026
8 of 9 checks passed
@gumadeiras
Copy link
Member

Merged via squash.

Thanks @widingmarcus-cyber!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate subagent announce delivery when main session is busy

2 participants

Comments