openclaw - ✅(Solved) Fix Bug: `/new` produces empty prompt → Anthropic API 400 "messages: at least one message is required" [2 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#73926Fetched 2026-04-29 06:13:10
View on GitHub
Comments
1
Participants
2
Timeline
8
Reactions
0
Author
Timeline (top)
cross-referenced ×4commented ×1mentioned ×1referenced ×1

When /new (or /reset) is invoked on an agent, there is a race condition between the active-memory subsystem and the bare-reset prompt injection that can cause runEmbeddedPiAgent to be called with an empty prompt AND an empty messages[] array. This produces a 400 invalid_request_error from the Anthropic API and leaves the session stuck in "processing" state. Failover to other models (e.g. gpt-5.5) also fails because the same empty body is forwarded.

Error Message

  • Failover model (e.g. openai-codex/gpt-5.5) also receives empty input → secondary error
  1. Strict guard at LLM dispatch boundary: never send a request with empty prompt AND empty messages AND no images — return early with structured error or auto-inject a safe greeting prompt.
  2. Better logging: log a WARN when an empty-payload dispatch is intercepted, so users can see the race instead of getting an opaque 400.

Root Cause

The pre-submission guard hasPromptSubmissionContent({ prompt, messages, imageCount }) correctly detects empty content and sets skipPromptSubmission = true, but in some race scenarios (active-memory mid-rebuild + bare-reset), it is reached AFTER the LLM dispatch has already started with the empty payload, OR activeSession.messages still contains stale empty entries from the previous session that pass the existence check but have empty text content.

Fix Action

Fix / Workaround

The pre-submission guard hasPromptSubmissionContent({ prompt, messages, imageCount }) correctly detects empty content and sets skipPromptSubmission = true, but in some race scenarios (active-memory mid-rebuild + bare-reset), it is reached AFTER the LLM dispatch has already started with the empty payload, OR activeSession.messages still contains stale empty entries from the previous session that pass the existence check but have empty text content.

Workaround / patch (currently applied locally)

async function runEmbeddedPiAgent(params) {
  // Guard: prevent empty prompt + empty messages from reaching LLM dispatch
  if (typeof params.prompt !== "string" || params.prompt.trim().length === 0) {
    params = {
      ...params,
      prompt: "A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. Do not mention internal steps, files, tools, or reasoning."
    };
  }
  // ... existing code
}

PR fix notes

PR #73929: fix(agents): skip blank replay history prompts

Description (problem / solution / changelog)

Summary

  • Tighten the embedded-run prompt content guard so blank replay-history entries do not count as model-visible input.
  • Add regression coverage for blank string/object/array replay content while preserving structured non-text history such as images.
  • Add an Unreleased changelog entry for #73926.

Root Cause

hasPromptSubmissionContent treated any replay-history entry as content by checking only messages.length. During /new or /reset races, stale empty user/assistant entries could therefore bypass the empty prompt/history/images skip path and reach provider dispatch with no model-visible input.

Linked Issue

Fixes #73926.

Why This Is Safe

The change keeps the existing dispatch boundary behavior: submissions with prompt text, prompt images, or model-visible replay history still run. It only changes the history side of the guard from array presence to visible content presence, matching the sanitizer behavior that already drops blank user replay content.

Security And Runtime Controls

No security or authorization controls change. This does not add a prompt-text policy, alter model routing, or relax tool/runtime permissions; it only prevents blank payloads from being submitted.

Tests Run

  • git diff --check
  • pnpm test src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts
  • pnpm test src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts test/scripts/check-changelog-attributions.test.ts
  • pnpm changed:lanes --json
  • pnpm check:changed

Out Of Scope

  • No reset/startup prompt injection changes.
  • No active-memory scheduling or rebuild-order changes.
  • No provider adapter changes beyond avoiding empty submissions at the existing guard.

Made with Cursor

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts (modified, +22/-0)
  • src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts (modified, +29/-1)

PR #74040: fix: guard against empty prompt race in runEmbeddedPiAgent

Description (problem / solution / changelog)

Fix for #73926

When /new or /reset races with active-memory mid-rebuild, activeSession.messages can contain entries that pass messages.length > 0 but have no actual text content. The existing hasPromptSubmissionContent check only guards on length, not content quality — so an empty prompt plus empty messages still reaches the LLM dispatch, causing a 400 messages: at least one message is required error.

What this fixes

Adds a secondary guard in src/agents/pi-embedded-runner/run/attempt.ts that:

  • Checks whether any user message in activeSession.messages has actual non-empty text content (covering both string and multi-block content arrays)
  • When prompt is empty, no images are present, and no non-empty user message exists → injects a safe greeting prompt instead of sending an empty payload to the API

Root cause

The existing hasPromptSubmissionContent check (line 2526) correctly detects messages.length === 0, but when active-memory mid-rebuild races with a bare reset, messages can have entries that pass the length check while containing only empty-text user messages. This sends an empty body to the LLM API → 400.

Change

+          // Secondary guard: even if messages.length > 0, verify they have actual content.
+          const hasNonEmptyUserMsg = (activeSession.messages ?? []).some((m) => {
+            if (!m || (m as any).role !== 'user') return false;
+            const c = (m as any).content;
+            if (typeof c === 'string') return c.trim().length > 0;
+            if (Array.isArray(c)) return c.some((p: any) => p?.type === 'text' && typeof p.text === 'string' && p.text.trim().length > 0);
+            return false;
+          });
+          if (
+            !skipPromptSubmission &&
+            (!promptSubmission.prompt || !promptSubmission.prompt.trim()) &&
+            imageResult.images.length === 0 &&
+            !hasNonEmptyUserMsg
+          ) {
+            skipPromptSubmission = true;
+            promptSubmission = {
+              prompt:
+                "A new session was started. Greet the user briefly in your configured persona and ask what they want to do.",
+            };
+            log.warn(
+              `[empty-prompt-guard] injected safe greeting for empty prompt race ` +
+                `runId=${params.runId} sessionId=${params.sessionId} trigger=${params.trigger} ` +
+                `provider=${params.provider}/${params.modelId}`,
+            );
+          }

Fixes #73926.

Changed files

  • src/agents/pi-embedded-runner/run/attempt.ts (modified, +29/-0)

Code Example

async function runEmbeddedPiAgent(params) {
  // Guard: prevent empty prompt + empty messages from reaching LLM dispatch
  if (typeof params.prompt !== "string" || params.prompt.trim().length === 0) {
    params = {
      ...params,
      prompt: "A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. Do not mention internal steps, files, tools, or reasoning."
    };
  }
  // ... existing code
}

---

if (!skipPromptSubmission && !promptSubmission.runtimeOnly &&
    (!promptSubmission.prompt || promptSubmission.prompt.trim().length === 0) &&
    imageResult.images.length === 0) {
  const hasNonEmptyUserMsg = (activeSession.messages || []).some((m) => {
    if (!m || m.role !== 'user') return false;
    const c = m.content;
    if (typeof c === 'string') return c.trim().length > 0;
    if (Array.isArray(c)) return c.some((p) => p && p.type === 'text' && typeof p.text === 'string' && p.text.trim().length > 0);
    return false;
  });
  if (!hasNonEmptyUserMsg) {
    promptSubmission.prompt = "A new session was started. Greet the user briefly in your configured persona and ask what they want to do.";
  }
}
RAW_BUFFERClick to expand / collapse

Bug: /new produces empty prompt → Anthropic API 400 "messages: at least one message is required"

Summary

When /new (or /reset) is invoked on an agent, there is a race condition between the active-memory subsystem and the bare-reset prompt injection that can cause runEmbeddedPiAgent to be called with an empty prompt AND an empty messages[] array. This produces a 400 invalid_request_error from the Anthropic API and leaves the session stuck in "processing" state. Failover to other models (e.g. gpt-5.5) also fails because the same empty body is forwarded.

Symptoms

  • /new on agent main results in:
    • Stuck "processing" sessions (UI shows pending dot indefinitely)
    • Trace events with data.prompt: "" and messages: []
    • 400 invalid_request_error from Anthropic API: messages: at least one message is required
    • Failover model (e.g. openai-codex/gpt-5.5) also receives empty input → secondary error
  • Subsequent session entries record [assistant turn failed before producing content] with errorMessage: "One of \"input\" or \"previous_response_id\" or 'prompt' or 'conversation_id' must be provided." (OpenAI Responses adapter)

Reproduction

  1. Configure agent main with active-memory plugin enabled.
  2. Trigger /new immediately after another /new or /reset, OR while active-memory is mid-rebuild.
  3. Observe: session is created, but assistant turn fails before producing content. Subsequent user messages may also fail until the session is hard-reset.

Conditions that increase frequency:

  • Multiple agents (main + secondary + tertiary + fdx-assistant) sharing active-memory subsystem.
  • Large workspaces where active-memory takes time to rebuild.
  • Quick repeated /new /reset from the user.

Affected files

  • dist/pi-embedded-*.jsrunEmbeddedPiAgent() entry point. Receives empty params.prompt without guard.
  • dist/selection-*.js — pre-submission flow. Builds payload with empty messages: [] when prompt is empty AND activeSession.messages has no non-empty user message.

Root cause

The pre-submission guard hasPromptSubmissionContent({ prompt, messages, imageCount }) correctly detects empty content and sets skipPromptSubmission = true, but in some race scenarios (active-memory mid-rebuild + bare-reset), it is reached AFTER the LLM dispatch has already started with the empty payload, OR activeSession.messages still contains stale empty entries from the previous session that pass the existence check but have empty text content.

Workaround / patch (currently applied locally)

Two-layer defensive guard:

Layer 1: pi-embedded-*.js entry guard

Inject a canonical bare-session-reset prompt at the entry of runEmbeddedPiAgent when params.prompt is empty:

async function runEmbeddedPiAgent(params) {
  // Guard: prevent empty prompt + empty messages from reaching LLM dispatch
  if (typeof params.prompt !== "string" || params.prompt.trim().length === 0) {
    params = {
      ...params,
      prompt: "A new session was started via /new or /reset. Execute your Session Startup sequence now - read the required files before responding to the user. If BOOTSTRAP.md exists in the provided Project Context, read it and follow its instructions first. Then greet the user in your configured persona, if one is provided. Be yourself - use your defined voice, mannerisms, and mood. Keep it to 1-3 sentences and ask what they want to do. Do not mention internal steps, files, tools, or reasoning."
    };
  }
  // ... existing code
}

Layer 2: selection-*.js pre-submission guard

Before hasPromptSubmissionContent check, force-inject a minimal prompt when:

  • promptSubmission.prompt is empty
  • imageResult.images.length === 0
  • No non-empty user message exists in activeSession.messages
if (!skipPromptSubmission && !promptSubmission.runtimeOnly &&
    (!promptSubmission.prompt || promptSubmission.prompt.trim().length === 0) &&
    imageResult.images.length === 0) {
  const hasNonEmptyUserMsg = (activeSession.messages || []).some((m) => {
    if (!m || m.role !== 'user') return false;
    const c = m.content;
    if (typeof c === 'string') return c.trim().length > 0;
    if (Array.isArray(c)) return c.some((p) => p && p.type === 'text' && typeof p.text === 'string' && p.text.trim().length > 0);
    return false;
  });
  if (!hasNonEmptyUserMsg) {
    promptSubmission.prompt = "A new session was started. Greet the user briefly in your configured persona and ask what they want to do.";
  }
}

Suggested upstream fix

  1. Strict guard at LLM dispatch boundary: never send a request with empty prompt AND empty messages AND no images — return early with structured error or auto-inject a safe greeting prompt.
  2. Active-memory race fix: ensure runEmbeddedPiAgent is invoked only AFTER active-memory has materialized the session-start prompt, or pass a sentinel value.
  3. Better logging: log a WARN when an empty-payload dispatch is intercepted, so users can see the race instead of getting an opaque 400.

Environment

  • OCPlatform: 2026.4.26-93430cbb9781 (also reproduced on 2026.4.25)
  • Node: v25.9.0
  • OS: WSL2 (Linux 6.6.87.2-microsoft-standard-WSL2 x64)
  • Affected adapters: anthropic (Anthropic API), openai-codex/gpt-5.5 (OpenAI Responses)
  • Active plugins: active-memory, custom hooks (session-flush, boot-md, bootstrap-extra-files, command-logger, session-memory: false)

Local patch status

The local patch script (patches/empty-prompt-guard.sh) has been applied since 2026-04-29 and resolves the issue. Re-application required after each openclaw upgrade because patches modify hashed dist files (pi-embedded-*.js, selection-*.js).

extent analysis

TL;DR

To fix the issue of empty prompts causing 400 errors with the Anthropic API, apply a two-layer defensive guard in pi-embedded-*.js and selection-*.js to prevent empty prompts and messages from reaching the LLM dispatch.

Guidance

  1. Implement entry guard in runEmbeddedPiAgent: Inject a canonical bare-session-reset prompt when params.prompt is empty to prevent empty prompts from reaching the LLM dispatch.
  2. Add pre-submission guard in selection-*.js: Force-inject a minimal prompt when promptSubmission.prompt is empty and no non-empty user message exists in activeSession.messages.
  3. Test with various scenarios: Verify the fix by triggering /new immediately after another /new or /reset, and while active-memory is mid-rebuild, to ensure the guards are working correctly.
  4. Monitor logs for WARN messages: Log a WARN when an empty-payload dispatch is intercepted to detect any remaining race conditions.

Example

The provided code snippets for the two-layer defensive guard can be used as a starting point:

// pi-embedded-*.js
async function runEmbeddedPiAgent(params) {
  if (typeof params.prompt !== "string" || params.prompt.trim().length === 0) {
    params = {
      ...params,
      prompt: "A new session was started via /new or /reset. Execute your Session Startup sequence now..."
    };
  }
  // ... existing code
}
// selection-*.js
if (!skipPromptSubmission && !promptSubmission.runtimeOnly &&
    (!promptSubmission.prompt || promptSubmission.prompt.trim().length === 0) &&
    imageResult.images.length === 0) {
  const hasNonEmptyUserMsg = (activeSession.messages || []).some((m) => {
    // ... existing code
  });
  if (!hasNonEmpty

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 Bug: `/new` produces empty prompt → Anthropic API 400 "messages: at least one message is required" [2 pull requests, 1 comments, 2 participants]