openclaw - 💡(How to fix) Fix Bug: tool-result middleware discards nested toolResult blocks; message tool returns become 'Tool output unavailable due to post-processing error'

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…

tool-result-middleware silently discards any tool result whose content[] contains a nested block of type toolResult / tool_result / tool / function. The middleware's validator (isValidMiddlewareToolResult) only accepts text and image content blocks; anything else causes the whole result to be rejected and replaced with the generic Tool output unavailable due to post-processing error string. The real tool output (send receipts, IDs, status, etc.) is lost.

The message tool's return shape — at least on the Codex app-server harness path — frequently arrives as a nested toolResult block, so this hits the most common send-and-confirm flow.

Error Message

tool-result-middleware silently discards any tool result whose content[] contains a nested block of type toolResult / tool_result / tool / function. The middleware's validator (isValidMiddlewareToolResult) only accepts text and image content blocks; anything else causes the whole result to be rejected and replaced with the generic Tool output unavailable due to post-processing error string. The real tool output (send receipts, IDs, status, etc.) is lost. Tool output unavailable due to post-processing error The send itself often succeeds (the user receives the Signal/Telegram/email message), but the model sees the post-processing error string and assumes the tool failed, often retrying or apologizing to the user about a non-existent failure. 3. Inspect the middleware output and the model's view of the tool result. The send goes through, but the model sees Tool output unavailable due to post-processing error and the harness log shows discarded invalid tool result middleware output for message. isValidMiddlewareContentBlock only accepts text and image. The Codex/message-tool path produces a content block of type: "toolResult" (a nested tool result), which is structurally valid for the runtime but fails this validator. The caller (if (isValidMiddlewareToolResult(candidate)) current = candidate; else { /* replace with generic error */ }) then nukes the real result. else { /* generic error / } else { / generic error — still the fallback for truly unsalvageable inputs */ }

Root Cause

dist/tool-result-middleware-*.jsfunction isValidMiddlewareToolResult(value):

function isValidMiddlewareToolResult(value) {
    if (!isRecord(value) || !Array.isArray(value.content)) return false;
    if (value.content.length > MAX_MIDDLEWARE_CONTENT_BLOCKS) return false;
    return value.content.every(isValidMiddlewareContentBlock) && isValidMiddlewareDetails(value.details);
}

isValidMiddlewareContentBlock only accepts text and image. The Codex/message-tool path produces a content block of type: "toolResult" (a nested tool result), which is structurally valid for the runtime but fails this validator. The caller (if (isValidMiddlewareToolResult(candidate)) current = candidate; else { /* replace with generic error */ }) then nukes the real result.

This is a strict-validation bug: the validator should either accept the nested shape, or — better — coerce it into the legal text/image shape so the real content reaches the model.

Fix Action

Fix / Workaround

Local patch

We've been running this fix locally as part of post-update-hook.sh Hook 39 (scripts/patch-message-tool-routing-and-result.sh) since the 2026.5.x line. Patch applies cleanly to dist/tool-result-middleware-*.js matched by the marker function isValidMiddlewareToolResult, and the resulting file passes node --check. Sentinel function coerceMiddlewareToolResult is what the script checks for idempotency.

Code Example

Tool output unavailable due to post-processing error

---

[agents/harness] [codex] discarded invalid tool result middleware output for message

---

function isValidMiddlewareToolResult(value) {
    if (!isRecord(value) || !Array.isArray(value.content)) return false;
    if (value.content.length > MAX_MIDDLEWARE_CONTENT_BLOCKS) return false;
    return value.content.every(isValidMiddlewareContentBlock) && isValidMiddlewareDetails(value.details);
}

---

// before
if (isValidMiddlewareToolResult(candidate)) current = candidate;
else { /* generic error */ }

// after
const coerced = coerceMiddlewareToolResult(candidate);
if (coerced) current = coerced;
else { /* generic error — still the fallback for truly unsalvageable inputs */ }
RAW_BUFFERClick to expand / collapse

Summary

tool-result-middleware silently discards any tool result whose content[] contains a nested block of type toolResult / tool_result / tool / function. The middleware's validator (isValidMiddlewareToolResult) only accepts text and image content blocks; anything else causes the whole result to be rejected and replaced with the generic Tool output unavailable due to post-processing error string. The real tool output (send receipts, IDs, status, etc.) is lost.

