diff --git a/CHANGELOG.md b/CHANGELOG.md index d1e542b11d8..99bdc5ad822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai - Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work. Thanks @codex. - Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex. - CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody. +- Agents/Anthropic: strip stale trailing assistant prefill turns from outbound replay so context-engine short circuits cannot send unsupported assistant-prefill payloads to provider APIs. Fixes #72556. Thanks @Veda-openclaw. - Control UI/Dreaming: require explicit confirmation before applying restart-impacting Dreaming mode changes, with restart warning copy and loading feedback. Fixes #63804. (#63807) Thanks @bbddbb1. - CLI/update: keep the automatic post-update completion refresh on the core-command tree so it no longer stages bundled plugin runtime deps before the Gateway restart path, avoiding `.24` update hangs and 1006 disconnect cascades. Fixes #72665. Thanks @sakalaboator and @He-Pin. - Agents/Bedrock: stop heartbeat runs from persisting blank user transcript turns and repair existing blank user text messages before replay, preventing AWS Bedrock `ContentBlock` blank-text validation failures. Fixes #72640 and #72622. Thanks @goldzulu. diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/pi-embedded-runner/run/attempt.test.ts index 962eecb5ad9..18760aea300 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/pi-embedded-runner/run/attempt.test.ts @@ -1633,6 +1633,44 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => { expect(seenContext.messages).toBe(messages); }); + it("strips trailing assistant prefill turns for Anthropic outbound replay", async () => { + const messages = [ + { + role: "user", + content: [{ type: "text", text: "earlier question" }], + }, + { + role: "assistant", + content: [{ type: "text", text: "stale assistant answer" }], + }, + ]; + const baseFn = vi.fn((_model, _context) => + createFakeStream({ events: [], resultMessage: { role: "assistant", content: [] } }), + ); + + const wrapped = wrapStreamFnSanitizeMalformedToolCalls(baseFn as never, new Set(["read"]), { + validateAnthropicTurns: true, + preserveSignatures: true, + dropThinkingBlocks: false, + } as never); + const stream = wrapped( + { api: "anthropic-messages" } as never, + { messages } as never, + {} as never, + ) as FakeWrappedStream | Promise; + await Promise.resolve(stream); + + expect(baseFn).toHaveBeenCalledTimes(1); + const seenContext = baseFn.mock.calls[0]?.[1] as { messages: unknown[] }; + expect(seenContext.messages).toEqual([ + { + role: "user", + content: [{ type: "text", text: "earlier question" }], + }, + ]); + expect(seenContext.messages).not.toBe(messages); + }); + it("drops signed thinking turns when sibling replay tool calls are not allowlisted", async () => { const messages = [ { diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts index 747f71be6d8..b3aec34d335 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts +++ b/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts @@ -525,6 +525,32 @@ function sanitizeAnthropicReplayToolResults( return changed ? out : messages; } +function assistantTurnHasReplayToolCall(message: AgentMessage): boolean { + if (!message || typeof message !== "object" || message.role !== "assistant") { + return false; + } + const content = (message as { content?: unknown }).content; + if (!Array.isArray(content)) { + return false; + } + return content.some((block) => isReplayToolCallBlock(block)); +} + +function stripTrailingAssistantPrefillTurns(messages: AgentMessage[]): AgentMessage[] { + let end = messages.length; + while (end > 0) { + const message = messages[end - 1]; + if (!message || typeof message !== "object" || message.role !== "assistant") { + break; + } + if (assistantTurnHasReplayToolCall(message)) { + break; + } + end -= 1; + } + return end === messages.length ? messages : messages.slice(0, end); +} + function normalizeToolCallIdsInMessage(message: unknown): void { if (!message || typeof message !== "object") { return; @@ -873,6 +899,7 @@ export function wrapStreamFnSanitizeMalformedToolCalls( nextMessages = sanitizeAnthropicReplayToolResults(nextMessages, { disallowEmbeddedUserToolResultsForSignedThinkingReplay: allowProviderOwnedThinkingReplay, }); + nextMessages = stripTrailingAssistantPrefillTurns(nextMessages); } if (nextMessages === messages) { return baseFn(model, context, options);