@@ -68,6 +68,43 @@ struct Cli {
6868 ce_machine_id : String ,
6969 #[ arg( long, default_value = "" ) ]
7070 ce_auth_token : String ,
71+
72+ // ──────────────── LLM transport / retry tunables ────────────────
73+ // Defaults match the v0.1.6/v0.1.7 frozen-policy values exactly; overrides are
74+ // for tuning the bench binary without rebuilding (e.g. raising the header
75+ // timeout for slow Kimi cold starts). Each accepts a CLI flag or env var.
76+ /// Header-arrival timeout (ms). 0 disables. Default 15000.
77+ #[ arg( long, env = "SUPERCODER_LLM_HEADER_TIMEOUT_MS" , default_value_t = 15_000 ) ]
78+ llm_header_timeout_ms : u64 ,
79+ /// Max LLM retry attempts after the initial call. Default 3.
80+ #[ arg( long, env = "SUPERCODER_LLM_MAX_RETRIES" , default_value_t = 3 ) ]
81+ llm_max_retries : u32 ,
82+ /// Initial backoff between LLM retries (ms). Default 1000.
83+ #[ arg( long, env = "SUPERCODER_LLM_RETRY_INITIAL_MS" , default_value_t = 1_000 ) ]
84+ llm_retry_initial_ms : u64 ,
85+ /// Backoff multiplier between LLM retries. Default 2.0.
86+ #[ arg( long, env = "SUPERCODER_LLM_RETRY_MULTIPLIER" , default_value_t = 2.0 ) ]
87+ llm_retry_multiplier : f64 ,
88+ /// Backoff cap between LLM retries (ms). Default 30000.
89+ #[ arg( long, env = "SUPERCODER_LLM_RETRY_MAX_MS" , default_value_t = 30_000 ) ]
90+ llm_retry_max_ms : u64 ,
91+
92+ // ──────────────── Tool tunables (bench ToolPolicy overrides) ────
93+ /// Bash timeout ceiling (ms). Default 300000 (5 min).
94+ #[ arg( long, env = "SUPERCODER_BASH_TIMEOUT_MS" , default_value_t = 300_000 ) ]
95+ bash_timeout_ms : u64 ,
96+ /// grep/glob wall timeout (ms). Default 60000.
97+ #[ arg( long, env = "SUPERCODER_SEARCH_TIMEOUT_MS" , default_value_t = 60_000 ) ]
98+ search_timeout_ms : u64 ,
99+ /// Default limit for codebase_search results when the model omits `limit`. Default 20.
100+ #[ arg( long, env = "SUPERCODER_CODEBASE_SEARCH_LIMIT" , default_value_t = 20 ) ]
101+ codebase_search_limit : u32 ,
102+ /// Per-chunk content cap for codebase_search results (bytes). Default 2048.
103+ #[ arg( long, env = "SUPERCODER_CODEBASE_SEARCH_CHUNK_BYTES" , default_value_t = 2048 ) ]
104+ codebase_search_chunk_bytes : usize ,
105+ /// Per-section render cap for codebase_graph. Default 50.
106+ #[ arg( long, env = "SUPERCODER_CODEBASE_GRAPH_SECTION_CAP" , default_value_t = 50 ) ]
107+ codebase_graph_section_cap : usize ,
71108}
72109
73110#[ derive( Copy , Clone , Debug , ValueEnum ) ]
@@ -202,19 +239,50 @@ async fn run(cli: &Cli, spec: &TaskSpec) -> RunResult {
202239 thinking : None ,
203240 disable_cache_control : false ,
204241 // Strict LLM transport policy: HTTP/1.1, no connection pooling, 15s
205- // header-arrival timeout. Matches opencode's working transport shape (which
206- // has zero decode deaths on the same router that gave bench-runner 26 on
207- // long Kimi runs). Part of the frozen harness identity. See LlmPolicy::bench().
208- policy : agent:: llm:: LlmPolicy :: bench ( ) ,
242+ // header-arrival timeout (CLI/env-tunable). Matches opencode's working
243+ // transport shape (which has zero decode deaths on the same router that gave
244+ // bench-runner 26 on long Kimi runs). http1_only + no_pool stay hardcoded
245+ // (they ARE the transport fix); the header timeout is exposed since slow
246+ // upstreams may legitimately exceed it.
247+ policy : {
248+ let mut p = agent:: llm:: LlmPolicy :: bench ( ) ;
249+ p. header_timeout_ms = if cli. llm_header_timeout_ms == 0 {
250+ None
251+ } else {
252+ Some ( cli. llm_header_timeout_ms )
253+ } ;
254+ p
255+ } ,
209256 } ;
210257
211258 let mut config = AgentConfig :: new ( llm, spec. working_dir . clone ( ) ) ;
212259 config. mode = ToolMode :: Coding ;
213260 config. max_iterations = spec. max_iterations ;
261+
262+ // Tune LLM retry behavior (CLI/env). Defaults match RetryConfig::default() (3
263+ // retries, 1s→2s→4s, cap 30s) — set --llm-retry-multiplier higher and
264+ // --llm-retry-max-ms wider when the upstream needs longer recovery windows
265+ // (e.g. proxy circuit-breaker open).
266+ config. retry_config = agent:: agent:: config:: RetryConfig {
267+ max_retries : cli. llm_max_retries ,
268+ initial_delay : std:: time:: Duration :: from_millis ( cli. llm_retry_initial_ms ) ,
269+ multiplier : cli. llm_retry_multiplier ,
270+ max_delay : std:: time:: Duration :: from_millis ( cli. llm_retry_max_ms ) ,
271+ } ;
272+
214273 // Strict tool policy is part of the frozen harness identity: bash timeout ceiling,
215- // grep/glob ignore-list + wall timeout, codebase_* result caps. Always on (the app
216- // keeps the permissive default). See ToolPolicy::bench().
217- config. tool_policy = agent:: tool:: ToolPolicy :: bench ( ) ;
274+ // grep/glob ignore-list + wall timeout, codebase_* result caps. The per-knob
275+ // values are CLI/env-tunable; defaults match ToolPolicy::bench() exactly. The
276+ // `search_default_ignores` flag stays hardcoded (it's the ignore-list behavior,
277+ // not a tuning value).
278+ config. tool_policy = agent:: tool:: ToolPolicy {
279+ bash_timeout_ceiling_ms : Some ( cli. bash_timeout_ms ) ,
280+ search_timeout_ms : Some ( cli. search_timeout_ms ) ,
281+ search_default_ignores : true ,
282+ codebase_search_limit : Some ( cli. codebase_search_limit ) ,
283+ codebase_search_chunk_bytes : Some ( cli. codebase_search_chunk_bytes ) ,
284+ codebase_graph_section_cap : Some ( cli. codebase_graph_section_cap ) ,
285+ } ;
218286 // Everything else (system_prompt, checkpoint_dir, skills, subagents, …) stays at the
219287 // AgentConfig::new defaults of None — headless.
220288
@@ -404,3 +472,61 @@ async fn extract_patch(working_dir: &PathBuf, base_commit: &str) -> Result<Strin
404472 . map_err ( |e| e. to_string ( ) ) ?;
405473 Ok ( diff. stdout )
406474}
475+
476+ #[ cfg( test) ]
477+ mod tests {
478+ use super :: Cli ;
479+ use clap:: Parser ;
480+
481+ /// Minimum args required to parse — model is the only required field.
482+ fn parse ( args : & [ & str ] ) -> Cli {
483+ let mut full = vec ! [ "bench-runner" , "--model" , "test" ] ;
484+ full. extend_from_slice ( args) ;
485+ Cli :: try_parse_from ( full) . expect ( "parse" )
486+ }
487+
488+ #[ test]
489+ fn cli_defaults_match_frozen_policy ( ) {
490+ // Defaults must NOT change behavior vs the hardcoded v0.1.6 LlmPolicy::bench()
491+ // and ToolPolicy::bench() values. If this test fails the eval team's previous
492+ // runs are not reproducible.
493+ let cli = parse ( & [ ] ) ;
494+ assert_eq ! ( cli. llm_header_timeout_ms, 15_000 ) ;
495+ assert_eq ! ( cli. llm_max_retries, 3 ) ;
496+ assert_eq ! ( cli. llm_retry_initial_ms, 1_000 ) ;
497+ assert_eq ! ( cli. llm_retry_multiplier, 2.0 ) ;
498+ assert_eq ! ( cli. llm_retry_max_ms, 30_000 ) ;
499+ assert_eq ! ( cli. bash_timeout_ms, 300_000 ) ;
500+ assert_eq ! ( cli. search_timeout_ms, 60_000 ) ;
501+ assert_eq ! ( cli. codebase_search_limit, 20 ) ;
502+ assert_eq ! ( cli. codebase_search_chunk_bytes, 2048 ) ;
503+ assert_eq ! ( cli. codebase_graph_section_cap, 50 ) ;
504+ }
505+
506+ #[ test]
507+ fn cli_flag_overrides ( ) {
508+ let cli = parse ( & [
509+ "--llm-header-timeout-ms" , "90000" ,
510+ "--llm-retry-multiplier" , "3.0" ,
511+ "--llm-retry-max-ms" , "60000" ,
512+ "--bash-timeout-ms" , "600000" ,
513+ "--codebase-search-limit" , "10" ,
514+ ] ) ;
515+ assert_eq ! ( cli. llm_header_timeout_ms, 90_000 ) ;
516+ assert_eq ! ( cli. llm_retry_multiplier, 3.0 ) ;
517+ assert_eq ! ( cli. llm_retry_max_ms, 60_000 ) ;
518+ assert_eq ! ( cli. bash_timeout_ms, 600_000 ) ;
519+ assert_eq ! ( cli. codebase_search_limit, 10 ) ;
520+ // Untouched flags keep their defaults
521+ assert_eq ! ( cli. llm_max_retries, 3 ) ;
522+ assert_eq ! ( cli. codebase_search_chunk_bytes, 2048 ) ;
523+ }
524+
525+ #[ test]
526+ fn header_timeout_zero_means_disabled ( ) {
527+ // Per the CLI doc: 0 disables the header timeout entirely. The run path
528+ // converts this to LlmPolicy::header_timeout_ms = None.
529+ let cli = parse ( & [ "--llm-header-timeout-ms" , "0" ] ) ;
530+ assert_eq ! ( cli. llm_header_timeout_ms, 0 ) ;
531+ }
532+ }
0 commit comments