The message tool's return shape — at least on the Codex app-server harness path — frequently arrives as a nested toolResult block, so this hits the most common send-and-confirm flow.

Symptom

User-visible (in transcripts and logs):

Tool output unavailable due to post-processing error

Harness log:

[agents/harness] [codex] discarded invalid tool result middleware output for message

The send itself often succeeds (the user receives the Signal/Telegram/email message), but the model sees the post-processing error string and assumes the tool failed, often retrying or apologizing to the user about a non-existent failure.

Repro

  1. Use the Codex app-server runtime (e.g. openai/gpt-5.5 with agentRuntime.id: "codex" or openai-codex/gpt-5.5).
  2. From inside an agent session, have the model invoke the message tool (any channel: Signal, Telegram, webchat).
  3. Inspect the middleware output and the model's view of the tool result. The send goes through, but the model sees Tool output unavailable due to post-processing error and the harness log shows discarded invalid tool result middleware output for message.

Root cause

dist/tool-result-middleware-*.jsfunction isValidMiddlewareToolResult(value):

function isValidMiddlewareToolResult(value) {
    if (!isRecord(value) || !Array.isArray(value.content)) return false;
    if (value.content.length > MAX_MIDDLEWARE_CONTENT_BLOCKS) return false;
    return value.content.every(isValidMiddlewareContentBlock) && isValidMiddlewareDetails(value.details);
}

isValidMiddlewareContentBlock only accepts text and image. The Codex/message-tool path produces a content block of type: "toolResult" (a nested tool result), which is structurally valid for the runtime but fails this validator. The caller (if (isValidMiddlewareToolResult(candidate)) current = candidate; else { /* replace with generic error */ }) then nukes the real result.

This is a strict-validation bug: the validator should either accept the nested shape, or — better — coerce it into the legal text/image shape so the real content reaches the model.

Proposed fix

Add three helpers and switch the call site to use a coercing variant:

  1. coerceMiddlewareText(value) — pull a string out of value from common fields (text/output/result/message/content), else JSON.stringify.
  2. coerceMiddlewareContentBlock(value) — pass through valid blocks unchanged; for toolresult / tool_result / tool / function types, recursively flatten value.content (or the value itself) into a single text block, truncated by truncateUtf16Safe(..., MAX_MIDDLEWARE_TEXT_CHARS).
  3. coerceMiddlewareToolResult(value) — return the value untouched if already valid; otherwise map each content[] entry through coerceMiddlewareContentBlock, keep only successful coercions (bounded by MAX_MIDDLEWARE_CONTENT_BLOCKS), preserve details if valid, and re-validate before returning. Returns null only when nothing could be salvaged.

Call site change:

// before
if (isValidMiddlewareToolResult(candidate)) current = candidate;
else { /* generic error */ }

// after
const coerced = coerceMiddlewareToolResult(candidate);
if (coerced) current = coerced;
else { /* generic error — still the fallback for truly unsalvageable inputs */ }

This preserves the existing safety bounds (MAX_MIDDLEWARE_CONTENT_BLOCKS, MAX_MIDDLEWARE_TEXT_CHARS, isValidMiddlewareDetails) while letting the real tool output reach the model.

Local patch

We've been running this fix locally as part of post-update-hook.sh Hook 39 (scripts/patch-message-tool-routing-and-result.sh) since the 2026.5.x line. Patch applies cleanly to dist/tool-result-middleware-*.js matched by the marker function isValidMiddlewareToolResult, and the resulting file passes node --check. Sentinel function coerceMiddlewareToolResult is what the script checks for idempotency.

Happy to open a PR — would appreciate guidance on whether the maintainers prefer coerce (our approach, preserves data) or expand the validator to accept the nested shape natively (smaller diff, but doesn't help other producers of unexpected shapes).

Related

This is the second half of the same Signal-delivery failure shape tracked in the companion routing issue (filing alongside this one). The two together cause the user-visible "agent replied but I didn't see it" failure mode: routing bug sends to the wrong sink, middleware bug hides the receipt that would have let the model self-correct.

Environment

  • openclaw 2026.5.12
  • Agent runtime: Codex app-server (openai-codex/gpt-5.5 and openai/gpt-5.5 + agentRuntime.id: codex)
  • Node v24.15.0
  • Reproduced on the message tool; expected to affect any tool whose result content includes a non-text/image nested block.

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 Bug: tool-result middleware discards nested toolResult blocks; message tool returns become 'Tool output unavailable due to post-processing error'