diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index be78bbec51a..ff0bfe4d48e 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -175,6 +175,16 @@ describe('SettingsSchema', () => { expect(voiceModel.showInDialog).toBe(false); }); + it('should define stopHookBlockingCap schema override as a positive integer', () => { + expect( + getSettingsSchema().stopHookBlockingCap.jsonSchemaOverride, + ).toEqual({ + type: 'integer', + minimum: 1, + default: 8, + }); + }); + it('should have voice dictation settings under general', () => { const voice = getSettingsSchema().general.properties.voice.properties ?? {}; diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index bc0c72f404f..46a2385ab4d 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -2589,6 +2589,11 @@ const SETTINGS_SCHEMA = { // This is an advanced safety valve for runaway hook loops, not a common // interactive preference. showInDialog: false, + jsonSchemaOverride: { + type: 'integer', + minimum: 1, + default: DEFAULT_STOP_HOOK_BLOCK_CAP, + }, }, hooks: { diff --git a/packages/core/src/hooks/stopHookCap.test.ts b/packages/core/src/hooks/stopHookCap.test.ts index becb10931ac..e8f966e7bf0 100644 --- a/packages/core/src/hooks/stopHookCap.test.ts +++ b/packages/core/src/hooks/stopHookCap.test.ts @@ -31,12 +31,12 @@ describe('stop hook blocking cap', () => { ); }); - it('normalizes finite fractional values down to whole iterations', () => { + it('normalizes finite fractional config values down to whole iterations', () => { expect(normalizeStopHookBlockingCap(3.7)).toBe(3); expect(normalizeStopHookBlockingCap(100.9)).toBe(MAX_STOP_HOOK_BLOCK_CAP); }); - it('caps large finite values to avoid unbounded recursive Stop loops', () => { + it('caps large integer values to avoid unbounded recursive Stop loops', () => { expect(normalizeStopHookBlockingCap(99999)).toBe(MAX_STOP_HOOK_BLOCK_CAP); }); @@ -46,6 +46,16 @@ describe('stop hook blocking cap', () => { expect(resolveStopHookBlockingCap(12)).toBe(3); }); + it('rejects fractional environment overrides', () => { + process.env[STOP_HOOK_BLOCK_CAP_ENV] = '1.5'; + + expect(resolveStopHookBlockingCap(12)).toBe(DEFAULT_STOP_HOOK_BLOCK_CAP); + }); + + it('preserves legacy fractional config values when no environment override is set', () => { + expect(resolveStopHookBlockingCap(3.7)).toBe(3); + }); + it('ignores an empty environment override', () => { process.env[STOP_HOOK_BLOCK_CAP_ENV] = ''; diff --git a/packages/core/src/hooks/stopHookCap.ts b/packages/core/src/hooks/stopHookCap.ts index 94924a1d7f8..b3113f6442e 100644 --- a/packages/core/src/hooks/stopHookCap.ts +++ b/packages/core/src/hooks/stopHookCap.ts @@ -19,11 +19,17 @@ export function normalizeStopHookBlockingCap(value: unknown): number { : DEFAULT_STOP_HOOK_BLOCK_CAP; } +function parseStopHookBlockingCapEnv(value: string): number { + const parsed = Number(value); + return Number.isInteger(parsed) + ? normalizeStopHookBlockingCap(parsed) + : DEFAULT_STOP_HOOK_BLOCK_CAP; +} + export function resolveStopHookBlockingCap(configValue?: number): number { const envValue = process.env[STOP_HOOK_BLOCK_CAP_ENV]; if (envValue !== undefined && envValue.trim() !== '') { - const parsed = Number(envValue); - return normalizeStopHookBlockingCap(parsed); + return parseStopHookBlockingCapEnv(envValue); } return normalizeStopHookBlockingCap(configValue); diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index 207d952e0f5..fc1b210e92f 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -1217,9 +1217,10 @@ "default": false }, "stopHookBlockingCap": { - "description": "Maximum consecutive blocking Stop/SubagentStop hook decisions before Qwen Code overrides the hook loop and ends the turn. Can be overridden by QWEN_CODE_STOP_HOOK_BLOCK_CAP.", - "type": "number", - "default": 8 + "type": "integer", + "minimum": 1, + "default": 8, + "description": "Maximum consecutive blocking Stop/SubagentStop hook decisions before Qwen Code overrides the hook loop and ends the turn. Can be overridden by QWEN_CODE_STOP_HOOK_BLOCK_CAP." }, "hooks": { "description": "Hook event configurations for extending CLI behavior at various lifecycle points.",