Files
openclaw/src/llm
Thomas Krohnfuß 48980a0f41 fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#88019) (#88067)
* fix(responses): drop orphaned assistant msg_* id when reasoning is dropped (#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 <223556219+Copilot@users.noreply.github.com>

* fix(responses): preserve phase metadata and guard malformed blocks (#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 <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-30 21:45:08 +01:00
..