From 8f1091da64f660827f38512cbbd073357445b138 Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 04:55:01 +0200 Subject: [PATCH] Let a Claude agent leave Auto mode when Claude Code uses Bedrock or Vertex (#6272) * fix(claude): let an agent leave Auto mode when Claude Code uses Bedrock or Vertex Changing the mode of a Claude agent whose process is not running starts the process first. It was started in the agent's current mode, so an agent in Auto on Bedrock or Vertex failed the Auto check before the new mode applied, and every mode change and message failed the same way. Start the process in the requested mode instead. Closes #5878 * fix(claude): relaunch a query whose mode change Claude Code rejected A mode change that starts the query now launches it in the requested mode. If Claude Code then rejects the mode, the agent keeps its previous mode, so mark that query for relaunch instead of reusing it in the rejected mode. --------- Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../providers/claude/agent.redesign.test.ts | 55 ++++++++++++++++++- .../server/agent/providers/claude/agent.ts | 21 ++++--- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts index 5069c3653f..747a466c32 100644 --- a/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts +++ b/packages/server/src/server/agent/providers/claude/agent.redesign.test.ts @@ -327,6 +327,57 @@ test("fails an auto mode turn when Claude Code uses Vertex", async () => { } }); +test("switches an auto mode session to another mode when Claude Code uses Bedrock", async () => { + const previousBedrock = process.env.CLAUDE_CODE_USE_BEDROCK; + process.env.CLAUDE_CODE_USE_BEDROCK = "1"; + const queryMock = createBaseQueryMock(vi.fn(async () => ({ done: true, value: undefined }))); + sdkQueryFactory.mockImplementation(() => queryMock); + const client = new ClaudeAgentClient({ + logger: createTestLogger(), + queryFactory: sdkQueryFactory, + resolveBinary: async () => "/test/claude/bin", + }); + const session = await client.createSession({ + provider: "claude", + cwd: process.cwd(), + modeId: "auto", + }); + + try { + await session.setMode("acceptEdits"); + + expect(await session.getCurrentMode()).toBe("acceptEdits"); + expect(sdkQueryFactory).toHaveBeenCalledTimes(1); + expect(sdkQueryFactory.mock.calls[0]?.[0]?.options?.permissionMode).toBe("acceptEdits"); + } finally { + restoreEnvValue("CLAUDE_CODE_USE_BEDROCK", previousBedrock); + await session.close(); + } +}); + +test("does not keep a query started in a mode that Claude Code rejected", async () => { + const launchedModes: Array = []; + sdkQueryFactory.mockImplementation(({ options }: { options: { permissionMode?: string } }) => { + launchedModes.push(options.permissionMode); + const queryMock = createBaseQueryMock(vi.fn(async () => ({ done: true, value: undefined }))); + if (launchedModes.length === 1) { + queryMock.setPermissionMode.mockRejectedValue(new Error("bypass refused")); + } + return queryMock; + }); + const session = await createSession(); + + try { + await expect(session.setMode("bypassPermissions")).rejects.toThrow("bypass refused"); + await session.listCommands(); + + expect(await session.getCurrentMode()).toBe("default"); + expect(launchedModes).toEqual(["bypassPermissions", "default"]); + } finally { + await session.close(); + } +}); + test("logs redacted query summary and never leaks sentinel secrets", async () => { const envSecret = "PASEO_ENV_SENTINEL_SECRET"; const runtimeSecret = "PASEO_RUNTIME_SENTINEL_SECRET"; @@ -1037,11 +1088,11 @@ test("preserves bypass capability across query restarts triggered by thinking ch expect(capturedOptions).toHaveLength(2); expect(capturedOptions[0]).toMatchObject({ - permissionMode: "default", + permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, }); expect(capturedOptions[1]).toMatchObject({ - permissionMode: "acceptEdits", + permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, effort: "high", }); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index f9f79862ad..ddac56a0eb 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -2441,8 +2441,15 @@ class ClaudeAgentSession implements AgentSession { const normalized = isPermissionMode(modeId) ? modeId : "default"; assertClaudeModeCanRun(normalized, this.harnessEnvironment); const previousMode = this.currentMode; - const activeQuery = await this.ensureQuery(); - await activeQuery.setPermissionMode(normalized); + const launchesQuery = !this.query || this.queryRestartNeeded; + const activeQuery = await this.ensureQuery(normalized); + try { + await activeQuery.setPermissionMode(normalized); + } catch (error) { + // The query was launched in the rejected mode; relaunch in the current mode next time. + if (launchesQuery) this.queryRestartNeeded = true; + throw error; + } if (normalized === "plan") { if (previousMode !== "plan") { this.planResumeMode = previousMode; @@ -3128,7 +3135,7 @@ class ClaudeAgentSession implements AgentSession { return { kind: "fresh-session" }; } - private async ensureQuery(): Promise { + private async ensureQuery(launchMode: PermissionMode = this.currentMode): Promise { if (this.query && !this.queryRestartNeeded) { return this.query; } @@ -3172,7 +3179,7 @@ class ClaudeAgentSession implements AgentSession { this.persistence = null; const input = createAsyncMessageInput(); - const options = await this.buildOptions(); + const options = await this.buildOptions(launchMode); this.logger.debug({ options: summarizeClaudeOptionsForLog(options) }, "claude query"); this.input = input; this.query = claudeQuery( @@ -3298,7 +3305,7 @@ class ClaudeAgentSession implements AgentSession { }); } - private async buildOptions(): Promise { + private async buildOptions(permissionMode: PermissionMode): Promise { const { thinking, effort, ultracode } = this.resolveThinkingConfig(); const appendedSystemPrompt = this.buildAppendedSystemPrompt(); const providerOptions = applyClaudeToolPolicy( @@ -3307,7 +3314,7 @@ class ClaudeAgentSession implements AgentSession { ); const settingsOptions = this.buildSettingsOptions(providerOptions, { ultracode }); const sdkEnv = this.harnessEnvironment; - assertClaudeModeCanRun(this.currentMode, sdkEnv); + assertClaudeModeCanRun(permissionMode, sdkEnv); const claudeBinary = await this.resolveBinary(); this.logger.debug( @@ -3330,7 +3337,7 @@ class ClaudeAgentSession implements AgentSession { const base: ClaudeOptions = { cwd: this.config.cwd, includePartialMessages: true, - permissionMode: this.currentMode, + permissionMode, // Dynamic mode switching can recreate the underlying Claude query. Keep the // bypass launch capability available so later setPermissionMode("bypassPermissions") // calls do not fail after a model/thinking/rewind-driven restart.