Skip to content

[Bug]: empty-error-retry skipped when stop_reason="error" turn contains a thinking block or non-zero output, causing silent mid-turn abort on multi-step tasks #91953

Description

@lml2468

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

empty-error-retry in runEmbeddedAgent only fires when the failed turn produced zero output tokens and zero content blocks, so an upstream stop_reason: "error" that returns with a thinking block + non-zero usage.output silently terminates a multi-step turn with no retry, no fallback engagement, and no user-visible error in the channel.

Steps to reproduce

  1. Run OpenClaw 2026.5.28 on top of an Anthropic-style provider that occasionally returns stop_reason: "error" mid-turn (observed against vertexai/claude-opus-4-8).
  2. Issue a multi-step prompt that triggers tool use before the final assistant message (e.g. "search the web, fetch 2 pages, then create 5 group threads and @ two people").
  3. Wait until the upstream provider returns stop_reason: "error" during the assistant turn that follows the read-only tool work. With a reasoning-enabled model, the streamed response will typically arrive with content: [{ type: "thinking", ... }] and usage.output > 0 before the gateway closes the stream with stop_reason: "error".
  4. Observe that:
    • anthropic-BEgJnt4r.js:425 (and equivalent in src/providers/anthropic) throws An unknown error occurred.
    • The attempt records stopReason: "error", errorMessage: "An unknown error occurred", aborted: false, timedOut: false, content.length === 1 (single thinking block), usage.output === 1120.
    • MAX_EMPTY_ERROR_RETRIES retry path at src/agents/embedded-agent-runner/run.ts:3454-3473 does not fire because (usage.output ?? 0) === 0 and (silentErrorContent?.length ?? 0) === 0 are both false.
    • The run returns with meta.livenessState === "blocked" and an isError: true incomplete-turn payload, but on the cron/isolated-session delivery path used here, that payload is not surfaced to the originating chat channel.

Expected behavior

A turn-level upstream provider error (stop_reason: "error") on a multi-step task should either:

  1. Be retried by empty-error-retry (as today's logic already does for output === 0 && content === []), since neither a thinking-only content nor a non-zero usage.output represents user-visible progress; or
  2. Engage the configured model.fallbacks chain so the user-facing agent stays available (related: model.fallbacks chain does not engage on reasoning-only / empty-visible-reply failures #85422); or
  3. At minimum, surface the ⚠️ Agent couldn't generate a response. Please try again. payload to the originating channel/thread so the user knows the turn died, instead of completing the session with no visible output.

In 2026.4.x series the same upstream behaviour also produced a silent end, so this is not a regression — but the existing empty-error-retry net was designed to catch exactly this failure shape and its current guards leak the most common reasoning-model variant of it.

Actual behavior

The run completes with livenessState: "blocked" and an isError: true incomplete-turn payload, but:

  • No retry attempt is logged ([empty-error-retry] line never appears).
  • No model.fallback_step event is emitted.
  • The user-visible group/thread receives nothing — neither the incomplete-turn warning string nor any other status. The originating session is marked ended.

Multi-step tasks like "search → fetch → create N threads → @ users" therefore silently abort midway with no observable failure signal on the chat surface.

OpenClaw version

2026.5.28 (also confirmed on main at bb6e47729c — guards unchanged)

Operating system

macOS 15 (Darwin 25.5.0 arm64)

Install method

npm global (/opt/homebrew/lib/node_modules/openclaw)

Model

vertexai/claude-opus-4-8 (reasoning-enabled). Behaviour is provider-agnostic: any Anthropic-style stream that ends with stop_reason: "error" after emitting a thinking block or any token of output will exhibit it.

Provider / routing chain

openclaw -> vertex-ai (claude-opus-4-8)

Additional provider/model setup details

Reasoning mode enabled on the agent so the assistant turn that crashed produced a single thinking content block before the stream errored.

Logs, screenshots, and evidence

Source references (relative to repo root at bb6e47729c):

src/agents/embedded-agent-runner/run.ts:3454-3473 — the actual guard:

const silentErrorContent = sessionLastAssistant?.content as Array<unknown> | undefined;
if (
  incompleteTurnText &&
  !aborted &&
  !promptError &&
  !timedOut &&
  sessionLastAssistant?.stopReason === "error" &&
  ((sessionLastAssistant?.usage as { output?: number } | undefined)?.output ?? 0) === 0 &&
  (silentErrorContent?.length ?? 0) === 0 &&
  (attempt.replayMetadata ? !attempt.replayMetadata.hadPotentialSideEffects : false) &&
  emptyErrorRetries < MAX_EMPTY_ERROR_RETRIES
) {
  emptyErrorRetries += 1;
  log.warn(`[empty-error-retry] stopReason=error output=0; resubmitting attempt=${emptyErrorRetries}/${MAX_EMPTY_ERROR_RETRIES} ...`);
  continue;
}

Comments at run.ts:3437-3452 document the intent ("Content-empty guard: a reasoning-only error is a distinct failure mode handled elsewhere; only retry when the assistant truly produced nothing"), but in practice the "elsewhere" path (resolveReasoningOnlyRetryInstruction in src/agents/embedded-agent-runner/run/incomplete-turn.ts) explicitly returns null when assistant?.stopReason === "error":

src/agents/embedded-agent-runner/run/incomplete-turn.tsresolveReasoningOnlyRetryInstruction (~line where it checks if (assistant?.stopReason === "error") return null;).

So the reasoning-only retry skips errored turns, and empty-error-retry skips turns whose content is a single thinking block + non-zero usage.output. The stop_reason=error + thinking-only outcome therefore falls into a gap between the two retry resolvers and reaches the terminal incomplete-turn detected branch.

Observed assistant turn fields from a real failure (vertexai/claude-opus-4-8):

content blocks: ['thinking']
usage.output: 1120
stopReason: "error"
errorMessage: "An unknown error occurred"
aborted: false
timedOut: false

Existing related issues for context (none cover this specific empty-error-retry gap):

Impact and severity

  • Affected: any agent run that uses a reasoning-enabled Anthropic-style model and performs multi-step tool work (search → fetch → write/create), on any channel that delivers via the cron / isolated-session path.
  • Severity: High when it hits. Multi-step user tasks silently abort partway through with no error surfaced to the channel; user assumes the agent is still working and only notices when nothing arrives.
  • Frequency: Intermittent. Tied to upstream provider stability — every stop_reason: "error" on a reasoning-model mid-turn reproduces it.
  • Consequence: Lost work for the user (downstream steps never run), no failure signal in the chat surface, and the configured model.fallbacks chain never engages so there is no automated recovery.

Additional information

Suggested fix direction (not a PR yet, filing for maintainer triage):

  1. Loosen the empty-error-retry guards so a stop_reason === "error" turn is retried when the only content blocks are thinking / redacted_thinking and there is no visible text emitted, regardless of usage.output. A thinking-only block carries no user-visible product, so resubmission cannot duplicate user-visible output.
  2. Keep the existing hadPotentialSideEffects short-circuit so mutating tool calls / committed delivery still block resubmission.
  3. When all retries exhaust on a stop_reason: "error" outcome, ensure the incompleteTurnText payload is delivered through the originating channel adapter (or at least logged with livenessState: "blocked" in a way the cron / isolated-session surfaces propagate to chat), so the failure is not silent at the user-visible layer.

Happy to send a PR once a maintainer confirms the preferred shape of fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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