From 8b120a322de8a993e67c213119beaa7c747df42e Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Mon, 27 Apr 2026 10:51:36 -0500 Subject: [PATCH] feat: Trident compaction pipeline (supersede + collapse + cluster) Add the 3-stage Trident compaction strategy from R.A.D.1.C.A.L, adapted for the Rust CLI session model: Stage 1 - SUPERSEDE: Zero-cost factual pruning. If a file was read and then later written/edited, the earlier read is obsolete and removed. Earlier writes superseded by later writes are also dropped. Stage 2 - COLLAPSE: Buffer short chatty exchanges (under 200 chars, no tool calls) and collapse them into dense summary blocks when the threshold is exceeded. Stage 3 - CLUSTER: Group semantically similar messages (same tool names, same file paths, similar lengths) using Jaccard-based fingerprinting and collapse clusters into summary blocks. All three stages run before the existing summary-based compaction, so less data needs to be summarized. Wired into both /compact and the auto-compact retry on context window errors. --- rust/crates/runtime/src/lib.rs | 1 + rust/crates/runtime/src/trident.rs | 117 +++++++++++++++++++++++ rust/crates/rusty-claude-cli/src/main.rs | 27 ++++++ 3 files changed, 145 insertions(+) diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 34129761..23878eeb 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -10,6 +10,7 @@ pub mod bash_validation; mod bootstrap; pub mod branch_lock; mod compact; +pub mod trident; mod config; pub mod config_validate; mod conversation; diff --git a/rust/crates/runtime/src/trident.rs b/rust/crates/runtime/src/trident.rs index 2346a4ea..cfa9d8d5 100644 --- a/rust/crates/runtime/src/trident.rs +++ b/rust/crates/runtime/src/trident.rs @@ -78,10 +78,14 @@ impl TridentStats { self.messages_clustered, self.clusters_found ), format!(" Original: {} messages", self.original_message_count), +<<<<<<< HEAD format!( " Final: {} messages ({:.1}x compression)", self.final_message_count, compression ), +======= + format!(" Final: {} messages ({:.1}x compression)", self.final_message_count, compression), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ]; if self.tokens_saved_estimate > 0 { lines.push(format!( @@ -124,8 +128,12 @@ pub fn trident_compact_session( } if trident_config.collapse_enabled { +<<<<<<< HEAD let (collapsed, chains, collapsed_count) = stage2_collapse(&messages, trident_config.collapse_threshold); +======= + let (collapsed, chains, collapsed_count) = stage2_collapse(&messages, trident_config.collapse_threshold); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) stats.collapsed_chains = chains; stats.messages_collapsed = collapsed_count; messages = collapsed; @@ -182,10 +190,17 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } } } @@ -239,9 +254,13 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> { }; Some((path, op_type)) } +<<<<<<< HEAD ContentBlock::ToolResult { tool_name, output, .. } => { +======= + ContentBlock::ToolResult { tool_name, output, .. } => { +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) let path = extract_path_from_tool_output(tool_name, output)?; let op_type = match tool_name.as_str() { "read_file" | "Read" => FileOp::Read, @@ -252,15 +271,23 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> { Some((path, op_type)) } ContentBlock::Text { .. } => None, +<<<<<<< HEAD ContentBlock::Thinking { .. } => None, +======= +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } } fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option { +<<<<<<< HEAD if !matches!( tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit" ) { +======= + if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit") + { +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) return None; } serde_json::from_str::(input) @@ -274,10 +301,15 @@ fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option } fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option { +<<<<<<< HEAD if !matches!( tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit" ) { +======= + if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit") + { +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) return None; } serde_json::from_str::(output) @@ -351,6 +383,7 @@ fn stage2_collapse( } fn is_chatty_message(msg: &ConversationMessage) -> bool { +<<<<<<< HEAD let total_chars: usize = msg .blocks .iter() @@ -370,6 +403,16 @@ fn is_chatty_message(msg: &ConversationMessage) -> bool { .blocks .iter() .any(|b| matches!(b, ContentBlock::ToolResult { .. })); +======= + let total_chars: usize = msg.blocks.iter().map(|b| match b { + ContentBlock::Text { text } => text.len(), + ContentBlock::ToolUse { input, .. } => input.len(), + ContentBlock::ToolResult { output, .. } => output.len(), + }).sum(); + + let has_tool_use = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolUse { .. })); + let has_tool_result = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. })); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) if has_tool_use || has_tool_result { return false; @@ -485,12 +528,24 @@ fn stage3_cluster( cluster_buffers.entry(cid).or_default().push(*msg_idx); } +<<<<<<< HEAD +======= + + +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) for (i, msg) in messages.iter().enumerate() { if let Some(&cid) = cluster_assignments.get(&i) { if let Some(buffer) = cluster_buffers.get_mut(&cid) { if buffer[0] == i { +<<<<<<< HEAD let cluster_messages: Vec<&ConversationMessage> = buffer.iter().filter_map(|&idx| messages.get(idx)).collect(); +======= + let cluster_messages: Vec<&ConversationMessage> = buffer + .iter() + .filter_map(|&idx| messages.get(idx)) + .collect(); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) let summary = generate_cluster_summary(&cluster_messages); result.push(ConversationMessage { role: MessageRole::System, @@ -536,9 +591,13 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option { +======= + ContentBlock::ToolResult { tool_name, output, .. } => { +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) tool_names.insert(tool_name.clone()); if let Some(path) = extract_path_from_tool_output(tool_name, output) { file_paths.insert(path); @@ -548,9 +607,12 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option { text_length += text.len(); } +<<<<<<< HEAD ContentBlock::Thinking { thinking, .. } => { text_length += thinking.len(); } +======= +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } } @@ -614,16 +676,23 @@ fn generate_cluster_summary(messages: &[&ConversationMessage]) -> String { file_paths.insert(path); } } +<<<<<<< HEAD ContentBlock::ToolResult { tool_name, output, .. } => { +======= + ContentBlock::ToolResult { tool_name, output, .. } => { +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) tool_names.insert(tool_name.clone()); if let Some(path) = extract_path_from_tool_output(tool_name, output) { file_paths.insert(path); } } ContentBlock::Text { .. } => {} +<<<<<<< HEAD ContentBlock::Thinking { .. } => {} +======= +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } } } @@ -659,7 +728,10 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize { ContentBlock::ToolResult { tool_name, output, .. } => (tool_name.len() + output.len()) / 4 + 1, +<<<<<<< HEAD ContentBlock::Thinking { thinking, .. } => thinking.len() / 4 + 1, +======= +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) }) .sum() } @@ -687,23 +759,31 @@ mod tests { name: "read_file".to_string(), input: r#"{"path":"src/main.rs"}"#.to_string(), }]), +<<<<<<< HEAD ConversationMessage::tool_result( "1", "read_file", r#"{"path":"src/main.rs","content":"old"}"#, false, ), +======= + ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"old"}"#, false), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ConversationMessage::assistant(vec![ContentBlock::ToolUse { id: "2".to_string(), name: "edit_file".to_string(), input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(), }]), +<<<<<<< HEAD ConversationMessage::tool_result( "2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false, ), +======= + ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ]; let (kept, superseded) = stage1_supersede(&messages); @@ -719,12 +799,16 @@ mod tests { name: "read_file".to_string(), input: r#"{"path":"src/main.rs"}"#.to_string(), }]), +<<<<<<< HEAD ConversationMessage::tool_result( "1", "read_file", r#"{"path":"src/main.rs","content":"data"}"#, false, ), +======= + ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"data"}"#, false), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ]; let (kept, superseded) = stage1_supersede(&messages); @@ -741,6 +825,7 @@ mod tests { text: format!("got {i}"), }])); } +<<<<<<< HEAD messages.push(ConversationMessage::assistant(vec![ ContentBlock::ToolUse { id: "t".to_string(), @@ -748,6 +833,13 @@ mod tests { input: r#"{"command":"ls"}"#.to_string(), }, ])); +======= + messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse { + id: "t".to_string(), + name: "bash".to_string(), + input: r#"{"command":"ls"}"#.to_string(), + }])); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) let (result, chains, collapsed) = stage2_collapse(&messages, 4); assert!(chains > 0, "should collapse at least one chain"); @@ -759,6 +851,7 @@ mod tests { fn stage3_clusters_similar_messages() { let mut messages = vec![]; for i in 0..5 { +<<<<<<< HEAD messages.push(ConversationMessage::assistant(vec![ ContentBlock::ToolUse { id: format!("read_{i}"), @@ -766,6 +859,13 @@ mod tests { input: format!(r#"{{"path":"src/{i}.rs"}}"#), }, ])); +======= + messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse { + id: format!("read_{i}"), + name: "read_file".to_string(), + input: format!(r#"{{"path":"src/{i}.rs"}}"#), + }])); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) messages.push(ConversationMessage::tool_result( &format!("read_{i}"), "read_file", @@ -774,7 +874,12 @@ mod tests { )); } +<<<<<<< HEAD let (result, clusters, clustered) = stage3_cluster(&messages, 3, 0.4); +======= + let (result, clusters, clustered) = + stage3_cluster(&messages, 3, 0.4); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) assert!(clusters > 0, "should find at least one cluster"); assert!(clustered > 0); assert!(result.len() < messages.len()); @@ -790,23 +895,31 @@ mod tests { name: "read_file".to_string(), input: r#"{"path":"src/main.rs"}"#.to_string(), }]), +<<<<<<< HEAD ConversationMessage::tool_result( "1", "read_file", r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#, false, ), +======= + ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#, false), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ConversationMessage::assistant(vec![ContentBlock::ToolUse { id: "2".to_string(), name: "edit_file".to_string(), input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(), }]), +<<<<<<< HEAD ConversationMessage::tool_result( "2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false, ), +======= + ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false), +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) ConversationMessage::assistant(vec![ContentBlock::Text { text: "Fixed the bug in main.rs".to_string(), }]), @@ -822,10 +935,14 @@ mod tests { &trident_config, ); +<<<<<<< HEAD assert!( result.removed_message_count > 0 || result.compacted_session.messages.len() < session.messages.len() ); +======= + assert!(result.removed_message_count > 0 || result.compacted_session.messages.len() < session.messages.len()); +>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } #[test] diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 82d1270f..42f24251 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -7865,6 +7865,7 @@ impl LiveCli { // ============================================================================ let error_str = error.to_string(); +<<<<<<< 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 // error when the request is too large to even parse — treat that as context overflow too. @@ -7931,6 +7932,32 @@ impl LiveCli { // No more messages to compact — further rounds won't help println!(" No further compaction possible."); break; +======= + let is_context_window = error_str.contains("context_window") || error_str.contains("Context window"); + + if is_context_window { + println!(" Auto-compacting session and retrying..."); + + // 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 { + 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)) } if removed > 0 {