diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0f4c612a6..e31d6a88b 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Raised Gemini header runaway threshold to prevent premature interruption of complex reasoning loops + ## [16.2.9] - 2026-06-30 ### Added diff --git a/packages/ai/src/utils/thinking-loop.ts b/packages/ai/src/utils/thinking-loop.ts index 1810d0a1c..16c52f3cb 100644 --- a/packages/ai/src/utils/thinking-loop.ts +++ b/packages/ai/src/utils/thinking-loop.ts @@ -287,12 +287,17 @@ export class ThinkingLoopDetector { * stream that trips the tool-call reminder. Gemini occasionally narrates a long * chain of titled summaries ("Examining Result Handling", "Refining Result * Rendering", …) without ever calling a tool, burning the whole budget on - * planning; at this many distinct titles it has almost certainly stalled. This - * is the over-planning shape {@link ThinkingLoopDetector} misses — those titles - * are stripped before its similarity analysis precisely because their wording - * keeps changing, so a genuinely-distinct planning runaway never trips it. + * planning. This is the over-planning shape {@link ThinkingLoopDetector} misses — + * those titles are stripped before its similarity analysis precisely because their + * wording keeps changing, so a genuinely-distinct planning runaway never trips it. + * + * Set well above legitimate hard-problem depth: a capable model can emit ~10 + * distinct, progressing hypotheses in a single reasoning block before acting (and + * a false trip is costly — the interrupt discards the whole reasoning turn). A + * real narration runaway burns dozens-to-hundreds of titles, so this still trips + * fast on the actual pathology. */ -export const GEMINI_HEADER_RUNAWAY_THRESHOLD = 10; +export const GEMINI_HEADER_RUNAWAY_THRESHOLD = 24; /** * True when a single trimmed line is a Gemini reasoning-summary title: a markdown diff --git a/packages/ai/test/thinking-loop.test.ts b/packages/ai/test/thinking-loop.test.ts index 357da95e6..629ade660 100644 --- a/packages/ai/test/thinking-loop.test.ts +++ b/packages/ai/test/thinking-loop.test.ts @@ -615,6 +615,12 @@ describe("GeminiHeaderRunDetector", () => { expect(feedHeaders(runaway)).toBe(true); }); + test("does not trip on a legitimate 10-header debugging block (regression)", () => { + // A real, productive debugging stretch emitted 10 distinct progressing headers; never interrupt that. + expect(feedHeaders(distinctPlanningRunaway(10))).toBe(false); + expect(feedHeaders(distinctPlanningRunaway(24))).toBe(true); + }); + test("counts headers across intervening paragraphs (one summary = one header)", () => { const detector = new GeminiHeaderRunDetector(); let tripped = false; diff --git a/packages/catalog/scripts/generate-models.ts b/packages/catalog/scripts/generate-models.ts index 0ae7cdcfe..eee29d44a 100644 --- a/packages/catalog/scripts/generate-models.ts +++ b/packages/catalog/scripts/generate-models.ts @@ -15,7 +15,6 @@ import type { OAuthAccess } from "@oh-my-pi/pi-ai/auth-storage"; import type { OAuthProvider } from "@oh-my-pi/pi-ai/oauth/types"; import { getGitLabDuoModels } from "@oh-my-pi/pi-ai/providers/gitlab-duo"; import { $env } from "@oh-my-pi/pi-utils"; -import { buildCompat } from "../src/build"; import { ANTIGRAVITY_PRIMARY_ENDPOINT, fetchAntigravityDiscoveryModels } from "../src/discovery/antigravity"; import { fetchCodexModels } from "../src/discovery/codex"; import { buildGitLabDuoWorkflowFallbackModel } from "../src/discovery/gitlab-duo-workflow"; @@ -645,7 +644,7 @@ Model Statistics:`); function canonicalizeModelCompat(model: ModelSpec): void { if (!model.compat) return; - if (model.compat.disableStrictTools === false) { + if ("disableStrictTools" in model.compat && model.compat.disableStrictTools === false) { delete model.compat.disableStrictTools; }