workflow: make the shared token budget opt-in instead of a silent 120k cap

[workflow].default_token_budget defaulted to 120_000 and was applied to a
whole run plus all of its children. A fan-out doing honest recon burns ~30k
per child, so runs died at the limit with no hand-back — the failure mode is
strictly worse than overrun, and the model never asked for the cap (#6189).
The parent turn loop already treats its token budget as advisory; the
default now matches that policy instead of splitting it.

0 now means "no shared cap" end to end: the launch-time fallback already
read 0 as unset, so the only real work is in the approval layer, where the
threshold did double duty as the "high budget" baseline. The flag now only
exists when a baseline is configured, and the card's budget label reports
the effective value (requested, else configured default, else "unbounded")
rather than the requested one. Explicit budgets — tool `token_budget`, spec
`budget.max_tokens`, or a configured default — still enforce.

Tests: cargo test -p codewhale-config -p codewhale-workflow ->
       704 + 265 passed, 0 failed
       scripts/dev-test.sh crates/tui/src/tools/workflow_plan_approval.rs ->
       tools:: 2196 passed, 1 failed (pre-change assert on the old default;
       updated, re-ran: workflow_plan_approval 13 passed, 0 failed)
       cargo check -p codewhale-tui --lib clean
This commit is contained in:
CodeWhale Bot committed 2026-09-15 01:10:07 -07:00
1 parent 74703422f2
commit a06d229652
7 files changed
+45 -15

No files matched your search

+7 -1
View File
@@ -2475,6 +2475,8 @@ pub struct WorkflowConfigToml {
#[serde(default = "default_workflow_max_depth")]
pub max_depth: u32,
/// Default shared token budget for a Workflow run and its children.
/// `0` applies no shared cap — the run is advisory-only like the parent
/// turn loop — while any positive value is enforced across the run.
#[serde(default = "default_workflow_default_token_budget")]
pub default_token_budget: u64,
/// How many parallel write children may share the parent worktree without
@@ -2521,7 +2523,11 @@ fn default_workflow_max_depth() -> u32 {
}
fn default_workflow_default_token_budget() -> u64 {
120_000
// Off by default: a cap the caller never asked for must not throttle a
// run — a 120k default silently killed real fan-outs mid-task (#6189).
// Spend discipline stays available as an explicit opt-in (tool
// `token_budget`, spec `budget.max_tokens`, or a configured value here).
0
}
fn default_workflow_max_parallel_writes_without_worktree() -> u32 {
+3 -1
View File
@@ -8252,7 +8252,9 @@ fn workflow_config_defaults_match_product_surface() {
assert_eq!(defaults.max_children, 1000);
assert_eq!(defaults.max_concurrent, 16);
assert_eq!(defaults.max_depth, 5);
assert_eq!(defaults.default_token_budget, 120_000);
// 0 = no shared cap; budgets are opt-in, matching the parent turn loop's
// advisory policy (#6189).
assert_eq!(defaults.default_token_budget, 0);
assert_eq!(defaults.max_parallel_writes_without_worktree, 0);
assert!(defaults.persist_completed_activity);
assert!(defaults.persist_completed_across_restarts);
+1 -1
View File
@@ -275,7 +275,7 @@ pub(in crate::commands) fn workflow_settings(app: &App) -> CommandResult {
cfg.max_children, cfg.max_concurrent, cfg.max_depth
),
format!(
"default_token_budget = {} · shared admission hint for a run and its children",
"default_token_budget = {} · shared admission cap for a run and its children (0 = none)",
cfg.default_token_budget
),
format!(
+1 -1
View File
@@ -1119,7 +1119,7 @@ impl ToolSpec for WorkflowTool {
"token_budget": {
"type": "integer",
"minimum": 1,
"description": "Optional shared Workflow admission hint. Usage is reconciled when children report completion; already-running parallel children can take aggregate spent past the hint, while later and descendant spawns are rejected once exhausted."
"description": "Optional shared Workflow admission cap; omit for no cap. Usage is reconciled when children report completion; already-running parallel children can take aggregate spent past the cap, while later and descendant spawns are rejected once exhausted."
},
"wait": {
"type": "boolean",
+25 -6
View File
@@ -184,7 +184,12 @@ pub fn analyze_workflow_plan_approval_with_config(
let network = script_suggests_network(script);
let worktree = script.contains("worktree") || script.contains("isolation");
let token_budget = optional_u64(input, "token_budget");
let high_budget = token_budget.is_some_and(|b| b > config.default_token_budget);
// The card reports what the run will actually get: the caller's budget,
// else the configured default when one is set — 0 means none applies.
let effective_budget = token_budget
.filter(|b| *b > 0)
.or((config.default_token_budget > 0).then_some(config.default_token_budget));
let high_budget = is_high_budget(token_budget, config);
// Unknown script authority: always elevated so the card is required.
let elevated = true;
let mut reasons = vec!["script_or_source".to_string()];
@@ -224,7 +229,7 @@ pub fn analyze_workflow_plan_approval_with_config(
high_budget,
broader_authority: false,
token_budget,
budget_label: budget_label(token_budget, high_budget),
budget_label: budget_label(effective_budget, high_budget),
elevated,
reasons,
}
@@ -432,7 +437,7 @@ fn analyze_plan_object(
worktree = true;
}
let high_budget = token_budget.is_some_and(|b| b > config.default_token_budget);
let high_budget = is_high_budget(token_budget, config);
let mut reasons = Vec::new();
if writes {
reasons.push("writes".into());
@@ -709,11 +714,17 @@ fn optional_u64(value: &Value, key: &str) -> Option<u64> {
value.get(key).and_then(Value::as_u64)
}
/// A budget is "high" only against a configured baseline; when
/// `[workflow].default_token_budget` is 0 (the default) nothing is high.
fn is_high_budget(token_budget: Option<u64>, config: &WorkflowConfigToml) -> bool {
config.default_token_budget > 0 && token_budget.is_some_and(|b| b > config.default_token_budget)
}
fn budget_label(token_budget: Option<u64>, high_budget: bool) -> String {
match token_budget {
Some(n) if high_budget => format!("{n} tokens (high)"),
Some(n) => format!("{n} tokens"),
None => "default".to_string(),
None => "unbounded".to_string(),
}
}
@@ -934,12 +945,20 @@ mod tests {
"children": [{ "prompt": "scan", "type": "explore" }]
}
});
let summary = analyze_workflow_plan_approval(&input);
// With no configured baseline (the default) nothing is "high" — the
// flag only exists relative to an operator-set cap (#6189).
let uncapped = analyze_workflow_plan_approval(&input);
assert!(!uncapped.high_budget, "{uncapped:?}");
let config = WorkflowConfigToml {
default_token_budget: 120_000,
..WorkflowConfigToml::default()
};
let summary = analyze_workflow_plan_approval_with_config(&input, &config);
assert!(summary.high_budget, "{summary:?}");
assert!(summary.elevated);
assert!(summary.budget_label.contains("high"));
assert_eq!(
workflow_approval_requirement_for(&input, &config()),
workflow_approval_requirement_for(&input, &config),
ApprovalRequirement::Required
);
}
+7 -4
View File
@@ -11,8 +11,9 @@ use crate::{
leaf_is_write_capable, leaf_wants_worktree,
};
/// Default soft token budget from product config (`[workflow].default_token_budget`).
/// Plans requesting more than this are treated as high-budget.
/// Fallback high-budget flag threshold for approval display when a caller
/// does not thread `[workflow].default_token_budget` through. Display-only:
/// it never caps a run.
pub const DEFAULT_HIGH_BUDGET_THRESHOLD: u64 = 120_000;
/// Options that refine elevation assessment beyond the IR itself.
@@ -20,7 +21,8 @@ pub const DEFAULT_HIGH_BUDGET_THRESHOLD: u64 = 120_000;
pub struct ElevationOptions {
/// Token budget declared on the tool call (may outrank `spec.budget`).
pub token_budget: Option<u64>,
/// Threshold above which a token budget is considered high.
/// Threshold above which a token budget is considered high. `0` disables
/// the flag — with no configured baseline there is nothing to exceed.
pub high_budget_threshold: u64,
/// Whether the parent session currently allows writes.
pub parent_allows_write: bool,
@@ -143,7 +145,8 @@ pub fn assess_workflow_elevation(
.token_budget
.or(spec.budget.max_tokens)
.filter(|n| *n > 0);
let high_budget = effective_tokens.is_some_and(|n| n > options.high_budget_threshold);
let high_budget = options.high_budget_threshold > 0
&& effective_tokens.is_some_and(|n| n > options.high_budget_threshold);
let broader_authority =
(!options.parent_allows_write && writes) || (!options.parent_allows_network && network);
+1 -1
View File
@@ -49,7 +49,7 @@ request orchestration explicitly.
| `require_approval_for_writes` | `true` | Gates the plan-approval card for writes / elevated starts |
| `auto_start_child_limit` | `16` | Soft cap on automatic child count |
| `max_children` / `max_concurrent` / `max_depth` | `1000` / `16` / `5` | Task count, concurrent children, and plan structure ceilings |
| `default_token_budget` | `120000` | Shared admission hint; not an exact mid-stream cutoff |
| `default_token_budget` | `0` | Shared admission cap for a run and its children; `0` = none — set it or pass `token_budget` on the call to bound spend |
| `persist_completed_activity` | `true` | Keep completed panel/history activity |
Elevated work (writes, shell beyond read-only, network, secrets, worktrees, high