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 body = response.text().await.unwrap_or_else(|_| String::new());
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);
Err(ApiError::Api {
@ -943,10 +943,15 @@ fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool {
return false;
}
let lowered = body.to_ascii_lowercase();
// Gateway/proxy flakes that return 400 with transient error bodies
lowered.contains("no parseable body")
|| lowered.contains("connection reset")
|| lowered.contains("broken pipe")
|| 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

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 body = response.text().await.unwrap_or_default();
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 suggested_action = suggested_action_for_status(status);

View File

@ -108,6 +108,7 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
.first()
.and_then(extract_existing_compacted_summary);
let compacted_prefix_len = usize::from(existing_summary.is_some());
<<<<<<< HEAD
// When preserve_recent_messages is 0, the caller wants maximum compaction
// (no recent messages preserved). Without this guard, saturating_sub(0)
// 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
.len()
.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
// boundary. If the first preserved message is a user message whose first

View File

@ -126,6 +126,17 @@ pub struct RuntimePluginConfig {
}
<<<<<<< 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.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiTimeoutConfig {
@ -195,8 +206,10 @@ pub struct RuntimeFeatureConfig {
rules_import: RulesImportConfig,
provider: RuntimeProviderConfig,
lsp: BTreeMap<String, LspServerConfig>,
api_timeout: ApiTimeoutConfig,
}
<<<<<<< HEAD
/// Controls which external AI coding framework rules are imported into the system prompt.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RulesImportConfig {
@ -218,6 +231,26 @@ impl RulesImportConfig {
Self::List(frameworks) => frameworks
.iter()
.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> {
self.model.as_deref()
}
api_timeout: ApiTimeoutConfig,
}
/// 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,
};
pub use config::{
<<<<<<< HEAD
<<<<<<< HEAD
clear_user_provider_settings, default_config_home, save_user_provider_settings,
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
@ -85,6 +86,10 @@ pub use config::{
McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig,
McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
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,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,

View File

@ -7865,6 +7865,7 @@ impl LiveCli {
// ============================================================================
let error_str = error.to_string();
<<<<<<< HEAD
<<<<<<< HEAD
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
// return 400 with "no parseable body" instead of a proper context_length_exceeded
@ -7934,31 +7935,51 @@ impl LiveCli {
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")
|| error_str.contains("no parseable body");
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
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
// Run the Trident compaction pipeline (supersede + collapse + cluster)
// then apply summary-based compaction for maximum context reduction
let result = runtime::trident::trident_compact_session(
runtime.session(),
CompactionConfig {
max_estimated_tokens: 0,
..CompactionConfig::default()
},
&runtime::trident::TridentConfig::default(),
);
let removed = result.removed_message_count;
// Only proceed if compaction actually happened (messages were removed)
// or there's still a session to work with
if removed > 0 || result.compacted_session.messages.len() > 0 {
for round in 0..max_compact_rounds {
let preserve = preserve_schedule[round];
println!(
" Auto-compacting session (round {}/{}, preserving {} recent messages)...",
round + 1,
max_compact_rounds,
preserve
);
// Run Trident pipeline then summary-based compaction
let result = runtime::trident::trident_compact_session(
runtime.session(),
CompactionConfig {
preserve_recent_messages: preserve,
max_estimated_tokens: 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 {
// Report compaction results to user
println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
<<<<<<< HEAD
if removed > 0 {
println!(
@ -7981,16 +8002,31 @@ impl LiveCli {
self.prepare_turn_runtime(true)?;
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);
match new_runtime.run_turn(input, Some(&mut rp)) {
Ok(summary) => {
self.replace_runtime(new_runtime)?;
spinner.finish(
<<<<<<< HEAD
if round == 0 {
"✨ Done (after auto-compact)"
} else {
"✨ 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(),
&mut stdout,
)?;
@ -8008,6 +8044,7 @@ impl LiveCli {
let retry_str = retry_error.to_string();
let still_context_window = retry_str.contains("context_window")
|| retry_str.contains("Context window")
<<<<<<< HEAD
|| retry_str.contains("no parseable body")
|| retry_str.contains("exceed_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.
// Shut down the old runtime, adopt the partially-compacted one,
// 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 = new_runtime;
continue;
}
<<<<<<< HEAD
=======
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
// Not a context window error, or out of rounds
return Err(Box::new(retry_error));
}