diff --git a/packages/core/src/permissions/classifier.test.ts b/packages/core/src/permissions/classifier.test.ts index f0741fb3255..8805ab7213f 100644 --- a/packages/core/src/permissions/classifier.test.ts +++ b/packages/core/src/permissions/classifier.test.ts @@ -189,7 +189,7 @@ describe('classifier configuration', () => { expect(opts.config?.thinkingConfig?.includeThoughts).toBe(false); }); - it('uses max_output_tokens=4096 with thinking enabled for stage 2', async () => { + it('uses max_output_tokens=4096 with thinking disabled for stage 2', async () => { runSideQueryMock .mockResolvedValueOnce({ shouldBlock: true }) .mockResolvedValueOnce({ thinking: 't', shouldBlock: false, reason: '' }); @@ -201,7 +201,8 @@ describe('classifier configuration', () => { }; }; expect(opts.config?.maxOutputTokens).toBe(4096); - expect(opts.config?.thinkingConfig?.includeThoughts).toBe(true); + // Thinking is disabled in every stage (latency-sensitive permission gate). + expect(opts.config?.thinkingConfig?.includeThoughts).toBe(false); }); it('does not pin a model — defaults to the fast model via sideQuery', async () => { diff --git a/packages/core/src/permissions/classifier.ts b/packages/core/src/permissions/classifier.ts index ab2b7006611..d47531158a6 100644 --- a/packages/core/src/permissions/classifier.ts +++ b/packages/core/src/permissions/classifier.ts @@ -9,8 +9,9 @@ * Stage 1 (fast): shouldBlock-only output, max_tokens=32, thinking off. * Allow path returns immediately (~300ms). * Stage 2 (review): full output { thinking, shouldBlock, reason }, - * max_tokens=4096, thinking on. Reviews stage-1 blocks - * to reduce false positives. + * max_tokens=4096, thinking off. Reviews stage-1 blocks + * to reduce false positives. (`thinking` is a plain output + * field, not an allocated reasoning budget.) * * Fail-closed: any non-abort failure (API error, timeout, schema failure, * context overflow) returns shouldBlock=true with unavailable=true. @@ -33,10 +34,14 @@ import { buildClassifierContents } from './classifier-transcript.js'; // the underlying API / timeout / context-overflow error. const debugLogger = createDebugLogger('CLASSIFIER'); -/** Stage-1 timeout: fast model p99 is ~1.5s; 3s catches stuck cases. */ -export const STAGE1_TIMEOUT_MS = 3_000; -/** Stage-2 timeout: thinking takes longer; 10s caps infrastructure failure. */ -export const STAGE2_TIMEOUT_MS = 10_000; +// A timeout is fail-closed (action BLOCKED as "unavailable"), so too tight a +// budget turns transient slowness into spurious blocks. The fast model's p99 +// is ~1.5s but the tail is long under load, so budgets are kept generous — +// better to wait than fail closed on a healthy call. +/** Stage-1 timeout: generous headroom over the fast model's p99 (~1.5s). */ +export const STAGE1_TIMEOUT_MS = 10_000; +/** Stage-2 timeout: review stage runs a larger prompt; cap infra failure. */ +export const STAGE2_TIMEOUT_MS = 30_000; /** Token usage attributed to a single classifier call. */ export interface ClassifierUsage { @@ -219,7 +224,11 @@ export async function classifyAction( config: { temperature: 0, maxOutputTokens: 4096, - thinkingConfig: { includeThoughts: true }, + // Thinking off: this gate is latency-sensitive (the user is waiting), + // and a reasoning budget would slow the review path and worsen the + // fail-closed timeout above. The `thinking` output field still carries + // the model's reasoning. + thinkingConfig: { includeThoughts: false }, }, })) as Stage2Response; } catch (err) {