openclaw - ✅(Solved) Fix buildEmbeddedRunPayloads leaks "commentary" phase text to channel delivery for codex / phase-tagged providers [1 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#80458Fetched 2026-05-11 03:14:25
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Timeline (top)
commented ×1cross-referenced ×1

When the active model returns assistant content as phase-tagged text blocks (type: "text" with textSignature.phase), buildEmbeddedRunPayloads ships the raw streamed assistantTexts to channel-delivery payloads, which includes the commentary phase blocks. The final_answer-only filter (extractAssistantVisibleText) is only consulted as a fallback when streaming captured nothing.

End-user impact: reasoning summaries ("Need answer. Current? ...", "Responding to user's food list...") get delivered to iMessage / Telegram / etc. alongside or instead of the final reply.

Version: 2026.5.7. Provider: openai-codex/gpt-5.5 via OAuth. The same issue would affect any provider whose responses include textSignature.phase blocks.

Root Cause

dist/pi-embedded-Bcz04p2i.js, function buildEmbeddedRunPayloads (around line 1346 in 2026.5.7):

const answerTexts = suppressAssistantArtifacts ? [] : (
    shouldPreferRawAnswerText && fallbackRawAnswerText ? [fallbackRawAnswerText]
    : hasAssistantTextPayload ? nonEmptyAssistantTexts     // <-- raw streamed chunks, includes commentary
    : fallbackAnswerText ? [fallbackAnswerText]            // <-- phase-filtered, only used as fallback
    : []
).filter((text) => !shouldSuppressRawErrorText(text));

nonEmptyAssistantTexts is params.assistantTexts.filter(...) — the raw streamed chunks the provider emitted during the run. The codex stream doesn't surface textSignature.phase per-chunk, so these contain both phases concatenated.

fallbackAnswerText is extractAssistantVisibleText(params.lastAssistant), which already correctly filters for phase: "final_answer" (or returns the unphased content when no phase tags exist). But it's only used when hasAssistantTextPayload is false — i.e. when streaming was empty, which is rare for non-empty turns.

The filter logic exists elsewhere in the codebase and works:

  • extractAssistantVisibleText (used by CLI/JSON output path)
  • shouldSuppressAssistantEventForLiveChat in live-chat-projector (used by TUI display)
  • The commentary enum is documented in protocol-validators with the comment "Classifies an assistant message as interim commentary or final answer text"

The channel-delivery payload builder is just the one place this filter isn't applied.

Fix Action

Fix / Workaround

I've been running this patch locally against 2026.5.7 for a few hours without regressions; reasoning-trigger probes and plain conversational turns both deliver only the final_answer block.

PR fix notes

PR #80491: fix(channel): filter commentary phase from channel delivery on phase-tagged providers

Description (problem / solution / changelog)

When providers like openai-codex/gpt-5.5 return phase-tagged content, the raw assistantTexts stream includes both 'commentary' and 'final_answer' blocks. buildEmbeddedRunPayloads was shipping the raw stream to channel delivery, causing reasoning summaries to leak into Telegram/iMessage/etc.

Root Cause

nonEmptyAssistantTexts is the raw streamed chunks from the provider. For codex stream, these contain both phases concatenated. fallbackAnswerText = extractAssistantVisibleText(params.lastAssistant) already correctly filters for phase: "final_answer", but it was only used when streaming was empty (rare).

Fix

Add lastAssistantHasPhaseMetadata detection before answerTexts routing. When phase tags are present, route through the already-phase-filtered fallbackAnswerText instead of the raw stream. Non-phase-tagged providers are unaffected.

Testing

  • Existing payloads.test.ts: 24 tests pass

Real behavior proof

Note on my setup

I do not currently have a working openai-codex/gpt-5.5 API key, so I cannot provide my own before/after screenshots from a live setup. However, the fix is a direct implementation of the patch the issue author independently verified.

Before (issue evidence)

The issue reporter confirmed that with model: "openai-codex/gpt-5.5", channel-delivered replies contained both commentary preamble and final_answer. Example leaked commentary: "Need answer. Current? ...", "Responding to user's food list..." — delivered to iMessage/Telegram alongside final reply.

The openclaw agent --json CLI probe was clean (separate path calls extractAssistantVisibleText), proving the bug is only in the channel-delivery payload builder.

After (verification via issue author)

The issue reporter applied the same patch (shown below) locally against v2026.5.7 and ran for several hours with reasoning-trigger probes and plain conversational turns. Both delivered only the final_answer block, no commentary leakage.

+  const lastAssistantHasPhaseMetadata = (() => {
+    const c = params.lastAssistant && params.lastAssistant.content;
+    if (!Array.isArray(c)) return false;
+    for (const b of c) {
+      if (!b || b.type !== "text" || !b.textSignature) continue;
+      try {
+        const sig = typeof b.textSignature === "string" ? JSON.parse(b.textSignature) : b.textSignature;
+        if (sig && sig.phase) return true;
+      } catch {}
+    }
+    return false;
+  })();
   const answerTexts = suppressAssistantArtifacts
     ? []
-    : (shouldUseCanonicalFinalAnswer
+    : (lastAssistantHasPhaseMetadata && fallbackAnswerText
+        ? [fallbackAnswerText]
+        : shouldUseCanonicalFinalAnswer
           ? [fallbackAnswerSourceText]
           : shouldPreferRawAnswerText && fallbackRawAnswerText
             ? [fallbackRawAnswerText]
             : hasAssistantTextPayload
               ? nonEmptyAssistantTexts
               : fallbackAnswerText
                 ? [fallbackAnswerText]
                 : []
       ).filter((text) => !shouldSuppressRawErrorText(text));

Why this qualifies as proof

  1. The bug is reproducible (issue #80458 has detailed reproduction steps)
  2. The fix is minimal and targeted (one routing branch change)
  3. The same patch was pre-verified by the issue author on real 2026.5.7 runtime
  4. Unit tests confirm no regressions for non-phase-tagged providers
  5. Non-phase-tagged providers (Anthropic, OpenAI API path) are not touched by this change

Fixes #80458


Checklist

  • Tests pass
  • Minimal change, focused scope
  • Follows existing code style

Changed files

  • src/agents/pi-embedded-runner/run/payloads.ts (modified, +28/-9)

Code Example

const answerTexts = suppressAssistantArtifacts ? [] : (
    shouldPreferRawAnswerText && fallbackRawAnswerText ? [fallbackRawAnswerText]
    : hasAssistantTextPayload ? nonEmptyAssistantTexts     // <-- raw streamed chunks, includes commentary
    : fallbackAnswerText ? [fallbackAnswerText]            // <-- phase-filtered, only used as fallback
    : []
).filter((text) => !shouldSuppressRawErrorText(text));

---

const lastAssistantHasPhaseMetadata = (() => {
    const c = params.lastAssistant && params.lastAssistant.content;
    if (!Array.isArray(c)) return false;
    for (const b of c) {
        if (!b || b.type !== "text" || !b.textSignature) continue;
        try {
            const sig = typeof b.textSignature === "string"
                ? JSON.parse(b.textSignature) : b.textSignature;
            if (sig && sig.phase) return true;
        } catch (e) {}
    }
    return false;
})();
const answerTexts = suppressAssistantArtifacts ? [] : (
    lastAssistantHasPhaseMetadata && fallbackAnswerText ? [fallbackAnswerText]
    : shouldPreferRawAnswerText && fallbackRawAnswerText ? [fallbackRawAnswerText]
    : hasAssistantTextPayload ? nonEmptyAssistantTexts
    : fallbackAnswerText ? [fallbackAnswerText]
    : []
).filter((text) => !shouldSuppressRawErrorText(text));
RAW_BUFFERClick to expand / collapse

Summary

When the active model returns assistant content as phase-tagged text blocks (type: "text" with textSignature.phase), buildEmbeddedRunPayloads ships the raw streamed assistantTexts to channel-delivery payloads, which includes the commentary phase blocks. The final_answer-only filter (extractAssistantVisibleText) is only consulted as a fallback when streaming captured nothing.

End-user impact: reasoning summaries ("Need answer. Current? ...", "Responding to user's food list...") get delivered to iMessage / Telegram / etc. alongside or instead of the final reply.

Version: 2026.5.7. Provider: openai-codex/gpt-5.5 via OAuth. The same issue would affect any provider whose responses include textSignature.phase blocks.

Reproduction

  1. Configure an agent with model: "openai-codex/gpt-5.5" (or any other provider that returns phase-tagged blocks).
  2. Send a non-trivial question (one that elicits some internal reasoning).
  3. Observe the channel-delivered reply (iMessage / Telegram / etc).

The delivered text frequently contains both a commentary-phase preamble and the final_answer. Sometimes only the commentary if the run terminated before final_answer was emitted.

An openclaw agent --json probe of the same agent + session is clean, because result.payloads[*].text comes from a separate construction path that already calls extractAssistantVisibleText correctly. So the bug isn't visible from CLI testing — only from real channel delivery.

Root cause

dist/pi-embedded-Bcz04p2i.js, function buildEmbeddedRunPayloads (around line 1346 in 2026.5.7):

const answerTexts = suppressAssistantArtifacts ? [] : (
    shouldPreferRawAnswerText && fallbackRawAnswerText ? [fallbackRawAnswerText]
    : hasAssistantTextPayload ? nonEmptyAssistantTexts     // <-- raw streamed chunks, includes commentary
    : fallbackAnswerText ? [fallbackAnswerText]            // <-- phase-filtered, only used as fallback
    : []
).filter((text) => !shouldSuppressRawErrorText(text));

nonEmptyAssistantTexts is params.assistantTexts.filter(...) — the raw streamed chunks the provider emitted during the run. The codex stream doesn't surface textSignature.phase per-chunk, so these contain both phases concatenated.

fallbackAnswerText is extractAssistantVisibleText(params.lastAssistant), which already correctly filters for phase: "final_answer" (or returns the unphased content when no phase tags exist). But it's only used when hasAssistantTextPayload is false — i.e. when streaming was empty, which is rare for non-empty turns.

The filter logic exists elsewhere in the codebase and works:

  • extractAssistantVisibleText (used by CLI/JSON output path)
  • shouldSuppressAssistantEventForLiveChat in live-chat-projector (used by TUI display)
  • The commentary enum is documented in protocol-validators with the comment "Classifies an assistant message as interim commentary or final answer text"

The channel-delivery payload builder is just the one place this filter isn't applied.

Suggested fix

When the assistant message has explicit phase metadata, route through the already-phase-filtered fallbackAnswerText instead of the raw stream. Non-phase-tagged providers (Anthropic API, OpenAI API path, etc.) are unaffected because lastAssistantHasPhaseMetadata returns false for them.

const lastAssistantHasPhaseMetadata = (() => {
    const c = params.lastAssistant && params.lastAssistant.content;
    if (!Array.isArray(c)) return false;
    for (const b of c) {
        if (!b || b.type !== "text" || !b.textSignature) continue;
        try {
            const sig = typeof b.textSignature === "string"
                ? JSON.parse(b.textSignature) : b.textSignature;
            if (sig && sig.phase) return true;
        } catch (e) {}
    }
    return false;
})();
const answerTexts = suppressAssistantArtifacts ? [] : (
    lastAssistantHasPhaseMetadata && fallbackAnswerText ? [fallbackAnswerText]
    : shouldPreferRawAnswerText && fallbackRawAnswerText ? [fallbackRawAnswerText]
    : hasAssistantTextPayload ? nonEmptyAssistantTexts
    : fallbackAnswerText ? [fallbackAnswerText]
    : []
).filter((text) => !shouldSuppressRawErrorText(text));

I've been running this patch locally against 2026.5.7 for a few hours without regressions; reasoning-trigger probes and plain conversational turns both deliver only the final_answer block.

Why this matters

Affects every channel-delivered reply on phase-tagging providers — iMessage (BlueBubbles), Telegram, etc. The CLI path being clean made it hard to spot until I started reading session JSONL files and comparing messageContent.content[*].textSignature.phase against what landed in the actual channel.

Happy to open a PR if helpful.

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 buildEmbeddedRunPayloads leaks "commentary" phase text to channel delivery for codex / phase-tagged providers [1 pull requests, 1 comments, 2 participants]