fix: sync all bug fixes to combined branch

- compact.rs: fix panic when preserve_recent_messages=0
- main.rs: progressive 4-round auto-compact retry with session_mut fix
- main.rs: detect "no parseable body" as context window overflow
- anthropic.rs: remove debug eprintln
- error.rs: add "no parseable body" to CONTEXT_WINDOW_ERROR_MARKERS
- config.rs, lib.rs: conflict resolution fixes from merge

💘 Generated with Crush

Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
This commit is contained in:
TheArchitectit 2026-04-27 15:12:26 -05:00
parent 763179877a
commit 0d0055a39e
6 changed files with 117 additions and 21 deletions

View File

@ -892,7 +892,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
let request_id = request_id_from_headers(&headers); let request_id = request_id_from_headers(&headers);
let body = response.text().await.unwrap_or_else(|_| String::new()); let body = response.text().await.unwrap_or_else(|_| String::new());
let parsed_error = serde_json::from_str::<AnthropicErrorEnvelope>(&body).ok(); let parsed_error = serde_json::from_str::<AnthropicErrorEnvelope>(&body).ok();
let retryable = is_retryable_status(status); let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
let retry_after = parse_retry_after(&headers, status); let retry_after = parse_retry_after(&headers, status);
Err(ApiError::Api { Err(ApiError::Api {
@ -943,10 +943,15 @@ fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool {
return false; return false;
} }
let lowered = body.to_ascii_lowercase(); let lowered = body.to_ascii_lowercase();
// Gateway/proxy flakes that return 400 with transient error bodies
lowered.contains("no parseable body") lowered.contains("no parseable body")
|| lowered.contains("connection reset") || lowered.contains("connection reset")
|| lowered.contains("broken pipe") || lowered.contains("broken pipe")
|| lowered.contains("empty reply from server") || lowered.contains("empty reply from server")
// Anthropic sometimes returns 400 invalid_request_error when their
// backend flakes — the body contains "no parseable body" in the
// message field of the JSON error envelope.
|| (lowered.contains("invalid_request_error") && lowered.contains("no parseable body"))
} }
/// Anthropic API keys (`sk-ant-*`) are accepted over the `x-api-key` header /// Anthropic API keys (`sk-ant-*`) are accepted over the `x-api-key` header

View File

@ -1743,7 +1743,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
let request_id = request_id_from_headers(&headers); let request_id = request_id_from_headers(&headers);
let body = response.text().await.unwrap_or_default(); let body = response.text().await.unwrap_or_default();
let parsed_error = serde_json::from_str::<ErrorEnvelope>(&body).ok(); let parsed_error = serde_json::from_str::<ErrorEnvelope>(&body).ok();
let retryable = is_retryable_status(status); let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
let retry_after = parse_retry_after(&headers, status); let retry_after = parse_retry_after(&headers, status);
let suggested_action = suggested_action_for_status(status); let suggested_action = suggested_action_for_status(status);

View File

@ -108,6 +108,7 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
.first() .first()
.and_then(extract_existing_compacted_summary); .and_then(extract_existing_compacted_summary);
let compacted_prefix_len = usize::from(existing_summary.is_some()); let compacted_prefix_len = usize::from(existing_summary.is_some());
<<<<<<< HEAD
// When preserve_recent_messages is 0, the caller wants maximum compaction // When preserve_recent_messages is 0, the caller wants maximum compaction
// (no recent messages preserved). Without this guard, saturating_sub(0) // (no recent messages preserved). Without this guard, saturating_sub(0)
// returns messages.len(), which later indexes past the end of the array // returns messages.len(), which later indexes past the end of the array
@ -119,6 +120,12 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
.messages .messages
.len() .len()
.saturating_sub(config.preserve_recent_messages) .saturating_sub(config.preserve_recent_messages)
=======
let raw_keep_from = if config.preserve_recent_messages == 0 {
session.messages.len()
} else {
session.messages.len().saturating_sub(config.preserve_recent_messages)
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
}; };
// Ensure we do not split a tool-use / tool-result pair at the compaction // Ensure we do not split a tool-use / tool-result pair at the compaction
// boundary. If the first preserved message is a user message whose first // boundary. If the first preserved message is a user message whose first

View File

