Skip to content

[Bug]: image_generate still duplicate-sends Discord attachments after terminal guard #87995

@compoodment

Description

@compoodment

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

This is a follow-up to #87989 because GitHub will not let me reopen that issue after it was closed as completed. The original issue is not fixed in the latest beta.

After updating the affected Discord bot instance to 2026.5.28-beta.1 (84fd629), a single successful image_generate completion still sent the same generated Discord attachment 12 times.

Steps to reproduce

  1. Run a Discord channel agent on OpenClaw 2026.5.28-beta.1.
  2. Ask the agent to generate an image for the current channel.
  3. Wait for the background image_generate:<taskId>:ok completion wake to route back to the requester session.
  4. Observe repeated message.send calls with the same generated media path and different captions.

Expected behavior

A successful image_generate completion should deliver each generated attachment exactly once to the original chat, then stop.

Historical message-tool calls, explicit route fields, or requester-session context should not be able to cause repeated delivery of the same completion artifact.

Actual behavior

One image_generate completion delivered the same generated JPEG 12 times to the same Discord channel. The run eventually stopped, so this is bounded duplicate delivery, not an infinite Discord retry loop.

Environment

OpenClaw 2026.5.28-beta.1 (84fd629)
Service: systemd user (enabled)
File logs: /tmp/openclaw/openclaw-2026-05-29.log
Install method: npm global
Gateway: systemd user service
Model for requester/completion session: ollama/glm-5.1:cloud
Image provider/model from completion event: google/gemini-3.1-flash-image-preview

Reproduction evidence

Run: image_generate:5e66cfd7-9cff-4edd-86b5-85d5aec8e2fe:ok
Session key: agent:main:discord:channel:1507887702379335791
Media path: ~/.openclaw/media/tool-image-generation/image-1---34799bf3-36d1-462a-8e08-3490a7e58a28.jpg
Discord attachment filename: image-1---34799bf3-36d1-462a-8e08-3490a7e58a28.jpg
Image size: 1197745 bytes, 1024x1024 JPEG

Requester session JSONL:
  message tool calls with that exact path: 12
  successful message tool results: 12
  delivery mirror idempotency keys: 12

Public Discord channel read:
  visible posts with that exact attachment filename: 12

Receipt summary:

01. 2026-05-29T10:40:36.989Z -> 1509869060245164055; routeKeys=channel; call=call_ulah6cgi
02. 2026-05-29T10:40:45.759Z -> 1509869098979561564; routeKeys=channel; call=call_ndkp55eb
03. 2026-05-29T10:40:53.712Z -> 1509869133053952060; routeKeys=channel; call=call_rl6o2078
04. 2026-05-29T10:41:00.283Z -> 1509869161067577508; routeKeys=target; call=call_fgt36hia
05. 2026-05-29T10:41:07.861Z -> 1509869192646492310; routeKeys=channel; call=call_94wri2z2
06. 2026-05-29T10:41:13.570Z -> 1509869216399097877; routeKeys=channel; call=call_jbqbwmkg
07. 2026-05-29T10:41:20.524Z -> 1509869244219785326; routeKeys=channel; call=call_uqiogzbp
08. 2026-05-29T10:41:25.994Z -> 1509869268496289884; routeKeys=channel; call=call_ljup701l
09. 2026-05-29T10:41:35.900Z -> 1509869305628463204; routeKeys=channel; call=call_1teg1h4j
10. 2026-05-29T10:41:41.837Z -> 1509869335076802711; routeKeys=channel; call=call_jz16jss4
11. 2026-05-29T10:41:59.827Z -> 1509869410498646137; routeKeys=channel; call=call_oqmy5de6
12. 2026-05-29T10:42:13.597Z -> 1509869466987794512; routeKeys=channel+target; call=call_qepozibv

The requester session then stopped only after the model noticed it had looped:

The image has already been sent to the channel (multiple times, unfortunately — I was looping again). I will NOT send it again.
MEDIA:~/.openclaw/media/tool-image-generation/image-1---34799bf3-36d1-462a-8e08-3490a7e58a28.jpg

Why #83022 / the terminal guard is not sufficient

The closure on #87989 pointed to the message-tool terminal guard as already fixing this. That guard exists in current main and in the tested beta, but it does not cover this reproduced path.

Current main's message-tool-terminal.ts returns false for explicit route sends:

if (!isMessageToolSendActionName(args.action) || hasExplicitMessageRoute(args)) {
  return false;
}

Every duplicate send in this reproduction used an explicit route key: channel, target, or both. So the terminal guard is present, but it deliberately does not terminate the duplicate sends seen here.

Completion delivery path still delegates to the requester LLM first

Current main's generated-media completion path still makes direct generated-media delivery fallback-only while the requester session is active:

const tryGeneratedMediaDirectDelivery = async (announceResponse?: unknown) => {
  if (requesterActivity.isActive && !activeRequesterWakeFailed) {
    return undefined;
  }
  ...
};

const completionSourceReplyDeliveryMode = requiresMessageToolDelivery
  ? "message_tool_only"
  : undefined;

const wakeOutcome = await resolveActiveWakeWithRetries(
  requesterActivity.sessionId,
  params.triggerMessage,
  wakeOptions,
  params.signal,
);

That means successful generated-media delivery is still handed to an LLM wake first, with direct delivery only as fallback. The current terminal guard only handles a subset of message-tool sends after delivery; it does not enforce artifact-level idempotency.

Impact and severity

Severity: Medium.

This does not crash the gateway, but it spams channels, repeats uploads, wastes tool/provider work, and makes image completion delivery unreliable.

Suggested fix

Either:

  1. Prefer deterministic direct delivery for generated-media completions when expected media URLs are present, before waking the requester LLM; or
  2. Add a hard idempotency guard keyed by image_generate:<taskId>:ok + target + mediaUrls, and stop/ignore any later send attempts for the same artifact.

Draft PR #87991 implements the direct-first approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions