openclaw - ✅(Solved) Fix bug(agents): OpenAI function-call replay silently replaces malformed arguments with {} — silent data loss on tool 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#61478Fetched 2026-04-08 02:58:11
View on GitHub
Comments
1
Participants
1
Timeline
12
Reactions
0
Participants
Timeline (top)
cross-referenced ×9commented ×1referenced ×1renamed ×1

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

arguments: (() => {
  try { return JSON.parse(item.arguments) } catch { return {} }
})(),
RAW_BUFFERClick to expand / collapse

Bug

In src/agents/openai-ws-message-conversion.ts:527-533, buildAssistantMessageFromResponse() silently replaces function-call arguments with {} when JSON.parse() fails:

arguments: (() => {
  try { return JSON.parse(item.arguments) } catch { return {} }
})(),

Impact

  • The original malformed-but-real argument payload is irrecoverably lost
  • Later replay back into OpenAI converts non-string arguments via JSON.stringify(block.arguments ?? {}) (:425-428), producing "{}" where the original content was something else
  • Tool replay fidelity breaks: downstream behavior can differ from the original turn
  • Especially problematic when arguments were partial/non-JSON from provider quirks or streaming interruptions

Suggested fix

Preserve the original string as-is when JSON parsing fails, rather than replacing with {}. The replay path should handle the raw string gracefully rather than silently destroying data.

Severity

P2 — silent data loss on a non-happy path.

Code location

  • src/agents/openai-ws-message-conversion.ts:527-533, 425-428

Related

  • Found during post-merge review of #59643
  • Not phase-related, but in the same file family

extent analysis

TL;DR

Preserve the original string as-is when JSON parsing fails in buildAssistantMessageFromResponse() to prevent silent data loss.

Guidance

  • Modify the buildAssistantMessageFromResponse() function to catch the JSON parsing error and return the original string instead of an empty object.
  • Update the replay path to handle the raw string gracefully, potentially by checking the type of block.arguments before applying JSON.stringify().
  • Verify the fix by testing the function with malformed JSON input and checking that the original string is preserved.
  • Review the code at src/agents/openai-ws-message-conversion.ts:425-428 to ensure it can handle non-JSON strings correctly.

Example

arguments: (() => {
  try { return JSON.parse(item.arguments) } catch (e) { return item.arguments }
})(),

Notes

This fix assumes that the original string can be safely replayed into OpenAI without causing further errors. Additional error handling may be necessary depending on the specific requirements of the OpenAI API.

Recommendation

Apply workaround: preserve the original string when JSON parsing fails, as this will prevent silent data loss and ensure that the tool replay fidelity is maintained.

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 bug(agents): OpenAI function-call replay silently replaces malformed arguments with {} — silent data loss on tool replay [5 pull requests, 1 comments, 1 participants]