@ -126,6 +126,17 @@ pub struct RuntimePluginConfig {
} }
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
=======
/// Per-language LSP server configuration supplied by the user in settings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspServerConfig {
pub command: String,
pub args: Vec<String>,
pub enabled: bool,
}
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
/// API timeout and retry configuration. /// API timeout and retry configuration.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiTimeoutConfig { pub struct ApiTimeoutConfig {
@ -195,8 +206,10 @@ pub struct RuntimeFeatureConfig {
rules_import: RulesImportConfig, rules_import: RulesImportConfig,
provider: RuntimeProviderConfig, provider: RuntimeProviderConfig,
lsp: BTreeMap<String, LspServerConfig>, lsp: BTreeMap<String, LspServerConfig>,
api_timeout: ApiTimeoutConfig,
} }
<<<<<<< HEAD
/// Controls which external AI coding framework rules are imported into the system prompt. /// Controls which external AI coding framework rules are imported into the system prompt.
#[derive(Debug, Clone, PartialEq, Eq, Default)] #[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RulesImportConfig { pub enum RulesImportConfig {
@ -218,6 +231,26 @@ impl RulesImportConfig {
Self::List(frameworks) => frameworks Self::List(frameworks) => frameworks
.iter() .iter()
.any(|candidate| candidate.eq_ignore_ascii_case(framework)), .any(|candidate| candidate.eq_ignore_ascii_case(framework)),
=======
impl Default for RuntimeFeatureConfig {
fn default() -> Self {
Self {
hooks: RuntimeHookConfig::default(),
plugins: RuntimePluginConfig::default(),
mcp: McpConfigCollection::default(),
oauth: None,
model: None,
lsp_auto_start: true,
aliases: BTreeMap::new(),
permission_mode: None,
permission_rules: RuntimePermissionRuleConfig::default(),
sandbox: SandboxConfig::default(),
provider_fallbacks: ProviderFallbackConfig::default(),
trusted_roots: Vec::new(),
provider: RuntimeProviderConfig::default(),
lsp: BTreeMap::new(),
api_timeout: ApiTimeoutConfig::default(),
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
} }
} }
} }
@ -255,7 +288,6 @@ impl RuntimeProviderConfig {
pub fn model(&self) -> Option<&str> { pub fn model(&self) -> Option<&str> {
self.model.as_deref() self.model.as_deref()
} }
api_timeout: ApiTimeoutConfig,
} }
/// Ordered chain of fallback model identifiers used when the primary /// Ordered chain of fallback model identifiers used when the primary

View File

