Skip to content

fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#88019)#88067

Merged
steipete merged 4 commits into
openclaw:mainfrom
BSG2000:fix/responses-replay-orphan-msgid
May 30, 2026
Merged

fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#88019)#88067
steipete merged 4 commits into
openclaw:mainfrom
BSG2000:fix/responses-replay-orphan-msgid

Conversation

@BSG2000

@BSG2000 BSG2000 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #88019.

When an Azure/OpenAI Responses session falls back to a non-Responses model and later resumes a Responses model, sanitizeSessionHistory drops the replayable reasoning (rs_*) item via downgradeOpenAIReasoningBlocks. However, the paired assistant text block kept its textSignature (which carries the msg_* id), so the transport replayed an assistant message item referencing msg_* without its required rs_* reasoning item. Azure Responses then rejected the next turn with:

400 Item 'msg_...' provided without its required 'reasoning' item: 'rs_...'

permanently poisoning the session.

The transport's isDifferentModel guard cannot catch this: after returning to the original Responses model, msg.model === model.id again, so only the sanitizer (which compares against the prior model snapshot) knows the reasoning was dropped.

Changes

  • downgradeOpenAIReasoningBlocks (src/agents/embedded-agent-helpers/openai.ts): when it drops a replayable reasoning item from an assistant turn, it now also strips the textSignature from that turn's text blocks, so the msg_* id and its rs_* reasoning are removed together. The conversion layer then falls back to a synthetic, unpaired id that Azure accepts. Absence of the signature is robust through the rest of the sanitize pipeline.
  • Unique synthetic fallback ids: the synthetic id is derived from the per-message msgIndex, so multiple id-less text blocks in one assistant turn (e.g. commentary + final_answer) would collide on the same id. The fallback is now made unique per text block in both Responses conversion sites:
    • src/llm/providers/openai-responses-shared.ts (shared provider path used by openai/azure/codex Responses)
    • src/agents/openai-transport-stream.ts (embedded agent transport)

Tests

  • embedded-agent-runner.sanitize-session-history.test.ts — model-switch path drops the paired msg_* id.
  • embedded-agent-helpers.sanitizeuserfacingtext.test.ts — downgrade strips paired text signature(s), including the multi-text-block case.
  • openai-responses.reasoning-replay.test.ts — multiple id-less text blocks get distinct item ids.

All acceptance tests from the review pass:

node scripts/run-vitest.mjs run \
  src/agents/embedded-agent-runner.sanitize-session-history.test.ts \
  src/agents/openai-transport-stream.test.ts \
  src/agents/openai-responses.reasoning-replay.test.ts \
  src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts \
  src/llm/providers/openai-responses-shared.test.ts
# 675 passed

oxlint and the plugin-sdk dts boundary typecheck are clean for the changed files.

Real behavior proof

Behavior or issue addressed: After an Azure/OpenAI Responses session (gpt-5.5) falls back to a non-Responses model and then resumes, openclaw replayed an assistant message item carrying the reasoning-paired msg_* id without its rs_* reasoning item, so Azure rejected every following turn with HTTP 400 Item 'msg_...' ... was provided without its required 'reasoning' item: 'rs_...', permanently poisoning the session. This patch strips the orphan msg_* id while preserving the {v:1,phase} metadata.

Real environment tested: The reporter's live Azure OpenAI Responses deployment, model gpt-5.5 (endpoint and ids redacted). A standalone Node script calls the real /openai/v1/responses API directly; the running gateway and chat channels were not touched.

Exact steps or command run after this patch: node proof.mjs against the live Azure endpoint — (1) a reasoning turn captures a real rs_*/msg_* pair; (2) replay the assistant msg_* id without its rs_* reasoning item, i.e. the pre-patch behavior; (3) replay the same turn with the orphan msg_* id stripped and {v:1,phase} preserved, i.e. the patched behavior.

Evidence after fix:

=== Real behavior proof: issue 88019 (live Azure OpenAI Responses, gpt-5.5) ===
Endpoint: https://<azure-endpoint>/openai/v1/responses

