openclaw - 💡(How to fix) Fix Heartbeat transcript writes empty user messages, breaks AWS Bedrock [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#72640Fetched 2026-04-28 06:33:44
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
closed ×1commented ×1cross-referenced ×1

Commit 58a31b12f7 ("fix(agents): keep runtime wakeups out of chat transcript", merged 2026-04-26) introduced a regression where heartbeat runs write empty user messages ({"role":"user","content":[{"type":"text","text":""}]}) into the session transcript. AWS Bedrock rejects these with:

The text field in the ContentBlock object at messages.X.content.0 is blank.
Add text to the text field, and try again.

This accumulates over time (one per heartbeat cycle, ~every 30 minutes) and eventually causes the Bedrock API call to fail when the empty message falls within the context window.

Error Message

  1. Send a message — eventually Bedrock rejects with the blank text error

Root Cause

In src/auto-reply/reply/get-reply-run.ts line 501-505:

const transcriptBodyBase = isHeartbeat
    ? ""                    // <-- Bug: empty string gets persisted as user message
    : hasUserBody
      ? baseBodyFinal
      : "[User sent media without caption]";

Before this commit, transcriptBodyBase was always non-empty (either the actual body text or a placeholder). The intent of the change was to keep heartbeat prompts out of the visible transcript, but the empty string still flows through to transcriptCommandBodytranscriptPrompt, which gets persisted as a user message with content: [{ type: "text", text: "" }].

The Bedrock provider (amazon-bedrock.js) has empty-text guards for assistant messages but not for user messages or toolResult messages, so the empty content passes through to the API.

Code Example

The text field in the ContentBlock object at messages.X.content.0 is blank.
Add text to the text field, and try again.

---

const transcriptBodyBase = isHeartbeat
    ? ""                    // <-- Bug: empty string gets persisted as user message
    : hasUserBody
      ? baseBodyFinal
      : "[User sent media without caption]";

---

2026-04-27 00:31:54 UTC  {"role":"user","content":[{"type":"text","text":""}]}
2026-04-27 01:00:12 UTC  (same)
2026-04-27 01:30:12 UTC  (same)
...every 30 mins...
2026-04-27 05:01:51 UTC  (same)

---

// In convertMessages, user case:
if (typeof m.content === "string") {
    const sanitized = sanitizeSurrogates(m.content);
    if (sanitized.trim().length === 0) continue; // Skip empty user messages
    ...
}
RAW_BUFFERClick to expand / collapse

Summary

Commit 58a31b12f7 ("fix(agents): keep runtime wakeups out of chat transcript", merged 2026-04-26) introduced a regression where heartbeat runs write empty user messages ({"role":"user","content":[{"type":"text","text":""}]}) into the session transcript. AWS Bedrock rejects these with:

The text field in the ContentBlock object at messages.X.content.0 is blank.
Add text to the text field, and try again.

This accumulates over time (one per heartbeat cycle, ~every 30 minutes) and eventually causes the Bedrock API call to fail when the empty message falls within the context window.

Root Cause

In src/auto-reply/reply/get-reply-run.ts line 501-505:

const transcriptBodyBase = isHeartbeat
    ? ""                    // <-- Bug: empty string gets persisted as user message
    : hasUserBody
      ? baseBodyFinal
      : "[User sent media without caption]";

Before this commit, transcriptBodyBase was always non-empty (either the actual body text or a placeholder). The intent of the change was to keep heartbeat prompts out of the visible transcript, but the empty string still flows through to transcriptCommandBodytranscriptPrompt, which gets persisted as a user message with content: [{ type: "text", text: "" }].

The Bedrock provider (amazon-bedrock.js) has empty-text guards for assistant messages but not for user messages or toolResult messages, so the empty content passes through to the API.

Steps to Reproduce

  1. Configure an AWS Bedrock model (e.g., amazon-bedrock/global.anthropic.claude-opus-4-6-v1)
  2. Enable heartbeats (default 30-minute interval)
  3. Wait for several heartbeat cycles
  4. Send a message — eventually Bedrock rejects with the blank text error

Evidence

Session transcript analysis shows 11 empty user messages, all at exact 30-minute intervals matching heartbeat timing:

2026-04-27 00:31:54 UTC  {"role":"user","content":[{"type":"text","text":""}]}
2026-04-27 01:00:12 UTC  (same)
2026-04-27 01:30:12 UTC  (same)
...every 30 mins...
2026-04-27 05:01:51 UTC  (same)

Suggested Fix

Two complementary fixes:

1. Root cause (get-reply-run.ts)

When isHeartbeat is true and transcriptBodyBase is empty, either:

  • Skip persisting the user message to the transcript entirely, or
  • Use a non-empty placeholder like "[heartbeat]" that gets filtered by the existing heartbeat pair filter

2. Defensive (amazon-bedrock.js in pi-ai)

Add the same empty-text guard that exists for assistant messages to user messages and toolResult messages:

// In convertMessages, user case:
if (typeof m.content === "string") {
    const sanitized = sanitizeSurrogates(m.content);
    if (sanitized.trim().length === 0) continue; // Skip empty user messages
    ...
}

Environment

  • OpenClaw version: 2026.4.26
  • OS: macOS (Darwin 25.4.0 arm64)
  • Provider: AWS Bedrock (Claude Opus 4.6)
  • Regression introduced in: commit 58a31b12f7 (2026-04-26)
  • Last working version: 2026.4.11

Related

  • #30117 (closed as not planned — same symptom, different root cause)

extent analysis

TL;DR

The most likely fix is to modify the get-reply-run.ts file to either skip persisting empty user messages or use a non-empty placeholder, and add an empty-text guard for user messages in amazon-bedrock.js.

Guidance

  • Identify the get-reply-run.ts file and update the transcriptBodyBase logic to handle heartbeat cases without persisting empty user messages.
  • Add a conditional check in amazon-bedrock.js to filter out empty user messages before sending them to the Bedrock API.
  • Verify the fix by checking the session transcript for empty user messages after several heartbeat cycles.
  • Consider adding a non-empty placeholder for heartbeat messages to ensure they are properly filtered by the existing heartbeat pair filter.

Example

// In get-reply-run.ts
const transcriptBodyBase = isHeartbeat
    ? "[heartbeat]"  // Use a non-empty placeholder
    : hasUserBody
      ? baseBodyFinal
      : "[User sent media without caption]";

Notes

The provided fix suggestions are complementary, and implementing both may be necessary to fully resolve the issue. The amazon-bedrock.js change adds a defensive measure to prevent empty user messages from being sent to the Bedrock API.

Recommendation

Apply the workaround by modifying the get-reply-run.ts file and adding the empty-text guard in amazon-bedrock.js, as this addresses the root cause and provides a defensive measure against similar issues.

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