Skip to content

fix(agents): normalize structured delta.content blocks to prevent [object Object] in chat replies#75339

Open
lonexreb wants to merge 2 commits intoopenclaw:mainfrom
lonexreb:fix/75268-mistral-thinking-object-object
Open

fix(agents): normalize structured delta.content blocks to prevent [object Object] in chat replies#75339
lonexreb wants to merge 2 commits intoopenclaw:mainfrom
lonexreb:fix/75268-mistral-thinking-object-object

Conversation

@lonexreb
Copy link
Copy Markdown
Contributor

@lonexreb lonexreb commented May 1, 2026

Bug being fixed

Closes #75268. Same family of bug as the closed #70806.

Mistral with native reasoning enabled returns OpenAI-compatible streaming delta.content as an array of typed blocks instead of a flat string (per Mistral native reasoning docs):

{"delta": {"content": [
  {"type": "thinking", "thinking": "Let me think..."},
  {"type": "text", "text": "Here is the answer."}
]}}

The transport loop at src/agents/openai-transport-stream.ts treated delta.content as a string unconditionally:

if (choice.delta.content) {
  appendTextDelta(choice.delta.content);  // gets an array, downstream concat → "[object Object]"
}

So the array fell through to appendTextDelta and downstream string concatenation produced "[object Object]" repeating once per block in user-visible chat replies. The corruption then propagated into session transcript files and memory, matching the reporter's symptoms exactly.

Fix

Add normalizeStructuredContentDelta() that handles the three observed shapes:

Shape Example Routed to
string "hello" text delta (existing behavior)
array of typed blocks [{type: "thinking", ...}, {type: "text", ...}] per-block: thinking or text
single block object {type: "thinking", thinking: "..."} per-block: thinking or text

Block types thinking / reasoning / reasoning.text route to thinking deltas; anything else with a text/content/thinking string field routes to text deltas. Unrecognized objects with no string-valued field produce zero parts (no [object Object] leak).

Reused the same normalizer in the reasoning_content / reasoning / reasoning_text fallback inside getCompletionsReasoningDeltas, so non-string payloads in those fields also stop collapsing to [object Object].

Why this is the best fix

  • Right layer: the OpenAI-compatible transport is shared across all OpenAI-compatible providers, not just Mistral. The fix protects every provider that ever ships structured delta blocks (including future ones), without provider-specific compat flags.
  • Defensive by design: any object/array that doesn't yield string text is dropped silently rather than coerced. The regression test explicitly asserts [object Object] never appears in normalized parts even for unrecognized object shapes.
  • Backwards compatible: the existing string fast path is unchanged; existing 96 tests in the same file still pass.
  • Complements PR fix(mistral): handle content blocks array in reasoning stream #67203: that PR (currently open) tried to extend getCompletionsReasoningDeltas to handle arrays, but it doesn't touch delta.content itself, which is the actual user-visible leak path described in [Bug]: [object Object][object Object]... in agents messages and in memory with mistral thinking #75268. This PR covers both delta.content and the reasoning_content/reasoning/reasoning_text family with one shared helper.

Test plan

  • pnpm test src/agents/openai-transport-stream.test.ts — 103/103 pass (7 new + 96 existing)
  • pnpm tsgo:core — clean
  • pnpm tsgo:core:test — clean
  • pnpm exec oxfmt --check — clean
  • 7 new regression cases cover: plain string, empty/null/undefined, mixed array, reasoning + reasoning.text block types, untyped block with content field, unrecognized objects (explicit [object Object] non-leak assertion), and skipped empty/non-string text fields.

#75268

Real behavior proof


@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels May 1, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented May 1, 2026

Codex review: needs real behavior proof before merge.

Summary
The PR adds a shared structured delta.content normalizer for OpenAI-compatible completions streams, reuses it for non-string reasoning fields, adds regression tests, and records an Unreleased changelog fix.

