openclaw - 💡(How to fix) Fix pi-embedded-runner: empty-reasoning-only completions on openai-codex-responses bypass failover (assistantTexts=[], usage.output>0)

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…

For openai-codex provider with gpt-5.5 over the openai-codex-responses API, the codex app-server can return a turn where the entire output is hidden reasoning tokens with assistantTexts=[] and zero visible content. Gateway accepts the turn as a successful completion (no failover triggered) and the channel transport delivers nothing — Discord/Telegram clients eventually show "The application did not respond."

The downstream agent and the user receive no useful content despite the model burning real tokens. This is a silent failure mode: decision=succeeded masks a fully empty turn.

Error Message

Error / trajectory evidence

if (assistant.stopReason === "error") { P2 — silent message loss on a production-facing channel. Not a crash, but the user sees no response and no error, and the model-fallback chain configured by the operator is bypassed. Different from #85192 (DeepSeek V4 unsigned thinking blocks) and #76048 (ZAI GLM-5 reasoning_content routing) — those are distinct providers; this is openai-codex-responses with non-empty usage.output and messagesSnapshot missing an assistant entry entirely.

Root Cause

In src/agents/pi-embedded-runner/run/incomplete-turn.ts, the empty-response retry predicate is gated through isEmptyResponseAssistantTurn():

// src/agents/pi-embedded-runner/run/incomplete-turn.ts:412
function isEmptyResponseAssistantTurn(params: {
  payloadCount: number;
  attempt: Pick<IncompleteTurnAttempt, "assistantTexts" | "currentAttemptAssistant" | "lastAssistant">;
}): boolean {
  if (params.payloadCount !== 0) {
    return false;
  }
  if (joinAssistantTexts(params.attempt.assistantTexts).length > 0) {
    return false;
  }
  const assistant = params.attempt.currentAttemptAssistant ?? params.attempt.lastAssistant;
  if (!assistant) {
    return true;
  }
  if (assistant.stopReason === "error") {
    return false;
  }
  if (
    isIncompleteTerminalAssistantTurn({ hasAssistantVisibleText: false, lastAssistant: assistant }) ||
    isReasoningOnlyAssistantTurn(assistant)  // ← problem
  ) {
    return false;
  }
  return true;
}

The predicate explicitly returns false when isReasoningOnlyAssistantTurn(assistant) is true, meaning a reasoning-only turn goes to resolveReasoningOnlyRetryInstruction instead of resolveEmptyResponseRetryInstruction. For the openai-codex-responses payload shape, the reasoning-only retry path doesn't appear to trigger either — messagesSnapshot has no assistant message at all, and the codex app-server's response carries hidden reasoning under usage but exposes nothing on the assistant role for isReasoningOnlyAssistantTurn to detect. The net effect: the turn passes through all empty/reasoning guards and is treated as success.

embedded_run_failover_decision (src/agents/pi-embedded-runner/run/failover-observation.ts:70) is therefore never fired, and the model-fallback chain configured for the agent (which works fine for HTTP 408/timeouts — proven by the ops/gpt-5.5 → claude-opus-4-7 fallback at 15:31:17 ICT in the same incident window) does not activate.

Fix Action

Workaround

Switch the agent's primary model off gpt-5.5 (the codex app-server path). Config-level swap in ~/.openclaw/agents/<id>/agent.json to e.g. claude-sonnet-4-6 avoids the failure mode entirely.

Code Example

{
  "type": "model.completed",
  "ts": "2026-05-22T10:18:35.515Z",
  "runId": "d10373ab-0a95-40a7-ae7f-7459c745ea61",
  "provider": "openai-codex",
  "modelId": "gpt-5.5",
  "modelApi": "openai-codex-responses",
  "data": {
    "timedOut": false,
    "yieldDetected": false,
    "aborted": false,
    "promptError": null,
    "usage": {"input": 24794, "output": 111, "cacheRead": 4608, "total": 29513},
    "assistantTexts": [],
    "messagesSnapshot": [{"role": "user", "content": "u there partner ??..."}]
  }
}

---

// src/agents/pi-embedded-runner/run/incomplete-turn.ts:412
function isEmptyResponseAssistantTurn(params: {
  payloadCount: number;
  attempt: Pick<IncompleteTurnAttempt, "assistantTexts" | "currentAttemptAssistant" | "lastAssistant">;
}): boolean {
  if (params.payloadCount !== 0) {
    return false;
  }
  if (joinAssistantTexts(params.attempt.assistantTexts).length > 0) {
    return false;
  }
  const assistant = params.attempt.currentAttemptAssistant ?? params.attempt.lastAssistant;
  if (!assistant) {
    return true;
  }
  if (assistant.stopReason === "error") {
    return false;
  }
  if (
    isIncompleteTerminalAssistantTurn({ hasAssistantVisibleText: false, lastAssistant: assistant }) ||
    isReasoningOnlyAssistantTurn(assistant)  // ← problem
  ) {
    return false;
  }
  return true;
}

---

// rough shape
function isCodexEmptyVisibleTurnDespiteOutputTokens(params: {
  modelApi?: string;
  attempt: IncompleteTurnAttempt;
  usage?: { output?: number };
}): boolean {
  return (
    params.modelApi === "openai-codex-responses" &&
    (params.usage?.output ?? 0) > 0 &&
    joinAssistantTexts(params.attempt.assistantTexts).length === 0 &&
    !params.attempt.messagesSnapshot?.some((m) => m.role === "assistant")
  );
}
RAW_BUFFERClick to expand / collapse

Summary

For openai-codex provider with gpt-5.5 over the openai-codex-responses API, the codex app-server can return a turn where the entire output is hidden reasoning tokens with assistantTexts=[] and zero visible content. Gateway accepts the turn as a successful completion (no failover triggered) and the channel transport delivers nothing — Discord/Telegram clients eventually show "The application did not respond."

The downstream agent and the user receive no useful content despite the model burning real tokens. This is a silent failure mode: decision=succeeded masks a fully empty turn.

Environment

  • OpenClaw 2026.5.20 (e510042) — npm install at ~/.local/lib/node_modules/openclaw
  • Node 25.8.1, macOS 25.3.0 (arm64)
  • Provider: openai-codex / model gpt-5.5 / modelApi: openai-codex-responses
  • Codex app-server: stdio
  • Channel: Discord direct (channel 1499764682443980892)
  • Auth: openai-codex OAuth profile [email protected]

Reproduction

  1. Configure an OpenClaw agent that uses openai-codex provider with gpt-5.5 (codex app-server transport).
  2. Have an active Discord (or any messaging) channel session for the agent.
  3. Send a short prompt that the model will answer with reasoning tokens (e.g. "u there partner ??" after the session has prior history).
  4. Periodically the codex app-server returns a turn where output tokens are non-zero but assistantTexts is [] and messagesSnapshot contains only the user message.

In our incident (2026-05-22), every other turn on this session succeeded; this is intermittent, not deterministic.

Error / trajectory evidence

Trajectory model.completed event for the empty turn (sanitized), ~/.openclaw/agents/chatgpt/sessions/3c0b493e-…trajectory.jsonl:

{
  "type": "model.completed",
  "ts": "2026-05-22T10:18:35.515Z",
  "runId": "d10373ab-0a95-40a7-ae7f-7459c745ea61",
  "provider": "openai-codex",
  "modelId": "gpt-5.5",
  "modelApi": "openai-codex-responses",
  "data": {
    "timedOut": false,
    "yieldDetected": false,
    "aborted": false,
    "promptError": null,
    "usage": {"input": 24794, "output": 111, "cacheRead": 4608, "total": 29513},
    "assistantTexts": [],
    "messagesSnapshot": [{"role": "user", "content": "u there partner ??..."}]
  }
}

The preceding (runId=5a61ca06) and following turns on the same session have non-empty assistantTexts. No embedded_run_failover_decision event was emitted for this run, no model-fallback chain was attempted — Gateway treated the turn as completed successfully.

Root cause

In src/agents/pi-embedded-runner/run/incomplete-turn.ts, the empty-response retry predicate is gated through isEmptyResponseAssistantTurn():

// src/agents/pi-embedded-runner/run/incomplete-turn.ts:412
function isEmptyResponseAssistantTurn(params: {
  payloadCount: number;
  attempt: Pick<IncompleteTurnAttempt, "assistantTexts" | "currentAttemptAssistant" | "lastAssistant">;
}): boolean {
  if (params.payloadCount !== 0) {
    return false;
  }
  if (joinAssistantTexts(params.attempt.assistantTexts).length > 0) {
    return false;
  }
  const assistant = params.attempt.currentAttemptAssistant ?? params.attempt.lastAssistant;
  if (!assistant) {
    return true;
  }
  if (assistant.stopReason === "error") {
    return false;
  }
  if (
    isIncompleteTerminalAssistantTurn({ hasAssistantVisibleText: false, lastAssistant: assistant }) ||
    isReasoningOnlyAssistantTurn(assistant)  // ← problem
  ) {
    return false;
  }
  return true;
}

The predicate explicitly returns false when isReasoningOnlyAssistantTurn(assistant) is true, meaning a reasoning-only turn goes to resolveReasoningOnlyRetryInstruction instead of resolveEmptyResponseRetryInstruction. For the openai-codex-responses payload shape, the reasoning-only retry path doesn't appear to trigger either — messagesSnapshot has no assistant message at all, and the codex app-server's response carries hidden reasoning under usage but exposes nothing on the assistant role for isReasoningOnlyAssistantTurn to detect. The net effect: the turn passes through all empty/reasoning guards and is treated as success.

embedded_run_failover_decision (src/agents/pi-embedded-runner/run/failover-observation.ts:70) is therefore never fired, and the model-fallback chain configured for the agent (which works fine for HTTP 408/timeouts — proven by the ops/gpt-5.5 → claude-opus-4-7 fallback at 15:31:17 ICT in the same incident window) does not activate.

Suggested fix

Add an emptyAssistantTextWithReasoningTokens failover predicate, triggered when:

  • payloadCount === 0
  • joinAssistantTexts(attempt.assistantTexts).length === 0
  • usage.output > 0 (the model spent tokens but emitted nothing visible)
  • messagesSnapshot has no assistant message
  • transport is openai-codex-responses (scope to the affected provider)

Either trigger the empty-response retry path (emptyResponseRetryInstruction), or classify the turn as FailoverReason="empty_response" and let the standard model-fallback chain decide.

// rough shape
function isCodexEmptyVisibleTurnDespiteOutputTokens(params: {
  modelApi?: string;
  attempt: IncompleteTurnAttempt;
  usage?: { output?: number };
}): boolean {
  return (
    params.modelApi === "openai-codex-responses" &&
    (params.usage?.output ?? 0) > 0 &&
    joinAssistantTexts(params.attempt.assistantTexts).length === 0 &&
    !params.attempt.messagesSnapshot?.some((m) => m.role === "assistant")
  );
}

Workaround

Switch the agent's primary model off gpt-5.5 (the codex app-server path). Config-level swap in ~/.openclaw/agents/<id>/agent.json to e.g. claude-sonnet-4-6 avoids the failure mode entirely.

Severity

P2 — silent message loss on a production-facing channel. Not a crash, but the user sees no response and no error, and the model-fallback chain configured by the operator is bypassed. Different from #85192 (DeepSeek V4 unsigned thinking blocks) and #76048 (ZAI GLM-5 reasoning_content routing) — those are distinct providers; this is openai-codex-responses with non-empty usage.output and messagesSnapshot missing an assistant entry entirely.

Related

  • #78384 — assistant can reply to stale user intent after reasoning-only/tool retry (overlaps with the reasoning-only detection path)
  • #85192 — DeepSeek V4 unsigned thinking blocks miss reasoning-only retry (same class, different provider)
  • #80918 — silent send miss: incomplete-turn classifier discards stopReason=stop final after update_plan (adjacent)
  • #84076 — Codex app-server stalls after item/completed (different symptom; this issue is a clean model.completed with empty content, not a stall)

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 pi-embedded-runner: empty-reasoning-only completions on openai-codex-responses bypass failover (assistantTexts=[], usage.output>0)