openclaw - ✅(Solved) Fix [Bug][Codex Runtime]: Discord progress reasoning stream overwrites prior reasoning chunks [1 pull requests, 2 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#83983Fetched 2026-05-20 03:45:36
View on GitHub
Comments
2
Participants
2
Timeline
10
Reactions
1
Timeline (top)
labeled ×6commented ×2cross-referenced ×1renamed ×1

In Discord streaming.mode: "progress", reasoning/thinking trace updates can render one token or word at a time, but each next reasoning chunk overwrites the previous chunk in the progress draft. The visible result can degrade to a single bullet with only the final tiny chunk, for example:

• !

This looks like a progress-draft rendering bug, not a model/runtime reasoning failure. The Codex app-server path receives reasoning deltas correctly, but Discord appears to format each delta as a complete Reasoning: snapshot before the draft-preview merge step.

Root Cause

Source-level repro / suspected root cause

Fix Action

Fixed

PR fix notes

PR #84202: fix(discord): preserve reasoning progress deltas

Description (problem / solution / changelog)

Summary

  • Keep Discord reasoning progress text raw until draft-preview merging decides whether incoming text is a delta or a snapshot.
  • Format the merged reasoning text once for Discord display, preserving the existing italic progress-line rendering.
  • Add a regression for fragment-style reasoning deltas so the progress draft no longer collapses to the final token.

Fixes #83983.

Real behavior proof

Behavior or issue addressed: Discord progress-mode reasoning deltas were formatted with the Thinking display prefix before merge, causing the draft preview to treat every delta as a full snapshot and replace earlier reasoning text.

Real environment tested: Local OpenClaw checkout on macOS using the real Discord draft-preview controller and a fake Discord REST client that records the outgoing preview message content.

Exact steps or command run after this patch: PATH=/Users/andy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH node --import tsx /private/tmp/proof-83983.mjs

Evidence after fix:

openclaw-discord-reasoning-delta-proof=ok
write_count=2
final_preview=Clawing...\n\n• _Considering plugin installation!_
final_delta_only_present=false

Observed result after fix: Four raw reasoning deltas (Considering, plugin, installation, !) produced one merged Discord progress line, and the final-delta-only preview was absent.

What was not tested: No live Discord guild was called; the proof uses the real draft-preview code path with a local fake REST client.

Validation

  • NODE_OPTIONS=--max-old-space-size=8192 OPENCLAW_VITEST_MAX_WORKERS=1 PATH=/Users/andy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH node scripts/run-vitest.mjs extensions/discord/src/monitor/message-handler.process.test.ts -t "reasoning" --pool forks --maxWorkers 1 --vmMemoryLimit 8192MB
  • NODE_OPTIONS=--max-old-space-size=8192 OPENCLAW_VITEST_MAX_WORKERS=1 PATH=/Users/andy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:$PATH node scripts/run-vitest.mjs extensions/discord/src/monitor/message-handler.process.test.ts extensions/codex/src/app-server/event-projector.test.ts src/agents/pi-embedded-utils.test.ts --pool forks --maxWorkers 1 --vmMemoryLimit 8192MB
  • PATH=/Users/andy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:/Users/andy/openclaw-83983/node_modules/.bin:$PATH oxfmt --check extensions/discord/src/monitor/message-handler.process.ts extensions/discord/src/monitor/message-handler.draft-preview.ts extensions/discord/src/monitor/message-handler.process.test.ts
  • PATH=/Users/andy/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/bin:/Users/andy/openclaw-83983/node_modules/.bin:$PATH oxlint extensions/discord/src/monitor/message-handler.process.ts extensions/discord/src/monitor/message-handler.draft-preview.ts extensions/discord/src/monitor/message-handler.process.test.ts
  • git diff --check

Maintainer note

If this PR is squashed or reworked, please preserve author attribution or include:

Co-authored-by: Andy Ye <[email protected]>

Changed files

  • extensions/discord/src/monitor/message-handler.draft-preview.ts (modified, +4/-2)
  • extensions/discord/src/monitor/message-handler.process.test.ts (modified, +32/-0)
  • extensions/discord/src/monitor/message-handler.process.ts (modified, +2/-9)

Code Example

!

---

const delta = readString(params, "delta") ?? "";
   ...
   this.reasoningTextByItem.set(itemId, `${this.reasoningTextByItem.get(itemId) ?? ""}${delta}`);
   await this.params.onReasoningStream?.({ text: delta });

---

const formattedText = payload?.text
     ? formatReasoningMessage(payload.text)
     : undefined;
   await draftPreview.pushReasoningProgress(formattedText);

---

return `Reasoning:\n${italicLines}`;

---

if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) {
     return incoming;
   }
   ...
   function isReasoningSnapshotText(text: string): boolean {
     return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
   }

---

function formatReasoningMessage(text) {
  const trimmed = text.trim();
  if (!trimmed) return "";
  return `Reasoning:\n_${trimmed}_`;
}

function normalizeReasoningProgressLine(text) {
  return text.replace(/^\s*(?:>\s*)?Reasoning:\s*/i, "").replace(/\s+/g, " ").trim();
}

function isReasoningSnapshotText(text) {
  return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
}

function mergeReasoningProgressText(current, incoming) {
  if (!current) return incoming;
  const normalizedCurrent = normalizeReasoningProgressLine(current);
  const normalizedIncoming = normalizeReasoningProgressLine(incoming);
  if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) return current;
  if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) return incoming;
  return `${current}${incoming}`;
}

let current = "";
for (const delta of ["Considering", " plugin", " installation", "!"]) {
  current = mergeReasoningProgressText(current, formatReasoningMessage(delta));
  console.log(current);
}
RAW_BUFFERClick to expand / collapse