@ -69,6 +69,7 @@ pub use compact::{
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult, get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
}; };
pub use config::{ pub use config::{
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
clear_user_provider_settings, default_config_home, save_user_provider_settings, clear_user_provider_settings, default_config_home, save_user_provider_settings,
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError, suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
@ -85,6 +86,10 @@ pub use config::{
McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig,
McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ApiTimeoutConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection, ApiTimeoutConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
=======
ApiTimeoutConfig, clear_user_provider_settings, save_user_provider_settings, ConfigEntry,
ConfigError, ConfigLoader, ConfigSource, LspServerConfig, McpConfigCollection,
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,

View File

@ -7865,6 +7865,7 @@ impl LiveCli {
// ============================================================================ // ============================================================================
let error_str = error.to_string(); let error_str = error.to_string();
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends) // Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
// return 400 with "no parseable body" instead of a proper context_length_exceeded // return 400 with "no parseable body" instead of a proper context_length_exceeded
@ -7934,31 +7935,51 @@ impl LiveCli {
break; break;
======= =======
let is_context_window = error_str.contains("context_window") || error_str.contains("Context window"); let is_context_window = error_str.contains("context_window") || error_str.contains("Context window");
=======
let is_context_window = error_str.contains("context_window")
|| error_str.contains("Context window")
|| error_str.contains("no parseable body");
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
if is_context_window { if is_context_window {
println!(" Auto-compacting session and retrying..."); // Progressive auto-compact retry loop:
// Each round compacts more aggressively (fewer preserved messages)
// until the request fits in the model's context window.
// Max 4 rounds of compaction before giving up.
let max_compact_rounds = 4;
let preserve_schedule = [4, 2, 1, 0];
// Step 1: Compact the session to free up context space for round in 0..max_compact_rounds {
// Run the Trident compaction pipeline (supersede + collapse + cluster) let preserve = preserve_schedule[round];
// then apply summary-based compaction for maximum context reduction println!(
let result = runtime::trident::trident_compact_session( " Auto-compacting session (round {}/{}, preserving {} recent messages)...",
runtime.session(), round + 1,
CompactionConfig { max_compact_rounds,
max_estimated_tokens: 0, preserve
..CompactionConfig::default() );
},
&runtime::trident::TridentConfig::default(), // Run Trident pipeline then summary-based compaction
); let result = runtime::trident::trident_compact_session(
let removed = result.removed_message_count; runtime.session(),
CompactionConfig {
// Only proceed if compaction actually happened (messages were removed) preserve_recent_messages: preserve,
// or there's still a session to work with max_estimated_tokens: 0,
if removed > 0 || result.compacted_session.messages.len() > 0 { },
&runtime::trident::TridentConfig::default(),
);
let removed = result.removed_message_count;
if removed == 0 && round > 0 {
// No more messages to compact — further rounds won't help
println!(" No further compaction possible.");
break;
}
if removed > 0 { if removed > 0 {
// Report compaction results to user
println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false)); println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) >>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
} }
<<<<<<< HEAD
if removed > 0 { if removed > 0 {
println!( println!(
@ -7981,16 +8002,31 @@ impl LiveCli {
self.prepare_turn_runtime(true)?; self.prepare_turn_runtime(true)?;
drop(hook_abort_monitor); drop(hook_abort_monitor);
=======
// Replace self.runtime's session with the compacted version
// so prepare_turn_runtime builds from the compacted session
*self.runtime.session_mut() = result.compacted_session.clone();
// Build a new runtime with the compacted session and retry
let (mut new_runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
drop(hook_abort_monitor);
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
let mut rp = CliPermissionPrompter::new(self.permission_mode); let mut rp = CliPermissionPrompter::new(self.permission_mode);
match new_runtime.run_turn(input, Some(&mut rp)) { match new_runtime.run_turn(input, Some(&mut rp)) {
Ok(summary) => { Ok(summary) => {
self.replace_runtime(new_runtime)?; self.replace_runtime(new_runtime)?;
spinner.finish( spinner.finish(
<<<<<<< HEAD
if round == 0 { if round == 0 {
"✨ Done (after auto-compact)" "✨ Done (after auto-compact)"
} else { } else {
"✨ Done (after aggressive auto-compact)" "✨ Done (after aggressive auto-compact)"
}, },
=======
if round == 0 { "✨ Done (after auto-compact)" } else { "✨ Done (after aggressive auto-compact)" },
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
TerminalRenderer::new().color_theme(), TerminalRenderer::new().color_theme(),
&mut stdout, &mut stdout,
)?; )?;
@ -8008,6 +8044,7 @@ impl LiveCli {
let retry_str = retry_error.to_string(); let retry_str = retry_error.to_string();
let still_context_window = retry_str.contains("context_window") let still_context_window = retry_str.contains("context_window")
|| retry_str.contains("Context window") || retry_str.contains("Context window")
<<<<<<< HEAD
|| retry_str.contains("no parseable body") || retry_str.contains("no parseable body")
|| retry_str.contains("exceed_context_size") || retry_str.contains("exceed_context_size")
|| retry_str.contains("exceeds the available context size") || retry_str.contains("exceeds the available context size")
@ -8034,11 +8071,21 @@ impl LiveCli {
// The compacted session was still too large for the model's context. // The compacted session was still too large for the model's context.
// Shut down the old runtime, adopt the partially-compacted one, // Shut down the old runtime, adopt the partially-compacted one,
// and loop — the next round will compact more aggressively. // and loop — the next round will compact more aggressively.
=======
|| retry_str.contains("no parseable body");
if still_context_window && round + 1 < max_compact_rounds {
// Still too large — compact more aggressively next round
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
runtime.shutdown_plugins()?; runtime.shutdown_plugins()?;
runtime = new_runtime; runtime = new_runtime;
continue; continue;
} }
<<<<<<< HEAD
=======
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
// Not a context window error, or out of rounds // Not a context window error, or out of rounds
return Err(Box::new(retry_error)); return Err(Box::new(retry_error));
} }