Reproducibility: yes. there is a high-confidence source-level reproduction path: current main passes non-string choice.delta.content directly into text append/queue paths, and the linked report plus Mistral docs show typed-block content payloads. No live Mistral run was performed in this read-only review.

Real behavior proof
Needs real behavior proof before merge: The external PR's real behavior proof section is empty, so it does not show the changed behavior in a real setup after the fix.

Next step before merge
The PR needs contributor real behavior proof and a maintainer decision on reconciling the overlapping implementation before merge; ClawSweeper repair should not substitute for the contributor proof gate.

Security
Cleared: The diff is limited to stream parsing, tests, and changelog text with no concrete security or supply-chain concern.

Review findings

  • [P2] Preserve fallback string fields after nested values — src/agents/openai-transport-stream.ts:1638-1653
Review details

Best possible solution:

Land one reconciled shared OpenAI-compatible stream parser fix that preserves sibling string fallbacks after unsupported nested structures, covers delta.content and reasoning-field block shapes with regression tests, includes the changelog entry, and shows after-fix real behavior proof.

Do we have a high-confidence way to reproduce the issue?

Yes, there is a high-confidence source-level reproduction path: current main passes non-string choice.delta.content directly into text append/queue paths, and the linked report plus Mistral docs show typed-block content payloads. No live Mistral run was performed in this read-only review.

Is this the best way to solve the issue?

No for the current PR revision. Normalizing at the shared OpenAI-compatible stream parser is the right boundary, but this branch should preserve fallback string fields after unsupported nested values and include after-fix real behavior proof before merge.

Full review comments:

  • [P2] Preserve fallback string fields after nested values — src/agents/openai-transport-stream.ts:1638-1653
    The normalizer selects a non-string text, thinking, or content field as nestedSource before checking sibling string fields. For a block like { text: {}, content: "final answer" }, recursion returns [] and the valid string is dropped, losing assistant output instead of only filtering unsupported objects.
    Confidence: 0.9

Overall correctness: patch is incorrect
Overall confidence: 0.88

Acceptance criteria:

  • pnpm test src/agents/openai-transport-stream.test.ts
  • pnpm tsgo:core
  • pnpm tsgo:core:test
  • pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/agents/openai-transport-stream.ts src/agents/openai-transport-stream.test.ts
  • after-fix real Mistral or equivalent OpenAI-compatible run proof showing no [object Object] in reply/transcript output

What I checked:

  • Current main unsafe append path: Current main still treats truthy choice.delta.content as text and passes it to queuePostToolCallDelta or appendTextDelta without a runtime string guard, so an array/object payload can reach downstream string concatenation. (src/agents/openai-transport-stream.ts:1494, 5fae1c32b5f8)
  • Upstream payload shape checked: Mistral's native reasoning docs show content arrays containing text and thinking blocks, including nested thinking arrays of text blocks; this matches the linked report and the PR's intended parser surface.
  • PR parser location: The PR introduces normalizeStructuredContentDelta and calls it before the existing reasoning/tool-call handling in processOpenAICompletionsStream. (src/agents/openai-transport-stream.ts:1494, 4ebe0cedc052)
  • Remaining parser defect: The new nestedSource selection prefers any non-string text/thinking/content field before checking sibling string fields, so { text: {}, content: "final answer" } recurses into the empty object and returns no parts instead of preserving the valid fallback string. (src/agents/openai-transport-stream.ts:1638, 4ebe0cedc052)
  • Regression coverage added but missing mixed fallback: The PR adds focused normalizer tests for strings, arrays, typed thinking/text blocks, nested arrays, and unrecognized objects, but not the mixed unsupported nested field plus valid sibling string fallback case. (src/agents/openai-transport-stream.test.ts:4435, 4ebe0cedc052)
  • Changelog present: The PR adds an Unreleased fix entry for structured delta.content blocks leaking as [object Object] and credits the contributor. (CHANGELOG.md:73, 4ebe0cedc052)

