From 74485f171bd495eac3d59086907305392c182dba Mon Sep 17 00:00:00 2001 From: Jefferson Warrior Date: Thu, 8 Jan 2026 18:12:23 -0600 Subject: [PATCH 1/3] CLI: fix Cloud Code Assist 400 API errors and missing session exports - Add sanitizeToolCallId() to fix Cloud Code Assist tool call ID validation - Apply sanitization in sanitizeSessionMessagesImages() for toolResult and assistant messages - Add legacy CONFIG_PATH_CLAWDIS and STATE_DIR_CLAWDIS exports for backward compatibility - Resolves Cloud Code Assist rejection of invalid tool call IDs with pipe characters - Fixes missing session export functions that were blocking system startup Addresses Cloud Code Assist API 400 errors from invalid tool call IDs like 'call_abc123|item_456' --- src/agents/pi-embedded-helpers.ts | 46 +++++++++++++++++++++++++++---- src/config/paths.ts | 6 ++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index ac945f2ce..d93a4dbeb 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -103,7 +103,15 @@ export async function sanitizeSessionMessagesImages( content as ContentBlock[], label, )) as unknown as typeof toolMsg.content; - out.push({ ...toolMsg, content: nextContent }); + // Sanitize tool call IDs for Google Cloud Code Assist compatibility + const sanitizedMsg = { + ...toolMsg, + content: nextContent, + ...(toolMsg.toolCallId && { + toolCallId: sanitizeToolCallId(toolMsg.toolCallId), + }), + }; + out.push(sanitizedMsg); continue; } @@ -133,14 +141,32 @@ export async function sanitizeSessionMessagesImages( if (rec.type !== "text" || typeof rec.text !== "string") return true; return rec.text.trim().length > 0; }); - const sanitizedContent = (await sanitizeContentBlocksImages( - filteredContent as unknown as ContentBlock[], + // Also sanitize tool call IDs in assistant messages (function call blocks) + const sanitizedContent = await Promise.all( + filteredContent.map(async (block) => { + if ( + block && + typeof block === "object" && + (block as { type?: unknown }).type === "functionCall" && + (block as { id?: unknown }).id + ) { + const functionBlock = block as { type: string; id: string }; + return { + ...functionBlock, + id: sanitizeToolCallId(functionBlock.id), + }; + } + return block; + }), + ); + const finalContent = (await sanitizeContentBlocksImages( + sanitizedContent as unknown as ContentBlock[], label, )) as unknown as typeof assistantMsg.content; - if (sanitizedContent.length === 0) { + if (finalContent.length === 0) { continue; } - out.push({ ...assistantMsg, content: sanitizedContent }); + out.push({ ...assistantMsg, content: finalContent }); continue; } } @@ -482,6 +508,16 @@ export function normalizeTextForComparison(text: string): string { * Uses substring matching to handle LLM elaboration (e.g., wrapping in quotes, * adding context, or slight rephrasing that includes the original). */ +// ── Tool Call ID Sanitization (Google Cloud Code Assist) ─────────────────────── +// Google Cloud Code Assist rejects tool call IDs that contain invalid characters. +// OpenAI Codex generates IDs like "call_abc123|item_456" with pipe characters, +// but Google requires IDs matching ^[a-zA-Z0-9_-]+$ pattern. +// This function sanitizes tool call IDs by replacing invalid characters with underscores. + +export function sanitizeToolCallId(id: string): string { + return id.replace(/[^a-zA-Z0-9_-]/g, "_"); +} + export function isMessagingToolDuplicate( text: string, sentTexts: string[], diff --git a/src/config/paths.ts b/src/config/paths.ts index 7c095e977..054fabc3f 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -44,6 +44,9 @@ function resolveUserPath(input: string): string { export const STATE_DIR_CLAWDBOT = resolveStateDir(); +// Legacy exports for backward compatibility during Clawdis → Clawdbot rebrand +export const STATE_DIR_CLAWDIS = STATE_DIR_CLAWDBOT; + /** * Config file path (JSON5). * Can be overridden via CLAWDBOT_CONFIG_PATH environment variable. @@ -60,6 +63,9 @@ export function resolveConfigPath( export const CONFIG_PATH_CLAWDBOT = resolveConfigPath(); +// Legacy exports for backward compatibility during Clawdis → Clawdbot rebrand +export const CONFIG_PATH_CLAWDIS = CONFIG_PATH_CLAWDBOT; + export const DEFAULT_GATEWAY_PORT = 18789; const OAUTH_FILENAME = "oauth.json"; From 58437339788e5721221e93f238049ba6b89c13eb Mon Sep 17 00:00:00 2001 From: Jefferson Warrior Date: Thu, 8 Jan 2026 18:34:08 -0600 Subject: [PATCH 2/3] Fix Cloud Code Assist API errors (429/400) - Enhanced rate limit detection for Cloud Code Assist quota exhaustion - Added tool call ID sanitization to fix invalid request format errors - Integrated Cloud Code Assist format error detection into failover system - Added comprehensive error pattern matching for Cloud Code Assist APIs Fixes #cloud-code-assist-api-errors --- src/agents/pi-embedded-helpers.ts | 45 +++++++++++++++++++++++++++---- src/agents/pi-embedded-runner.ts | 14 +++++++++- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index d93a4dbeb..2e10029bf 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -103,12 +103,14 @@ export async function sanitizeSessionMessagesImages( content as ContentBlock[], label, )) as unknown as typeof toolMsg.content; - // Sanitize tool call IDs for Google Cloud Code Assist compatibility + const sanitizedToolCallId = toolMsg.toolCallId + ? sanitizeToolCallId(toolMsg.toolCallId) + : undefined; const sanitizedMsg = { ...toolMsg, content: nextContent, - ...(toolMsg.toolCallId && { - toolCallId: sanitizeToolCallId(toolMsg.toolCallId), + ...(sanitizedToolCallId && { + toolCallId: sanitizedToolCallId, }), }; out.push(sanitizedMsg); @@ -283,7 +285,10 @@ export function isRateLimitErrorMessage(raw: string): boolean { const value = raw.toLowerCase(); return ( /rate[_ ]limit|too many requests|429/.test(value) || - value.includes("exceeded your current quota") + value.includes("exceeded your current quota") || + value.includes("resource has been exhausted") || + value.includes("quota exceeded") || + value.includes("resource_exhausted") ); } @@ -333,11 +338,26 @@ export function isAuthErrorMessage(raw: string): boolean { value.includes("unauthorized") || value.includes("forbidden") || value.includes("access denied") || + value.includes("expired") || + value.includes("token has expired") || /\b401\b/.test(value) || /\b403\b/.test(value) ); } +export function isCloudCodeAssistFormatError(raw: string): boolean { + const value = raw.toLowerCase(); + if (!value) return false; + return ( + value.includes("invalid_request_error") || + value.includes("string should match pattern") || + value.includes("tool_use.id") || + value.includes("tool_use_id") || + value.includes("messages.1.content.1.tool_use.id") || + value.includes("invalid request format") + ); +} + export function isAuthAssistantError( msg: AssistantMessage | undefined, ): boolean { @@ -515,7 +535,22 @@ export function normalizeTextForComparison(text: string): string { // This function sanitizes tool call IDs by replacing invalid characters with underscores. export function sanitizeToolCallId(id: string): string { - return id.replace(/[^a-zA-Z0-9_-]/g, "_"); + if (!id || typeof id !== "string") return "default_tool_id"; + + const cloudCodeAssistPatternReplacement = id.replace(/[^a-zA-Z0-9_-]/g, "_"); + const trimmedInvalidStartChars = cloudCodeAssistPatternReplacement.replace( + /^[^a-zA-Z0-9_-]+/, + "", + ); + + return trimmedInvalidStartChars.length > 0 + ? trimmedInvalidStartChars + : "sanitized_tool_id"; +} + +export function isValidCloudCodeAssistToolId(id: string): boolean { + if (!id || typeof id !== "string") return false; + return /^[a-zA-Z0-9_-]+$/.test(id); } export function isMessagingToolDuplicate( diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 230c75b25..6f2835d9b 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -61,6 +61,7 @@ import { ensureSessionHeader, formatAssistantErrorText, isAuthAssistantError, + isCloudCodeAssistFormatError, isContextOverflowError, isFailoverAssistantError, isFailoverErrorMessage, @@ -1523,9 +1524,15 @@ export async function runEmbeddedPiAgent(params: { const assistantFailoverReason = classifyFailoverReason( lastAssistant?.errorMessage ?? "", ); + const cloudCodeAssistFormatError = lastAssistant?.errorMessage + ? isCloudCodeAssistFormatError(lastAssistant.errorMessage) + : false; // Treat timeout as potential rate limit (Antigravity hangs on rate limit) - const shouldRotate = (!aborted && failoverFailure) || timedOut; + const shouldRotate = + (!aborted && + (failoverFailure || cloudCodeAssistFormatError)) || + timedOut; if (shouldRotate) { // Mark current profile for cooldown before rotating @@ -1546,6 +1553,11 @@ export async function runEmbeddedPiAgent(params: { `Profile ${lastProfileId} timed out (possible rate limit). Trying next account...`, ); } + if (cloudCodeAssistFormatError) { + log.warn( + `Profile ${lastProfileId} hit Cloud Code Assist format error. Tool calls will be sanitized on retry.`, + ); + } } const rotated = await advanceAuthProfile(); if (rotated) { From 251ed836806d9cb1dd1e32a00f03bbedbf1da7de Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 01:12:46 +0100 Subject: [PATCH 3/3] fix: harden Cloud Code Assist failover (#544) (thanks @jeffersonwarrior) --- CHANGELOG.md | 1 + src/agents/pi-embedded-helpers.test.ts | 38 ++++++++++++++++++++++++++ src/agents/pi-embedded-runner.ts | 4 +-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d1d0779..c0e7649f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ - Discord: stop provider when gateway reconnects are exhausted and surface errors. (#514) — thanks @joshp123 - Agents: strip empty assistant text blocks from session history to avoid Claude API 400s. (#210) - Agents: scrub unsupported JSON Schema keywords from tool schemas for Cloud Code Assist API compatibility. (#567) — thanks @erikpr1994 +- Agents: sanitize Cloud Code Assist tool call IDs and detect format/quota errors for failover. (#544) — thanks @jeffersonwarrior - Agents: simplify session tool schemas for Gemini compatibility. (#599) — thanks @mcinteerj - Agents: add `session_status` agent tool for `/status`-equivalent status (incl. usage/cost) + per-session model overrides. — thanks @steipete - Auto-reply: preserve block reply ordering with timeout fallback for streaming. (#503) — thanks @joshp123 diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts index 5b5802255..c280c1b86 100644 --- a/src/agents/pi-embedded-helpers.test.ts +++ b/src/agents/pi-embedded-helpers.test.ts @@ -6,12 +6,14 @@ import { classifyFailoverReason, formatAssistantErrorText, isBillingErrorMessage, + isCloudCodeAssistFormatError, isContextOverflowError, isFailoverErrorMessage, isMessagingToolDuplicate, normalizeTextForComparison, sanitizeGoogleTurnOrdering, sanitizeSessionMessagesImages, + sanitizeToolCallId, validateGeminiTurns, } from "./pi-embedded-helpers.js"; import { @@ -258,12 +260,34 @@ describe("classifyFailoverReason", () => { it("returns a stable reason", () => { expect(classifyFailoverReason("invalid api key")).toBe("auth"); expect(classifyFailoverReason("429 too many requests")).toBe("rate_limit"); + expect(classifyFailoverReason("resource has been exhausted")).toBe( + "rate_limit", + ); expect(classifyFailoverReason("credit balance too low")).toBe("billing"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); + expect(classifyFailoverReason("string should match pattern")).toBeNull(); expect(classifyFailoverReason("bad request")).toBeNull(); }); }); +describe("isCloudCodeAssistFormatError", () => { + it("matches format errors", () => { + const samples = [ + "INVALID_REQUEST_ERROR: string should match pattern", + "messages.1.content.1.tool_use.id", + "tool_use.id should match pattern", + "invalid request format", + ]; + for (const sample of samples) { + expect(isCloudCodeAssistFormatError(sample)).toBe(true); + } + }); + + it("ignores unrelated errors", () => { + expect(isCloudCodeAssistFormatError("rate limit exceeded")).toBe(false); + }); +}); + describe("formatAssistantErrorText", () => { const makeAssistantError = (errorMessage: string): AssistantMessage => ({ @@ -277,6 +301,20 @@ describe("formatAssistantErrorText", () => { }); }); +describe("sanitizeToolCallId", () => { + it("keeps valid tool call IDs", () => { + expect(sanitizeToolCallId("call_abc-123")).toBe("call_abc-123"); + }); + + it("replaces invalid characters with underscores", () => { + expect(sanitizeToolCallId("call_abc|item:456")).toBe("call_abc_item_456"); + }); + + it("returns default for empty IDs", () => { + expect(sanitizeToolCallId("")).toBe("default_tool_id"); + }); +}); + describe("sanitizeGoogleTurnOrdering", () => { it("prepends a synthetic user turn when history starts with assistant", () => { const input = [ diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts index 6f2835d9b..5b21371a9 100644 --- a/src/agents/pi-embedded-runner.ts +++ b/src/agents/pi-embedded-runner.ts @@ -104,7 +104,6 @@ import { // Optional features can be implemented as Pi extensions that run in the same Node process. - /** * Resolve provider-specific extraParams from model config. * Auto-enables thinking mode for GLM-4.x models unless explicitly disabled. @@ -1530,8 +1529,7 @@ export async function runEmbeddedPiAgent(params: { // Treat timeout as potential rate limit (Antigravity hangs on rate limit) const shouldRotate = - (!aborted && - (failoverFailure || cloudCodeAssistFormatError)) || + (!aborted && (failoverFailure || cloudCodeAssistFormatError)) || timedOut; if (shouldRotate) {