From cc85c5ff0348c8e781f9a326ee39dc60b550d9be Mon Sep 17 00:00:00 2001 From: lishuceo Date: Tue, 19 May 2026 23:41:27 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20agent=20maxBudgetUsd/maxTurns=20fall?= =?UTF-8?q?back=20=E4=BD=BF=E7=94=A8=20config.claude=20=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E7=A1=AC=E7=BC=96=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未配置 maxBudgetUsd 的 agent(如 pm)原先回退到硬编码 $5,导致 CLAUDE_MAX_BUDGET_USD env 完全失效。修复后回退顺序:agent 显式 > 文件 defaults > config.claude(env)。 maxTurns 同步修复。 Co-Authored-By: Claude Opus 4.7 --- src/agent/config-loader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent/config-loader.ts b/src/agent/config-loader.ts index 785af2da..7ab06a93 100644 --- a/src/agent/config-loader.ts +++ b/src/agent/config-loader.ts @@ -92,8 +92,8 @@ function mergeAgentConfig(input: AgentConfigInput, defaults: AgentDefaults): Age toolPolicy, readOnly, settingSources: (input.settingSources ?? defaults.settingSources ?? BUILTIN_DEFAULTS.settingSources!) as ('user' | 'project')[], - maxBudgetUsd: input.maxBudgetUsd ?? defaults.maxBudgetUsd ?? BUILTIN_DEFAULTS.maxBudgetUsd!, - maxTurns: input.maxTurns ?? defaults.maxTurns ?? BUILTIN_DEFAULTS.maxTurns!, + maxBudgetUsd: input.maxBudgetUsd ?? defaults.maxBudgetUsd ?? config.claude.maxBudgetUsd, + maxTurns: input.maxTurns ?? defaults.maxTurns ?? config.claude.maxTurns, requiresApproval: input.requiresApproval ?? defaults.requiresApproval ?? BUILTIN_DEFAULTS.requiresApproval!, replyMode: input.replyMode ?? defaults.replyMode ?? BUILTIN_DEFAULTS.replyMode! as 'direct' | 'thread', persona: input.persona ?? defaults.persona, From 018683b8ea9b4f436701d7db85937106bce724a0 Mon Sep 17 00:00:00 2001 From: lishuceo Date: Tue, 19 May 2026 23:41:33 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20agent=20budget/turns=20fallback=20?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖: - 缺省时回退到 config.claude.maxBudgetUsd / maxTurns - 文件级 defaults 优先于 config.claude - agent 级显式值优先于 defaults 和 config.claude Co-Authored-By: Claude Opus 4.7 --- src/agent/__tests__/config-loader.test.ts | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/agent/__tests__/config-loader.test.ts b/src/agent/__tests__/config-loader.test.ts index 01e8b00a..cf8dec97 100644 --- a/src/agent/__tests__/config-loader.test.ts +++ b/src/agent/__tests__/config-loader.test.ts @@ -340,3 +340,74 @@ describe('editablePathPatterns', () => { expect(cfg!.editablePathPatterns).toBeUndefined(); }); }); + +describe('maxBudgetUsd / maxTurns fallback', () => { + beforeEach(() => { + vi.resetModules(); + mkdirSync(KNOWLEDGE_DIR, { recursive: true }); + }); + + afterEach(() => { + rmSync(TEST_DIR, { recursive: true, force: true }); + }); + + // Regression: agents without an explicit maxBudgetUsd/maxTurns previously fell + // back to hardcoded BUILTIN_DEFAULTS (5 / 100), ignoring CLAUDE_MAX_BUDGET_USD env. + // Triggered "Reached maximum budget ($5)" for pm agent in prod. + async function setupWithClaudeConfig(configObj: Record, claudeOverrides: Record) { + writeFileSync(CONFIG_FILE, JSON.stringify(configObj)); + vi.stubEnv('AGENT_CONFIG_PATH', CONFIG_FILE); + vi.doMock('../../config.js', () => ({ + config: { + agent: { configPath: CONFIG_FILE }, + claude: { model: 'claude-sonnet-4-6', maxBudgetUsd: 5, maxTurns: 100, ...claudeOverrides }, + }, + })); + const loader = await import('../config-loader.js'); + const result = loader.loadAgentConfig(); + expect(result.loaded).toBe(true); + return loader; + } + + it('should inherit maxBudgetUsd from config.claude when agent omits it', async () => { + await setupWithClaudeConfig( + { agents: [{ id: 'pm' }] }, + { maxBudgetUsd: 50 }, + ); + + const { agentRegistry } = await import('../registry.js'); + const cfg = agentRegistry.get('pm'); + expect(cfg!.maxBudgetUsd).toBe(50); + }); + + it('should inherit maxTurns from config.claude when agent omits it', async () => { + await setupWithClaudeConfig( + { agents: [{ id: 'pm' }] }, + { maxTurns: 500 }, + ); + + const { agentRegistry } = await import('../registry.js'); + const cfg = agentRegistry.get('pm'); + expect(cfg!.maxTurns).toBe(500); + }); + + it('should let file-level defaults override config.claude fallback', async () => { + await setupWithClaudeConfig( + { defaults: { maxBudgetUsd: 20 }, agents: [{ id: 'pm' }] }, + { maxBudgetUsd: 50 }, + ); + + const { agentRegistry } = await import('../registry.js'); + expect(agentRegistry.get('pm')!.maxBudgetUsd).toBe(20); + }); + + it('should let agent-level value override both defaults and config.claude', async () => { + await setupWithClaudeConfig( + { defaults: { maxBudgetUsd: 20 }, agents: [{ id: 'pm', maxBudgetUsd: 100 }] }, + { maxBudgetUsd: 50 }, + ); + + const { agentRegistry } = await import('../registry.js'); + expect(agentRegistry.get('pm')!.maxBudgetUsd).toBe(100); + }); +});