openclaw - ✅(Solved) Fix bug(agents): OpenAI WS replay drops message-level phase when block has explicit textSignature — commentary leaks on history replay [5 pull requests, 1 comments, 1 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#61476Fetched 2026-04-08 02:58:14
View on GitHub
Comments
1
Participants
1
Timeline
19
Reactions
0
Participants
Timeline (top)
referenced ×9cross-referenced ×7closed ×1commented ×1

Root Cause

When replaying assistant history, if any text block has an explicit phase in its textSignature, then every other block without an explicit phase is forced to undefined instead of inheriting m.phase:

const blockPhase =
  parsedSignature?.phase ?? (hasExplicitBlockPhase ? undefined : assistantMessagePhase);

Once hasExplicitBlockPhase is true, untagged blocks stop inheriting the message phase. So a message with m.phase = "final_answer" plus one explicitly-tagged block and one untagged block gets replayed as two logical messages: one final_answer, one unphased.

On the return path, unphased text sets hasUnphasedAssistantText = true (:500-502), which prevents message.phase from being reconstructed (:575-579). This creates malformed/contradictory phase metadata.

Fix Action

Fixed

PR fix notes

PR #61463: fix(agents,gateway): phase-aware assistant text extraction — suppress OpenAI commentary leaks in sessions-helpers, TUI, and REST history

Description (problem / solution / changelog)

What this fixes (plain English)

Several places in the codebase that display assistant text were not aware of the phase system (commentary vs. final answer). This meant internal "thinking out loud" commentary could leak into user-visible surfaces like the TUI and HTTP/SSE session history endpoints. This PR makes those surfaces phase-aware, and hardens heartbeat session resolution against subagent key leaks.

Technical details

Follow-up to #59643 and #59150, which fixed phase separation in the core WS path. Post-merge audit found adjacent surfaces still using phase-blind extraction.

Surfaces fixed:

  • src/tui/tui-formatters.ts — assistant text extraction now uses extractAssistantVisibleText() to prefer final_answer over commentary phase blocks
  • src/infra/heartbeat-runner.ts — heartbeat session resolution now rejects subagent session keys, falling back to the main session key instead of leaking into subagent scopes
  • src/gateway/sessions-history-http.test.ts — regression test: REST history applies chat.history sanitization (strips NO_REPLY messages, preserves phase blocks)
  • src/tui/tui-formatters.test.ts — regression test: mixed commentary + final_answer blocks extract only the visible final answer

Explicitly deferred: extractAssistantTextForSilentCheck and buffered delta/final rendering — lower-confidence, more behaviorally sensitive.

Related

  • Follow-up to #59643 and #59150
  • Follow-up issues: #61474, #61475, #61476, #61477, #61478
  • Companion PRs: #61529, #61528, #61527
  • Related PRs: #61855, #61816

Test plan

  • 27/27 TUI formatter tests pass
  • Regression test for mixed commentary + final_answer in TUI extraction
  • HTTP history regression test (REST path shares chat.history sanitization, strips NO_REPLY messages)
  • SSE seq validation for NO_REPLY message stripping
  • sessions-history-http gateway integration tests have 2 pre-existing infra failures (also fail on upstream/main)

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/infra/heartbeat-runner.subagent-session-guard.test.ts (added, +72/-0)
  • src/infra/heartbeat-runner.ts (modified, +24/-17)
  • src/tui/tui-formatters.test.ts (modified, +20/-0)
  • src/tui/tui-formatters.ts (modified, +25/-0)

PR #61481: fix(agents): harden OpenAI phase-aware visible text — suppress commentary partials, prevent empty final_answer fallback leak

Description (problem / solution / changelog)

Summary

  • fix phase-aware visible text extraction so an explicit final_answer block never falls back to commentary or legacy unphased text when it sanitizes to empty
  • suppress all commentary-phase partial streaming output regardless of whether extracted visible text is non-empty
  • keep session-history HTTP/SSE sanitization aligned with the hardened chat history path
  • add regression tests covering both leak paths and the session-history follow-through

Context

This hardens the merged #59643 behavior against two P1 leaks:

  • fixes #61474
  • fixes #61475

Related issues / bug family

  • related to #25592
  • related to #59536
  • related to #59918
  • related to #44213
  • related to #49438
  • related to #53960

Parent / sibling PRs

  • parent: #59643 — core phase-separation fix (merged)
  • sibling: #61463 — phase-aware extraction in sessions-helpers, TUI, and history paths

Remaining follow-ups from the same adversarial review

  • #61476 — replay splitting corrupts phase on mixed messages
  • #61477 — late-map buffering gates on key existence, not phase validity
  • #61478 — function-call replay silently loses malformed arguments

Related open PRs

  • #59920 — prefer terminal reply fields in CLI JSONL parser
  • #61151 — drop partialJson streaming artifacts from session history
  • #61337 — disable OpenAI tool-use pairing repair

Testing

  • npm exec -- node --no-maglev ./node_modules/vitest/vitest.mjs run --config vitest.config.ts src/agents/pi-embedded-utils.test.ts src/agents/pi-embedded-subscribe.handlers.messages.test.ts
  • npm exec -- node --no-maglev ./node_modules/vitest/vitest.mjs run --config vitest.config.ts src/gateway/sessions-history-http.test.ts

Changed files

  • .agents/skills/openclaw-parallels-smoke/SKILL.md (modified, +13/-0)
  • .agents/skills/openclaw-qa-testing/SKILL.md (added, +86/-0)
  • .agents/skills/openclaw-qa-testing/agents/openai.yaml (added, +4/-0)
  • .github/labeler.yml (modified, +4/-0)
  • .github/workflows/ci.yml (modified, +7/-1)
  • .github/workflows/control-ui-locale-refresh.yml (modified, +2/-2)
  • .github/workflows/openclaw-npm-release.yml (modified, +1/-1)
  • CHANGELOG.md (modified, +40/-12)
  • appcast.xml (modified, +248/-116)
  • apps/android/app/build.gradle.kts (modified, +2/-2)
  • apps/ios/Config/Version.xcconfig (modified, +3/-3)
  • apps/macos/Sources/OpenClaw/Resources/Info.plist (modified, +2/-2)
  • apps/macos/Sources/OpenClawProtocol/GatewayModels.swift (modified, +14/-0)
  • apps/shared/OpenClawKit/Sources/OpenClawKit/Resources/tool-display.json (modified, +23/-0)
  • apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift (modified, +14/-0)
  • docs/.generated/config-baseline.sha256 (modified, +4/-4)
  • docs/.generated/plugin-sdk-api-baseline.sha256 (modified, +2/-2)
  • docs/automation/tasks.md (modified, +5/-0)
  • docs/channels/discord.md (modified, +1/-1)
  • docs/channels/matrix.md (modified, +29/-5)
  • docs/cli/memory.md (modified, +43/-15)
  • docs/cli/update.md (modified, +3/-1)
  • docs/concepts/dreaming.md (modified, +121/-194)
  • docs/concepts/memory-qmd.md (modified, +17/-1)
  • docs/concepts/memory-search.md (modified, +9/-8)
  • docs/concepts/memory.md (modified, +12/-8)
  • docs/concepts/model-providers.md (modified, +2/-0)
  • docs/concepts/models.md (modified, +2/-0)
  • docs/docs.json (modified, +8/-1)
  • docs/gateway/configuration-reference.md (modified, +31/-12)
  • docs/help/faq.md (modified, +36/-0)
  • docs/help/testing.md (modified, +22/-0)
  • docs/install/updating.md (modified, +1/-0)
  • docs/plugins/architecture.md (modified, +1/-0)
  • docs/plugins/building-plugins.md (modified, +1/-0)
  • docs/plugins/manifest.md (modified, +76/-30)
  • docs/plugins/sdk-migration.md (modified, +11/-1)
  • docs/plugins/sdk-overview.md (modified, +22/-9)
  • docs/providers/bedrock-mantle.md (modified, +20/-7)
  • docs/providers/bedrock.md (modified, +29/-0)
  • docs/providers/comfy.md (added, +201/-0)
  • docs/providers/fal.md (modified, +2/-1)
  • docs/providers/google.md (modified, +30/-0)
  • docs/providers/index.md (modified, +4/-0)
  • docs/providers/minimax.md (modified, +29/-0)
  • docs/providers/models.md (modified, +4/-0)
  • docs/providers/openai.md (modified, +10/-2)
  • docs/providers/runway.md (added, +63/-0)
  • docs/providers/vydra.md (added, +123/-0)
  • docs/reference/memory-config.md (modified, +117/-98)
  • docs/tools/image-generation.md (modified, +21/-17)
  • docs/tools/index.md (modified, +14/-7)
  • docs/tools/lobster.md (modified, +11/-9)
  • docs/tools/music-generation.md (added, +208/-0)
  • docs/tools/plugin.md (modified, +1/-0)
  • docs/tools/slash-commands.md (modified, +1/-1)
  • docs/tools/video-generation.md (modified, +147/-84)
  • docs/web/control-ui.md (modified, +4/-1)
  • docs/web/dashboard.md (modified, +2/-0)
  • dream-diary-preview-v2.html (added, +399/-0)
  • dream-diary-preview-v3.html (added, +323/-0)
  • extensions/amazon-bedrock-mantle/api.ts (modified, +2/-0)
  • extensions/amazon-bedrock-mantle/bedrock-token-generator.d.ts (added, +6/-0)
  • extensions/amazon-bedrock-mantle/discovery.test.ts (modified, +101/-3)
  • extensions/amazon-bedrock-mantle/discovery.ts (modified, +64/-13)
  • extensions/amazon-bedrock-mantle/package.json (modified, +3/-0)
  • extensions/bluebubbles/src/accounts.ts (modified, +5/-1)
  • extensions/bluebubbles/src/monitor.ts (modified, +1/-1)
  • extensions/browser/src/browser/chrome.default-browser.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/client-fetch.loopback-auth.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/control-service.plugin-disabled.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/profiles-service.test.ts (modified, +5/-8)
  • extensions/browser/src/browser/pw-tools-core.clamps-timeoutms-scrollintoview.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/pw-tools-core.interactions.batch.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/pw-tools-core.interactions.evaluate.abort.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/pw-tools-core.interactions.set-input-files.test.ts (modified, +2/-4)
  • extensions/browser/src/browser/pw-tools-core.last-file-chooser-arm-wins.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/pw-tools-core.screenshots-element-selector.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/routes/agent.existing-session.test.ts (modified, +3/-8)
  • extensions/browser/src/browser/routes/basic.existing-session.test.ts (modified, +3/-8)
  • extensions/browser/src/browser/server-context.existing-session.test.ts (modified, +3/-8)
  • extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts (modified, +6/-12)
  • extensions/browser/src/browser/server-context.remote-profile-tab-ops.fallback.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/server-context.remote-profile-tab-ops.playwright.test.ts (modified, +2/-6)
  • extensions/browser/src/browser/server-lifecycle.test.ts (modified, +3/-8)
  • extensions/browser/src/browser/server.control-server.test-harness.ts (modified, +2/-1)
  • extensions/browser/src/browser/server.evaluate-disabled-does-not-block-storage.test.ts (modified, +3/-8)
  • extensions/browser/src/cli/browser-cli.test-support.ts (modified, +1/-1)
  • extensions/browser/src/cli/command-format.ts (modified, +1/-1)
  • extensions/browser/src/config/config.ts (modified, +1/-1)
  • extensions/browser/src/core-api.ts (modified, +25/-20)
  • extensions/browser/src/doctor-browser.ts (modified, +1/-1)
  • extensions/browser/src/gateway/auth.ts (modified, +1/-1)
  • extensions/browser/src/gateway/startup-auth.ts (modified, +1/-1)
  • extensions/browser/src/infra/errors.ts (modified, +1/-1)
  • extensions/browser/src/infra/fs-safe.ts (modified, +1/-1)
  • extensions/browser/src/infra/net/proxy-env.ts (modified, +1/-1)
  • extensions/browser/src/infra/net/ssrf.ts (modified, +1/-1)
  • extensions/browser/src/infra/path-guards.ts (modified, +1/-1)
  • extensions/browser/src/infra/ports.ts (modified, +1/-1)

PR #61527: fix(agents): OpenAI function-call replay — preserve malformed arguments instead of silent {} replacement

Description (problem / solution / changelog)

What this fixes (plain English)

When a provider sends malformed (unparseable) JSON in tool call arguments, the system was silently replacing the original content with an empty object {}. This destroyed the actual payload, making debugging impossible and causing replay failures. Now the original malformed string is preserved as-is.

Technical details

Root cause: In buildAssistantMessageFromResponse(), the catch block for JSON.parse(item.arguments) replaced the raw string with {} on failure. On replay, JSON.stringify({}) produces "{}" instead of the original content.

Fix:

  • Parse failure path: preserve the raw argument string as-is instead of replacing with {}
  • Replay path: already handles string arguments correctly (passes through without re-stringifying)

Files changed:

  • src/agents/openai-ws-message-conversion.ts — preserve raw arguments on parse failure
  • src/agents/openai-ws-stream.ts — defer ws text flush until phase is known
  • src/agents/openai-ws-stream.test.ts — regression test for malformed argument preservation; corrected phase-buffering test for requireKnownPhase behavior
  • src/agents/pi-embedded-helpers.validate-turns.test.ts — updated test fixtures
  • src/agents/subagent-orphan-recovery.ts — orphan reconciliation DI seam
  • src/agents/subagent-registry-helpers.ts — orphan grace window helper
  • src/agents/subagent-registry.ts — sweep ordering and overlap guard

Related

  • Fixes #61478
  • Found during post-merge review of #59643
  • Part of phase-aware extraction family: #61463, #61481, #61476, #61477
  • Companion PRs: #61528, #61529, #61525

Test plan

  • 120/120 tests pass (openai-ws-stream + validate-turns suites)
  • Regression test for malformed argument preservation through replay
  • Phase-buffering test corrected for requireKnownPhase behavior

Changed files

  • src/agents/openai-ws-message-conversion.ts (modified, +5/-13)
  • src/agents/openai-ws-stream.test.ts (modified, +205/-20)
  • src/agents/openai-ws-stream.ts (modified, +10/-1)
  • src/agents/pi-embedded-helpers.validate-turns.test.ts (modified, +39/-0)
  • src/agents/subagent-orphan-recovery.ts (modified, +17/-2)
  • src/agents/subagent-registry-helpers.ts (modified, +15/-0)
  • src/agents/subagent-registry.ts (modified, +7/-5)

PR #61529: fix(agents): OpenAI WS replay — inherit message-level phase for untagged blocks when siblings have explicit textSignature

Description (problem / solution / changelog)

What this fixes (plain English)

When replaying chat history that mixes phase-tagged and untagged text blocks, untagged blocks lost their phase label — causing internal commentary to leak into visible conversation output. This fix ensures untagged blocks correctly inherit their parent message's phase.

Technical details

Root cause: The hasExplicitBlockPhase scan in openai-ws-message-conversion.ts was intended to separate legacy unphased content from new phased content, but it overcorrected — it forced undefined phase on untagged blocks whenever any sibling block had an explicit textSignature.phase.

Fix: Remove the hasExplicitBlockPhase scan entirely. Untagged blocks now always inherit assistantMessagePhase (from m.phase). Blocks with an explicit textSignature.phase still use their own value.

Files changed:

  • src/agents/openai-ws-message-conversion.ts — remove overcorrective phase scan
  • src/agents/openai-ws-stream.test.ts — regression test for mixed explicit/untagged blocks; corrected phase-buffering expectation for mid-stream deltas

Related

  • Fixes #61476
  • Parent fix: #59643
  • Follow-up family: #61463, #61829, #61477, #61478
  • Companion PR: #61528

Test plan

  • 94/94 existing tests pass
  • Regression test added: mixed explicit/untagged blocks verify phase inheritance
  • Phase-buffering test corrected for mid-stream delta timing

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/agents/openai-ws-message-conversion.ts (modified, +6/-2)
  • src/agents/openai-ws-stream.test.ts (modified, +192/-4)

PR #61528: fix(agents): OpenAI WS stream — gate buffered text delta on valid phase value, not just outputItemPhaseById key existence

Description (problem / solution / changelog)

What this fixes (plain English)

The WebSocket streaming code was supposed to hold back text until it knew the phase (commentary vs. visible), but a subtle bug meant it released text as soon as the item was registered — even if the phase was still unknown. This caused internal commentary to leak into user-visible output.

Technical details

Root cause: outputItemPhaseById.has(event.item_id) returns true even when the stored value is undefined (set by an output_item.added event before phase metadata arrives). This defeats the buffering protection entirely.

Fix: Changed both emission gates (.delta and .done handlers) from .has(id) to .get(id) !== undefined so text stays buffered until a valid phase value arrives.

Files changed:

  • src/agents/openai-ws-stream.ts — 2-line gate fix (.has() -> .get() !== undefined)

Related

  • Fixes #61477
  • Parent fix: #59643
  • Companion PRs: #61529, #61463, #61478

Test plan

  • 92/92 existing tests pass
  • 2-line change, minimal blast radius
  • No scope creep — single file, single fix

Changed files

  • src/agents/openai-ws-stream.ts (modified, +2/-2)

Code Example

const blockPhase =
  parsedSignature?.phase ?? (hasExplicitBlockPhase ? undefined : assistantMessagePhase);
RAW_BUFFERClick to expand / collapse

Bug

After #59643 merged, replay conversion in src/agents/openai-ws-message-conversion.ts:356-398 can corrupt phase metadata on mixed messages.

Root cause

When replaying assistant history, if any text block has an explicit phase in its textSignature, then every other block without an explicit phase is forced to undefined instead of inheriting m.phase:

const blockPhase =
  parsedSignature?.phase ?? (hasExplicitBlockPhase ? undefined : assistantMessagePhase);

Once hasExplicitBlockPhase is true, untagged blocks stop inheriting the message phase. So a message with m.phase = "final_answer" plus one explicitly-tagged block and one untagged block gets replayed as two logical messages: one final_answer, one unphased.

On the return path, unphased text sets hasUnphasedAssistantText = true (:500-502), which prevents message.phase from being reconstructed (:575-579). This creates malformed/contradictory phase metadata.

Impact

Final-answer text can become partially unphased on replay, making downstream visibility/suppression logic inconsistent. This is especially problematic when the replayed context feeds a new turn where the model sees corrupted phase state.

Code location

  • src/agents/openai-ws-message-conversion.ts:356-398, 500-502, 575-579

Severity

P1 — affects replay fidelity and downstream phase-aware suppression.

Related

  • Parent fix: #59643

extent analysis

TL;DR

The issue can be fixed by modifying the phase inheritance logic in src/agents/openai-ws-message-conversion.ts to correctly handle mixed messages with explicit and implicit phase signatures.

Guidance

  • Review the blockPhase assignment logic to ensure that untagged blocks inherit the message phase correctly, even when hasExplicitBlockPhase is true.
  • Consider adding a conditional statement to check if hasExplicitBlockPhase is true and parsedSignature?.phase is undefined, and if so, use assistantMessagePhase as the blockPhase.
  • Verify that the fix does not introduce any unintended behavior when replaying messages with multiple text blocks and different phase signatures.
  • Test the fix with various scenarios, including messages with explicit and implicit phase signatures, to ensure that the phase metadata is correctly reconstructed.

Example

const blockPhase =
  parsedSignature?.phase ?? (hasExplicitBlockPhase && parsedSignature?.phase === undefined ? assistantMessagePhase : undefined);

However, this example may not be the final solution and requires further review and testing.

Notes

The provided code snippet is a potential solution, but it may require additional modifications to ensure correct functionality. The fix should be thoroughly tested to prevent any unintended consequences.

Recommendation

Apply a workaround by modifying the blockPhase assignment logic to correctly handle mixed messages, as the root cause is identified and a potential solution is proposed. This will help prevent corrupted phase metadata and ensure consistent downstream logic.

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