From 38e08da7558b28aaeadca60819d8661a1345ef79 Mon Sep 17 00:00:00 2001 From: Fsocietyhhh <1211904451@qq.com> Date: Mon, 27 Apr 2026 23:52:50 -0700 Subject: [PATCH 1/2] feat(config): make per-turn spend limit user-configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hard \$0.25 per-turn spend ceiling in loop.ts has been hard-coded since it was added. It's a safe default for typical chat, but it routinely fires for legitimate workloads where a single user prompt triggers expensive paid tools (image-to-image with gpt-image-2, multi-step VideoGen, large WebFetch+research turns), even though the user is fully aware they're spending more. When that happens the user gets: ⚠️ Turn spend limit reached (\$0.269 > \$0.25). Stopping to protect your wallet. Try again with a clearer prompt or a different model. …and there's no way out short of editing the source. This change introduces a new config key \`max-turn-spend-usd\` so the ceiling can be raised (or removed) per-user without forking: \`\`\`bash franklin config set max-turn-spend-usd 1.0 # bump to \$1 franklin config set max-turn-spend-usd 0 # disable entirely franklin config unset max-turn-spend-usd # back to default \$0.25 \`\`\` Behavior: - Unset / non-numeric → keeps the original \$0.25 default - 0 or negative → cap removed (Infinity), warning never fires - Positive number → that ceiling The existing warning text in the loop already shows the active value, so users always see exactly what their limit is. Scope deliberately small — config schema + one read at turn start. No change to the breaker logic, the warning message, or any other spend-tracking behavior. --- src/agent/loop.ts | 14 +++++++++++++- src/commands/config.ts | 8 ++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/agent/loop.ts b/src/agent/loop.ts index e99da715..c2af5152 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -17,6 +17,7 @@ import { resetToolSessionState } from '../tools/index.js'; import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js'; import { createActivateToolCapability } from '../tools/activate.js'; import { recordUsage } from '../stats/tracker.js'; +import { loadConfig } from '../commands/config.js'; import { recordSessionUsage } from '../stats/session-tracker.js'; import { appendAudit, extractLastUserPrompt } from '../stats/audit.js'; import { estimateCost, OPUS_PRICING } from '../pricing.js'; @@ -546,7 +547,18 @@ export async function interactiveSession( let consecutiveTinyResponses = 0; // Count of consecutive calls with <10 output tokens const MAX_TINY_RESPONSES = 2; // Break after N tiny responses — if 2 calls return near-empty, something is wrong let turnSpend = 0; // Cost spent this user turn (USD) - const MAX_TURN_SPEND_USD = 0.25; // Hard circuit breaker per user message (lowered — user wallets are real money) + // Hard circuit breaker per user message — defends user wallets against + // a runaway model+tool combo on a single prompt. User-overridable via + // `franklin config set max-turn-spend-usd `. A value of "0" + // (or negative / non-numeric) disables the cap entirely. + const turnSpendCap = (() => { + const raw = loadConfig()['max-turn-spend-usd']; + if (raw == null) return 0.25; + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) return Infinity; + return parsed; + })(); + const MAX_TURN_SPEND_USD = turnSpendCap; // ── Turn analysis (one classifier call, drives routing + prefetch) ── // Single LLM pass that answers every routing-adjacent question the diff --git a/src/commands/config.ts b/src/commands/config.ts index 3c3c7702..2411e4c0 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -14,6 +14,7 @@ const VALID_KEYS = [ 'smart-routing', 'permission-mode', 'max-turns', + 'max-turn-spend-usd', 'auto-compact', 'session-save', 'debug', @@ -29,6 +30,13 @@ export interface AppConfig { 'smart-routing'?: string; 'permission-mode'?: string; 'max-turns'?: string; + /** + * Hard per-turn spend ceiling in USD (default $0.25). Numeric string, + * e.g. "0.5" or "2". Set to "0" to disable the cap. The agent loop + * stops a turn the moment cumulative cost crosses this threshold, + * preventing a runaway model + tool combo from draining the wallet. + */ + 'max-turn-spend-usd'?: string; 'auto-compact'?: string; 'session-save'?: string; 'debug'?: string; From 59ed2a9c44540931a9fbe61ad0b6204c26bb9262 Mon Sep 17 00:00:00 2001 From: 1bcMax <195689928+1bcMax@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:22:34 -0400 Subject: [PATCH 2/2] fix(config): non-numeric max-turn-spend-usd should keep default, not disable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous code branched typos (NaN) into Infinity, silently removing the wallet guard. A circuit breaker should fail safe — only an explicit '0' or negative value opts out; anything unparseable falls back to the $0.25 default. --- src/agent/loop.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/agent/loop.ts b/src/agent/loop.ts index c2af5152..997200da 100644 --- a/src/agent/loop.ts +++ b/src/agent/loop.ts @@ -549,13 +549,16 @@ export async function interactiveSession( let turnSpend = 0; // Cost spent this user turn (USD) // Hard circuit breaker per user message — defends user wallets against // a runaway model+tool combo on a single prompt. User-overridable via - // `franklin config set max-turn-spend-usd `. A value of "0" - // (or negative / non-numeric) disables the cap entirely. + // `franklin config set max-turn-spend-usd `. Explicit "0" or a + // negative number disables the cap; a non-numeric / unparseable value + // is treated as a typo and falls back to the safe default rather than + // silently removing the wallet guard. const turnSpendCap = (() => { const raw = loadConfig()['max-turn-spend-usd']; if (raw == null) return 0.25; const parsed = Number(raw); - if (!Number.isFinite(parsed) || parsed <= 0) return Infinity; + if (!Number.isFinite(parsed)) return 0.25; // typo → keep default + if (parsed <= 0) return Infinity; // explicit opt-out return parsed; })(); const MAX_TURN_SPEND_USD = turnSpendCap;