mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 22:54:46 +00:00
fix: normalize malformed assistant replay content (#82748)
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- System prompts: clarify MEMORY guidance over generic TTS hints in the embedded speech-core/system-prompt scaffolding so agents prefer memory-store usage over speech defaults. Fixes #81930. Thanks @giodl73-repo.
|
||||
- Agents/auth: include the checked credential source in missing API key errors, so users can see which env var, profile, or config path to fix. Fixes #82785. Thanks @loeclos.
|
||||
- Providers/GitHub Copilot: hash Responses replay item ids with sha256 instead of a weak 32-bit hash and build same-provider Copilot tool-call ids distinctly, so concurrent tool-call replays no longer collide and reject follow-up turns.
|
||||
- Agents/replay: normalize malformed assistant replay content before transport conversion while preserving empty-stop replay repair, so bad provider history no longer crashes with non-iterable content. Fixes #43795. (#82748) Thanks @IWhatsskill.
|
||||
- Providers/Anthropic-messages: extract `reasoning_content` from `thinking` blocks during assistant replay so proxy providers that route through the Anthropic-messages transport preserve reasoning context across tool-call follow-up turns. Thanks @Sunnyone2three.
|
||||
- Agents/GitHub Copilot: normalize replayed Responses tool-call IDs before dispatch so resumed sessions with historical overlong tool IDs continue instead of failing Copilot schema validation. (#82750) Thanks @galiniliev.
|
||||
- CLI/web: resolve provider-scoped web search/fetch SecretRefs for `infer web ... --provider ...` while leaving unrelated plugin secrets untouched. Fixes #82621. Thanks @leno23.
|
||||
|
||||
@@ -126,6 +126,15 @@ describe("normalizeAssistantReplayContent", () => {
|
||||
expect(repaired.content).toEqual([{ type: "text", text: FALLBACK_TEXT }]);
|
||||
});
|
||||
|
||||
it("converts mid-turn zero-usage null stop turns to a replay sentinel", () => {
|
||||
const falseSuccessStop = bedrockAssistant(null, "stop");
|
||||
const messages = [userMessage("hello"), falseSuccessStop, userMessage("retry")];
|
||||
const out = normalizeAssistantReplayContent(messages);
|
||||
expect(out).not.toBe(messages);
|
||||
const repaired = out[1] as AgentMessage & { content: { type: string; text: string }[] };
|
||||
expect(repaired.content).toEqual([{ type: "text", text: FALLBACK_TEXT }]);
|
||||
});
|
||||
|
||||
it("preserves empty content with non-error stopReasons (toolUse, length) untouched", () => {
|
||||
// Boundary lock: only `stopReason:"error"` should trip the sentinel
|
||||
// substitution. `toolUse` and `length` are reachable in practice when a
|
||||
|
||||
@@ -59,6 +59,7 @@ type ModelSnapshotEntry = {
|
||||
modelApi?: string | null;
|
||||
modelId?: string;
|
||||
};
|
||||
type AssistantReplayMessage = Extract<AgentMessage, { role: "assistant" }>;
|
||||
|
||||
type ProviderReplayHookParams = {
|
||||
config?: OpenClawConfig;
|
||||
@@ -355,7 +356,7 @@ export function normalizeAssistantReplayContent(messages: AgentMessage[]): Agent
|
||||
touched = true;
|
||||
continue;
|
||||
}
|
||||
let assistantMessage = message;
|
||||
let assistantMessage: AssistantReplayMessage = message;
|
||||
let replayContent = (message as { content?: unknown }).content;
|
||||
if (typeof replayContent === "string") {
|
||||
const normalized = normalizeAssistantReplayTextContent(message, replayContent);
|
||||
@@ -368,7 +369,7 @@ export function normalizeAssistantReplayContent(messages: AgentMessage[]): Agent
|
||||
if (!Array.isArray(replayContent)) {
|
||||
replayContent =
|
||||
replayContent != null && typeof replayContent === "object" ? [replayContent] : [];
|
||||
assistantMessage = { ...message, content: replayContent } as AgentMessage;
|
||||
assistantMessage = { ...message, content: replayContent } as AssistantReplayMessage;
|
||||
touched = true;
|
||||
}
|
||||
if (Array.isArray(replayContent)) {
|
||||
@@ -398,10 +399,10 @@ export function normalizeAssistantReplayContent(messages: AgentMessage[]): Agent
|
||||
// or completion and no content. Leaving other non-error empty-content
|
||||
// turns untouched preserves silent-reply semantics on every other code
|
||||
// path.
|
||||
const stopReason = (message as { stopReason?: unknown }).stopReason;
|
||||
if (stopReason === "error" || isZeroUsageEmptyStopAssistantTurn(message)) {
|
||||
const stopReason = (assistantMessage as { stopReason?: unknown }).stopReason;
|
||||
if (stopReason === "error" || isZeroUsageEmptyStopAssistantTurn(assistantMessage)) {
|
||||
out.push({
|
||||
...message,
|
||||
...assistantMessage,
|
||||
content: [{ type: "text", text: STREAM_ERROR_FALLBACK_TEXT }],
|
||||
});
|
||||
touched = true;
|
||||
|
||||
Reference in New Issue
Block a user