Likely related people:

  • steipete: GitHub path history shows repeated recent work in src/agents/openai-transport-stream.ts, including OpenAI/Codex transport and reasoning-adjacent changes. (role: recent maintainer; confidence: high; commits: 15d3fd83bb78, 11e05e86a233, 399d7f61783f; files: src/agents/openai-transport-stream.ts, src/agents/openai-transport-stream.test.ts)
  • bladin: Merged PR fix(openrouter): handle reasoning_details in Qwen3 stream parsing #66905 added reasoning_details handling and tests in the same OpenAI-compatible completions stream parser family. (role: introduced adjacent behavior; confidence: medium; commits: e0bf756b50b5; files: src/agents/openai-transport-stream.ts, src/agents/openai-transport-stream.test.ts)
  • vincentkoc: GitHub history shows adjacent OpenRouter reasoning-details parser/test hardening and recent test maintenance in this file pair. (role: adjacent maintainer; confidence: medium; commits: 68502c90d1fb, 306a5822947d; files: src/agents/openai-transport-stream.ts, src/agents/openai-transport-stream.test.ts)

Remaining risk / open question:

  • The current normalizer can drop valid sibling string output when an unsupported nested text, thinking, or content field appears first.
  • The PR body has no after-fix real behavior proof from a real OpenClaw/Mistral or equivalent OpenAI-compatible setup.
  • The same bug family has overlapping open PR fix(agents): unpack typed-block delta.content arrays in openai-completions stream #68418, so maintainers should choose one reconciled canonical implementation.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 5fae1c32b5f8.

lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 1, 2026
…gelog credit

Round-1 review on PR openclaw#75339 caught two gaps:

1. Nested thinking arrays were dropped. The original normalizer only read
   `record.thinking` as a string, but the Mistral typed-block reproduction
   includes `type:"thinking"` blocks whose `thinking` value is itself
   an array of `{type:"text", text:"..."}` parts. Recurse into nested
   text/thinking/content arrays-and-objects, then re-tag the resulting
   parts as thinking when the outer block declared a thinking-style type
   (`thinking`, `reasoning`, or `reasoning.text`). Same recursion is
   safe for nested `text` and `content` shapes — they fall through as
   text deltas.

