feat: Phase 3 Performance — KB-Cache, Bridge-Warmstart, maxTurns 200
All checks were successful
Build AppImage / build (push) Has been skipped

- KB-Cache im RAM: Suchergebnisse 60s gecacht, kein MySQL-Roundtrip pro Nachricht
- Bridge wird beim App-Start sofort gestartet (kein Cold-Start bei erster Nachricht)
- Bridge-Start-Wait von 500ms auf 200ms reduziert
- maxTurns von 25 auf 200 erhoeht (verhindert "maximum turns reached" bei komplexen Tasks)
- invalidate_kb_cache Command fuer manuelles Cache-Leeren

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-20 22:02:23 +02:00
parent 5b857ebba4
commit 3d84feab6f
4 changed files with 111 additions and 7 deletions

View file

@ -332,7 +332,7 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
model: useModel, model: useModel,
promptLength: message.length, promptLength: message.length,
contextLength: useContext?.length || 0, contextLength: useContext?.length || 0,
maxTurns: 25, maxTurns: 200,
resumeSessionId: resumeSessionId || null, resumeSessionId: resumeSessionId || null,
}); });
@ -373,7 +373,7 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
// Query-Optionen zusammenstellen // Query-Optionen zusammenstellen
const queryOptions = { const queryOptions = {
model: useModel, model: useModel,
maxTurns: 25, maxTurns: 200,
abortController: activeAbort, abortController: activeAbort,
}; };

View file

@ -390,8 +390,8 @@ pub async fn send_message(app: AppHandle, message: String) -> Result<String, Str
if needs_start { if needs_start {
start_bridge(&app)?; start_bridge(&app)?;
// Kurz warten bis Bridge bereit // Kurz warten bis Bridge bereit — parallel den Context laden
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
} }
// Context aus DB laden (Schicht 1: Sticky Context) // Context aus DB laden (Schicht 1: Sticky Context)
@ -534,7 +534,7 @@ pub async fn set_model(app: AppHandle, model: String) -> Result<String, String>
if needs_start { if needs_start {
start_bridge(&app)?; start_bridge(&app)?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
} }
// Modell an Bridge senden // Modell an Bridge senden
@ -603,7 +603,7 @@ pub async fn set_agent_mode(app: AppHandle, mode: String) -> Result<String, Stri
if needs_start { if needs_start {
start_bridge(&app)?; start_bridge(&app)?;
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
} }
// Modus an Bridge senden // Modus an Bridge senden

View file