STEP 1 - reasoning turn
HTTP 200
output items: [{"type":"reasoning","id":"rs_016e6627…0f4c"},{"type":"message","id":"msg_016e662…1765","phase":"final_answer"}]
paired ids share a correlation prefix: reasoning=rs_016e6627…0f4c  message=msg_016e662…1765
message item carries phase="final_answer" (the metadata the fix preserves)

STEP 2 - BUG: replay assistant message item with its real msg_* id but WITHOUT the rs_* reasoning item
(this is what openclaw did after a model fallback dropped the reasoning but kept the paired id)
HTTP 400
Azure error: Item 'msg_016e6627…1765' of type 'message' was provided without its required 'reasoning' item: 'rs_016e6627…0f4c'.

STEP 3 - FIX: same replay with the orphan msg_* id stripped (phase metadata preserved)
HTTP 200
conversation continues; assistant replied: "DONE"

RESULT
STEP 2 (orphan id replayed): HTTP 400 → Azure 400, bug reproduced
STEP 3 (orphan id stripped): HTTP 200 → accepted, fix works

Observed result after fix: STEP 2 (orphan msg_* id replayed) returns live Azure HTTP 400 with the issue's verbatim error. STEP 3 (orphan id stripped — the patched behavior) returns live Azure HTTP 200 and the conversation continues (assistant replied: "DONE"). STEP 1 also shows the real rs_* and msg_* ids share a correlation prefix and the message carries phase: "final_answer", confirming the id is reasoning-coupled and that phase must be preserved.

What was not tested: Non-Azure Responses providers (OpenAI.com, codex) were not exercised against a live endpoint; they share the same conversion path covered by the regression suite.

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: M triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 29, 2026
@clawsweeper

clawsweeper Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed May 30, 2026, 3:56 PM ET / 19:56 UTC.

Summary
The PR strips reasoning-paired Responses assistant message ids when replayable reasoning is dropped, preserves phase-only text signatures, generates unique fallback ids, and adds regression coverage.

PR surface: Source +78, Tests +265. Total +343 across 6 files.

Reproducibility: yes. Source inspection shows current main drops replayable reasoning on model change while retaining textSignature message ids, and the PR body supplies redacted live Azure before/after output for the rejected and accepted replay shapes.

Review metrics: 1 noteworthy metric.

  • Responses conversion surfaces: 2 sibling conversion sites updated. Both the embedded transport and shared provider path replay assistant message ids, so covering both lowers one-sided session replay risk.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Risk before merge

  • [P1] The PR intentionally changes replay of persisted Responses session history: sanitized turns lose provider-issued assistant message ids when their paired reasoning was dropped, trading provider-side continuity for avoiding a hard provider 400.
  • [P1] The overlapping open PR fix(openai): avoid stale Responses message id replay #85277 also changes stale Responses message-id replay policy, so whichever lands second needs re-review for duplicate or conflicting rules.
  • [P1] Live after-fix proof is Azure-only; OpenAI.com and Codex Responses rely on the shared conversion path plus regression tests rather than separate live provider proof.

Maintainer options:

  1. Land targeted fix and reconcile sibling replay work (recommended)
    Maintainers can accept the replay-id tradeoff for this P1 session-poisoning bug, then re-review the overlapping stale-id replay PR before it lands a second policy.
  2. Fold both replay policies before merge
    If maintainers want one pre-merge policy decision, pause this PR and compare it directly with the overlapping stale-id replay PR so only one canonical rule is merged.

Next step before merge

  • No automated repair is needed; maintainer review should decide landing order and canonical policy relative to the overlapping stale Responses replay PR.

Security
Cleared: The diff changes sanitizer/conversion code and tests only; it does not add dependencies, workflows, secrets handling, permissions, downloads, or package scripts.

Review details

Best possible solution:

Land this targeted sanitizer and conversion fix after maintainer review, then rebase, narrow, or close the overlapping stale-id replay PR so OpenClaw keeps one canonical Responses replay policy.

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

