openclaw - 💡(How to fix) Fix buildExecEventPrompt: 'user delivery disabled' branch races runtime silent-reply detection, causes incomplete-turn errors [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#75322Fetched 2026-05-01 05:35:14
View on GitHub
Comments
1
Participants
2
Timeline
1
Reactions
1
Timeline (top)
commented ×1

When buildExecEventPrompt runs with deliverToUser=false, the system message asks the agent to "reply HEARTBEAT_OK only." Capable models that have learned OpenClaw's silent-reply convention often substitute the runtime's actual silent-reply sentinel NO_REPLY — but emit it without <final>...</final> wrapping, since the prompt implies no user-visible answer is needed. The runtime then sees payloads=0, classifies the turn as an empty response, retries with the visible-answer continuation, exhausts retries, and surfaces an incomplete turn detected error to the user channel — producing exactly the kind of noise async-completion suppression is meant to avoid.

Error Message

When buildExecEventPrompt runs with deliverToUser=false, the system message asks the agent to "reply HEARTBEAT_OK only." Capable models that have learned OpenClaw's silent-reply convention often substitute the runtime's actual silent-reply sentinel NO_REPLY — but emit it without <final>...</final> wrapping, since the prompt implies no user-visible answer is needed. The runtime then sees payloads=0, classifies the turn as an empty response, retries with the visible-answer continuation, exhausts retries, and surfaces an incomplete turn detected error to the user channel — producing exactly the kind of noise async-completion suppression is meant to avoid. — surfacing incomplete-turn error — surfacing error to user 6. Retries exhaustedincomplete-turn error surfaces to user. Cosmetic noise on the user-facing channel even though deliverToUser=false. Cosmetic but persistent. Each affected agent surfaces an incomplete turn error to its channel on every async-completion event — for a heartbeat-style agent that runs openclaw doctor or similar every 15 minutes, that's ~96 false errors/day, which defeats the suppression mechanism's purpose.

Root Cause

In dist/heartbeat-events-filter-DZ-HBajV.js:

function buildExecEventPrompt(pendingEvents, opts) {
    const deliverToUser = opts?.deliverToUser ?? true;
    // ...
    if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run.";
    if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.";
    // ...
}

The prompt has two issues working together:

  1. It uses HEARTBEAT_OK (a noise-filter token, not the runtime's silent-reply sentinel). Models that know about NO_REPLY (the actual silent token defined in dist/tokens-C_v_J0E7.js as SILENT_REPLY_TOKEN = "NO_REPLY") will often prefer it because the prompt's phrasing — "user delivery is disabled," "handle the result internally" — semantically matches the silent-reply concept much more than it matches the noise-filter concept.

  2. It does not specify the <final>...</final> wrapping requirement. With "user delivery is disabled," the model reasonably concludes that user-visible wrapping is unnecessary. But the runtime's empty-response detector keys off the <final> block, so unwrapped tokens (including NO_REPLY) trip payloads=0.

Fix Action

Fix / Workaround

This:

  • Uses the runtime's actual silent-reply sentinel — guaranteed recognized by dist/dispatch-Dw_9PM8V.js and dist/normalize-reply-BIXTOBH2.js
  • Explicitly demonstrates the <final> wrapping the runtime requires
  • Removes the dual-token ambiguity (no more HEARTBEAT_OK vs NO_REPLY decision for the model)
  • Keeps the noise-suppression intent intact

Workaround applied locally

Code Example

[agent/embedded] empty response detected: runId=... sessionId=...
                  provider=google/gemini-2.5-flash-lite
                  — retrying 1/1 with visible-answer continuation
[agent/embedded] empty response retries exhausted: ... attempts=1/1
                  — surfacing incomplete-turn error
[agent/embedded] incomplete turn detected: ... stopReason=stop payloads=0
                  — surfacing error to user

---

function buildExecEventPrompt(pendingEvents, opts) {
    const deliverToUser = opts?.deliverToUser ?? true;
    // ...
    if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run.";
    if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.";
    // ...
}

---

- if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run.";
- if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.";
+ if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse output from any earlier run.";
+ if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse command output.";
RAW_BUFFERClick to expand / collapse

Summary

When buildExecEventPrompt runs with deliverToUser=false, the system message asks the agent to "reply HEARTBEAT_OK only." Capable models that have learned OpenClaw's silent-reply convention often substitute the runtime's actual silent-reply sentinel NO_REPLY — but emit it without <final>...</final> wrapping, since the prompt implies no user-visible answer is needed. The runtime then sees payloads=0, classifies the turn as an empty response, retries with the visible-answer continuation, exhausts retries, and surfaces an incomplete turn detected error to the user channel — producing exactly the kind of noise async-completion suppression is meant to avoid.

Reproducible signal

Gateway version: 2026.4.24 (cbcfdf6) Agent: arya running google/gemini-2.5-flash-lite (with gemini-2.5-flash fallback)

Journal pattern (recurring on every async-completion event today):

[agent/embedded] empty response detected: runId=... sessionId=...
                  provider=google/gemini-2.5-flash-lite
                  — retrying 1/1 with visible-answer continuation
[agent/embedded] empty response retries exhausted: ... attempts=1/1
                  — surfacing incomplete-turn error
[agent/embedded] incomplete turn detected: ... stopReason=stop payloads=0
                  — surfacing error to user

Trace

Session JSONL (agents/<agent>/sessions/<sessionId>.jsonl) shows the full sequence:

  1. Turn N (good): User says "run openclaw doctor". Agent emits <final>The openclaw doctor command is running...</final>payloads=1, runtime accepts, delivers to Discord. ✓

  2. Turn N+1 (system message): Runtime sends:

    System (untrusted): [timestamp] Exec completed (..., code 0) :: ... An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.

  3. Turn N+1 (model's reply): Model generates plain NO_REPLY (no <final> tags). Stop reason stop, output text "NO_REPLY", but payloads=0 because no <final> block was emitted.

  4. Runtime retries with continuation prompt: "The previous attempt did not produce a user-visible answer. Continue from the current state and produce the visible answer now. Do not restart from scratch."

  5. Turn N+1 retry: Model generates NO_REPLY again, same shape.

  6. Retries exhaustedincomplete-turn error surfaces to user. Cosmetic noise on the user-facing channel even though deliverToUser=false.

Root cause

In dist/heartbeat-events-filter-DZ-HBajV.js:

function buildExecEventPrompt(pendingEvents, opts) {
    const deliverToUser = opts?.deliverToUser ?? true;
    // ...
    if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run.";
    if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.";
    // ...
}

The prompt has two issues working together:

  1. It uses HEARTBEAT_OK (a noise-filter token, not the runtime's silent-reply sentinel). Models that know about NO_REPLY (the actual silent token defined in dist/tokens-C_v_J0E7.js as SILENT_REPLY_TOKEN = "NO_REPLY") will often prefer it because the prompt's phrasing — "user delivery is disabled," "handle the result internally" — semantically matches the silent-reply concept much more than it matches the noise-filter concept.

  2. It does not specify the <final>...</final> wrapping requirement. With "user delivery is disabled," the model reasonably concludes that user-visible wrapping is unnecessary. But the runtime's empty-response detector keys off the <final> block, so unwrapped tokens (including NO_REPLY) trip payloads=0.

Suggested fix

Change the !deliverToUser branch (and ideally the empty-eventText branch) to point at the actual silent-reply sentinel and demonstrate the required wrapping:

- if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply HEARTBEAT_OK only. Do not mention, summarize, or reuse output from any earlier run.";
- if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Handle the result internally and reply HEARTBEAT_OK only. Do not mention, summarize, or reuse command output.";
+ if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse output from any earlier run.";
+ if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse command output.";

This:

  • Uses the runtime's actual silent-reply sentinel — guaranteed recognized by dist/dispatch-Dw_9PM8V.js and dist/normalize-reply-BIXTOBH2.js
  • Explicitly demonstrates the <final> wrapping the runtime requires
  • Removes the dual-token ambiguity (no more HEARTBEAT_OK vs NO_REPLY decision for the model)
  • Keeps the noise-suppression intent intact

Severity

Cosmetic but persistent. Each affected agent surfaces an incomplete turn error to its channel on every async-completion event — for a heartbeat-style agent that runs openclaw doctor or similar every 15 minutes, that's ~96 false errors/day, which defeats the suppression mechanism's purpose.

Workaround applied locally

Updated agent's HEARTBEAT.md and IDENTITY.md to explicitly require <final>...</final> wrapping on every reply and to forbid bare NO_REPLY. This catches agents that read their own prompt files; it doesn't help fresh agents or agents whose system messages come purely from runtime-generated content.

extent analysis

TL;DR

Update the buildExecEventPrompt function to use the actual silent-reply sentinel NO_REPLY with required <final> wrapping when deliverToUser is false.

Guidance

  • Identify and update the buildExecEventPrompt function in dist/heartbeat-events-filter-DZ-HBajV.js to reflect the correct silent-reply sentinel and wrapping.
  • Verify that the updated function returns the expected prompt with <final>NO_REPLY</final> when deliverToUser is false.
  • Test the updated function with capable models to ensure they respond correctly with the wrapped NO_REPLY sentinel.
  • Consider updating the empty-eventText branch to also use the correct silent-reply sentinel and wrapping.

Example

function buildExecEventPrompt(pendingEvents, opts) {
    const deliverToUser = opts?.deliverToUser ?? true;
    // ...
    if (!eventText) return "An async command completion event was triggered, but no command output was found. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse output from any earlier run.";
    if (!deliverToUser) return "An async command completion event was triggered, but user delivery is disabled for this run. Reply with exactly `<final>NO_REPLY</final>` and nothing else. Do not mention, summarize, or reuse command output.";
    // ...
}

Notes

This fix assumes that the NO_REPLY sentinel is defined in dist/tokens-C_v_J0E7.js as SILENT_REPLY_TOKEN = "NO_REPLY". If this is not the case, the correct sentinel should be used instead.

Recommendation

Apply the suggested fix to update the buildExecEventPrompt function to use the correct silent-reply sentinel and wrapping. This should resolve the issue and prevent cosmetic errors from being surfaced to the user channel.

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 - 💡(How to fix) Fix buildExecEventPrompt: 'user delivery disabled' branch races runtime silent-reply detection, causes incomplete-turn errors [1 comments, 2 participants]