diff --git a/CHANGELOG.md b/CHANGELOG.md index e013f902e..b84c324b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.clawd.bot ### Fixes - Media: accept MEDIA paths with spaces/tilde and prefer the message tool hint for image replies. +- Google Antigravity: drop unsigned thinking blocks for Claude models to avoid signature errors. - Config: avoid stack traces for invalid configs and log the config path. - CLI: read Codex CLI account_id for workspace billing. (#1422) Thanks @aj47. - Doctor: avoid recreating WhatsApp config when only legacy routing keys remain. (#900) diff --git a/docs/start/faq.md b/docs/start/faq.md index c292303ab..915890c99 100644 --- a/docs/start/faq.md +++ b/docs/start/faq.md @@ -1276,7 +1276,7 @@ Fix: either provide Google auth, or remove/avoid Google models in `agents.defaul Cause: the session history contains **thinking blocks without signatures** (often from an aborted/partial stream). Google Antigravity requires signatures for thinking blocks. -Fix: start a **new session** or set `/thinking off` for that agent. +Fix: Clawdbot now strips unsigned thinking blocks for Google Antigravity Claude. If it still appears, start a **new session** or set `/thinking off` for that agent. ## Auth profiles: what they are and how to manage them diff --git a/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts b/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts index b79a88f5c..5e58e6d6c 100644 --- a/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts +++ b/src/agents/pi-embedded-runner.google-sanitize-thinking.test.ts @@ -86,7 +86,7 @@ describe("sanitizeSessionHistory (google thinking)", () => { expect(assistant.content?.[0]?.thinking).toBe("reasoning"); }); - it("keeps unsigned thinking blocks for Antigravity Claude", async () => { + it("drops unsigned thinking blocks for Antigravity Claude", async () => { const sessionManager = SessionManager.inMemory(); const input = [ { @@ -107,11 +107,37 @@ describe("sanitizeSessionHistory (google thinking)", () => { sessionId: "session:antigravity-claude", }); + const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant"); + expect(assistant).toBeUndefined(); + }); + + it("maps base64 signatures to thinkingSignature for Antigravity Claude", async () => { + const sessionManager = SessionManager.inMemory(); + const input = [ + { + role: "user", + content: "hi", + }, + { + role: "assistant", + content: [{ type: "thinking", thinking: "reasoning", signature: "c2ln" }], + }, + ] satisfies AgentMessage[]; + + const out = await sanitizeSessionHistory({ + messages: input, + modelApi: "google-antigravity", + modelId: "anthropic/claude-3.5-sonnet", + sessionManager, + sessionId: "session:antigravity-claude", + }); + const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as { - content?: Array<{ type?: string; thinking?: string }>; + content?: Array<{ type?: string; thinking?: string; thinkingSignature?: string }>; }; expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]); expect(assistant.content?.[0]?.thinking).toBe("reasoning"); + expect(assistant.content?.[0]?.thinkingSignature).toBe("c2ln"); }); it("preserves order for mixed assistant content", async () => { diff --git a/src/agents/pi-embedded-runner/google.ts b/src/agents/pi-embedded-runner/google.ts index 6050090a4..5f8fa1b18 100644 --- a/src/agents/pi-embedded-runner/google.ts +++ b/src/agents/pi-embedded-runner/google.ts @@ -55,6 +55,15 @@ const MISTRAL_MODEL_HINTS = [ "ministral", "mistralai", ]; +const ANTIGRAVITY_SIGNATURE_RE = /^[A-Za-z0-9+/]+={0,2}$/; + +function isValidAntigravitySignature(value: unknown): value is string { + if (typeof value !== "string") return false; + const trimmed = value.trim(); + if (!trimmed) return false; + if (trimmed.length % 4 !== 0) return false; + return ANTIGRAVITY_SIGNATURE_RE.test(trimmed); +} function shouldSanitizeToolCallIds(modelApi?: string | null): boolean { if (!modelApi) return false; @@ -69,6 +78,61 @@ function isMistralModel(params: { provider?: string | null; modelId?: string | n return MISTRAL_MODEL_HINTS.some((hint) => modelId.includes(hint)); } +function sanitizeAntigravityThinkingBlocks(messages: AgentMessage[]): AgentMessage[] { + let touched = false; + const out: AgentMessage[] = []; + for (const msg of messages) { + if (!msg || typeof msg !== "object" || msg.role !== "assistant") { + out.push(msg); + continue; + } + const assistant = msg as Extract; + if (!Array.isArray(assistant.content)) { + out.push(msg); + continue; + } + const nextContent = []; + let contentChanged = false; + for (const block of assistant.content) { + if ( + !block || + typeof block !== "object" || + (block as { type?: unknown }).type !== "thinking" + ) { + nextContent.push(block); + continue; + } + const rec = block as { + thinkingSignature?: unknown; + signature?: unknown; + thought_signature?: unknown; + thoughtSignature?: unknown; + }; + const candidate = + rec.thinkingSignature ?? rec.signature ?? rec.thought_signature ?? rec.thoughtSignature; + if (!isValidAntigravitySignature(candidate)) { + contentChanged = true; + continue; + } + if (rec.thinkingSignature !== candidate) { + nextContent.push({ ...rec, thinkingSignature: candidate }); + contentChanged = true; + } else { + nextContent.push(block); + } + } + if (contentChanged) { + touched = true; + } + if (nextContent.length === 0) { + touched = true; + continue; + } + out.push(contentChanged ? { ...assistant, content: nextContent } : msg); + } + return touched ? out : messages; +} + function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[] { if (!schema || typeof schema !== "object") return []; if (Array.isArray(schema)) { @@ -226,7 +290,11 @@ export async function sanitizeSessionHistory(params: { ? { allowBase64Only: true, includeCamelCase: true } : undefined, }); - const repairedTools = sanitizeToolUseResultPairing(sanitizedImages); + const sanitizedThinking = + params.modelApi === "google-antigravity" && isAntigravityClaudeModel + ? sanitizeAntigravityThinkingBlocks(sanitizedImages) + : sanitizedImages; + const repairedTools = sanitizeToolUseResultPairing(sanitizedThinking); return applyGoogleTurnOrderingFix({ messages: repairedTools,