Yes. Source inspection shows current main drops replayable reasoning on model change while retaining textSignature message ids, and the PR body supplies redacted live Azure before/after output for the rejected and accepted replay shapes.

Is this the best way to solve the issue?

Yes. The sanitizer is the narrow boundary that knows a prior model snapshot caused reasoning to be dropped, and the PR also updates both Responses conversion paths so phase-only/id-less replay remains valid.

AGENTS.md: found and applied where relevant.

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

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted live Azure Responses output showing the orphan-id replay fail with HTTP 400 and the patched id-stripped replay succeed with HTTP 200.

Label justifications:

  • P1: The linked bug can permanently poison an Azure/OpenAI Responses session after fallback, breaking an active agent/provider workflow.
  • merge-risk: 🚨 session-state: The diff changes how persisted assistant history is sanitized and replayed after model fallback, directly affecting session continuity.
  • merge-risk: 🚨 auth-provider: The changed replay behavior is specific to Azure/OpenAI Responses provider routing and model fallback recovery.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes redacted live Azure Responses output showing the orphan-id replay fail with HTTP 400 and the patched id-stripped replay succeed with HTTP 200.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted live Azure Responses output showing the orphan-id replay fail with HTTP 400 and the patched id-stripped replay succeed with HTTP 200.
Evidence reviewed

PR surface:

Source +78, Tests +265. Total +343 across 6 files.

View PR surface stats
Area Files Added Removed Net
Source 3 93 15 +78
Tests 3 266 1 +265
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 6 359 16 +343

What I checked:

  • Repository policy applied: Root AGENTS.md and scoped src/agents/AGENTS.md were read fully; provider routing, auth/session state, fallback behavior, and sibling Responses conversion surfaces are compatibility-sensitive review areas for this PR. (AGENTS.md:13, eb5e80f58a69)
  • Current-main source keeps the orphaned text signature: On current main, downgradeOpenAIReasoningBlocks drops replayable reasoning when requested, then pushes nextContent unchanged, so a text block can retain the paired textSignature message id after the rs_* item is removed. (src/agents/embedded-agent-helpers/openai.ts:450, eb5e80f58a69)
  • Model-switch sanitizer reaches this path: sanitizeSessionHistory passes dropReplayableReasoning: modelChanged for OpenAI Responses history, matching the fallback-away and return-to-Responses failure mode described by the linked bug. (src/agents/embedded-agent-runner/replay-history.ts:768, eb5e80f58a69)
  • PR sanitizer fix: At PR head, the sanitizer records when replayable reasoning was dropped, removes the paired textSignature id, preserves {v:1,phase} when present, and guards null/primitive blocks before reading type. (src/agents/embedded-agent-helpers/openai.ts:498, 3f982e55d6e3)
  • Sibling conversion paths updated: The PR updates both embedded transport and shared provider Responses conversion to accept phase-only signatures and generate distinct synthetic ids for multiple id-less text blocks in one assistant turn. (src/agents/openai-transport-stream.ts:1114, 3f982e55d6e3)
  • Dependency contract checked: OpenAI's Responses type source documents assistant message phase as replay metadata to preserve on follow-up requests, supporting the review feedback that phase must survive while the stale id is removed.

