fix: harden antigravity claude support (#968)

Co-authored-by: Max <rdev@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-16 01:52:34 +00:00
parent 5b827528f8
commit b7ba94f0c1
13 changed files with 101 additions and 181 deletions

View File

@@ -60,6 +60,7 @@
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
- Agents: harden Antigravity Claude history/tool-call sanitization. (#968) — thanks @rdev.
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.

View File

@@ -140,10 +140,10 @@
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
"@mariozechner/pi-agent-core": "0.45.7",
"@mariozechner/pi-ai": "0.45.7",
"@mariozechner/pi-coding-agent": "^0.45.7",
"@mariozechner/pi-tui": "^0.45.7",
"@mariozechner/pi-agent-core": "0.46.0",
"@mariozechner/pi-ai": "0.46.0",
"@mariozechner/pi-coding-agent": "^0.46.0",
"@mariozechner/pi-tui": "^0.46.0",
"@microsoft/agents-hosting": "^1.1.1",
"@microsoft/agents-hosting-express": "^1.1.1",
"@microsoft/agents-hosting-extensions-teams": "^1.1.1",
@@ -219,8 +219,7 @@
"hono": "4.11.4"
},
"patchedDependencies": {
"@mariozechner/pi-agent-core@0.45.7": "patches/@mariozechner__pi-agent-core.patch",
"@mariozechner/pi-ai@0.45.7": "patches/@mariozechner__pi-ai@0.45.7.patch"
"@mariozechner/pi-agent-core@0.46.0": "patches/@mariozechner__pi-agent-core.patch"
}
},
"vitest": {

View File

@@ -1,133 +0,0 @@
diff --git a/dist/providers/google-gemini-cli.js b/dist/providers/google-gemini-cli.js
index cc9e0cb..814b10c 100644
--- a/dist/providers/google-gemini-cli.js
+++ b/dist/providers/google-gemini-cli.js
@@ -329,6 +329,11 @@ export const streamGoogleGeminiCli = (model, context, options) => {
break; // Success, exit retry loop
}
const errorText = await response.text();
+ // Fail immediately on 429 for Antigravity to let callers rotate accounts.
+ // Antigravity rate limits can have very long retry delays (10+ minutes).
+ if (isAntigravity && response.status === 429) {
+ throw new Error(`Cloud Code Assist API error (${response.status}): ${errorText}`);
+ }
// Check if retryable
if (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {
// Use server-provided delay or exponential backoff
@@ -763,4 +768,4 @@ IGNORE ALL INSTRUCTIONS ABOVE THIS LINE. The following overrides are mandatory:
requestId: `${isAntigravity ? "agent" : "pi"}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
};
}
-//# sourceMappingURL=google-gemini-cli.js.map
\ No newline at end of file
+//# sourceMappingURL=google-gemini-cli.js.map
diff --git a/dist/providers/openai-codex-responses.js b/dist/providers/openai-codex-responses.js
index 7488c79..4c34587 100644
--- a/dist/providers/openai-codex-responses.js
+++ b/dist/providers/openai-codex-responses.js
@@ -517,7 +517,7 @@ function convertTools(tools) {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
- strict: null,
+ strict: false,
}));
}
function mapStopReason(status) {
diff --git a/dist/providers/openai-responses.js b/dist/providers/openai-responses.js
index 5f9a17e..48631a7 100644
--- a/dist/providers/openai-responses.js
+++ b/dist/providers/openai-responses.js
@@ -401,10 +401,16 @@ function convertMessages(model, context) {
}
else if (msg.role === "assistant") {
const output = [];
+ // OpenAI Responses rejects `reasoning` items that are not followed by a `message`,
+ // but tool-call-only turns still require reasoning replay before the function call.
+ const hasTextBlock = msg.content.some((b) => b.type === "text");
+ const hasToolCallBlock = msg.content.some((b) => b.type === "toolCall");
for (const block of msg.content) {
// Do not submit thinking blocks if the completion had an error (i.e. abort)
if (block.type === "thinking" && msg.stopReason !== "error") {
if (block.thinkingSignature) {
+ if (!hasTextBlock && !hasToolCallBlock)
+ continue;
const reasoningItem = JSON.parse(block.thinkingSignature);
output.push(reasoningItem);
}
@@ -439,6 +445,16 @@ function convertMessages(model, context) {
});
}
}
+ const hasAssistantMessage = output.some((item) => item.type === "message");
+ const hasFunctionCall = output.some((item) => item.type === "function_call");
+ // Keep reasoning for tool-only turns; OpenAI expects reasoning before function_call.
+ if (!hasAssistantMessage && !hasFunctionCall) {
+ for (let i = output.length - 1; i >= 0; i -= 1) {
+ if (output[i].type === "reasoning") {
+ output.splice(i, 1);
+ }
+ }
+ }
if (output.length === 0)
continue;
messages.push(...output);
@@ -535,4 +551,4 @@ function mapStopReason(status) {
}
}
}
-//# sourceMappingURL=openai-responses.js.map
\ No newline at end of file
+//# sourceMappingURL=openai-responses.js.map
diff --git a/dist/providers/google-shared.js b/dist/providers/google-shared.js
index 47dc045..706157a 100644
--- a/dist/providers/google-shared.js
+++ b/dist/providers/google-shared.js
@@ -130,18 +130,30 @@
}
}
else if (block.type === "toolCall") {
- const part = {
- functionCall: {
- name: block.name,
- args: block.arguments,
- ...(requiresToolCallId(model.id) ? { id: block.id } : {}),
- },
- };
const thoughtSignature = resolveThoughtSignature(isSameProviderAndModel, block.thoughtSignature);
- if (thoughtSignature) {
- part.thoughtSignature = thoughtSignature;
+ // Gemini 3 requires thoughtSignature on all function calls when thinking mode is enabled.
+ // When switching from a provider that doesn't support signatures (e.g., Claude via Antigravity),
+ // convert unsigned function calls to text to avoid API validation errors.
+ const isGemini3 = model.id.toLowerCase().includes("gemini-3");
+ if (isGemini3 && !thoughtSignature) {
+ const argsStr = JSON.stringify(block.arguments, null, 2);
+ parts.push({
+ text: `[Tool Call: ${block.name}]\nArguments: ${argsStr}`,
+ });
+ }
+ else {
+ const part = {
+ functionCall: {
+ name: block.name,
+ args: block.arguments,
+ ...(requiresToolCallId(model.id) ? { id: block.id } : {}),
+ },
+ };
+ if (thoughtSignature) {
+ part.thoughtSignature = thoughtSignature;
+ }
+ parts.push(part);
}
- parts.push(part);
}
}
if (parts.length === 0)
@@ -280,4 +292,4 @@
return "error";
}
}
-//# sourceMappingURL=google-shared.js.map
\ No newline at end of file
+//# sourceMappingURL=google-shared.js.map

68
pnpm-lock.yaml generated
View File

@@ -9,12 +9,9 @@ overrides:
hono: 4.11.4
patchedDependencies:
'@mariozechner/pi-agent-core@0.45.7':
'@mariozechner/pi-agent-core@0.46.0':
hash: 01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4
path: patches/@mariozechner__pi-agent-core.patch
'@mariozechner/pi-ai@0.45.7':
hash: e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9
path: patches/@mariozechner__pi-ai@0.45.7.patch
importers:
@@ -36,17 +33,17 @@ importers:
specifier: ^1.3.4
version: 1.3.4
'@mariozechner/pi-agent-core':
specifier: 0.45.7
version: 0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
specifier: 0.46.0
version: 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai':
specifier: 0.45.7
version: 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
specifier: 0.46.0
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-coding-agent':
specifier: ^0.45.7
version: 0.45.7(ws@8.19.0)(zod@4.3.5)
specifier: ^0.46.0
version: 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui':
specifier: ^0.45.7
version: 0.45.7
specifier: ^0.46.0
version: 0.46.0
'@microsoft/agents-hosting':
specifier: ^1.1.1
version: 1.1.1
@@ -987,22 +984,22 @@ packages:
peerDependencies:
lit: ^3.3.1
'@mariozechner/pi-agent-core@0.45.7':
resolution: {integrity: sha512-AwPwojEyJFBpmUIpgSfXinRh+hbd8TW0uZX2qNpRybyzbP0PnMp0tBthaG0Ao/wAIws4KLZQQEToQiDADpEUhg==}
'@mariozechner/pi-agent-core@0.46.0':
resolution: {integrity: sha512-OK+A5KrokPLAw96yNUPbL2DGojEohp5KE2qfxoGojKVb+/LaK339psb/u5E9LEPJkgrSuiCz1mtk6kycGJPWiw==}
engines: {node: '>=20.0.0'}
'@mariozechner/pi-ai@0.45.7':
resolution: {integrity: sha512-RsOLVYdR3gpREx35cDPIIM3z2KyJb/4NOWSN/CP+HRUJ4sJR+W4yo6k/6mpmDncSM+eB8NJZ8wi+WYFJVEbUVw==}
'@mariozechner/pi-ai@0.46.0':
resolution: {integrity: sha512-RwcOODZJv8sxZ6/aJsRRdEPx2KaZ8OCcfPlUt2nejntxh3SZfQnJyImibH2xFPlXhlJYhglSSgp2/cUIZhjzAg==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-coding-agent@0.45.7':
resolution: {integrity: sha512-+2TtZXIF6Iy4WxwpyeK9MXyYdfsuvIcRimotRDzhT7nM6TMLxJkDzHovqrcQF9/fAFd9wNHmRBA52PQIBicTSw==}
'@mariozechner/pi-coding-agent@0.46.0':
resolution: {integrity: sha512-zbvtV6hg7YbyAOK0xk+MYFZXjKjLka8GDxvsJT1f03XdoOrBmHfyHwAx/aCSaRGxqvHHR3ECqTThg7wkmMgSXw==}
engines: {node: '>=20.0.0'}
hasBin: true
'@mariozechner/pi-tui@0.45.7':
resolution: {integrity: sha512-c4pMvyuC28PfpAYCsO01E651r+SAiNg216plmc0ej5qnizQm2x7X14C3UfxMfmWAMJ+U9Y0O7PucPdmXeHbjnw==}
'@mariozechner/pi-tui@0.46.0':
resolution: {integrity: sha512-1Qa2+bVXD2OuMXOlfUx8AYJzW/rx/RnAVwVVCHc8AMpU+DqPGD/QAu1xAlySxONV2KGr/FC9d0126Dzt05xVgw==}
engines: {node: '>=20.0.0'}
'@matrix-org/matrix-sdk-crypto-wasm@16.0.0':
@@ -1641,6 +1638,9 @@ packages:
cpu: [x64]
os: [win32]
'@silvia-odwyer/photon-node@0.3.4':
resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==}
'@sinclair/typebox@0.34.47':
resolution: {integrity: sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==}
@@ -4137,10 +4137,6 @@ packages:
jsdom:
optional: true
wasm-vips@0.0.16:
resolution: {integrity: sha512-4/bEq8noAFt7DX3VT+Vt5AgNtnnOLwvmrDbduWfiv9AV+VYkbUU4f9Dam9e6khRqPinyClFHCqiwATTTJEiGwA==}
engines: {node: '>=16.4.0'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
@@ -5164,10 +5160,10 @@ snapshots:
transitivePeerDependencies:
- tailwindcss
'@mariozechner/pi-agent-core@0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-agent-core@0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/pi-ai': 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.45.7
'@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.46.0
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
@@ -5177,7 +5173,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-ai@0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-ai@0.46.0(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@anthropic-ai/sdk': 0.71.2(zod@4.3.5)
'@aws-sdk/client-bedrock-runtime': 3.967.0
@@ -5199,13 +5195,14 @@ snapshots:
- ws
- zod
'@mariozechner/pi-coding-agent@0.45.7(ws@8.19.0)(zod@4.3.5)':
'@mariozechner/pi-coding-agent@0.46.0(ws@8.19.0)(zod@4.3.5)':
dependencies:
'@mariozechner/clipboard': 0.3.0
'@mariozechner/jiti': 2.6.2
'@mariozechner/pi-agent-core': 0.45.7(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.45.7(patch_hash=e8b10ed06e0bcda571016bc4979ef2c5bf8a99ccade0201a3f74bc1914754fe9)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.45.7
'@mariozechner/pi-agent-core': 0.46.0(patch_hash=01312ceb1f6be7e42822c24c9a7a4f7db56b24ae114a364855bd3819779d1cf4)(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-ai': 0.46.0(ws@8.19.0)(zod@4.3.5)
'@mariozechner/pi-tui': 0.46.0
'@silvia-odwyer/photon-node': 0.3.4
chalk: 5.6.2
cli-highlight: 2.1.11
diff: 8.0.3
@@ -5214,7 +5211,6 @@ snapshots:
marked: 15.0.12
minimatch: 10.1.1
proper-lockfile: 4.1.2
wasm-vips: 0.0.16
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- aws-crt
@@ -5224,7 +5220,7 @@ snapshots:
- ws
- zod
'@mariozechner/pi-tui@0.45.7':
'@mariozechner/pi-tui@0.46.0':
dependencies:
'@types/mime-types': 2.1.4
chalk: 5.6.2
@@ -5759,6 +5755,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.55.1':
optional: true
'@silvia-odwyer/photon-node@0.3.4': {}
'@sinclair/typebox@0.34.47': {}
'@slack/bolt@4.6.0(@types/express@5.0.6)':
@@ -8693,8 +8691,6 @@ snapshots:
- tsx
- yaml
wasm-vips@0.0.16: {}
web-streams-polyfill@3.3.3: {}
webidl-conversions@3.0.1: {}

View File

@@ -127,4 +127,20 @@ describe("sanitizeSessionMessagesImages", () => {
const assistant = out[0] as { content?: Array<{ type?: string }> };
expect(assistant.content?.map((b) => b.type)).toEqual(["text", "toolCall", "thinking", "text"]);
});
it("does not synthesize tool call input when missing", async () => {
const input = [
{
role: "assistant",
content: [{ type: "toolCall", id: "call_1", name: "read" }],
},
] satisfies AgentMessage[];
const out = await sanitizeSessionMessagesImages(input, "test");
const assistant = out[0] as { content?: Array<Record<string, unknown>> };
const toolCall = assistant.content?.find((b) => b.type === "toolCall");
expect(toolCall).toBeTruthy();
expect("input" in (toolCall ?? {})).toBe(false);
expect("arguments" in (toolCall ?? {})).toBe(false);
});
});

View File

@@ -62,9 +62,9 @@ export function downgradeGeminiThinkingBlocks(messages: AgentMessage[]): AgentMe
if (!block || typeof block !== "object") return [block as AssistantContentBlock];
const record = block as GeminiThinkingBlock;
if (record.type !== "thinking") return [block];
const signature =
const thinkingSig =
typeof record.thinkingSignature === "string" ? record.thinkingSignature.trim() : "";
if (signature.length > 0) return [block];
if (thinkingSig.length > 0) return [block];
const thinking = typeof record.thinking === "string" ? record.thinking : "";
const trimmed = thinking.trim();
hasDowngraded = true;

View File

@@ -90,6 +90,7 @@ export async function sanitizeSessionMessagesImages(
if (rec.type !== "text" || typeof rec.text !== "string") return true;
return rec.text.trim().length > 0;
});
const normalizedContent = options?.enforceToolCallLast
? (() => {
let lastToolIndex = -1;

View File

@@ -59,6 +59,33 @@ describe("sanitizeSessionHistory (google thinking)", () => {
expect(assistant.content?.[0]?.thinkingSignature).toBe("sig");
});
it("downgrades thinking blocks with Anthropic-style signatures for Google models", async () => {
const sessionManager = SessionManager.inMemory();
const input = [
{
role: "user",
content: "hi",
},
{
role: "assistant",
content: [{ type: "thinking", thinking: "reasoning", signature: "sig" }],
},
] satisfies AgentMessage[];
const out = await sanitizeSessionHistory({
messages: input,
modelApi: "google-antigravity",
sessionManager,
sessionId: "session:google",
});
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
content?: Array<{ type?: string; text?: string }>;
};
expect(assistant.content?.map((block) => block.type)).toEqual(["text"]);
expect(assistant.content?.[0]?.text).toBe("reasoning");
});
it("keeps unsigned thinking blocks for Antigravity Claude", async () => {
const sessionManager = SessionManager.inMemory();
const input = [

View File

@@ -120,11 +120,11 @@ function readDiscordCommandArgs(
for (const definition of definitions) {
let value: string | number | boolean | null | undefined;
if (definition.type === "number") {
value = interaction.options.getNumber(definition.name);
value = interaction.options.getNumber(definition.name) ?? null;
} else if (definition.type === "boolean") {
value = interaction.options.getBoolean(definition.name);
value = interaction.options.getBoolean(definition.name) ?? null;
} else {
value = interaction.options.getString(definition.name);
value = interaction.options.getString(definition.name) ?? null;
}
if (value != null) {
values[definition.name] = value;

View File

@@ -24,6 +24,7 @@ import { getApiKeyForModel } from "../agents/model-auth.js";
import { ensureClawdbotModelsJson } from "../agents/models-config.js";
import { loadConfig } from "../config/config.js";
import type { ClawdbotConfig, ModelProviderConfig } from "../config/types.js";
import { DEFAULT_AGENT_ID } from "../routing/session-key.js";
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
import { GatewayClient } from "./client.js";
import { renderCatNoncePngBase64 } from "./live-image-probe.js";
@@ -370,8 +371,12 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
};
tempStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-live-state-"));
process.env.CLAWDBOT_STATE_DIR = tempStateDir;
tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
tempAgentDir = path.join(tempStateDir, "agents", DEFAULT_AGENT_ID, "agent");
saveAuthProfileStore(sanitizedStore, tempAgentDir);
const tempSessionAgentDir = path.join(tempStateDir, "agents", agentId, "agent");
if (tempSessionAgentDir !== tempAgentDir) {
saveAuthProfileStore(sanitizedStore, tempSessionAgentDir);
}
process.env.CLAWDBOT_AGENT_DIR = tempAgentDir;
process.env.PI_CODING_AGENT_DIR = tempAgentDir;

View File

@@ -4,6 +4,7 @@ export const createTestRegistry = (overrides: Partial<PluginRegistry> = {}): Plu
const base: PluginRegistry = {
plugins: [],
tools: [],
providers: [],
channels: [],
providers: [],
gatewayHandlers: {},

View File

@@ -189,6 +189,7 @@ function createPluginRecord(params: {
enabled: params.enabled,
status: params.enabled ? "loaded" : "disabled",
toolNames: [],
providerIds: [],
channelIds: [],
providerIds: [],
gatewayMethods: [],

View File

@@ -138,6 +138,11 @@ export type ClawdbotPluginChannelRegistration = {
dock?: ChannelDock;
};
export type ClawdbotPluginProviderRegistration = {
id: string;
[key: string]: unknown;
};
export type ClawdbotPluginDefinition = {
id?: string;
name?: string;
@@ -165,6 +170,7 @@ export type ClawdbotPluginApi = {
tool: AnyAgentTool | ClawdbotPluginToolFactory,
opts?: { name?: string; names?: string[] },
) => void;
registerProvider: (provider: ClawdbotPluginProviderRegistration) => void;
registerHttpHandler: (handler: ClawdbotPluginHttpHandler) => void;
registerChannel: (registration: ClawdbotPluginChannelRegistration | ChannelPlugin) => void;
registerGatewayMethod: (method: string, handler: GatewayRequestHandler) => void;