From 3982a252bb2de964e2d6582f2dbe658d493efc98 Mon Sep 17 00:00:00 2001 From: cynthialong0-0 <82900738+cynthialong0-0@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:23:19 -0700 Subject: [PATCH] fix(browser): reset action counter for each agent session and let it ignore internal actions (#24228) --- .../src/agents/browser/automationOverlay.ts | 2 ++ .../src/agents/browser/browserManager.test.ts | 24 +++++++++++++++++++ .../core/src/agents/browser/browserManager.ts | 20 ++++++++++------ .../src/agents/browser/inputBlocker.test.ts | 6 +++++ .../core/src/agents/browser/inputBlocker.ts | 4 ++++ .../src/agents/browser/mcpToolWrapper.test.ts | 2 ++ 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/core/src/agents/browser/automationOverlay.ts b/packages/core/src/agents/browser/automationOverlay.ts index a1aa40d58b..e87a70b6da 100644 --- a/packages/core/src/agents/browser/automationOverlay.ts +++ b/packages/core/src/agents/browser/automationOverlay.ts @@ -94,6 +94,7 @@ export async function injectAutomationOverlay( 'evaluate_script', { function: buildInjectionScript() }, signal, + true, ); if (result.isError) { @@ -120,6 +121,7 @@ export async function removeAutomationOverlay( 'evaluate_script', { function: buildRemovalScript() }, signal, + true, ); if (result.isError) { diff --git a/packages/core/src/agents/browser/browserManager.test.ts b/packages/core/src/agents/browser/browserManager.test.ts index 9813fd721f..edf3c46664 100644 --- a/packages/core/src/agents/browser/browserManager.test.ts +++ b/packages/core/src/agents/browser/browserManager.test.ts @@ -980,5 +980,29 @@ describe('BrowserManager', () => { /maximum action limit \(3\)/, ); }); + + it('should NOT increment action counter when shouldCount is false', async () => { + const limitedConfig = makeFakeConfig({ + agents: { + browser: { + maxActionsPerTask: 1, + }, + }, + }); + const manager = new BrowserManager(limitedConfig); + + // Multiple calls with isInternal: true should NOT exhaust the limit + await manager.callTool('evaluate_script', {}, undefined, true); + await manager.callTool('evaluate_script', {}, undefined, true); + await manager.callTool('evaluate_script', {}, undefined, true); + + // This should still work + await manager.callTool('take_snapshot', {}); + + // Next one should throw (limit 1 allows exactly 1 call with >= check) + await expect(manager.callTool('take_snapshot', {})).rejects.toThrow( + /maximum action limit \(1\)/, + ); + }); }); }); diff --git a/packages/core/src/agents/browser/browserManager.ts b/packages/core/src/agents/browser/browserManager.ts index 81f9db8250..5aebc93823 100644 --- a/packages/core/src/agents/browser/browserManager.ts +++ b/packages/core/src/agents/browser/browserManager.ts @@ -215,26 +215,30 @@ export class BrowserManager { * @param toolName The name of the tool to call * @param args Arguments to pass to the tool * @param signal Optional AbortSignal to cancel the call + * @param isInternal Determine if the tool is for internal execution * @returns The result from the MCP server */ async callTool( toolName: string, args: Record, signal?: AbortSignal, + isInternal: boolean = false, ): Promise { if (signal?.aborted) { throw signal.reason ?? new Error('Operation cancelled'); } // Hard enforcement of per-action rate limit - if (this.actionCounter > this.maxActionsPerTask) { - const error = new Error( - `Browser agent reached maximum action limit (${this.maxActionsPerTask}). ` + - `Task terminated to prevent runaway execution. To config the limit, use maxActionsPerTask in the settings.`, - ); - throw error; + if (!isInternal) { + if (this.actionCounter >= this.maxActionsPerTask) { + const error = new Error( + `Browser agent reached maximum action limit (${this.maxActionsPerTask}). ` + + `Task terminated to prevent runaway execution. To config the limit, use maxActionsPerTask in the settings.`, + ); + throw error; + } + this.actionCounter++; } - this.actionCounter++; const errorMessage = this.checkNavigationRestrictions(toolName, args); if (errorMessage) { @@ -588,6 +592,8 @@ export class BrowserManager { debugLogger.log('MCP client connected to chrome-devtools-mcp'); await this.discoverTools(); this.registerInputBlockerHandler(); + // clear the action counter for each connection + this.actionCounter = 0; })(), new Promise((_, reject) => { timeoutId = setTimeout( diff --git a/packages/core/src/agents/browser/inputBlocker.test.ts b/packages/core/src/agents/browser/inputBlocker.test.ts index abccac70c3..4723b9b607 100644 --- a/packages/core/src/agents/browser/inputBlocker.test.ts +++ b/packages/core/src/agents/browser/inputBlocker.test.ts @@ -34,6 +34,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }, undefined, + true, ); }); @@ -96,6 +97,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }), undefined, + true, ); expect(mockBrowserManager.callTool).toHaveBeenNthCalledWith( 2, @@ -104,6 +106,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }), undefined, + true, ); }); }); @@ -118,6 +121,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }, undefined, + true, ); }); @@ -163,6 +167,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }), undefined, + true, ); expect(mockBrowserManager.callTool).toHaveBeenNthCalledWith( 2, @@ -171,6 +176,7 @@ describe('inputBlocker', () => { function: expect.stringContaining('__gemini_input_blocker'), }), undefined, + true, ); }); }); diff --git a/packages/core/src/agents/browser/inputBlocker.ts b/packages/core/src/agents/browser/inputBlocker.ts index 0d6b9610cf..d7c6d8ce16 100644 --- a/packages/core/src/agents/browser/inputBlocker.ts +++ b/packages/core/src/agents/browser/inputBlocker.ts @@ -205,6 +205,7 @@ export async function injectInputBlocker( 'evaluate_script', { function: INPUT_BLOCKER_FUNCTION }, signal, + true, ); debugLogger.log('Input blocker injected successfully'); } catch (error) { @@ -232,6 +233,7 @@ export async function removeInputBlocker( 'evaluate_script', { function: REMOVE_BLOCKER_FUNCTION }, signal, + true, ); debugLogger.log('Input blocker removed successfully'); } catch (error) { @@ -257,6 +259,7 @@ export async function suspendInputBlocker( 'evaluate_script', { function: SUSPEND_BLOCKER_FUNCTION }, signal, + true, ); } catch { // Non-critical — tool call will still attempt to proceed @@ -276,6 +279,7 @@ export async function resumeInputBlocker( 'evaluate_script', { function: RESUME_BLOCKER_FUNCTION }, signal, + true, ); } catch { // Non-critical diff --git a/packages/core/src/agents/browser/mcpToolWrapper.test.ts b/packages/core/src/agents/browser/mcpToolWrapper.test.ts index fa9aa228a5..7a03a1daec 100644 --- a/packages/core/src/agents/browser/mcpToolWrapper.test.ts +++ b/packages/core/src/agents/browser/mcpToolWrapper.test.ts @@ -225,6 +225,7 @@ describe('mcpToolWrapper', () => { function: expect.stringContaining('__gemini_input_blocker'), }), expect.any(AbortSignal), + true, ); // Second call: click @@ -243,6 +244,7 @@ describe('mcpToolWrapper', () => { function: expect.stringContaining('__gemini_input_blocker'), }), expect.any(AbortSignal), + true, ); });