@ -6,8 +6,68 @@ use mysql_async::{Pool, prelude::*};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use std::sync::Arc; use std::sync::Arc;
use std::sync::Mutex;
use std::collections::HashMap;
use std::time::{Duration, Instant};
use tauri::{AppHandle, Manager}; use tauri::{AppHandle, Manager};
// ============ KB-Cache (RAM) ============
// Cached KB-Suchergebnisse im RAM. Spart MySQL-Roundtrip pro Nachricht.
// Invalidierung: automatisch nach 60 Sekunden (TTL).
struct CacheEntry {
result: String,
created_at: Instant,
}
struct KbCache {
entries: HashMap<String, CacheEntry>,
ttl: Duration,
}
impl KbCache {
fn new(ttl_secs: u64) -> Self {
Self {
entries: HashMap::new(),
ttl: Duration::from_secs(ttl_secs),
}
}
fn get(&self, key: &str) -> Option<&str> {
if let Some(entry) = self.entries.get(key) {
if entry.created_at.elapsed() < self.ttl {
return Some(&entry.result);
}
}
None
}
fn insert(&mut self, key: String, result: String) {
// Max 100 Eintraege — aelteste raus wenn voll
if self.entries.len() >= 100 {
self.evict_oldest();
}
self.entries.insert(key, CacheEntry { result, created_at: Instant::now() });
}
fn evict_oldest(&mut self) {
if let Some(oldest_key) = self.entries.iter()
.min_by_key(|(_, v)| v.created_at)
.map(|(k, _)| k.clone())
{
self.entries.remove(&oldest_key);
}
}
fn invalidate_expired(&mut self) {
self.entries.retain(|_, v| v.created_at.elapsed() < self.ttl);
}
}
// Globaler Cache — 60 Sekunden TTL
static KB_CACHE: std::sync::LazyLock<Mutex<KbCache>> =
std::sync::LazyLock::new(|| Mutex::new(KbCache::new(60)));
/// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults /// Verbindungskonfiguration — aus ENV-Variablen, Fallback auf Defaults
const MYSQL_PORT: u16 = 3306; const MYSQL_PORT: u16 = 3306;
@ -168,7 +228,20 @@ pub async fn search_knowledge_internal(query: &str, limit: i32) -> Result<String
/// Sucht in der KB mit einem vorbereiteten Query-String /// Sucht in der KB mit einem vorbereiteten Query-String
/// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird /// Zentrale Suchfunktion die von search_knowledge_internal und proactive_session_hints genutzt wird
/// Phase 3: Mit RAM-Cache — gleiche Query innerhalb 60s wird instant aus dem Cache bedient
pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result<String, String> { pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result<String, String> {
// Cache-Key: query + limit
let cache_key = format!("{}:{}", search_query, limit);
// Cache-Hit prüfen
{
let cache = KB_CACHE.lock().unwrap();
if let Some(cached) = cache.get(&cache_key) {
println!("⚡ KB-Cache HIT für '{}'", &search_query[..search_query.len().min(40)]);
return Ok(cached.to_string());
}
}
let pool = create_pool(); let pool = create_pool();
// Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren // Verbindung mit Timeout — DB nicht erreichbar soll nicht blockieren
@ -209,6 +282,9 @@ pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result
if results.is_empty() { if results.is_empty() {
println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]); println!("🔍 KB-Hints für '{}': keine Treffer", &search_query[..search_query.len().min(40)]);
// Leere Ergebnisse auch cachen — verhindert wiederholte DB-Abfragen
let mut cache = KB_CACHE.lock().unwrap();
cache.insert(cache_key, String::new());
return Ok(String::new()); return Ok(String::new());
} }
@ -230,9 +306,15 @@ pub async fn search_knowledge_by_query(search_query: &str, limit: i32) -> Result
hints.push("</knowledge-hints>".to_string()); hints.push("</knowledge-hints>".to_string());
let block = hints.join("\n"); let block = hints.join("\n");
println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes", println!("🔍 KB-Hints für '{}': {} Treffer, {} Bytes (cached)",
&search_query[..search_query.len().min(40)], results.len(), block.len()); &search_query[..search_query.len().min(40)], results.len(), block.len());
// In Cache speichern
{
let mut cache = KB_CACHE.lock().unwrap();
cache.insert(cache_key, block.clone());
}
Ok(block) Ok(block)
} }
@ -718,3 +800,14 @@ pub async fn auto_save_error_pattern(
) -> Result<i64, String> { ) -> Result<i64, String> {
save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await save_error_pattern_to_kb(&error_hash, &error_message, &tool, occurrence_count).await
} }
/// Phase 3: KB-Cache invalidieren (alle Eintraege loeschen)
/// Aufrufen wenn neue KB-Eintraege gespeichert wurden
#[tauri::command]
pub async fn invalidate_kb_cache() -> Result<String, String> {
let mut cache = KB_CACHE.lock().unwrap();
let count = cache.entries.len();
cache.entries.clear();
println!("🗑️ KB-Cache invalidiert ({} Eintraege geloescht)", count);
Ok(format!("{} Cache-Eintraege geloescht", count))
}

View file

@ -105,6 +105,8 @@ pub fn run() {
knowledge::extract_message_keywords, knowledge::extract_message_keywords,
knowledge::get_session_hints, knowledge::get_session_hints,
knowledge::auto_save_error_pattern, knowledge::auto_save_error_pattern,
// Phase 3: KB-Cache
knowledge::invalidate_kb_cache,
// Context-Management // Context-Management
context::get_sticky_context, context::get_sticky_context,
context::set_sticky_context, context::set_sticky_context,
@ -240,6 +242,15 @@ pub fn run() {
// Lock-Datei erstellen (Instanz-Schutz + Update-Safety) // Lock-Datei erstellen (Instanz-Schutz + Update-Safety)
update::create_lock_file(); update::create_lock_file();
// Phase 3: Bridge sofort beim App-Start starten (kein Cold-Start bei erster Nachricht)
let bridge_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
match claude::start_bridge(&bridge_handle) {
Ok(()) => println!("🔌 Bridge beim Start initialisiert (warm)"),
Err(e) => println!("⚠️ Bridge-Start beim App-Start fehlgeschlagen: {} (wird bei erster Nachricht erneut versucht)", e),
}
});
Ok(()) Ok(())
}) })
.build(tauri::generate_context!()) .build(tauri::generate_context!())