From 445a9895c136f04725773833d81b45cf51d43fe5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 28 Sep 2026 16:23:33 -0400 Subject: [PATCH 1/7] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20fix:=20Fit=20Workspace?= =?UTF-8?q?=20Command=20Timeouts=20Inside=20the=20Configured=20Request=20B?= =?UTF-8?q?udget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/api/src/code/command.spec.ts | 66 +++++++++++++++++++++++++++ packages/api/src/code/command.ts | 16 ++++++- packages/api/src/code/workspace.ts | 17 +++++++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/packages/api/src/code/command.spec.ts b/packages/api/src/code/command.spec.ts index b4cc19e0c95..7769d17365f 100644 --- a/packages/api/src/code/command.spec.ts +++ b/packages/api/src/code/command.spec.ts @@ -395,6 +395,72 @@ describe('createAttachedWorkspaceBashTool', () => { expect(foregroundRequest).toMatchObject({ timeoutMs: 30_000 }); }); + test('fits the command ceiling inside a configured total HTTP budget', () => { + expect( + resolveAttachedWorkspaceCommandTimeoutMax({ + limits: { maxCommandTimeoutMs: 80_000, maxRequestTimeoutMs: 90_000 }, + }), + ).toBe(70_000); + expect( + resolveAttachedWorkspaceCommandTimeoutMax({ + limits: { maxCommandTimeoutMs: 60_000, maxRequestTimeoutMs: 90_000 }, + }), + ).toBe(60_000); + expect( + resolveAttachedWorkspaceCommandTimeoutMax( + { limits: { maxCommandTimeoutMs: 120_000, maxRequestTimeoutMs: 125_000 } }, + 90_000, + ), + ).toBe(90_000); + expect( + resolveAttachedWorkspaceCommandTimeoutMax({ limits: { maxCommandTimeoutMs: 120_000 } }), + ).toBe(120_000); + }); + + test('starts an omitted background timeout that the configured budget can carry', async () => { + jest.spyOn(Date, 'now').mockReturnValue(1_000); + const configSchema = { limits: { maxCommandTimeoutMs: 80_000, maxRequestTimeoutMs: 90_000 } }; + const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); + const bashTool = createAttachedWorkspaceBashTool({ + baseUrl: 'https://code.example.com/v1', + authHeaders: () => ({}), + workspaceId: 'project-a', + maxTimeoutMs: resolveAttachedWorkspaceCommandTimeoutMax(configSchema), + maxRequestTimeoutMs: resolveAttachedWorkspaceRequestTimeoutMs(configSchema), + fetchImpl, + }); + + await bashTool.invoke( + { command: 'npm test' }, + { configurable: { [BACKGROUND_TOOL_INVOCATION_CONFIG_KEY]: true } }, + ); + + const [, init] = (fetchImpl as jest.Mock).mock.calls[0]; + expect(JSON.parse(String(init?.body))).toMatchObject({ timeoutMs: 70_000 }); + expect(init?.headers['X-LibreChat-Workspace-Queue-Wait-Ms']).toBe('10000'); + jest.restoreAllMocks(); + }); + + test('advertises and enforces only the timeout that fits when given both limits directly', async () => { + const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); + const bashTool = createAttachedWorkspaceBashTool({ + baseUrl: 'https://code.example.com/v1', + authHeaders: () => ({}), + workspaceId: 'project-a', + maxTimeoutMs: 80_000, + maxRequestTimeoutMs: 90_000, + fetchImpl, + }); + + expect(bashTool.schema).toMatchObject({ + properties: { timeoutMs: expect.objectContaining({ maximum: 70_000 }) }, + }); + await expect( + bashTool.func({ command: 'npm test', timeoutMs: 80_000 }, undefined, {}), + ).rejects.toThrow('deployment limit of 70000 milliseconds'); + expect(fetchImpl).not.toHaveBeenCalled(); + }); + test('resolves the administrator-configured retry horizon', () => { expect(resolveAttachedWorkspaceQueueWaitMs()).toBe(5 * 60_000); expect(resolveAttachedWorkspaceQueueWaitMs({ limits: { maxQueueWaitMs: 30_000 } })).toBe( diff --git a/packages/api/src/code/command.ts b/packages/api/src/code/command.ts index da191a5ec9b..e4a5930ba54 100644 --- a/packages/api/src/code/command.ts +++ b/packages/api/src/code/command.ts @@ -17,6 +17,7 @@ import type { WorkspaceExecuteCommandResult } from './workspace'; import type { CodeExecutionContext } from '~/agents/execution'; import type { CodeBridgeFetch } from './bridge'; import { + fitWorkspaceCommandTimeoutToBudget, executeWorkspaceTool, WORKSPACE_COMMAND_DEFAULT_TIMEOUT_MS, WORKSPACE_COMMAND_MAX_TIMEOUT_MS, @@ -102,7 +103,15 @@ export function resolveAttachedWorkspaceCommandTimeoutMax( } else if (upstreamMaxTimeoutMs != null) { requested = upstream; } - return Math.min(requested, upstream); + return fitCommandTimeoutMaxToBudget( + Math.min(requested, upstream), + resolveAttachedWorkspaceRequestTimeoutMs(configSchema), + ); +} + +function fitCommandTimeoutMaxToBudget(maxTimeoutMs: number, maxRequestTimeoutMs?: number): number { + if (maxRequestTimeoutMs == null) return maxTimeoutMs; + return Math.min(maxTimeoutMs, fitWorkspaceCommandTimeoutToBudget(maxRequestTimeoutMs)); } /** @@ -321,7 +330,10 @@ export function createAttachedWorkspaceBashTool({ maxRequestTimeoutMs?: number; fetchImpl?: CodeBridgeFetch; }): DynamicStructuredTool { - const effectiveMaxTimeoutMs = normalizeAttachedWorkspaceCommandTimeoutMax(maxTimeoutMs); + const effectiveMaxTimeoutMs = fitCommandTimeoutMaxToBudget( + normalizeAttachedWorkspaceCommandTimeoutMax(maxTimeoutMs), + maxRequestTimeoutMs, + ); const schema = structuredClone( buildAttachedWorkspaceBashSchema(effectiveMaxTimeoutMs, environment), ); diff --git a/packages/api/src/code/workspace.ts b/packages/api/src/code/workspace.ts index 7f43a2ea984..c039428878b 100644 --- a/packages/api/src/code/workspace.ts +++ b/packages/api/src/code/workspace.ts @@ -31,6 +31,23 @@ const WORKSPACE_QUEUE_WAIT_HEADER = 'X-LibreChat-Workspace-Queue-Wait-Ms'; export const WORKSPACE_QUEUE_MAX_WAIT_MS: number = CODE_ENVIRONMENT_QUEUE_WAIT_DEFAULT_MS; const WORKSPACE_QUEUE_RETRY_DELAY_MS = 1_000; const WORKSPACE_COMMAND_SETTLEMENT_GRACE_MS = 5_000; +/** Queue time a command keeps inside a total HTTP budget, so it can still wait for a busy worker. */ +const WORKSPACE_COMMAND_MIN_ADMISSION_MS = 10_000; + +/** + * Longest command timeout a total HTTP budget can carry: the budget minus settlement and delivery + * grace and a minimum admission allowance. A command whose reserve reaches the budget is refused + * before dispatch, so a larger ceiling would only advertise timeouts that can never start. + */ +export function fitWorkspaceCommandTimeoutToBudget(maxRequestTimeoutMs: number): number { + return Math.max( + 1, + maxRequestTimeoutMs - + WORKSPACE_COMMAND_SETTLEMENT_GRACE_MS - + WORKSPACE_COMMAND_TRANSPORT_GRACE_MS - + WORKSPACE_COMMAND_MIN_ADMISSION_MS, + ); +} const MAX_RESPONSE_BYTES = 4 * 1024 * 1024; const MAX_ERROR_BODY_BYTES = 4096; const ERROR_BODY_TIMEOUT_MS = 1000; From e9c0ee7ce7d207922528f8df3544d078dcf369a0 Mon Sep 17 00:00:00 2001 From: Lia Date: Mon, 28 Sep 2026 21:41:29 +0000 Subject: [PATCH 2/7] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20fix:=20Make=20Workspac?= =?UTF-8?q?e=20Command=20Admission=20Reserve=20Configurable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/services/ToolService.js | 2 + .../services/__tests__/ToolService.spec.js | 13 ++++- packages/api/src/code/command.spec.ts | 55 +++++++++++++++++++ packages/api/src/code/command.ts | 16 +++++- packages/api/src/code/workspace.ts | 10 ++-- packages/data-provider/src/config.spec.ts | 35 +++++++++++- packages/data-provider/src/config.ts | 10 ++++ 7 files changed, 131 insertions(+), 10 deletions(-) diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js index b047447ec16..5ce4e4d603b 100644 --- a/api/server/services/ToolService.js +++ b/api/server/services/ToolService.js @@ -2321,6 +2321,8 @@ async function loadToolsForExecution({ maxRequestTimeoutMs: resolveAttachedWorkspaceRequestTimeoutMs( codeExecutionContext.codeEnvironmentConfigSchema, ), + minCommandAdmissionMs: + codeExecutionContext.codeEnvironmentConfigSchema?.limits?.minCommandAdmissionMs, }) : createBashExecutionTool({ authHeaders, diff --git a/api/server/services/__tests__/ToolService.spec.js b/api/server/services/__tests__/ToolService.spec.js index 9c4d57eefda..e8156e9f6c9 100644 --- a/api/server/services/__tests__/ToolService.spec.js +++ b/api/server/services/__tests__/ToolService.spec.js @@ -2980,7 +2980,14 @@ describe('ToolService - Action Capability Gating', () => { environmentType: 'attached', environmentId: 'personal-machine', bridgeWorkerId: 'worker-abc', - codeEnvironmentConfigSchema: { limits: { maxCommandTimeoutMs: 120000, maxQueueWaitMs: 0 } }, + codeEnvironmentConfigSchema: { + limits: { + maxCommandTimeoutMs: 80_000, + maxQueueWaitMs: 0, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 15_000, + }, + }, }); const toolRegistry = new Map([ [AgentConstants.BASH_TOOL, { name: AgentConstants.BASH_TOOL }], @@ -3006,9 +3013,11 @@ describe('ToolService - Action Capability Gating', () => { baseUrl: 'http://attached-code.test/v1', workspaceId: 'project-a', gitIdentity: { name: 'LibreChat Agent', email: 'agent@example.com' }, - maxTimeoutMs: 120000, + maxTimeoutMs: 65_000, maxQueueWaitMs: 0, codeApiMaxRetryWaitMs: 0, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 15_000, }); expect(mockResolveCodeExecutionWorkspaceContext).toHaveBeenCalledWith( expect.objectContaining({ requestedSelections: req.body.codeWorkspaces }), diff --git a/packages/api/src/code/command.spec.ts b/packages/api/src/code/command.spec.ts index 7769d17365f..3c4c7a62daa 100644 --- a/packages/api/src/code/command.spec.ts +++ b/packages/api/src/code/command.spec.ts @@ -406,6 +406,24 @@ describe('createAttachedWorkspaceBashTool', () => { limits: { maxCommandTimeoutMs: 60_000, maxRequestTimeoutMs: 90_000 }, }), ).toBe(60_000); + expect( + resolveAttachedWorkspaceCommandTimeoutMax({ + limits: { + maxCommandTimeoutMs: 80_000, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 15_000, + }, + }), + ).toBe(65_000); + expect( + resolveAttachedWorkspaceCommandTimeoutMax({ + limits: { + maxCommandTimeoutMs: 80_000, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 1, + }, + }), + ).toBe(79_999); expect( resolveAttachedWorkspaceCommandTimeoutMax( { limits: { maxCommandTimeoutMs: 120_000, maxRequestTimeoutMs: 125_000 } }, @@ -441,6 +459,43 @@ describe('createAttachedWorkspaceBashTool', () => { jest.restoreAllMocks(); }); + test('reserves the configured admission allowance for an omitted background timeout', async () => { + jest.spyOn(Date, 'now').mockReturnValue(1_000); + const configSchema = { + limits: { + maxCommandTimeoutMs: 80_000, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 15_000, + }, + }; + const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); + const bashTool = createAttachedWorkspaceBashTool({ + baseUrl: 'https://code.example.com/v1', + authHeaders: () => ({}), + workspaceId: 'project-a', + maxTimeoutMs: resolveAttachedWorkspaceCommandTimeoutMax(configSchema), + maxRequestTimeoutMs: resolveAttachedWorkspaceRequestTimeoutMs(configSchema), + minCommandAdmissionMs: configSchema.limits.minCommandAdmissionMs, + fetchImpl, + }); + + expect(bashTool.schema).toMatchObject({ + properties: { timeoutMs: expect.objectContaining({ maximum: 65_000 }) }, + }); + await bashTool.invoke( + { command: 'npm test' }, + { configurable: { [BACKGROUND_TOOL_INVOCATION_CONFIG_KEY]: true } }, + ); + const [, init] = (fetchImpl as jest.Mock).mock.calls[0]; + expect(JSON.parse(String(init?.body))).toMatchObject({ timeoutMs: 65_000 }); + expect(init?.headers['X-LibreChat-Workspace-Queue-Wait-Ms']).toBe('15000'); + await expect( + bashTool.func({ command: 'npm test', timeoutMs: 80_000 }, undefined, {}), + ).rejects.toThrow('deployment limit of 65000 milliseconds'); + expect(fetchImpl).toHaveBeenCalledTimes(1); + jest.restoreAllMocks(); + }); + test('advertises and enforces only the timeout that fits when given both limits directly', async () => { const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); const bashTool = createAttachedWorkspaceBashTool({ diff --git a/packages/api/src/code/command.ts b/packages/api/src/code/command.ts index e4a5930ba54..7981204f2ad 100644 --- a/packages/api/src/code/command.ts +++ b/packages/api/src/code/command.ts @@ -106,12 +106,20 @@ export function resolveAttachedWorkspaceCommandTimeoutMax( return fitCommandTimeoutMaxToBudget( Math.min(requested, upstream), resolveAttachedWorkspaceRequestTimeoutMs(configSchema), + configSchema?.limits?.minCommandAdmissionMs, ); } -function fitCommandTimeoutMaxToBudget(maxTimeoutMs: number, maxRequestTimeoutMs?: number): number { +function fitCommandTimeoutMaxToBudget( + maxTimeoutMs: number, + maxRequestTimeoutMs?: number, + minCommandAdmissionMs?: number, +): number { if (maxRequestTimeoutMs == null) return maxTimeoutMs; - return Math.min(maxTimeoutMs, fitWorkspaceCommandTimeoutToBudget(maxRequestTimeoutMs)); + return Math.min( + maxTimeoutMs, + fitWorkspaceCommandTimeoutToBudget(maxRequestTimeoutMs, minCommandAdmissionMs), + ); } /** @@ -313,6 +321,7 @@ export function createAttachedWorkspaceBashTool({ maxQueueWaitMs, codeApiMaxRetryWaitMs, maxRequestTimeoutMs, + minCommandAdmissionMs, fetchImpl, }: { baseUrl: string; @@ -328,11 +337,14 @@ export function createAttachedWorkspaceBashTool({ codeApiMaxRetryWaitMs?: number; /** Verified total HTTP budget; omission keeps the legacy per-attempt timeout. */ maxRequestTimeoutMs?: number; + /** Minimum time for command admission inside an opted-in HTTP budget. */ + minCommandAdmissionMs?: number; fetchImpl?: CodeBridgeFetch; }): DynamicStructuredTool { const effectiveMaxTimeoutMs = fitCommandTimeoutMaxToBudget( normalizeAttachedWorkspaceCommandTimeoutMax(maxTimeoutMs), maxRequestTimeoutMs, + minCommandAdmissionMs, ); const schema = structuredClone( buildAttachedWorkspaceBashSchema(effectiveMaxTimeoutMs, environment), diff --git a/packages/api/src/code/workspace.ts b/packages/api/src/code/workspace.ts index c039428878b..c953dffbfa9 100644 --- a/packages/api/src/code/workspace.ts +++ b/packages/api/src/code/workspace.ts @@ -1,5 +1,6 @@ import { CODE_ENVIRONMENT_ADMISSION_MAX_MS, + CODE_ENVIRONMENT_COMMAND_ADMISSION_DEFAULT_MS, CODE_ENVIRONMENT_QUEUE_WAIT_DEFAULT_MS, CODE_ENVIRONMENT_REQUEST_TIMEOUT_HARD_MAX_MS, } from 'librechat-data-provider'; @@ -31,21 +32,22 @@ const WORKSPACE_QUEUE_WAIT_HEADER = 'X-LibreChat-Workspace-Queue-Wait-Ms'; export const WORKSPACE_QUEUE_MAX_WAIT_MS: number = CODE_ENVIRONMENT_QUEUE_WAIT_DEFAULT_MS; const WORKSPACE_QUEUE_RETRY_DELAY_MS = 1_000; const WORKSPACE_COMMAND_SETTLEMENT_GRACE_MS = 5_000; -/** Queue time a command keeps inside a total HTTP budget, so it can still wait for a busy worker. */ -const WORKSPACE_COMMAND_MIN_ADMISSION_MS = 10_000; /** * Longest command timeout a total HTTP budget can carry: the budget minus settlement and delivery * grace and a minimum admission allowance. A command whose reserve reaches the budget is refused * before dispatch, so a larger ceiling would only advertise timeouts that can never start. */ -export function fitWorkspaceCommandTimeoutToBudget(maxRequestTimeoutMs: number): number { +export function fitWorkspaceCommandTimeoutToBudget( + maxRequestTimeoutMs: number, + minCommandAdmissionMs: number = CODE_ENVIRONMENT_COMMAND_ADMISSION_DEFAULT_MS, +): number { return Math.max( 1, maxRequestTimeoutMs - WORKSPACE_COMMAND_SETTLEMENT_GRACE_MS - WORKSPACE_COMMAND_TRANSPORT_GRACE_MS - - WORKSPACE_COMMAND_MIN_ADMISSION_MS, + minCommandAdmissionMs, ); } const MAX_RESPONSE_BYTES = 4 * 1024 * 1024; diff --git a/packages/data-provider/src/config.spec.ts b/packages/data-provider/src/config.spec.ts index 2dd82eaa88a..c8bbe538090 100644 --- a/packages/data-provider/src/config.spec.ts +++ b/packages/data-provider/src/config.spec.ts @@ -7,6 +7,7 @@ import { bedrockModels, configSchema, codeEnvironmentUserConfigSchema, + CODE_ENVIRONMENT_ADMISSION_MAX_MS, excludedKeys, resolveEndpointType, webSearchSchema, @@ -583,6 +584,30 @@ describe('attached code environment user config schema', () => { expect(codeEnvironmentUserConfigSchema.parse({ limits: {} })).toEqual({ limits: {} }); }); + it.each([1, 15_000, CODE_ENVIRONMENT_ADMISSION_MAX_MS])( + 'accepts a bounded %i ms command admission allowance', + (minCommandAdmissionMs) => { + expect(codeEnvironmentUserConfigSchema.parse({ limits: { minCommandAdmissionMs } })).toEqual({ + limits: { minCommandAdmissionMs }, + }); + }, + ); + + it.each([0, -1, 0.5, CODE_ENVIRONMENT_ADMISSION_MAX_MS + 1, NaN, Infinity])( + 'rejects an invalid command admission allowance of %s', + (minCommandAdmissionMs) => { + expect( + codeEnvironmentUserConfigSchema.safeParse({ limits: { minCommandAdmissionMs } }).success, + ).toBe(false); + }, + ); + + it('preserves omission of the command admission allowance alongside a request budget', () => { + expect( + codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs: 90_000 } }), + ).toEqual({ limits: { maxRequestTimeoutMs: 90_000 } }); + }); + it('accepts typed permission controls exposed by the administrator', () => { const result = configSchema.safeParse({ version: '1.0', @@ -602,7 +627,11 @@ describe('attached code environment user config schema', () => { fileWrite: { allowed: ['allow', 'ask', 'deny'], default: 'ask' }, commandExecution: { allowed: ['ask', 'deny'], default: 'ask' }, }, - limits: { maxCommandTimeoutMs: 120000, maxRequestTimeoutMs: 125_000 }, + limits: { + maxCommandTimeoutMs: 120000, + maxRequestTimeoutMs: 125_000, + minCommandAdmissionMs: 15_000, + }, }, }, ], @@ -621,7 +650,9 @@ describe('attached code environment user config schema', () => { statefulCodeSessions: { environments: [ { - configSchema: { limits: { maxRequestTimeoutMs: 125_000 } }, + configSchema: { + limits: { maxRequestTimeoutMs: 125_000, minCommandAdmissionMs: 15_000 }, + }, }, ], }, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index bc84677a2fd..dd5c460a20c 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1207,6 +1207,8 @@ export const CODE_ENVIRONMENT_COMMAND_TIMEOUT_HARD_MAX_MS = 5 * 60_000; export const CODE_ENVIRONMENT_QUEUE_WAIT_DEFAULT_MS = 5 * 60_000; /** Code API's per-request admission ceiling, independent of the retry horizon. */ export const CODE_ENVIRONMENT_ADMISSION_MAX_MS = 5 * 60_000; +/** Minimum command admission time reserved inside an opted-in HTTP budget. */ +export const CODE_ENVIRONMENT_COMMAND_ADMISSION_DEFAULT_MS = 10_000; /** Maximum opt-in HTTP budget: five minutes of admission and execution plus ten seconds for settlement and delivery. */ export const CODE_ENVIRONMENT_REQUEST_TIMEOUT_HARD_MAX_MS = CODE_ENVIRONMENT_ADMISSION_MAX_MS + CODE_ENVIRONMENT_COMMAND_TIMEOUT_HARD_MAX_MS + 10_000; @@ -1256,6 +1258,14 @@ export const codeEnvironmentUserConfigSchema = z .min(1) .max(CODE_ENVIRONMENT_REQUEST_TIMEOUT_HARD_MAX_MS) .optional(), + /** Minimum admission time left for a Bash command inside maxRequestTimeoutMs. + * Omission reserves ten seconds; ignored without a total HTTP budget. */ + minCommandAdmissionMs: z + .number() + .int() + .min(1) + .max(CODE_ENVIRONMENT_ADMISSION_MAX_MS) + .optional(), }) .strict() .optional(), From da76f75a265b0fa8c1b893d579c3fe726556764f Mon Sep 17 00:00:00 2001 From: Lia Date: Mon, 28 Sep 2026 22:50:05 +0000 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A7=AA=20test:=20Match=20Steering=20E?= =?UTF-8?q?2E=20Labels=20on=20Dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/specs/mock/steering-escalation.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/specs/mock/steering-escalation.spec.ts b/e2e/specs/mock/steering-escalation.spec.ts index 615bfc33659..dfb1b9f1eb3 100644 --- a/e2e/specs/mock/steering-escalation.spec.ts +++ b/e2e/specs/mock/steering-escalation.spec.ts @@ -206,14 +206,14 @@ test.describe('escalating waiting messages to an interrupt', () => { // The toggle lives in the row menu's separated Preferences section. await row.getByRole('button', { name: 'More options' }).click(); await expect(page.getByText('Preferences', { exact: true })).toBeVisible({ timeout: 5000 }); - await page.getByRole('menuitem', { name: 'Always interrupt instead', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Steer sooner by default', exact: true }).click(); // Verify the preference flips while this row is guaranteed to remain // parked. After the interrupt is submitted the run may seal and auto-drain // the row before another locator action can observe it. await row.getByRole('button', { name: 'More options' }).click(); await expect( - page.getByRole('menuitem', { name: 'Wait for tool steps instead', exact: true }), + page.getByRole('menuitem', { name: 'Wait for the next step instead', exact: true }), ).toBeVisible({ timeout: 5000 }); await page.keyboard.press('Escape'); From 8aa19f1357b614324507fa91a87d4900658edc70 Mon Sep 17 00:00:00 2001 From: Lia Date: Tue, 29 Sep 2026 01:18:46 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20fix:=20Reject=20Imposs?= =?UTF-8?q?ible=20Workspace=20Command=20Admission=20Reserves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/data-provider/src/config.spec.ts | 59 +++++++++++++++++++++++ packages/data-provider/src/config.ts | 23 ++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/data-provider/src/config.spec.ts b/packages/data-provider/src/config.spec.ts index c8bbe538090..1c94288c973 100644 --- a/packages/data-provider/src/config.spec.ts +++ b/packages/data-provider/src/config.spec.ts @@ -606,6 +606,65 @@ describe('attached code environment user config schema', () => { expect( codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs: 90_000 } }), ).toEqual({ limits: { maxRequestTimeoutMs: 90_000 } }); + expect( + codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs: 5_000 } }), + ).toEqual({ limits: { maxRequestTimeoutMs: 5_000 } }); + }); + + it.each([ + { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 79_999 }, + { maxRequestTimeoutMs: 10_002, minCommandAdmissionMs: 1 }, + { maxRequestTimeoutMs: 610_000, minCommandAdmissionMs: 300_000 }, + ])('accepts an admission reserve with execution time left: %j', (limits) => { + expect(codeEnvironmentUserConfigSchema.parse({ limits })).toEqual({ limits }); + }); + + it.each([ + { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 80_000 }, + { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 100_000 }, + { maxRequestTimeoutMs: 10_001, minCommandAdmissionMs: 1 }, + ])('rejects a command reserve that cannot fit inside its request budget: %j', (limits) => { + const parsed = codeEnvironmentUserConfigSchema.safeParse({ limits }); + expect(parsed.success).toBe(false); + if (!parsed.success) { + expect(parsed.error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ path: ['limits', 'minCommandAdmissionMs'] }), + ]), + ); + } + }); + + it('allows a command reserve without a request budget (the legacy per-attempt path)', () => { + expect( + codeEnvironmentUserConfigSchema.parse({ limits: { minCommandAdmissionMs: 300_000 } }), + ).toEqual({ limits: { minCommandAdmissionMs: 300_000 } }); + }); + + it('rejects an impossible reserve in the top-level deployment config', () => { + expect( + configSchema.safeParse({ + version: '1.0', + endpoints: { + agents: { + statefulCodeSessions: { + allowedEnvironments: ['user'], + environments: [ + { + id: 'personal-vm', + name: 'Personal VM', + type: 'attached', + baseURL: 'https://code.example.com/v1', + configSchema: { + limits: { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 100_000 }, + }, + }, + ], + }, + }, + }, + }).success, + ).toBe(false); }); it('accepts typed permission controls exposed by the administrator', () => { diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index dd5c460a20c..9d3f1c5472d 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1209,9 +1209,13 @@ export const CODE_ENVIRONMENT_QUEUE_WAIT_DEFAULT_MS = 5 * 60_000; export const CODE_ENVIRONMENT_ADMISSION_MAX_MS = 5 * 60_000; /** Minimum command admission time reserved inside an opted-in HTTP budget. */ export const CODE_ENVIRONMENT_COMMAND_ADMISSION_DEFAULT_MS = 10_000; -/** Maximum opt-in HTTP budget: five minutes of admission and execution plus ten seconds for settlement and delivery. */ +/** Five seconds each for command settlement and transport delivery. */ +const CODE_ENVIRONMENT_COMMAND_BUDGET_GRACE_MS = 10_000; +/** Maximum opt-in HTTP budget: five minutes of admission and execution plus settlement and delivery. */ export const CODE_ENVIRONMENT_REQUEST_TIMEOUT_HARD_MAX_MS = - CODE_ENVIRONMENT_ADMISSION_MAX_MS + CODE_ENVIRONMENT_COMMAND_TIMEOUT_HARD_MAX_MS + 10_000; + CODE_ENVIRONMENT_ADMISSION_MAX_MS + + CODE_ENVIRONMENT_COMMAND_TIMEOUT_HARD_MAX_MS + + CODE_ENVIRONMENT_COMMAND_BUDGET_GRACE_MS; /** * Typed user-tunable surface for one attached code environment. Omitted fields @@ -1268,6 +1272,21 @@ export const codeEnvironmentUserConfigSchema = z .optional(), }) .strict() + .superRefine((limits, context) => { + if ( + limits.maxRequestTimeoutMs == null || + limits.minCommandAdmissionMs == null || + limits.maxRequestTimeoutMs > + limits.minCommandAdmissionMs + CODE_ENVIRONMENT_COMMAND_BUDGET_GRACE_MS + ) { + return; + } + context.addIssue({ + code: z.ZodIssueCode.custom, + path: ['minCommandAdmissionMs'], + message: 'Command admission and settlement reserves must leave time for execution', + }); + }) .optional(), }) .strict(); From 69e099ee443b00b6d8e9eaab197c093e22760038 Mon Sep 17 00:00:00 2001 From: Lia Date: Tue, 29 Sep 2026 01:27:38 +0000 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A7=AA=20test:=20Match=20Failed=20Cal?= =?UTF-8?q?l=20Count=20in=20Dev=20E2E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/specs/mock/activity-fold.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/specs/mock/activity-fold.spec.ts b/e2e/specs/mock/activity-fold.spec.ts index 05d689900c0..7a588ce22fe 100644 --- a/e2e/specs/mock/activity-fold.spec.ts +++ b/e2e/specs/mock/activity-fold.spec.ts @@ -89,7 +89,7 @@ test.describe('activity fold', () => { const pill = messagesView(page).getByTestId('failed-reveal-pill'); const peek = messagesView(page).getByTestId('activity-phase-failed-peek'); await expect(pill).toBeVisible(); - await expect(pill).toHaveAccessibleName('Show failed call'); + await expect(pill).toHaveAccessibleName('Show 1 failed call out of 4 calls'); await expect(peek).toBeVisible(); await expect(peek).toContainText('Failed:'); await expect(peek).toContainText('Show error'); From 2bb5e18b81c3feef4da708e1b67caeb28314b2d2 Mon Sep 17 00:00:00 2001 From: Lia Date: Tue, 29 Sep 2026 01:47:37 +0000 Subject: [PATCH 6/7] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20fix:=20Bound=20Workspa?= =?UTF-8?q?ce=20Admission=20Headroom=20and=20Document=20Its=20Budget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- librechat.example.yaml | 10 ++++++-- packages/api/src/code/command.spec.ts | 30 +++++++++++++++++++++-- packages/data-provider/src/config.spec.ts | 8 +++--- packages/data-provider/src/config.ts | 6 ++--- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/librechat.example.yaml b/librechat.example.yaml index 2f8caedec6d..a241d6ad093 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -728,9 +728,15 @@ endpoints: # # ingress, and caller can all outlast this value. The Nginx example # # (120s) cannot establish the live limit. Omission keeps 30s admission. # # With 125s total, a read can queue <=90s, a default command <=85s, - # # a 90s command <=25s. Commands >115s cannot fit; the 120s command - # # ceiling above requires a larger VERIFIED total budget to be usable. + # # a 90s command <=25s. With the default 10s admission reserve, + # # commands >105s cannot fit; the 120s command ceiling above requires + # # a VERIFIED total budget of at least 140s to be usable. # # maxRequestTimeoutMs: 125000 + # # Optional command admission allowance before local dispatch overhead. + # # Omission reserves 10s; valid range: 1000–300000 ms. The HTTP budget + # # must exceed this allowance plus 10s settlement/delivery grace. + # # Smaller allowances give a busy worker less time to accept the call. + # # minCommandAdmissionMs: 10000 # permissions: # fileWrite: # allowed: [allow, ask, deny] diff --git a/packages/api/src/code/command.spec.ts b/packages/api/src/code/command.spec.ts index 3c4c7a62daa..b027583ef6a 100644 --- a/packages/api/src/code/command.spec.ts +++ b/packages/api/src/code/command.spec.ts @@ -420,10 +420,10 @@ describe('createAttachedWorkspaceBashTool', () => { limits: { maxCommandTimeoutMs: 80_000, maxRequestTimeoutMs: 90_000, - minCommandAdmissionMs: 1, + minCommandAdmissionMs: 1_000, }, }), - ).toBe(79_999); + ).toBe(79_000); expect( resolveAttachedWorkspaceCommandTimeoutMax( { limits: { maxCommandTimeoutMs: 120_000, maxRequestTimeoutMs: 125_000 } }, @@ -496,6 +496,32 @@ describe('createAttachedWorkspaceBashTool', () => { jest.restoreAllMocks(); }); + test('still dispatches at the smallest supported reserve after credential-signing time', async () => { + const now = jest.spyOn(Date, 'now').mockReturnValue(1_000); + const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); + const bashTool = createAttachedWorkspaceBashTool({ + baseUrl: 'https://code.example.com/v1', + authHeaders: () => { + now.mockReturnValue(1_200); + return {}; + }, + workspaceId: 'project-a', + maxTimeoutMs: 80_000, + maxRequestTimeoutMs: 90_000, + minCommandAdmissionMs: 1_000, + fetchImpl, + }); + + await bashTool.invoke( + { command: 'npm test' }, + { configurable: { [BACKGROUND_TOOL_INVOCATION_CONFIG_KEY]: true } }, + ); + const [, init] = (fetchImpl as jest.Mock).mock.calls[0]; + expect(JSON.parse(String(init?.body))).toMatchObject({ timeoutMs: 79_000 }); + expect(init?.headers['X-LibreChat-Workspace-Queue-Wait-Ms']).toBe('800'); + jest.restoreAllMocks(); + }); + test('advertises and enforces only the timeout that fits when given both limits directly', async () => { const fetchImpl: CodeBridgeFetch = jest.fn(async () => commandResponse()); const bashTool = createAttachedWorkspaceBashTool({ diff --git a/packages/data-provider/src/config.spec.ts b/packages/data-provider/src/config.spec.ts index 1c94288c973..0f068bbe37d 100644 --- a/packages/data-provider/src/config.spec.ts +++ b/packages/data-provider/src/config.spec.ts @@ -584,7 +584,7 @@ describe('attached code environment user config schema', () => { expect(codeEnvironmentUserConfigSchema.parse({ limits: {} })).toEqual({ limits: {} }); }); - it.each([1, 15_000, CODE_ENVIRONMENT_ADMISSION_MAX_MS])( + it.each([1_000, 15_000, CODE_ENVIRONMENT_ADMISSION_MAX_MS])( 'accepts a bounded %i ms command admission allowance', (minCommandAdmissionMs) => { expect(codeEnvironmentUserConfigSchema.parse({ limits: { minCommandAdmissionMs } })).toEqual({ @@ -593,7 +593,7 @@ describe('attached code environment user config schema', () => { }, ); - it.each([0, -1, 0.5, CODE_ENVIRONMENT_ADMISSION_MAX_MS + 1, NaN, Infinity])( + it.each([0, -1, 0.5, 999, CODE_ENVIRONMENT_ADMISSION_MAX_MS + 1, NaN, Infinity])( 'rejects an invalid command admission allowance of %s', (minCommandAdmissionMs) => { expect( @@ -613,7 +613,7 @@ describe('attached code environment user config schema', () => { it.each([ { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 79_999 }, - { maxRequestTimeoutMs: 10_002, minCommandAdmissionMs: 1 }, + { maxRequestTimeoutMs: 11_001, minCommandAdmissionMs: 1_000 }, { maxRequestTimeoutMs: 610_000, minCommandAdmissionMs: 300_000 }, ])('accepts an admission reserve with execution time left: %j', (limits) => { expect(codeEnvironmentUserConfigSchema.parse({ limits })).toEqual({ limits }); @@ -622,7 +622,7 @@ describe('attached code environment user config schema', () => { it.each([ { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 80_000 }, { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 100_000 }, - { maxRequestTimeoutMs: 10_001, minCommandAdmissionMs: 1 }, + { maxRequestTimeoutMs: 11_000, minCommandAdmissionMs: 1_000 }, ])('rejects a command reserve that cannot fit inside its request budget: %j', (limits) => { const parsed = codeEnvironmentUserConfigSchema.safeParse({ limits }); expect(parsed.success).toBe(false); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 9d3f1c5472d..d026a4f2ce5 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1262,12 +1262,12 @@ export const codeEnvironmentUserConfigSchema = z .min(1) .max(CODE_ENVIRONMENT_REQUEST_TIMEOUT_HARD_MAX_MS) .optional(), - /** Minimum admission time left for a Bash command inside maxRequestTimeoutMs. - * Omission reserves ten seconds; ignored without a total HTTP budget. */ + /** Admission allowance before local dispatch overhead for a Bash command inside + * maxRequestTimeoutMs. Omission reserves ten seconds; ignored without a total HTTP budget. */ minCommandAdmissionMs: z .number() .int() - .min(1) + .min(1_000) .max(CODE_ENVIRONMENT_ADMISSION_MAX_MS) .optional(), }) From 4b3fb5d8af0fb0ae2a64e5a15cbbb87f82e8ab30 Mon Sep 17 00:00:00 2001 From: Lia Date: Tue, 29 Sep 2026 02:43:28 +0000 Subject: [PATCH 7/7] =?UTF-8?q?=E2=8F=B1=EF=B8=8F=20fix:=20Validate=20Defa?= =?UTF-8?q?ult=20Workspace=20Command=20Admission=20Reserve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- librechat.example.yaml | 3 +- packages/data-provider/src/config.spec.ts | 40 ++++++++++++++++------- packages/data-provider/src/config.ts | 8 +++-- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/librechat.example.yaml b/librechat.example.yaml index a241d6ad093..fcacb78c34c 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -734,7 +734,8 @@ endpoints: # # maxRequestTimeoutMs: 125000 # # Optional command admission allowance before local dispatch overhead. # # Omission reserves 10s; valid range: 1000–300000 ms. The HTTP budget - # # must exceed this allowance plus 10s settlement/delivery grace. + # # must exceed this allowance plus 10s settlement/delivery grace; + # # with the 10s default, a budget of 20s or less is rejected. # # Smaller allowances give a busy worker less time to accept the call. # # minCommandAdmissionMs: 10000 # permissions: diff --git a/packages/data-provider/src/config.spec.ts b/packages/data-provider/src/config.spec.ts index 0f068bbe37d..6ef043dbe16 100644 --- a/packages/data-provider/src/config.spec.ts +++ b/packages/data-provider/src/config.spec.ts @@ -602,14 +602,29 @@ describe('attached code environment user config schema', () => { }, ); - it('preserves omission of the command admission allowance alongside a request budget', () => { - expect( - codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs: 90_000 } }), - ).toEqual({ limits: { maxRequestTimeoutMs: 90_000 } }); - expect( - codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs: 5_000 } }), - ).toEqual({ limits: { maxRequestTimeoutMs: 5_000 } }); - }); + it.each([20_001, 90_000])( + 'preserves omission of the command admission allowance with a fitting %i ms budget', + (maxRequestTimeoutMs) => { + expect(codeEnvironmentUserConfigSchema.parse({ limits: { maxRequestTimeoutMs } })).toEqual({ + limits: { maxRequestTimeoutMs }, + }); + }, + ); + + it.each([5_000, 10_002, 15_000, 20_000])( + 'rejects an undersized %i ms request budget with the default command reserve', + (maxRequestTimeoutMs) => { + const parsed = codeEnvironmentUserConfigSchema.safeParse({ limits: { maxRequestTimeoutMs } }); + expect(parsed.success).toBe(false); + if (!parsed.success) { + expect(parsed.error.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ path: ['limits', 'maxRequestTimeoutMs'] }), + ]), + ); + } + }, + ); it.each([ { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 79_999 }, @@ -641,7 +656,10 @@ describe('attached code environment user config schema', () => { ).toEqual({ limits: { minCommandAdmissionMs: 300_000 } }); }); - it('rejects an impossible reserve in the top-level deployment config', () => { + it.each([ + { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 100_000 }, + { maxRequestTimeoutMs: 15_000 }, + ])('rejects an impossible command reserve in the top-level deployment config: %j', (limits) => { expect( configSchema.safeParse({ version: '1.0', @@ -655,9 +673,7 @@ describe('attached code environment user config schema', () => { name: 'Personal VM', type: 'attached', baseURL: 'https://code.example.com/v1', - configSchema: { - limits: { maxRequestTimeoutMs: 90_000, minCommandAdmissionMs: 100_000 }, - }, + configSchema: { limits }, }, ], }, diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index d026a4f2ce5..815ec69d87e 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1275,15 +1275,17 @@ export const codeEnvironmentUserConfigSchema = z .superRefine((limits, context) => { if ( limits.maxRequestTimeoutMs == null || - limits.minCommandAdmissionMs == null || limits.maxRequestTimeoutMs > - limits.minCommandAdmissionMs + CODE_ENVIRONMENT_COMMAND_BUDGET_GRACE_MS + (limits.minCommandAdmissionMs ?? CODE_ENVIRONMENT_COMMAND_ADMISSION_DEFAULT_MS) + + CODE_ENVIRONMENT_COMMAND_BUDGET_GRACE_MS ) { return; } context.addIssue({ code: z.ZodIssueCode.custom, - path: ['minCommandAdmissionMs'], + path: [ + limits.minCommandAdmissionMs == null ? 'maxRequestTimeoutMs' : 'minCommandAdmissionMs', + ], message: 'Command admission and settlement reserves must leave time for execution', }); })