openclaw - ✅(Solved) Fix bug(agents): OpenAI WS late-map buffering gates on key existence not phase validity — unphased commentary can leak to UI [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#61477Fetched 2026-04-08 02:58:13
View on GitHub
Comments
1
Participants
1
Timeline
19
Reactions
0
Participants
Timeline (top)
cross-referenced ×9referenced ×7closed ×1commented ×1

Root Cause

Text deltas are buffered until outputItemPhaseById has the item id. But the map is populated on the first response.output_item.added/done seen for that item, and stores undefined if event.item.phase is missing/invalid at that moment:

outputItemPhaseById.set(event.item.id, itemPhase); // may be undefined
...
if (outputItemPhaseById.has(event.item_id)) emitBufferedTextDelta(...)

The gate is presence of the map key, not validity of the phase. Once an item event arrives with undefined phase, all subsequent text for that item emits immediately as unphased — even if a later output_item.done event would carry the correct phase.

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 #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)

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)

Code Example

outputItemPhaseById.set(event.item.id, itemPhase); // may be undefined
...
if (outputItemPhaseById.has(event.item_id)) emitBufferedTextDelta(...)
RAW_BUFFERClick to expand / collapse

Bug

After #59643 merged, the WS late-map buffering in src/agents/openai-ws-stream.ts can permanently misattribute early text deltas as "no phase."

Root cause

Text deltas are buffered until outputItemPhaseById has the item id. But the map is populated on the first response.output_item.added/done seen for that item, and stores undefined if event.item.phase is missing/invalid at that moment:

outputItemPhaseById.set(event.item.id, itemPhase); // may be undefined
...
if (outputItemPhaseById.has(event.item_id)) emitBufferedTextDelta(...)

The gate is presence of the map key, not validity of the phase. Once an item event arrives with undefined phase, all subsequent text for that item emits immediately as unphased — even if a later output_item.done event would carry the correct phase.

Impact

If a provider emits output_item.added before phase is populated (or emits contradictory metadata between added/done), commentary text can leak as unphased visible content. This is the "malformed/missing phase metadata" edge case that the buffering was meant to protect against.

Code location

  • src/agents/openai-ws-stream.ts:922-945, 953-969, 1029-1049, 1052-1075

Suggested fix

Gate on outputItemPhaseById.get(event.item_id) !== undefined (phase is known and valid), not just outputItemPhaseById.has(event.item_id) (any item event arrived).

Severity

P2 — requires a specific provider timing quirk to trigger, but when it does, it defeats the buffering protection entirely.

Related

  • Parent fix: #59643

extent analysis

TL;DR

Update the condition for emitting buffered text deltas to check for a valid phase in outputItemPhaseById instead of just the presence of the item ID.

Guidance

  • Identify the lines in src/agents/openai-ws-stream.ts where outputItemPhaseById.has(event.item_id) is used to decide whether to emit buffered text deltas.
  • Replace this condition with outputItemPhaseById.get(event.item_id) !== undefined to ensure that only items with a known and valid phase are emitted.
  • Verify that this change fixes the issue by testing with providers that previously triggered the "malformed/missing phase metadata" edge case.
  • Review the code changes in the suggested fix to understand how the buffering protection was meant to work and how the update addresses the problem.

Example

// Before
if (outputItemPhaseById.has(event.item_id)) emitBufferedTextDelta(...)

// After
if (outputItemPhaseById.get(event.item_id) !== undefined) emitBufferedTextDelta(...)

Notes

This fix assumes that the issue is solely due to the incorrect condition in the buffering logic. If other factors contribute to the problem, additional changes may be necessary.

Recommendation

Apply the suggested workaround by updating the condition for emitting buffered text deltas to check for a valid phase, as this directly addresses the identified root cause of the issue.

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