diff --git a/src/agents/embedded-agent-runner/google-prompt-cache.test.ts b/src/agents/embedded-agent-runner/google-prompt-cache.test.ts index 5e99f2a9057..f9b87ac8b61 100644 --- a/src/agents/embedded-agent-runner/google-prompt-cache.test.ts +++ b/src/agents/embedded-agent-runner/google-prompt-cache.test.ts @@ -385,6 +385,53 @@ describe("google prompt cache", () => { expect(getCapturedPayload()?.cachedContent).toBe("cachedContents/system-cache-3"); }); + it("does not bypass failed-cache backoff when the process clock is invalid", async () => { + const systemPromptDigest = crypto.createHash("sha256").update("Follow policy.").digest("hex"); + const sessionManager = makeSessionManager([ + { + id: "entry-1", + parentId: null, + timestamp: new Date(1_000).toISOString(), + type: "custom", + customType: "openclaw.google-prompt-cache", + data: { + status: "failed", + timestamp: 1_000, + provider: "google", + modelId: "gemini-3.1-pro-preview", + modelApi: "google-generative-ai", + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + systemPromptDigest, + cacheRetention: "long", + retryAfter: Date.parse("2030-01-01T00:00:00.000Z"), + }, + }, + ]); + const fetchMock = createCacheFetchMock({ + name: "cachedContents/system-cache-invalid-clock", + expireTime: "2030-01-01T00:00:00.000Z", + }); + const innerStreamFn = vi.fn(() => "stream" as never); + + const wrapped = await preparePromptCacheStream({ + fetchMock, + now: Number.NaN, + sessionManager, + streamFn: innerStreamFn, + }); + + await Promise.resolve( + wrapped?.( + makeGoogleModel(), + { systemPrompt: "Follow policy.", messages: [] } as never, + {} as never, + ), + ); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(innerStreamFn).toHaveBeenCalledTimes(1); + }); + it("stays out of the way when cachedContent is already configured explicitly", async () => { const fetchMock = vi.fn(); diff --git a/src/agents/embedded-agent-runner/google-prompt-cache.ts b/src/agents/embedded-agent-runner/google-prompt-cache.ts index ec32dbcb6be..273f6ef67c4 100644 --- a/src/agents/embedded-agent-runner/google-prompt-cache.ts +++ b/src/agents/embedded-agent-runner/google-prompt-cache.ts @@ -3,6 +3,11 @@ import { parseGeminiAuth } from "../../infra/gemini-auth.js"; import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js"; import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js"; import type { Model } from "../../llm/types.js"; +import { + asDateTimestampMs, + isFutureDateTimestampMs, + resolveExpiresAtMsFromDurationMs, +} from "../../shared/number-coercion.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { buildGuardedModelFetch } from "../provider-transport-fetch.js"; import type { StreamFn } from "../runtime/index.js"; @@ -185,8 +190,7 @@ function parseExpireTimeMs(expireTime: string | undefined): number | null { if (!expireTime) { return null; } - const timestamp = Date.parse(expireTime); - return Number.isFinite(timestamp) ? timestamp : null; + return asDateTimestampMs(Date.parse(expireTime)) ?? null; } function convertManagedGoogleTools(tools: NonNullable) { @@ -342,7 +346,10 @@ async function ensureGooglePromptCache( deps: GooglePromptCacheDeps, ): Promise { const baseUrl = normalizeGoogleApiBaseUrl(params.model.baseUrl); - const now = deps.now?.() ?? Date.now(); + const now = asDateTimestampMs(deps.now?.() ?? Date.now()); + if (now === undefined) { + return null; + } const systemPromptDigest = digestSystemPrompt(params.systemPrompt); const matchKey = buildGooglePromptCacheMatchKey({ provider: params.provider, @@ -354,7 +361,10 @@ async function ensureGooglePromptCache( }); const latestEntry = readLatestGooglePromptCacheEntry(params.sessionManager, matchKey); - if (latestEntry?.status === "failed" && latestEntry.retryAfter > now) { + if ( + latestEntry?.status === "failed" && + isFutureDateTimestampMs(latestEntry.retryAfter, { nowMs: now }) + ) { return null; } @@ -362,7 +372,7 @@ async function ensureGooglePromptCache( const refreshWindowMs = resolveGooglePromptCacheRefreshWindowMs(params.cacheRetention); if (latestEntry?.status === "ready" && latestEntry.cachedContent) { const expiresAt = parseExpireTimeMs(latestEntry.expireTime); - const isExpired = expiresAt !== null && expiresAt <= now; + const isExpired = expiresAt !== null && !isFutureDateTimestampMs(expiresAt, { nowMs: now }); if (!isExpired) { const needsRefresh = expiresAt !== null && expiresAt - now <= refreshWindowMs; if (!needsRefresh) { @@ -420,7 +430,8 @@ async function ensureGooglePromptCache( systemPromptDigest, cacheConfigDigest: params.cacheConfigDigest, cacheRetention: params.cacheRetention, - retryAfter: now + GOOGLE_PROMPT_CACHE_RETRY_BACKOFF_MS, + retryAfter: + resolveExpiresAtMsFromDurationMs(GOOGLE_PROMPT_CACHE_RETRY_BACKOFF_MS, { nowMs: now }) ?? 0, }); return null; }