From cd21201bdd99ce4f8fc6b8a2fc7b88d7bbb87938 Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 12:07:32 +0000 Subject: [PATCH 1/6] feat(config): add [reasoning_only] section for retry count and custom reprompt message --- crates/tui/src/config.rs | 56 +++++++++++++++++++++++++ crates/tui/src/config/tests.rs | 56 +++++++++++++++++++++++++ crates/tui/src/core/engine.rs | 14 ++++++- crates/tui/src/core/engine/tests.rs | 4 +- crates/tui/src/core/engine/turn_loop.rs | 38 +++++++++++++---- crates/tui/src/exec_agent.rs | 2 + crates/tui/src/runtime_threads.rs | 2 + crates/tui/src/tui/ui/frame.rs | 2 + docs/CONFIGURATION.md | 33 +++++++++++++++ 9 files changed, 194 insertions(+), 13 deletions(-) diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 4487270590..5d2f26e34a 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -47,6 +47,14 @@ pub(crate) use codewhale_config::{ConfigApiKeyValueKind, classify_config_api_key pub const DEFAULT_ZAI_PROVIDER_MAX_CONCURRENCY: usize = 3; pub const MAX_PROVIDER_REQUEST_CONCURRENCY: usize = 64; +/// Default maximum number of automatic re-requests when a reasoning model +/// returns only hidden thinking without any answer text or tool call. +pub const DEFAULT_REASONING_ONLY_REPROMPTS: u32 = 2; + +/// Default custom message sent to the model on each re-request. +/// Used when the user does not set `[reasoning_only] reprompt_message`. +pub const DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE: &str = "So, what's up ? Keep running !"; + pub fn default_stop_words() -> Vec { ["stop", "wait", "pause"] .into_iter() @@ -2307,6 +2315,24 @@ pub struct GoalConfig { pub continuation_delay_seconds: Option, } +/// Reasoning-only recovery controls (`[reasoning_only]` table in config.toml). +/// When the model returns only hidden reasoning (thinking) without any answer +/// text or tool call, the engine can re-request the answer automatically. +#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq)] +#[serde(default)] +pub struct ReasoningOnlyConfig { + /// Maximum number of automatic re-requests when the model returns only + /// reasoning without any answer or tool call. + /// Defaults to 2. Set to 0 to disable automatic recovery. + pub max_reprompts: Option, + /// Optional custom message sent to the model on each re-request. + /// When set, the engine inserts this as a user message before re-issuing + /// the request, nudging the model to produce an actual answer. + /// When unset (default), the engine uses the built-in default message + /// ("So, what's up ? Keep running !"). + pub reprompt_message: Option, +} + /// One configurable footer item. /// /// Order in the user's `Vec` is preserved: items in the left @@ -3178,6 +3204,12 @@ pub struct Config { #[serde(default)] pub goal: Option, + /// Reasoning-only recovery controls. When absent, the engine uses the + /// built-in default (2 retries, no custom message). Configure with + /// `[reasoning_only] max_reprompts` and/or `[reasoning_only] reprompt_message`. + #[serde(default)] + pub reasoning_only: Option, + /// User-level memory (#489). Default behaviour is **opt-in**: /// loading + injection happens only when `[memory] enabled = true` or /// `DEEPSEEK_MEMORY=on` is set. The surviving store is the native @@ -7190,6 +7222,29 @@ impl Config { .min(crate::goal_loop::MAX_GOAL_CONTINUATION_DELAY_SECONDS) } + /// Maximum number of automatic re-requests when the model returns only + /// reasoning without any answer or tool call. Defaults to 2. + /// Set via `[reasoning_only] max_reprompts` in config.toml. + #[must_use] + pub fn reasoning_only_max_reprompts(&self) -> u32 { + self.reasoning_only + .as_ref() + .and_then(|cfg| cfg.max_reprompts) + .unwrap_or(DEFAULT_REASONING_ONLY_REPROMPTS) + } + + /// Optional custom message sent to the model on each re-request when the + /// model returns only reasoning without any answer or tool call. + /// Set via `[reasoning_only] reprompt_message` in config.toml. + /// When unset, the built-in default is used. + #[must_use] + pub fn reasoning_only_reprompt_message(&self) -> &str { + self.reasoning_only + .as_ref() + .and_then(|cfg| cfg.reprompt_message.as_deref()) + .unwrap_or(DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE) + } + /// Resolve the explicit local-memory backend. #[must_use] pub fn memory_backend(&self) -> MemoryBackend { @@ -10582,6 +10637,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config { runtime_thread_inference_unrelated: override_cfg.runtime_thread_inference_unrelated || base.runtime_thread_inference_unrelated, mini_window: override_cfg.mini_window.or(base.mini_window), + reasoning_only: override_cfg.reasoning_only.or(base.reasoning_only), title: override_cfg.title.or(base.title), } } diff --git a/crates/tui/src/config/tests.rs b/crates/tui/src/config/tests.rs index f44facbc4c..5cb476ff40 100644 --- a/crates/tui/src/config/tests.rs +++ b/crates/tui/src/config/tests.rs @@ -257,6 +257,62 @@ continuation_delay_seconds = 999999999 Ok(()) } +#[test] +fn reasoning_only_config_loads_from_table() -> Result<()> { + // Absent table → built-in defaults. + let config: Config = toml::from_str("")?; + assert_eq!( + config.reasoning_only_max_reprompts(), + crate::config::DEFAULT_REASONING_ONLY_REPROMPTS + ); + assert_eq!( + config.reasoning_only_reprompt_message(), + crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE + ); + + // Explicit max_reprompts override. + let config: Config = toml::from_str( + r#" +[reasoning_only] +max_reprompts = 10 +"#, + )?; + assert_eq!(config.reasoning_only_max_reprompts(), 10); + assert_eq!( + config.reasoning_only_reprompt_message(), + crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE + ); + + // Explicit reprompt_message override. + let config: Config = toml::from_str( + r#" +[reasoning_only] +max_reprompts = 5 +reprompt_message = "tu n'as rien a dire" +"#, + )?; + assert_eq!(config.reasoning_only_max_reprompts(), 5); + assert_eq!( + config.reasoning_only_reprompt_message(), + "tu n'as rien a dire" + ); + + // 0 = disable automatic recovery. + let config: Config = toml::from_str( + r#" +[reasoning_only] +max_reprompts = 0 +"#, + )?; + assert_eq!(config.reasoning_only_max_reprompts(), 0); + assert_eq!( + config.reasoning_only_reprompt_message(), + crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE + ); + + Ok(()) +} + #[test] fn modelstudio_coding_plan_mode_resolves_the_official_chat_base_url() { // The picker represents Coding Plan as the primary Model Studio provider diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 87e43a5714..9efe73a878 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -397,6 +397,16 @@ pub struct EngineConfig { /// immediately; positive values opt coordinator goals into a cancellable /// quiet period (#5508). pub goal_continuation_delay_seconds: u64, + /// Maximum number of automatic re-requests when the model returns only + /// reasoning without any answer or tool call. Defaults to 2. + /// Resolved from `[reasoning_only] max_reprompts` in config.toml. + pub reasoning_only_max_reprompts: u32, + /// Optional custom message sent to the model on each re-request when the + /// model returns only reasoning without any answer or tool call. + /// When set, the engine inserts this as a user message before re-issuing + /// the request, nudging the model to produce an actual answer. + /// Resolved from `[reasoning_only] reprompt_message` in config.toml. + pub reasoning_only_reprompt_message: Option, /// Tool restriction from custom slash command frontmatter. /// `None` means the current turn may use the normal tool set. pub allowed_tools: Option>, @@ -557,6 +567,8 @@ impl Default for EngineConfig { goal_status: GoalStatus::Active, goal_max_continuations: crate::goal_loop::DEFAULT_MAX_GOAL_CONTINUATIONS, goal_continuation_delay_seconds: 0, + reasoning_only_max_reprompts: crate::config::DEFAULT_REASONING_ONLY_REPROMPTS, + reasoning_only_reprompt_message: Some(crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE.to_string()), allowed_tools: None, disallowed_tools: None, max_tool_calls: None, @@ -7729,8 +7741,6 @@ use self::tool_catalog::{ }; pub(crate) use self::tool_execution::emit_tool_audit; use self::tool_preparation::{prepare_tool_call, reprepare_tool_call_after_hook}; -#[cfg(test)] -use self::turn_loop::MAX_REASONING_ONLY_REPROMPTS; use crate::tools::js_execution::execute_js_execution_tool; #[cfg(test)] diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 437ad9f225..9d85aefbb5 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -20177,8 +20177,8 @@ async fn reasoning_only_forever_is_bounded_then_fails() { assert_eq!( model.calls.load(std::sync::atomic::Ordering::SeqCst), - 1 + super::MAX_REASONING_ONLY_REPROMPTS as usize, - "reasoning-only retries are bounded by MAX_REASONING_ONLY_REPROMPTS" + 1 + crate::config::DEFAULT_REASONING_ONLY_REPROMPTS as usize, + "reasoning-only retries are bounded by DEFAULT_REASONING_ONLY_REPROMPTS" ); let status = events .iter() diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 956f9aaa8d..634a776bb7 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -2324,7 +2324,7 @@ impl Engine { false, ) && !stop_reason_is_output_limit(stop_reason.as_deref()) - && reasoning_only_reprompts < MAX_REASONING_ONLY_REPROMPTS + && reasoning_only_reprompts < self.config.reasoning_only_max_reprompts { // Reasoning-only, clean stop: recover instead of dead-ending // the turn. Nothing was persisted for this response (a bare @@ -2334,13 +2334,38 @@ impl Engine { // because retrying would only reproduce it. reasoning_only_reprompts += 1; let attempt = reasoning_only_reprompts; + let max_reprompts = self.config.reasoning_only_max_reprompts; + + // If a custom reprompt message is configured, insert it as + // a runtime-generated user message to nudge the model. + if let Some(ref msg) = self.config.reasoning_only_reprompt_message { + if !msg.is_empty() { + let reprompt = self.runtime_text_message_with_turn_metadata( + msg.clone(), + UserInputProvenance::Runtime, + ); + self.add_session_message(reprompt).await; + let _ = self + .tx_event + .send(Event::status(format!( + "Model returned only reasoning; sending reprompt message ({attempt}/{max_reprompts})" + ))) + .await; + crate::logging::warn(format!( + "Model returned only reasoning with no answer or tool call (attempt {attempt}/{max_reprompts}); sending reprompt message" + )); + turn_error = None; + continue; + } + } + crate::logging::warn(format!( - "Model returned only reasoning with no answer or tool call (attempt {attempt}/{MAX_REASONING_ONLY_REPROMPTS}); re-requesting the answer" + "Model returned only reasoning with no answer or tool call (attempt {attempt}/{max_reprompts}); re-requesting the answer" )); let _ = self .tx_event .send(Event::status(format!( - "Model returned only reasoning; re-requesting the answer ({attempt}/{MAX_REASONING_ONLY_REPROMPTS})" + "Model returned only reasoning; re-requesting the answer ({attempt}/{max_reprompts})" ))) .await; turn_error = None; @@ -5699,12 +5724,7 @@ fn resolve_tool_definition<'a>( /// point the turn truly ends; emitting it earlier (at the persist site) would /// show a spurious terminal error immediately before the turn resumed for a /// steer or a sub-agent completion. -/// Ceiling on reasoning-only auto re-requests within a single turn. A reasoning -/// model that answers with only hidden reasoning is recovered up to this many -/// times before the turn fails honestly; the prefix is cached, so each retry is -/// cheap, and the bound keeps a persistently-answerless model from looping. -pub(super) const MAX_REASONING_ONLY_REPROMPTS: u32 = 2; - +// /// Whether a provider stop reason names an output-length cap. Re-requesting /// after one only reproduces it, so those fail honestly (the user needs a /// larger max-tokens or a shorter turn) rather than retry. diff --git a/crates/tui/src/exec_agent.rs b/crates/tui/src/exec_agent.rs index 15c77e3da7..25090094ee 100644 --- a/crates/tui/src/exec_agent.rs +++ b/crates/tui/src/exec_agent.rs @@ -298,6 +298,8 @@ pub(crate) async fn run_exec_agent( goal_status: crate::tools::goal::GoalStatus::Active, goal_max_continuations: execution_config.goal_max_continuations(), goal_continuation_delay_seconds: execution_config.goal_continuation_delay_seconds(), + reasoning_only_max_reprompts: execution_config.reasoning_only_max_reprompts(), + reasoning_only_reprompt_message: Some(execution_config.reasoning_only_reprompt_message().to_string()), allowed_tools: allowed_tools.clone(), disallowed_tools: disallowed_tools.clone(), max_tool_calls, diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 9ff7b0925a..9d8e940228 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -8087,6 +8087,8 @@ impl RuntimeThreadManager { goal_status, goal_max_continuations: cfg.goal_max_continuations(), goal_continuation_delay_seconds: cfg.goal_continuation_delay_seconds(), + reasoning_only_max_reprompts: cfg.reasoning_only_max_reprompts(), + reasoning_only_reprompt_message: Some(cfg.reasoning_only_reprompt_message().to_string()), allowed_tools: isolated_chat.then(Vec::new), disallowed_tools: None, max_tool_calls: None, diff --git a/crates/tui/src/tui/ui/frame.rs b/crates/tui/src/tui/ui/frame.rs index 792792d325..3c5086fe81 100644 --- a/crates/tui/src/tui/ui/frame.rs +++ b/crates/tui/src/tui/ui/frame.rs @@ -610,6 +610,8 @@ pub(crate) fn build_engine_config(app: &App, config: &Config) -> EngineConfig { goal_status: app.goal.status, goal_max_continuations: config.goal_max_continuations(), goal_continuation_delay_seconds: config.goal_continuation_delay_seconds(), + reasoning_only_max_reprompts: config.reasoning_only_max_reprompts(), + reasoning_only_reprompt_message: Some(config.reasoning_only_reprompt_message().to_string()), locale_tag: app.ui_locale.tag().to_string(), workshop: { crate::tools::large_output_router::WorkshopConfig::install_active( diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 2f6c975647..a716cb00ab 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2321,6 +2321,39 @@ starts. Failed turns and policy/route failures never schedule another turn. Only the numeric cadence is stored in config; no prompt, credential, or secret is persisted for the loop. +### Reasoning-only recovery (`[reasoning_only]`) + +When a reasoning model (thinking mode) finishes a response with only hidden +reasoning and no answer text or tool call, the engine can automatically +re-request the answer. Configure this behavior with the `[reasoning_only]` +table: + +```toml +[reasoning_only] +# Maximum number of automatic re-requests. Default: 2. +# Set to 0 to disable automatic recovery (fail immediately). +max_reprompts = 10 + +# Optional custom message sent to the model on each re-request. +# When set, overrides the built-in default message. +# When unset (or commented out), the engine uses: +# "So, what's up ? Keep running !" +reprompt_message = "Allez, répond quelque chose !" +``` + +This only applies when the model returns a clean `stop` finish reason with +only thinking content. An output-length stop (`length`/`max_tokens`) is never +retried, and a persistently answerless model still fails honestly after the +configured bound. + +To disable the reprompt message entirely (silent retry), set it to an empty +string: + +```toml +[reasoning_only] +reprompt_message = "" +``` + ### Notifications The TUI can emit a desktop notification (OSC 9 escape or plain BEL) when a turn **completes successfully** and took longer than a threshold, so you can tab away while a long task runs. Failed or cancelled turns are intentionally silent — the notification is a "your task is ready" cue, not a generic ping. Configuration lives under `[notifications]`: From ceac64de655f438e5b61f80125a3d92dba88005b Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 14:14:56 +0000 Subject: [PATCH 2/6] fixup! cargo fmt --- crates/tui/src/core/engine.rs | 4 +++- crates/tui/src/exec_agent.rs | 6 +++++- crates/tui/src/runtime_threads.rs | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 9efe73a878..ec21798e60 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -568,7 +568,9 @@ impl Default for EngineConfig { goal_max_continuations: crate::goal_loop::DEFAULT_MAX_GOAL_CONTINUATIONS, goal_continuation_delay_seconds: 0, reasoning_only_max_reprompts: crate::config::DEFAULT_REASONING_ONLY_REPROMPTS, - reasoning_only_reprompt_message: Some(crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE.to_string()), + reasoning_only_reprompt_message: Some( + crate::config::DEFAULT_REASONING_ONLY_REPROMPT_MESSAGE.to_string(), + ), allowed_tools: None, disallowed_tools: None, max_tool_calls: None, diff --git a/crates/tui/src/exec_agent.rs b/crates/tui/src/exec_agent.rs index 25090094ee..cce54ff1c4 100644 --- a/crates/tui/src/exec_agent.rs +++ b/crates/tui/src/exec_agent.rs @@ -299,7 +299,11 @@ pub(crate) async fn run_exec_agent( goal_max_continuations: execution_config.goal_max_continuations(), goal_continuation_delay_seconds: execution_config.goal_continuation_delay_seconds(), reasoning_only_max_reprompts: execution_config.reasoning_only_max_reprompts(), - reasoning_only_reprompt_message: Some(execution_config.reasoning_only_reprompt_message().to_string()), + reasoning_only_reprompt_message: Some( + execution_config + .reasoning_only_reprompt_message() + .to_string(), + ), allowed_tools: allowed_tools.clone(), disallowed_tools: disallowed_tools.clone(), max_tool_calls, diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 9d8e940228..10eef3bf57 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -8088,7 +8088,9 @@ impl RuntimeThreadManager { goal_max_continuations: cfg.goal_max_continuations(), goal_continuation_delay_seconds: cfg.goal_continuation_delay_seconds(), reasoning_only_max_reprompts: cfg.reasoning_only_max_reprompts(), - reasoning_only_reprompt_message: Some(cfg.reasoning_only_reprompt_message().to_string()), + reasoning_only_reprompt_message: Some( + cfg.reasoning_only_reprompt_message().to_string(), + ), allowed_tools: isolated_chat.then(Vec::new), disallowed_tools: None, max_tool_calls: None, From 682e2903f926c6f16d23b8055a0b92128bf31e63 Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 15:36:19 +0000 Subject: [PATCH 3/6] fixup! clippy: collapse nested if --- crates/tui/src/core/engine/turn_loop.rs | 38 ++++++++++++------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 634a776bb7..8a1e04e031 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -2338,25 +2338,25 @@ impl Engine { // If a custom reprompt message is configured, insert it as // a runtime-generated user message to nudge the model. - if let Some(ref msg) = self.config.reasoning_only_reprompt_message { - if !msg.is_empty() { - let reprompt = self.runtime_text_message_with_turn_metadata( - msg.clone(), - UserInputProvenance::Runtime, - ); - self.add_session_message(reprompt).await; - let _ = self - .tx_event - .send(Event::status(format!( - "Model returned only reasoning; sending reprompt message ({attempt}/{max_reprompts})" - ))) - .await; - crate::logging::warn(format!( - "Model returned only reasoning with no answer or tool call (attempt {attempt}/{max_reprompts}); sending reprompt message" - )); - turn_error = None; - continue; - } + if let Some(ref msg) = self.config.reasoning_only_reprompt_message + && !msg.is_empty() + { + let reprompt = self.runtime_text_message_with_turn_metadata( + msg.clone(), + UserInputProvenance::Runtime, + ); + self.add_session_message(reprompt).await; + let _ = self + .tx_event + .send(Event::status(format!( + "Model returned only reasoning; sending reprompt message ({attempt}/{max_reprompts})" + ))) + .await; + crate::logging::warn(format!( + "Model returned only reasoning with no answer or tool call (attempt {attempt}/{max_reprompts}); sending reprompt message" + )); + turn_error = None; + continue; } crate::logging::warn(format!( From 2bd55fee4a0b164e533af176d1ab62aabc0f25f0 Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 15:37:46 +0000 Subject: [PATCH 4/6] fixup! tests: update recovery message assertions for reprompt behavior --- crates/tui/src/core/engine/tests.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index 9d85aefbb5..cb7224c2b7 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -20116,10 +20116,12 @@ async fn reasoning_only_clean_stop_is_retried_and_recovers() { .expect("terminal TurnComplete"); assert_eq!(status, TurnOutcomeStatus::Completed); + // With the default reprompt message configured, the engine emits a status + // notice saying it sent a reprompt message rather than staying silent. let recovery = events .iter() .filter_map(|event| match event { - Event::Status { message } if message.contains("re-requesting the answer") => { + Event::Status { message } if message.contains("sending reprompt message") => { Some(message.clone()) } _ => None, @@ -20152,10 +20154,12 @@ async fn reasoning_only_length_stop_fails_without_retry() { 1, "a length stop must not be retried" ); + // Neither the old silent-retry message nor the new reprompt message should + // appear: a length stop must not retry at all. assert!( !events.iter().any(|event| matches!( event, - Event::Status { message } if message.contains("re-requesting the answer") + Event::Status { message } if message.contains("reprompt") )), "a length stop must not announce a retry: {events:?}" ); From 718cc861637f3c67cc5ba503d9755f1f5571bc37 Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 15:54:32 +0000 Subject: [PATCH 5/6] fixup! windows: fix unused variable warnings in tests --- crates/tui/src/plugins/marketplace/document.rs | 10 +++++----- crates/tui/src/runtime_api/tests.rs | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/tui/src/plugins/marketplace/document.rs b/crates/tui/src/plugins/marketplace/document.rs index f0427ac1cb..2ccaa2f619 100644 --- a/crates/tui/src/plugins/marketplace/document.rs +++ b/crates/tui/src/plugins/marketplace/document.rs @@ -213,16 +213,16 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let real = dir.path().join("real.json"); std::fs::write(&real, "{}").unwrap(); - let link = dir.path().join("link.json"); + let _link = dir.path().join("link.json"); #[cfg(unix)] - std::os::unix::fs::symlink(&real, &link).unwrap(); + std::os::unix::fs::symlink(&real, &_link).unwrap(); #[cfg(not(unix))] - let link = real.clone(); + let _link = real.clone(); - let error = load_catalog_document("test", dir.path(), link.to_str().unwrap()) + let _error = load_catalog_document("test", dir.path(), _link.to_str().unwrap()) .expect_err("symlink document must be refused"); #[cfg(unix)] - assert!(error.contains("symlink"), "{error}"); + assert!(_error.contains("symlink"), "{_error}"); } #[test] diff --git a/crates/tui/src/runtime_api/tests.rs b/crates/tui/src/runtime_api/tests.rs index 2b80060e01..184641319d 100644 --- a/crates/tui/src/runtime_api/tests.rs +++ b/crates/tui/src/runtime_api/tests.rs @@ -11202,6 +11202,7 @@ async fn marketplace_add_rejects_symlink_documents_over_http() -> Result<()> { fs::create_dir_all(&catalog_dir)?; let real = catalog_dir.join("real.json"); fs::write(&real, r#"{"plugins":[]}"#)?; + #[cfg(unix)] let link = catalog_dir.join("link.json"); #[cfg(unix)] std::os::unix::fs::symlink(&real, &link)?; From 223b8a8e68989f92241c399680cbeeab3cdba787 Mon Sep 17 00:00:00 2001 From: Gabriel Degret Date: Thu, 3 Sep 2026 16:36:01 +0000 Subject: [PATCH 6/6] docs: add [reasoning_only] section to config.example.toml --- config.example.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config.example.toml b/config.example.toml index 18761e9cf4..2ad636b6c8 100644 --- a/config.example.toml +++ b/config.example.toml @@ -196,6 +196,16 @@ memory_path = "~/.codewhale/memory.md" [speech] # output_dir = "./speech" +# ───────────────────────────────────────────────────────────────────────────────── +# Reasoning-only recovery +# ───────────────────────────────────────────────────────────────────────────────── +# When a reasoning model returns only hidden thinking without any answer text +# or tool call, the engine can re-request the answer automatically. +# Set max_reprompts = 0 to disable automatic recovery entirely. +# [reasoning_only] +# max_reprompts = 2 +# reprompt_message = "So, what's up ? Keep running !" + # Native tool catalog controls (#2076). By default only the core tool surface # is loaded into the model context; less common native tools are discoverable # through ToolSearch and loaded on first use.