Summary

In Discord streaming.mode: "progress", reasoning/thinking trace updates can render one token or word at a time, but each next reasoning chunk overwrites the previous chunk in the progress draft. The visible result can degrade to a single bullet with only the final tiny chunk, for example:

• !

This looks like a progress-draft rendering bug, not a model/runtime reasoning failure. The Codex app-server path receives reasoning deltas correctly, but Discord appears to format each delta as a complete Reasoning: snapshot before the draft-preview merge step.

Environment where observed

  • OpenClaw: 2026.5.18 (50a2481)
  • Channel: Discord guild text channel
  • Discord streaming mode: progress
  • Progress config includes tool progress enabled and maxLines > 1
  • Runtime/model: native Codex app-server / gpt-5.5
  • Reasoning visibility: streaming reasoning enabled

User-visible symptom

During a Discord turn, the reasoning trace appears to stream one word at a time. Instead of accumulating into a readable thinking/progress line, the next word immediately replaces the last word. At the end, the progress draft can contain only a bullet and the final chunk, such as !.

Expected behavior:

  • Reasoning deltas should accumulate or coalesce into a stable readable reasoning/progress line.
  • The final visible progress line should not be only the last delta.
  • Display formatting such as Reasoning: should not be used to infer whether a payload is a full snapshot versus a delta.

Source-level repro / suspected root cause

On current upstream main at b86435f0b597e58cd2eb904fe7fa12f881ca4dba:

  1. Codex app-server accumulates reasoning internally, but forwards only the current delta to the reply callback:

    const delta = readString(params, "delta") ?? "";
    ...
    this.reasoningTextByItem.set(itemId, `${this.reasoningTextByItem.get(itemId) ?? ""}${delta}`);
    await this.params.onReasoningStream?.({ text: delta });
  2. Discord formats each callback payload before passing it into the draft-preview reasoning merge:

    const formattedText = payload?.text
      ? formatReasoningMessage(payload.text)
      : undefined;
    await draftPreview.pushReasoningProgress(formattedText);
  3. formatReasoningMessage() wraps every non-empty chunk with a Reasoning: prefix:

    return `Reasoning:\n${italicLines}`;
  4. The Discord draft-preview merge treats any incoming text that starts with Reasoning: as a full snapshot, so a formatted delta is mistaken for a complete replacement:

    if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) {
      return incoming;
    }
    ...
    function isReasoningSnapshotText(text: string): boolean {
      return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
    }
  5. The reasoning progress renderer then replaces the previous reasoning line with the newly merged line:

Because each delta is formatted as Reasoning:\n_<delta>_, every delta satisfies isReasoningSnapshotText(incoming), causing the previous accumulated reasoning line to be replaced.

A minimal reproduction of the merge logic shape:

function formatReasoningMessage(text) {
  const trimmed = text.trim();
  if (!trimmed) return "";
  return `Reasoning:\n_${trimmed}_`;
}

function normalizeReasoningProgressLine(text) {
  return text.replace(/^\s*(?:>\s*)?Reasoning:\s*/i, "").replace(/\s+/g, " ").trim();
}

function isReasoningSnapshotText(text) {
  return /^\s*(?:>\s*)?Reasoning:\s*/i.test(text);
}

function mergeReasoningProgressText(current, incoming) {
  if (!current) return incoming;
  const normalizedCurrent = normalizeReasoningProgressLine(current);
  const normalizedIncoming = normalizeReasoningProgressLine(incoming);
  if (!normalizedIncoming || normalizedIncoming === normalizedCurrent) return current;
  if (isReasoningSnapshotText(incoming) || normalizedIncoming.startsWith(normalizedCurrent)) return incoming;
  return `${current}${incoming}`;
}

let current = "";
for (const delta of ["Considering", " plugin", " installation", "!"]) {
  current = mergeReasoningProgressText(current, formatReasoningMessage(delta));
  console.log(current);
}

The displayed reasoning candidate ends up as the latest formatted chunk instead of an accumulated sentence.

Suggested fix direction

The robust fix is to separate reasoning payload semantics from display formatting:

  • Merge raw reasoning text first, then apply formatReasoningMessage() once for display.
  • Or pass explicit payload metadata such as { text, delta, mode: "delta" | "snapshot", itemId }, so Discord does not infer delta-vs-snapshot from the Reasoning: display prefix.
  • Avoid feeding already display-formatted Reasoning: text into mergeReasoningProgressText() unless the payload is actually a full snapshot.

A narrow fix could be in the Discord path:

  • pass raw payload.text into pushReasoningProgress,
  • let pushReasoningProgress merge raw text,
  • format the normalized/merged reasoning only when inserting it into previewToolProgressLines.

Acceptance criteria

  • In Discord streaming.mode: "progress", simulated reasoning deltas such as "Considering", " plugin", " installation", "!" render as one accumulated/coalesced reasoning line, not only !.
  • A true full reasoning snapshot still replaces the previous reasoning line when appropriate.
  • Existing final-answer delivery behavior is unchanged.
  • Tool progress rows continue to merge/update as before.
  • Regression coverage exists for delta-style reasoning and snapshot-style reasoning in message-handler.draft-preview or adjacent Discord process tests.

Related issues

  • #83831 is related only in that it affects Discord progress/final delivery state; this issue is about reasoning-progress text being overwritten by later reasoning deltas.
  • #83307 is related to Discord progress commentary UX; this issue is narrower and concerns reasoning stream delta/snapshot semantics.

What was not tested

I do not have a live Discord recording attached here. The symptom was observed in a downstream Discord deployment, and the likely cause is source-reproducible from the current delta formatting/merge path above.

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