From 7d95007cc301647c4e45421279662f36b700b82a Mon Sep 17 00:00:00 2001 From: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> Date: Wed, 2 Sep 2026 08:25:29 -0700 Subject: [PATCH] accumulator: size prompts conservatively for runner limits Signed-off-by: Sol <478bb5a31222ea2b28a3d1afb8b1d598940628f19c2a87efc3c4b822299eeec6@buzz.block.builderlab.xyz> --- crates/buzz-accumulator/src/estimate.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/crates/buzz-accumulator/src/estimate.rs b/crates/buzz-accumulator/src/estimate.rs index 1136bd57de..0659198ef8 100644 --- a/crates/buzz-accumulator/src/estimate.rs +++ b/crates/buzz-accumulator/src/estimate.rs @@ -80,10 +80,19 @@ pub struct ContextBudget { /// Tokens the planned input may use (window − reservations, or the /// fallback for an unknown model). pub input_budget_tokens: u64, - /// The same budget in chars (tokens × 4), the unit planning works in. + /// The planning budget in characters, conservatively derived from tokens. + /// This is intentionally smaller than the chars/4 display estimate. pub input_budget_chars: usize, } +/// Conservative characters-per-token planning ratio. Fold inputs contain +/// hashes, URLs, source code, JSON, and markdown—not just English prose—so the +/// common chars/4 display heuristic can materially undercount the tokens the +/// Claude CLI actually receives. Planning at 3 chars/token keeps that heuristic +/// for estimates while preventing a preflight-approved prompt from crossing +/// the runner's real context boundary. +pub const PLANNING_CHARS_PER_TOKEN: usize = 3; + /// Compute the planning budget for `model`. /// /// Known window: `window − reserved output − safety margin`, in chars, capped @@ -93,12 +102,13 @@ pub fn context_budget(model: &str) -> ContextBudget { match MODEL_WINDOWS.iter().find(|(m, _)| *m == model) { Some((_, window)) => { let tokens = window.saturating_sub(RESERVED_OUTPUT_TOKENS + SAFETY_MARGIN_TOKENS); - let chars = ((tokens as usize) * 4).min(EMERGENCY_MAX_INPUT_CHARS); + let chars = + ((tokens as usize) * PLANNING_CHARS_PER_TOKEN).min(EMERGENCY_MAX_INPUT_CHARS); ContextBudget { model_window: Some(*window), reserved_output_tokens: RESERVED_OUTPUT_TOKENS, safety_margin_tokens: SAFETY_MARGIN_TOKENS, - input_budget_tokens: (chars / 4) as u64, + input_budget_tokens: tokens, input_budget_chars: chars, } } @@ -186,10 +196,13 @@ mod tests { b.input_budget_tokens, 200_000 - RESERVED_OUTPUT_TOKENS - SAFETY_MARGIN_TOKENS ); - assert_eq!(b.input_budget_chars, b.input_budget_tokens as usize * 4); + assert_eq!( + b.input_budget_chars, + b.input_budget_tokens as usize * PLANNING_CHARS_PER_TOKEN + ); assert!( - b.input_budget_chars > 4 * FALLBACK_INPUT_BUDGET_CHARS, - "a 200k window must not be capped near the 30k-token fallback" + b.input_budget_chars > 3 * FALLBACK_INPUT_BUDGET_CHARS, + "a 200k window must remain materially larger than the fallback" ); assert!(b.input_budget_chars <= EMERGENCY_MAX_INPUT_CHARS); }