2. Missing CHANGELOG entry. Add an Unreleased `### Fixes` bullet with
   contributor credit per the repo policy in CLAUDE.md ("every added
   entry must include at least one Thanks @author attribution").

Add 4 round-2 regression tests:

- type:thinking block with thinking: [array of text parts] flattens to
  per-part thinking deltas keyed on the outer signature
- type:reasoning block with text: [array of text parts] flattens to
  per-part thinking deltas
- untyped block with content: [array of text parts] flattens to text
- nested thinking block with only empty sub-blocks returns no parts (the
  "never emit [object Object]" invariant still holds for the recursive
  case)

Refs openclaw#75268, openclaw#70806
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 1, 2026

Round-1 review addressed in 8248b3e. Both P2 findings closed.

P2: nested thinking arrays

The bot was right — the original normalizer only read record.thinking as a string, so a Mistral block of the form

{ "type": "thinking", "thinking": [ {"type":"text","text":"step one"}, {"type":"text","text":"step two"} ] }

silently produced zero parts and the user lost the reasoning content entirely.

Fixed by recursing into nested text / thinking / content arrays-and-objects, then re-tagging the flattened parts as thinking when the outer block declared a thinking-style type (thinking, reasoning, or reasoning.text). The same recursion is safe for nested untyped content shapes — they fall through as text deltas.

P2: missing CHANGELOG

Added an Unreleased ### Fixes bullet with Thanks @lonexreb. per the repo policy in CLAUDE.md ("every added entry must include at least one Thanks @author attribution").

New regression tests (4)

  • type:thinking block with thinking: [array of text parts] flattens to per-part thinking deltas keyed on the outer signature
  • type:reasoning block with text: [array of text parts] flattens to per-part thinking deltas
  • untyped block with content: [array of text parts] flattens to text
  • nested thinking block containing only empty sub-blocks returns no parts (the "never emit [object Object]" invariant still holds for the recursive case)

Acceptance criteria checked locally

  • pnpm test src/agents/openai-transport-stream.test.ts — 107/107 pass
  • pnpm tsgo:core — clean
  • pnpm tsgo:core:test — clean
  • pnpm exec oxfmt --check --threads=1 CHANGELOG.md src/agents/openai-transport-stream.ts src/agents/openai-transport-stream.test.ts — clean

PTAL.

lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 3, 2026
…gelog credit

Round-1 review on PR openclaw#75339 caught two gaps:

1. Nested thinking arrays were dropped. The original normalizer only read
   `record.thinking` as a string, but the Mistral typed-block reproduction
   includes `type:"thinking"` blocks whose `thinking` value is itself
   an array of `{type:"text", text:"..."}` parts. Recurse into nested
   text/thinking/content arrays-and-objects, then re-tag the resulting
   parts as thinking when the outer block declared a thinking-style type
   (`thinking`, `reasoning`, or `reasoning.text`). Same recursion is
   safe for nested `text` and `content` shapes — they fall through as
   text deltas.

2. Missing CHANGELOG entry. Add an Unreleased `### Fixes` bullet with
   contributor credit per the repo policy in CLAUDE.md ("every added
   entry must include at least one Thanks @author attribution").

Add 4 round-2 regression tests:

- type:thinking block with thinking: [array of text parts] flattens to
  per-part thinking deltas keyed on the outer signature
- type:reasoning block with text: [array of text parts] flattens to
  per-part thinking deltas
- untyped block with content: [array of text parts] flattens to text
- nested thinking block with only empty sub-blocks returns no parts (the
  "never emit [object Object]" invariant still holds for the recursive
  case)

Refs openclaw#75268, openclaw#70806
@lonexreb lonexreb force-pushed the fix/75268-mistral-thinking-object-object branch from 8248b3e to 8723d35 Compare May 3, 2026 08:00
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 4, 2026
…gelog credit

Round-1 review on PR openclaw#75339 caught two gaps:

1. Nested thinking arrays were dropped. The original normalizer only read
   `record.thinking` as a string, but the Mistral typed-block reproduction
   includes `type:"thinking"` blocks whose `thinking` value is itself
   an array of `{type:"text", text:"..."}` parts. Recurse into nested
   text/thinking/content arrays-and-objects, then re-tag the resulting
   parts as thinking when the outer block declared a thinking-style type
   (`thinking`, `reasoning`, or `reasoning.text`). Same recursion is
   safe for nested `text` and `content` shapes — they fall through as
   text deltas.

2. Missing CHANGELOG entry. Add an Unreleased `### Fixes` bullet with
   contributor credit per the repo policy in CLAUDE.md ("every added
   entry must include at least one Thanks @author attribution").

Add 4 round-2 regression tests:

- type:thinking block with thinking: [array of text parts] flattens to
  per-part thinking deltas keyed on the outer signature
- type:reasoning block with text: [array of text parts] flattens to
  per-part thinking deltas
- untyped block with content: [array of text parts] flattens to text
- nested thinking block with only empty sub-blocks returns no parts (the
  "never emit [object Object]" invariant still holds for the recursive
  case)

Refs openclaw#75268, openclaw#70806
@lonexreb lonexreb force-pushed the fix/75268-mistral-thinking-object-object branch from 8723d35 to 3f45c4a Compare May 4, 2026 09:48
lonexreb added a commit to lonexreb/paloa-claw that referenced this pull request May 5, 2026
…gelog credit

Round-1 review on PR openclaw#75339 caught two gaps:

1. Nested thinking arrays were dropped. The original normalizer only read
   `record.thinking` as a string, but the Mistral typed-block reproduction
   includes `type:"thinking"` blocks whose `thinking` value is itself
   an array of `{type:"text", text:"..."}` parts. Recurse into nested
   text/thinking/content arrays-and-objects, then re-tag the resulting
   parts as thinking when the outer block declared a thinking-style type
   (`thinking`, `reasoning`, or `reasoning.text`). Same recursion is
   safe for nested `text` and `content` shapes — they fall through as
   text deltas.

2. Missing CHANGELOG entry. Add an Unreleased `### Fixes` bullet with
   contributor credit per the repo policy in CLAUDE.md ("every added
   entry must include at least one Thanks @author attribution").

Add 4 round-2 regression tests:

- type:thinking block with thinking: [array of text parts] flattens to
  per-part thinking deltas keyed on the outer signature
- type:reasoning block with text: [array of text parts] flattens to
  per-part thinking deltas
- untyped block with content: [array of text parts] flattens to text
- nested thinking block with only empty sub-blocks returns no parts (the
  "never emit [object Object]" invariant still holds for the recursive
  case)

Refs openclaw#75268, openclaw#70806
@lonexreb lonexreb force-pushed the fix/75268-mistral-thinking-object-object branch from 3f45c4a to 35086db Compare May 5, 2026 05:30
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 5, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35086db45e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1639 to +1643
record.text !== undefined && record.text !== null && typeof record.text !== "string"
? record.text
: record.thinking !== undefined &&
record.thinking !== null &&
typeof record.thinking !== "string"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve string fallback when structured text is non-string

This branch prioritizes any non-string record.text as nestedSource, so payloads like { text: {...}, content: "final answer" } or { text: {...}, thinking: "..." } recurse into the object and return [], dropping the valid string field entirely. That is a regression introduced by the new normalizer: a chunk can now disappear even though it still carries usable text in content/thinking, which loses assistant output instead of only filtering unsupported structures.

Useful? React with 👍 / 👎.

lonexreb added 2 commits May 5, 2026 13:11
…ject Object] in chat replies

Mistral with native reasoning enabled (https://docs.mistral.ai/studio-api/conversations/reasoning/native)
returns OpenAI-compatible streaming `delta.content` as an array of typed
blocks like:

  {"delta": {"content": [{"type": "thinking", "thinking": "..."},
                          {"type": "text", "text": "..."}]}}

The OpenAI baseline ships `delta.content` as a flat string. The transport
loop in src/agents/openai-transport-stream.ts treated content as a string
unconditionally, so the array fell through to `appendTextDelta` and
downstream string concatenation produced "[object Object]" repeating per
block in user-visible chat replies. The corruption then leaked into session
files and memory, exactly the symptoms in openclaw#75268 (and the closed predecessor
openclaw#70806). The same family of structured payloads can also appear under
`reasoning_content`.

Add `normalizeStructuredContentDelta()` that handles the three observed
shapes (string, array of typed blocks, single block object) and routes
`thinking` / `reasoning` / `reasoning.text` types to thinking deltas
and everything else to text deltas. Reuse the same normalizer from the
`reasoning_content`/`reasoning`/`reasoning_text` fallback path inside
`getCompletionsReasoningDeltas` so non-string payloads in those fields
also stop collapsing to "[object Object]".

Add 7 regression tests:

- plain string content unchanged
- empty/null/undefined content yields no parts
- array of mixed thinking + text blocks flattens correctly
- `reasoning` and `reasoning.text` block types are recognized as thinking
- untyped block with `content` field falls back to text
- unrecognized objects never emit the literal "[object Object]"
- empty/non-string text fields are skipped, valid ones kept

Refs openclaw#75268, openclaw#70806
…gelog credit

Round-1 review on PR openclaw#75339 caught two gaps:

1. Nested thinking arrays were dropped. The original normalizer only read
   `record.thinking` as a string, but the Mistral typed-block reproduction
   includes `type:"thinking"` blocks whose `thinking` value is itself
   an array of `{type:"text", text:"..."}` parts. Recurse into nested
   text/thinking/content arrays-and-objects, then re-tag the resulting
   parts as thinking when the outer block declared a thinking-style type
   (`thinking`, `reasoning`, or `reasoning.text`). Same recursion is
   safe for nested `text` and `content` shapes — they fall through as
   text deltas.

2. Missing CHANGELOG entry. Add an Unreleased `### Fixes` bullet with
   contributor credit per the repo policy in CLAUDE.md ("every added
   entry must include at least one Thanks @author attribution").

Add 4 round-2 regression tests:

- type:thinking block with thinking: [array of text parts] flattens to
  per-part thinking deltas keyed on the outer signature
- type:reasoning block with text: [array of text parts] flattens to
  per-part thinking deltas
- untyped block with content: [array of text parts] flattens to text
- nested thinking block with only empty sub-blocks returns no parts (the
  "never emit [object Object]" invariant still holds for the recursive
  case)

Refs openclaw#75268, openclaw#70806
@lonexreb lonexreb force-pushed the fix/75268-mistral-thinking-object-object branch from 35086db to 4ebe0ce Compare May 5, 2026 18:11
@lonexreb
Copy link
Copy Markdown
Contributor Author

lonexreb commented May 5, 2026

Follow-up notes for reviewers — invariant lock and extension surface

Posting a deeper write-up of what the new tests actually pin down, since the [object Object] regression has bitten this surface twice already (#70806 closed, #75268 here) and a tight invariant statement helps the next person evaluating a delta-shape PR.

Soundness invariant pinned by the tests

Let δ be a streamed delta.content value and N(δ) the output of normalizeStructuredContentDelta(δ). The tests collectively pin:

Property Statement Test that locks it
Identity on strings N("s") = [{kind:"text", text:"s"}] "plain string fast path"
Empty-safety N("") = N(null) = N(undefined) = [] "empty/null/undefined"
Compositionality on arrays N([b₁, …, bₙ]) = concat_i N(bᵢ) "mixed array of blocks"
Block-type routing b.type ∈ {thinking, reasoning, reasoning.text}kind = "thinking", else "text" "reasoning + reasoning.text block types"
Soundness against [object Object] For every input δ, no element of N(δ) has text field equal to String(o) for any non-null object o. "unrecognized objects" — explicit [object Object] non-leak assertion

That last row is the load-bearing one — it's what the bot review and reporter actually care about, and the test makes it impossible to regress without breaking the assertion.

Edge case worth noting for future block-type expansion

The block-type allow-list (thinking | reasoning | reasoning.text) is closed-set today. Two emerging shapes I've seen in OpenAI-compatible providers but didn't include in this PR (out of scope, would expand surface):

  • {type: "tool_use", id, name, input} — Anthropic's content block; if a Mistral-like provider ever proxies it through OpenAI-completions, today's normalizer drops it silently (per the soundness rule). Probably correct, but worth a follow-up to surface those as structured tool deltas instead of dropping.
  • {type: "image"} / {type: "input_image"} — also dropped today. If/when chat reply rendering can carry inline images from streaming, this is the obvious extension point.

Why the helper lives where it does

I deliberately kept normalizeStructuredContentDelta inside openai-transport-stream.ts rather than promoting it to openclaw/plugin-sdk/*. Reason: the function encodes OpenAI-completions-family transport semantics, not a portable plugin contract. Promoting it would imply provider plugins are expected to call it, which would muddy the extensions/ boundary rules in extensions/CLAUDE.md ("transport/replay/tool compat families belong in shared helpers"). If a second consumer family ever needs the same normalization, the right move is to extract a shared normalizeStructuredContentBlocks family helper rather than re-export this one.

Cross-reference with #67203

PR #67203 extended getCompletionsReasoningDeltas to handle arrays in the reasoning family but didn't touch delta.content, which is the actual user-visible leak path described in #75268. After this PR lands, #67203 becomes a strict subset of the behavior already covered here (since the same normalizer is reused inside getCompletionsReasoningDeltas for the reasoning_content / reasoning / reasoning_text fields). Worth closing #67203 as superseded once this merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: [object Object][object Object]... in agents messages and in memory with mistral thinking [Bug]: multiple "[object Object]" in chat

1 participant