diff --git a/CHANGELOG.md b/CHANGELOG.md index 93bbf498fdc..b7d017d6cda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Agents: retry empty final turns for generic `anthropic-messages` providers instead of limiting non-visible recovery to Kimi, so custom/proxied Anthropic-compatible routes can recover with a visible answer. Addresses #46080. Thanks @wmgx, @w1tv, and @iFwu. - Control UI: rotate browser service-worker caches per build so updated Gateways are less likely to keep serving stale dashboard bundles that trigger protocol mismatch errors. - Discord: report unresolved configured bot-token SecretRefs during startup instead of treating the account as unconfigured. (#82009) Thanks @giodl73-repo. - CLI/config: preserve numeric-looking object keys such as Discord guild IDs during `config patch` recursive merges. (#81999) Thanks @giodl73-repo. diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index 795a63bd8e4..74f9ed45e2f 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -817,6 +817,75 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expectWarnMessageWith("empty response detected"); }); + it("retries empty Anthropic-compatible stop turns even when the provider is not Kimi", async () => { + mockedClassifyFailoverReason.mockReturnValue(null); + mockedResolveModelAsync.mockResolvedValue({ + model: { + id: "claude-opus-4-7", + provider: "sub2api", + contextWindow: 200000, + api: "anthropic-messages", + }, + error: null, + authStorage: { + setRuntimeApiKey: vi.fn(), + }, + modelRegistry: {}, + }); + mockedRunEmbeddedAttempt.mockResolvedValueOnce( + makeAttemptResult({ + assistantTexts: [], + lastAssistant: { + role: "assistant", + api: "anthropic-messages", + stopReason: "stop", + provider: "sub2api", + model: "claude-opus-4-7", + content: [], + usage: { + input: 2048, + output: 3100, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 5148, + }, + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + ); + mockedRunEmbeddedAttempt.mockResolvedValueOnce( + makeAttemptResult({ + assistantTexts: ["Visible Anthropic-compatible answer."], + lastAssistant: { + role: "assistant", + api: "anthropic-messages", + stopReason: "stop", + provider: "sub2api", + model: "claude-opus-4-7", + content: [{ type: "text", text: "Visible Anthropic-compatible answer." }], + usage: { + input: 2300, + output: 8, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 2308, + }, + } as unknown as EmbeddedRunAttemptResult["lastAssistant"], + }), + ); + + await runEmbeddedPiAgent({ + ...overflowBaseRunParams, + provider: "sub2api", + model: "claude-opus-4-7", + runId: "run-empty-anthropic-compatible-stop-continuation", + }); + + expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); + const secondCall = runAttemptCall(1); + expect(secondCall.prompt).toContain(EMPTY_RESPONSE_RETRY_INSTRUCTION); + expectWarnMessageWith("empty response detected"); + }); + it("surfaces an error after exhausting empty-response retries", async () => { mockedClassifyFailoverReason.mockReturnValue(null); mockedRunEmbeddedAttempt.mockResolvedValue( diff --git a/src/agents/pi-embedded-runner/run/incomplete-turn.ts b/src/agents/pi-embedded-runner/run/incomplete-turn.ts index f7c4fa3b712..6d74f4d2ff5 100644 --- a/src/agents/pi-embedded-runner/run/incomplete-turn.ts +++ b/src/agents/pi-embedded-runner/run/incomplete-turn.ts @@ -131,7 +131,6 @@ const GEMINI_INCOMPLETE_TURN_MODEL_ID_PATTERN = /^gemini(?:[.-]|$)/; // Ollama native `/api/chat` can finish with only thinking/internal blocks when // constrained, but it should not inherit the stricter planning-only/ack prompts. const OLLAMA_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^ollama(?:-|$)/; -const KIMI_INCOMPLETE_TURN_PROVIDER_ID_PATTERN = /^kimi(?:-|$)/; const DEFAULT_PLANNING_ONLY_RETRY_LIMIT = 1; const STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT = 2; // Allow one immediate continuation plus one follow-up continuation before @@ -620,14 +619,9 @@ function shouldApplyNonVisibleTurnRetryGuard(params: { if (shouldApplyPlanningOnlyRetryGuard(params)) { return true; } - if (normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "openai-completions") { - return true; - } if ( - normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "anthropic-messages" && - KIMI_INCOMPLETE_TURN_PROVIDER_ID_PATTERN.test( - normalizeLowercaseStringOrEmpty(params.provider ?? ""), - ) + normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "openai-completions" || + normalizeLowercaseStringOrEmpty(params.modelApi ?? "") === "anthropic-messages" ) { return true; }