openclaw - ✅(Solved) Fix [runtime] subagent-announce Branch 2 missing SILENT_REPLY_TOKEN guard — causes duplicate messages [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#78368Fetched 2026-05-07 03:37:43
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
3
Timeline (top)
commented ×1cross-referenced ×1

The subagent-announce flow's "expectsCompletionMessage" branch (Branch 2) lacks the SILENT_REPLY_TOKEN guard that Branches 1 and 3 have. This causes the parent agent to be re-prompted to deliver child results even when the parent has already delivered them in a prior assistant message — producing user-visible duplicate messages.

Root Cause

In subagent-announce-4mg0K78I.js around line 32, three branches build the prompt for the parent. Branches 1 and 3 include language like "Reply ONLY: ${SILENT_REPLY_TOKEN} if you have already delivered...". Branch 2 (the expectsCompletionMessage path) does not.

Result: the LLM has no canonical signal to short-circuit. Even with NO_REPLY discipline in agent prompts, the absence of the runtime-injected silent-token instruction makes duplicate suppression unreliable.

Fix Action

Fixed

PR fix notes

PR #78425: fix(subagents): add SILENT_REPLY_TOKEN guard to completion-mode announce prompt

Description (problem / solution / changelog)

Summary

  • Problem: The expectsCompletionMessage branch (Branch 2) in buildAnnounceReplyInstruction lacks the SILENT_REPLY_TOKEN duplicate suppression guard that Branches 1 and 3 have. This causes parent agents to receive duplicate prompts to deliver child results they already sent.
  • What changed: Added the same SILENT_REPLY_TOKEN guard to Branch 2, allowing LLMs to reply with NO_REPLY when they have already delivered the result.
  • Why it matters: Prevents user-visible duplicate messages when parent agents deliver results inline before child completion events arrive.

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security
  • Chore

Scope

  • Gateway/orchestration
  • Skills/tool execution (subagent announce flow)
  • Auth/tokens
  • Memory/storage
  • Integrations
  • API/contracts
  • UI/DX
  • CI/CD

Linked Issue

  • Fixes #78368
  • This PR fixes a prompt parity bug in subagent-announce

Root Cause

Three branches in buildAnnounceReplyInstruction build prompts for the parent agent:

BranchConditionSILENT_REPLY_TOKEN Guard
1requesterIsSubagent✅ Present
2expectsCompletionMessageMissing
3Default✅ Present

Branch 2 lacked the duplicate suppression mechanism, causing re-prompts.

Regression Test Plan

  • Unit test added
  • Target test: src/agents/subagent-announce.format.e2e.test.ts - "routes manual spawn completion"
  • New assertion verifies Branch 2 prompt contains SILENT_REPLY_TOKEN and "already delivered the substance" instruction.

Security Impact

  • No new permissions/capabilities
  • No security implications - purely prompt text change

User-visible Changes

  • Parent agents can now suppress duplicate delivery prompts
  • No config changes required

Verification

  • Code review of buildAnnounceReplyInstruction logic
  • Test added to verify Branch 2 prompt parity
  • Existing tests for Branches 1 and 3 still pass

Compatibility

  • Backward compatible
  • No migration needed

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/subagent-announce.format.e2e.test.ts (modified, +4/-1)
  • src/agents/subagent-announce.ts (modified, +1/-1)

Code Example

if (params.expectsCompletionMessage)
  return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (...).`;

---

if (params.expectsCompletionMessage)
  return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the internal event text verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if you have already delivered the substance of this result to the user in your most recent assistant message (look at the last assistant turn in your transcript above; if its content already conveys the same finding, this is a duplicate).`;

---

const recentAssistant = await getMostRecentAssistantMessage(targetRequesterSessionKey);
if (
  recentAssistant &&
  params.endedAt &&
  recentAssistant.ts >= params.endedAt &&
  fingerprintMatches(recentAssistant.text, findings)
) {
  return true;  // already delivered, no announce needed
}
RAW_BUFFERClick to expand / collapse

Summary

The subagent-announce flow's "expectsCompletionMessage" branch (Branch 2) lacks the SILENT_REPLY_TOKEN guard that Branches 1 and 3 have. This causes the parent agent to be re-prompted to deliver child results even when the parent has already delivered them in a prior assistant message — producing user-visible duplicate messages.

Repro

  1. Parent agent spawns a subagent for a verification task.
  2. Parent receives intermediate progress, decides to deliver the result inline rather than waiting for the child's completion event.
  3. Parent posts user-facing message with the result.
  4. Child eventually completes; runtime fires the announce flow.
  5. Parent receives synthetic user message: "A completed subagent task is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now."
  6. Parent re-sends the same content. User sees duplicate.

Root cause

In subagent-announce-4mg0K78I.js around line 32, three branches build the prompt for the parent. Branches 1 and 3 include language like "Reply ONLY: ${SILENT_REPLY_TOKEN} if you have already delivered...". Branch 2 (the expectsCompletionMessage path) does not.

Result: the LLM has no canonical signal to short-circuit. Even with NO_REPLY discipline in agent prompts, the absence of the runtime-injected silent-token instruction makes duplicate suppression unreliable.

Proposed fix — minimal (Branch 2 SILENT guard)

Change Branch 2 from:

if (params.expectsCompletionMessage)
  return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (...).`;

to:

if (params.expectsCompletionMessage)
  return `A completed ${params.announceType} is ready for user delivery. Convert the result above into your normal assistant voice and send that user-facing update now. Keep this internal context private (don't mention system/log/stats/session details or announce type), and do not copy the internal event text verbatim. Reply ONLY: ${SILENT_REPLY_TOKEN} if you have already delivered the substance of this result to the user in your most recent assistant message (look at the last assistant turn in your transcript above; if its content already conveys the same finding, this is a duplicate).`;

5-line change. Brings Branch 2 into parity with the other two paths.

Proposed fix — structural (recommended follow-up)

The above relies on the LLM correctly self-detecting duplicates. A more robust fix: in runSubagentAnnounceFlow, before building the announce prompt, check whether the parent session's most recent assistant message has a timestamp after params.endedAt AND fingerprint-matches the child's findings. If so, skip the announce entirely.

Pseudocode:

const recentAssistant = await getMostRecentAssistantMessage(targetRequesterSessionKey);
if (
  recentAssistant &&
  params.endedAt &&
  recentAssistant.ts >= params.endedAt &&
  fingerprintMatches(recentAssistant.text, findings)
) {
  return true;  // already delivered, no announce needed
}

fingerprintMatches: normalise both to lowercase + collapse whitespace, check for any contiguous ~60-char window match. Conservative — false negative (still announce) is far better than false positive (skip when actually duplicate).

This is the structural fix: runtime stops re-prompting when it has direct evidence the parent already covered the result.

Optional: spawn-side cooperative hint

Add a new spawn parameter finalAnswerWillBeFromParent: true. When set, force the announce branch to the SILENT-guarded path regardless of expectsCompletionMessage. Lets orchestrator agents declare "I will write the final answer myself" at spawn time.

subagent-spawn-Btdu5pK2.js line 652 area.

Effort

  • Branch 2 SILENT guard: ~5 lines, trivial
  • Structural short-circuit: ~30 lines + tests
  • Cooperative hint: ~10 lines + schema entry

Context

Discovered during a series of orchestration tasks where Ghost (parent) delivered results inline and child completion events triggered visible duplicates. Branch 2 is the most-exercised path and has the highest user impact.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [runtime] subagent-announce Branch 2 missing SILENT_REPLY_TOKEN guard — causes duplicate messages [1 pull requests, 1 comments, 2 participants]