Likely related people:

  • steipete: GitHub path history shows recent OpenAI provider identity and the broad agent-runtime refactor carrying the current provider/session runtime shape used by this PR. (role: recent area contributor; confidence: medium; commits: bb46b79d3c14, d92b3b5cc2bd; files: src/agents/openai-transport-stream.ts, src/llm/providers/openai-responses-shared.ts, src/agents/embedded-agent-helpers/openai.ts)
  • vincentkoc: GitHub path history shows recent shared Responses stream lifecycle work on the shared provider path that this PR updates. (role: recent Responses lifecycle contributor; confidence: medium; commits: 9366d0a873e2, bb46b79d3c14; files: src/llm/providers/openai-responses-shared.ts, src/agents/openai-transport-stream.ts, src/agents/embedded-agent-helpers/openai.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

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

Copy link
Copy Markdown
Contributor

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: 4da0deaf5b

ℹ️ 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 +480 to +482
if (contentBlock.type === "text" && contentBlock.textSignature !== undefined) {
const { textSignature: _droppedTextSignature, ...rest } = contentBlock;
return rest;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Preserve phase metadata when stripping response item ids

When dropReplayableReasoning handles a model/fallback switch, this removes the whole textSignature, but that field also carries the Responses phase (commentary/final_answer) that convertResponsesMessages replays and resolveAssistantMessagePhase uses to keep commentary out of user-visible output. For any prior Responses turn with reasoning plus phased commentary/final text blocks, the sanitizer now stores those blocks with neither the orphaned msg_* id nor the phase, so the next Responses request replays commentary as an unphased assistant message instead of preserving the existing phase contract. Strip only the id or otherwise retain the phase metadata while removing the paired item id.

Useful? React with 👍 / 👎.

// turn (issue #88019). Strip the signature so the id and reasoning go together.
const finalContent = droppedReplayableReasoning
? nextContent.map((contentBlock) => {
if (contentBlock.type === "text" && contentBlock.textSignature !== undefined) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Badge Guard malformed blocks during model-switch cleanup

If a persisted assistant turn contains a malformed null/primitive content block, the loop above explicitly preserves it, but after a replayable reasoning item is dropped this new map dereferences contentBlock.type unconditionally. That means a corrupted transcript that previously sanitized through sanitizeSessionHistory can now throw during a Responses model switch instead of recovering the session. Check that contentBlock is an object before reading type.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 29, 2026
BSG2000 and others added 2 commits May 30, 2026 11:00
…opped (openclaw#88019)

When an Azure/OpenAI Responses session falls back to a non-Responses model
and later resumes a Responses model, sanitizeSessionHistory drops the
replayable reasoning (rs_*) item via downgradeOpenAIReasoningBlocks. The
paired assistant text block still carried its textSignature (the msg_* id),
so the transport replayed an assistant message item referencing msg_* with
no accompanying rs_* reasoning item. Azure Responses then rejected the next
turn with:

  400 Item 'msg_...' provided without its required 'reasoning' item: 'rs_...'

permanently poisoning the session.

Fix:
- downgradeOpenAIReasoningBlocks now strips the textSignature from a turn's
  text blocks whenever it drops a replayable reasoning item, so the msg_* id
  and its rs_* reasoning are removed together. The transport then falls back
  to a synthetic, unpaired id that Azure accepts.
- Because the synthetic fallback id is derived from the per-message msgIndex,
  multiple id-less text blocks in one assistant turn (e.g. commentary +
  final_answer) would collide on the same id. Make the fallback unique per
  text block in both Responses conversion sites
  (openai-transport-stream.ts and the shared llm provider
  openai-responses-shared.ts).

Tests:
- sanitize-session-history: model-switch path drops the paired msg_* id.
- embedded-agent-helpers: downgrade strips paired text signature(s).
- reasoning-replay: multiple id-less text blocks get distinct item ids.

Co-authored-by: Copilot <[email protected]>
…penclaw#88019)

Address PR review feedback on the orphaned msg_* replay fix:

- Preserve Responses phase metadata: dropping the paired msg_* id when its
  rs_* reasoning is removed previously stripped the entire textSignature,
  which also discarded the phase (commentary/final_answer). Phased text now
  keeps a phase-only signature ({v:1,phase}) so commentary is not replayed
  as user-visible output. Both parseTextSignature copies (shared provider and
  embedded transport) now accept id-less phase-only signatures and fall back
  to a synthetic id while preserving the phase.
- Guard malformed content blocks: the post-drop map no longer dereferences
  contentBlock.type unconditionally, so a corrupted transcript with a
  null/primitive block can still sanitize through a model switch.

Tests:
- sanitize-session-history: phase metadata is preserved while the paired id
  is dropped on a model switch.
- reasoning-replay: id-less phase-only signatures get distinct synthetic ids
  and retain their phase.

Co-authored-by: Copilot <[email protected]>
@BSG2000
BSG2000 force-pushed the fix/responses-replay-orphan-msgid branch from 4da0dea to 89c98bf Compare May 30, 2026 09:26
@BSG2000

BSG2000 commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in the latest push (rebased onto current main):

  • Preserve phase metadata (Codex/ClawSweeper P1): instead of stripping the whole textSignature when the paired rs_* reasoning is dropped, only the id is removed now and the Responses phase (commentary/final_answer) is kept as a phase-only signature ({v:1,phase}). Both parseTextSignature copies (shared provider + embedded transport) accept id-less phase-only signatures and fall back to a unique synthetic id while preserving the phase, so commentary is no longer replayed as user-visible output.
  • Guard malformed blocks (Codex P2): the post-drop map no longer dereferences contentBlock.type unconditionally, so a corrupted transcript with a null/primitive block can still sanitize through a model switch instead of throwing.

New regression tests cover both: phase metadata preserved while the paired id is dropped, and id-less phase-only signatures getting distinct synthetic ids while retaining their phase. All five acceptance test files pass; lint and the plugin-sdk dts boundary typecheck are clean. The earlier check-additional-boundaries-bcd failure was base drift and is resolved by the rebase.

Real-behavior proof from a live Azure/OpenAI Responses fallback→return→next-turn recovery is being prepared and will be added to the PR body.

@clawsweeper clawsweeper Bot added P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels May 30, 2026
@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels May 30, 2026
@BSG2000

BSG2000 commented May 30, 2026

Copy link
Copy Markdown
Contributor Author

Added a Real behavior proof section to the PR description: a live Azure OpenAI Responses run (model gpt-5.5, endpoint/ids redacted) that

  1. captures a real rs_*/msg_* reasoning↔message pair (shared correlation prefix, phase: "final_answer"),
  2. reproduces the issue's verbatim HTTP 400 by replaying the assistant msg_* id without its rs_* item, and
  3. confirms the fix: stripping the orphan msg_* id (preserving {v:1,phase}) makes the same turn return HTTP 200.

@clawsweeper re-review please.

@clawsweeper

clawsweeper Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. proof: sufficient ClawSweeper judged the real behavior proof convincing. labels May 30, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label May 30, 2026
@steipete steipete self-assigned this May 30, 2026
@steipete

Copy link
Copy Markdown
Contributor

Verification before merge:

Behavior addressed: Azure/OpenAI Responses replay no longer keeps a reasoning-paired assistant msg_* id after the paired rs_* reasoning item is dropped on a persisted model-snapshot change.

Real environment tested: contributor supplied live Azure OpenAI Responses proof for gpt-5.5 in the PR body. The Real behavior proof check is green: https://github.com/openclaw/openclaw/actions/runs/26693459846/job/78673824930

Exact steps or command run after this patch:

  • .agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main --prompt 'Focus on PR fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#88019) #88067. Verify the replay-id stripping only takes effect when a persisted model snapshot changed; Azure-only narrowing is not required, but flag any broader same-model replay behavior or Responses phase/id regression.'
  • git diff --check origin/main...HEAD
  • gh pr checks 88067 --repo openclaw/openclaw --watch=false

Evidence after fix: autoreview returned clean with no accepted/actionable findings and specifically confirmed msg_* id removal is gated by dropReplayableReasoning, which sanitizeSessionHistory only enables when the stored model snapshot differs. CI is green, including build/check/lint/type/test shards and Real behavior proof.

Observed result after fix: model-switch replay drops orphaned Responses msg_* ids while preserving commentary/final_answer phase metadata and generating distinct synthetic fallback ids for id-less text blocks.

What was not tested: I did not rerun the full local Vitest bundle; relied on the contributor's targeted tests, green CI, the live Azure proof, git diff --check, and autoreview.

@steipete steipete left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved after maintainer review.

The reviewed blast radius is Responses-family replay after a persisted provider/API/model snapshot change. The orphan msg_* id stripping is gated by dropReplayableReasoning, which sanitizeSessionHistory only enables when the stored model snapshot differs. Autoreview returned clean with no accepted/actionable findings, CI is green, and the PR includes live Azure proof for the original 400-to-200 replay behavior.

@steipete
steipete merged commit 48980a0 into openclaw:main May 30, 2026
116 of 117 checks passed
@BSG2000
BSG2000 deleted the fix/responses-replay-orphan-msgid branch May 30, 2026 21:14
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 31, 2026
…opped (openclaw#88019) (openclaw#88067)

* fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (openclaw#88019)

When an Azure/OpenAI Responses session falls back to a non-Responses model
and later resumes a Responses model, sanitizeSessionHistory drops the
replayable reasoning (rs_*) item via downgradeOpenAIReasoningBlocks. The
paired assistant text block still carried its textSignature (the msg_* id),
so the transport replayed an assistant message item referencing msg_* with
no accompanying rs_* reasoning item. Azure Responses then rejected the next
turn with:

  400 Item 'msg_...' provided without its required 'reasoning' item: 'rs_...'

permanently poisoning the session.

Fix:
- downgradeOpenAIReasoningBlocks now strips the textSignature from a turn's
  text blocks whenever it drops a replayable reasoning item, so the msg_* id
  and its rs_* reasoning are removed together. The transport then falls back
  to a synthetic, unpaired id that Azure accepts.
- Because the synthetic fallback id is derived from the per-message msgIndex,
  multiple id-less text blocks in one assistant turn (e.g. commentary +
  final_answer) would collide on the same id. Make the fallback unique per
  text block in both Responses conversion sites
  (openai-transport-stream.ts and the shared llm provider
  openai-responses-shared.ts).

Tests:
- sanitize-session-history: model-switch path drops the paired msg_* id.
- embedded-agent-helpers: downgrade strips paired text signature(s).
- reasoning-replay: multiple id-less text blocks get distinct item ids.

Co-authored-by: Copilot <[email protected]>

* fix(responses): preserve phase metadata and guard malformed blocks (openclaw#88019)

Address PR review feedback on the orphaned msg_* replay fix:

- Preserve Responses phase metadata: dropping the paired msg_* id when its
  rs_* reasoning is removed previously stripped the entire textSignature,
  which also discarded the phase (commentary/final_answer). Phased text now
  keeps a phase-only signature ({v:1,phase}) so commentary is not replayed
  as user-visible output. Both parseTextSignature copies (shared provider and
  embedded transport) now accept id-less phase-only signatures and fall back
  to a synthetic id while preserving the phase.
- Guard malformed content blocks: the post-drop map no longer dereferences
  contentBlock.type unconditionally, so a corrupted transcript with a
  null/primitive block can still sanitize through a model switch.

Tests:
- sanitize-session-history: phase metadata is preserved while the paired id
  is dropped on a model switch.
- reasoning-replay: id-less phase-only signatures get distinct synthetic ids
  and retain their phase.

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
SYU8384 pushed a commit to SYU8384/openclaw that referenced this pull request Jun 3, 2026
…opped (openclaw#88019) (openclaw#88067)

* fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (openclaw#88019)

When an Azure/OpenAI Responses session falls back to a non-Responses model
and later resumes a Responses model, sanitizeSessionHistory drops the
replayable reasoning (rs_*) item via downgradeOpenAIReasoningBlocks. The
paired assistant text block still carried its textSignature (the msg_* id),
so the transport replayed an assistant message item referencing msg_* with
no accompanying rs_* reasoning item. Azure Responses then rejected the next
turn with:

  400 Item 'msg_...' provided without its required 'reasoning' item: 'rs_...'

permanently poisoning the session.

Fix:
- downgradeOpenAIReasoningBlocks now strips the textSignature from a turn's
  text blocks whenever it drops a replayable reasoning item, so the msg_* id
  and its rs_* reasoning are removed together. The transport then falls back
  to a synthetic, unpaired id that Azure accepts.
- Because the synthetic fallback id is derived from the per-message msgIndex,
  multiple id-less text blocks in one assistant turn (e.g. commentary +
  final_answer) would collide on the same id. Make the fallback unique per
  text block in both Responses conversion sites
  (openai-transport-stream.ts and the shared llm provider
  openai-responses-shared.ts).

Tests:
- sanitize-session-history: model-switch path drops the paired msg_* id.
- embedded-agent-helpers: downgrade strips paired text signature(s).
- reasoning-replay: multiple id-less text blocks get distinct item ids.

Co-authored-by: Copilot <[email protected]>

* fix(responses): preserve phase metadata and guard malformed blocks (openclaw#88019)

Address PR review feedback on the orphaned msg_* replay fix:

- Preserve Responses phase metadata: dropping the paired msg_* id when its
  rs_* reasoning is removed previously stripped the entire textSignature,
  which also discarded the phase (commentary/final_answer). Phased text now
  keeps a phase-only signature ({v:1,phase}) so commentary is not replayed
  as user-visible output. Both parseTextSignature copies (shared provider and
  embedded transport) now accept id-less phase-only signatures and fall back
  to a synthetic id while preserving the phase.
- Guard malformed content blocks: the post-drop map no longer dereferences
  contentBlock.type unconditionally, so a corrupted transcript with a
  null/primitive block can still sanitize through a model switch.

Tests:
- sanitize-session-history: phase metadata is preserved while the paired id
  is dropped on a model switch.
- reasoning-replay: id-less phase-only signatures get distinct synthetic ids
  and retain their phase.

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…opped (openclaw#88019) (openclaw#88067)

* fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (openclaw#88019)

When an Azure/OpenAI Responses session falls back to a non-Responses model
and later resumes a Responses model, sanitizeSessionHistory drops the
replayable reasoning (rs_*) item via downgradeOpenAIReasoningBlocks. The
paired assistant text block still carried its textSignature (the msg_* id),
so the transport replayed an assistant message item referencing msg_* with
no accompanying rs_* reasoning item. Azure Responses then rejected the next
turn with:

  400 Item 'msg_...' provided without its required 'reasoning' item: 'rs_...'

permanently poisoning the session.

Fix:
- downgradeOpenAIReasoningBlocks now strips the textSignature from a turn's
  text blocks whenever it drops a replayable reasoning item, so the msg_* id
  and its rs_* reasoning are removed together. The transport then falls back
  to a synthetic, unpaired id that Azure accepts.
- Because the synthetic fallback id is derived from the per-message msgIndex,
  multiple id-less text blocks in one assistant turn (e.g. commentary +
  final_answer) would collide on the same id. Make the fallback unique per
  text block in both Responses conversion sites
  (openai-transport-stream.ts and the shared llm provider
  openai-responses-shared.ts).

Tests:
- sanitize-session-history: model-switch path drops the paired msg_* id.
- embedded-agent-helpers: downgrade strips paired text signature(s).
- reasoning-replay: multiple id-less text blocks get distinct item ids.

Co-authored-by: Copilot <[email protected]>

* fix(responses): preserve phase metadata and guard malformed blocks (openclaw#88019)

Address PR review feedback on the orphaned msg_* replay fix:

- Preserve Responses phase metadata: dropping the paired msg_* id when its
  rs_* reasoning is removed previously stripped the entire textSignature,
  which also discarded the phase (commentary/final_answer). Phased text now
  keeps a phase-only signature ({v:1,phase}) so commentary is not replayed
  as user-visible output. Both parseTextSignature copies (shared provider and
  embedded transport) now accept id-less phase-only signatures and fall back
  to a synthetic id while preserving the phase.
- Guard malformed content blocks: the post-drop map no longer dereferences
  contentBlock.type unconditionally, so a corrupted transcript with a
  null/primitive block can still sanitize through a model switch.

Tests:
- sanitize-session-history: phase metadata is preserved while the paired id
  is dropped on a model switch.
- reasoning-replay: id-less phase-only signatures get distinct synthetic ids
  and retain their phase.

Co-authored-by: Copilot <[email protected]>

---------

Co-authored-by: Copilot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P1 High-priority user-facing bug, regression, or broken workflow. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Azure Responses session replay keeps msg id without required reasoning after fallback

2 participants