Phase 6: Session-Management Verbesserungen

- Session Auto-Load bei App-Start (aktive Session + Nachrichten)
- agent_id Spalte in messages-Tabelle für Agent-Zuordnung
- DbMessage Interface erweitert (agent_id)
- Session-Compacting: compact_session() fasst alte Nachrichten zusammen
- Standard: 30 letzte Nachrichten behalten, Rest als Summary

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 13:05:16 +02:00
parent 0e1cfe1b67
commit abaf4eb9bf
4 changed files with 184 additions and 5 deletions

View file

@ -35,6 +35,7 @@ pub struct ChatMessage {
pub role: String, // "user", "assistant", "system" pub role: String, // "user", "assistant", "system"
pub content: String, pub content: String,
pub model: Option<String>, pub model: Option<String>,
pub agent_id: Option<String>, // Agent der die Nachricht erzeugt hat
pub timestamp: String, pub timestamp: String,
} }
@ -160,10 +161,13 @@ impl Database {
role TEXT NOT NULL, role TEXT NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
model TEXT, model TEXT,
agent_id TEXT,
timestamp TEXT NOT NULL, timestamp TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
); );
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp); CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, timestamp);
"
", ",
)?; )?;
Ok(()) Ok(())
@ -517,19 +521,51 @@ impl Database {
Ok(()) Ok(())
} }
/// Holt die zuletzt aktualisierte aktive Session
pub fn get_active_session(&self) -> SqlResult<Option<Session>> {
let result = self.conn.query_row(
"SELECT id, claude_session_id, title, working_dir, message_count,
token_input, token_output, cost_usd, status, created_at, updated_at, last_message
FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1",
[],
|row| {
Ok(Session {
id: row.get(0)?,
claude_session_id: row.get(1)?,
title: row.get(2)?,
working_dir: row.get(3)?,
message_count: row.get(4)?,
token_input: row.get(5)?,
token_output: row.get(6)?,
cost_usd: row.get(7)?,
status: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
last_message: row.get(11)?,
})
},
);
match result {
Ok(s) => Ok(Some(s)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e),
}
}
// ============ Messages ============ // ============ Messages ============
/// Speichert eine Nachricht /// Speichert eine Nachricht
pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> { pub fn save_message(&self, msg: &ChatMessage) -> SqlResult<()> {
self.conn.execute( self.conn.execute(
"INSERT OR REPLACE INTO messages (id, session_id, role, content, model, timestamp) "INSERT OR REPLACE INTO messages (id, session_id, role, content, model, agent_id, timestamp)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![ params![
msg.id, msg.id,
msg.session_id, msg.session_id,
msg.role, msg.role,
msg.content, msg.content,
msg.model, msg.model,
msg.agent_id,
msg.timestamp, msg.timestamp,
], ],
)?; )?;
@ -539,7 +575,7 @@ impl Database {
/// Lädt alle Nachrichten einer Session /// Lädt alle Nachrichten einer Session
pub fn load_messages(&self, session_id: &str) -> SqlResult<Vec<ChatMessage>> { pub fn load_messages(&self, session_id: &str) -> SqlResult<Vec<ChatMessage>> {
let mut stmt = self.conn.prepare( let mut stmt = self.conn.prepare(
"SELECT id, session_id, role, content, model, timestamp "SELECT id, session_id, role, content, model, agent_id, timestamp
FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC" FROM messages WHERE session_id = ?1 ORDER BY timestamp ASC"
)?; )?;
@ -550,7 +586,8 @@ impl Database {
role: row.get(2)?, role: row.get(2)?,
content: row.get(3)?, content: row.get(3)?,
model: row.get(4)?, model: row.get(4)?,
timestamp: row.get(5)?, agent_id: row.get(5)?,
timestamp: row.get(6)?,
}) })
})?.collect::<SqlResult<Vec<_>>>()?; })?.collect::<SqlResult<Vec<_>>>()?;
@ -563,6 +600,83 @@ impl Database {
Ok(()) Ok(())
} }
/// Zählt Nachrichten einer Session
pub fn count_messages(&self, session_id: &str) -> SqlResult<usize> {
self.conn.query_row(
"SELECT COUNT(*) FROM messages WHERE session_id = ?1",
params![session_id],
|row| row.get(0),
)
}
/// Kompaktiert eine Session: Behält die letzten N Nachrichten, fasst ältere zusammen
/// Gibt die Anzahl der kompaktierten Nachrichten zurück
pub fn compact_session(&self, session_id: &str, keep_last: usize) -> SqlResult<usize> {
let total = self.count_messages(session_id)?;
if total <= keep_last {
return Ok(0); // Nichts zu kompaktieren
}
let to_compact = total - keep_last;
// Alte Nachrichten holen (die kompaktiert werden sollen)
let mut stmt = self.conn.prepare(
"SELECT id, role, content FROM messages
WHERE session_id = ?1
ORDER BY timestamp ASC
LIMIT ?2"
)?;
let old_messages: Vec<(String, String, String)> = stmt.query_map(
params![session_id, to_compact as i64],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?))
)?.collect::<SqlResult<Vec<_>>>()?;
if old_messages.is_empty() {
return Ok(0);
}
// Summary erstellen
let mut summary_parts: Vec<String> = Vec::new();
for (_, role, content) in &old_messages {
let preview = if content.len() > 200 {
format!("{}...", &content[..200])
} else {
content.clone()
};
summary_parts.push(format!("[{}] {}", role, preview));
}
let summary_content = format!(
"📦 **Kompaktierter Kontext** ({} Nachrichten)\n\n{}",
old_messages.len(),
summary_parts.join("\n\n---\n\n")
);
// IDs der alten Nachrichten
let old_ids: Vec<&str> = old_messages.iter().map(|(id, _, _)| id.as_str()).collect();
// Alte Nachrichten löschen
let placeholders: String = old_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
let delete_sql = format!("DELETE FROM messages WHERE id IN ({})", placeholders);
let params: Vec<&dyn rusqlite::ToSql> = old_ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect();
self.conn.execute(&delete_sql, params.as_slice())?;
// Summary-Nachricht einfügen (mit Timestamp vor allen anderen)
let summary_msg = ChatMessage {
id: uuid::Uuid::new_v4().to_string(),
session_id: session_id.to_string(),
role: "system".to_string(),
content: summary_content,
model: None,
agent_id: None,
timestamp: "1970-01-01T00:00:00Z".to_string(), // Ganz am Anfang
};
self.save_message(&summary_msg)?;
Ok(old_messages.len())
}
// ============ Settings ============ // ============ Settings ============
/// Speichert eine Einstellung /// Speichert eine Einstellung
@ -732,3 +846,22 @@ pub async fn clear_messages(app: AppHandle, session_id: String) -> Result<(), St
let db = state.lock().unwrap(); let db = state.lock().unwrap();
db.clear_messages(&session_id).map_err(|e| e.to_string()) db.clear_messages(&session_id).map_err(|e| e.to_string())
} }
/// Session kompaktieren — fasst alte Nachrichten zusammen
#[tauri::command]
pub async fn compact_session(
app: AppHandle,
session_id: String,
keep_last: Option<usize>,
) -> Result<usize, String> {
let keep = keep_last.unwrap_or(30); // Standard: letzte 30 Nachrichten behalten
let state = app.state::<DbState>();
let db = state.lock().unwrap();
let compacted = db.compact_session(&session_id, keep).map_err(|e| e.to_string())?;
if compacted > 0 {
println!("📦 Session {} kompaktiert: {} Nachrichten zusammengefasst", session_id, compacted);
}
Ok(compacted)
}

View file

@ -65,6 +65,7 @@ pub fn run() {
db::save_message, db::save_message,
db::load_messages, db::load_messages,
db::clear_messages, db::clear_messages,
db::compact_session,
]) ])
.setup(|app| { .setup(|app| {
let handle = app.handle().clone(); let handle = app.handle().clone();

View file

@ -231,6 +231,7 @@ export interface DbMessage {
role: string; role: string;
content: string; content: string;
model: string | null; model: string | null;
agent_id: string | null; // Agent der die Nachricht erzeugt hat
timestamp: string; timestamp: string;
} }
@ -242,6 +243,7 @@ export function messageToDb(msg: Message, sessionId: string): DbMessage {
role: msg.role, role: msg.role,
content: msg.content, content: msg.content,
model: msg.model || null, model: msg.model || null,
agent_id: msg.agentId || null,
timestamp: msg.timestamp.toISOString(), timestamp: msg.timestamp.toISOString(),
}; };
} }
@ -253,6 +255,7 @@ export function dbToMessage(db: DbMessage): Message {
role: db.role as Message['role'], role: db.role as Message['role'],
content: db.content, content: db.content,
model: db.model || undefined, model: db.model || undefined,
agentId: db.agent_id || undefined,
timestamp: new Date(db.timestamp), timestamp: new Date(db.timestamp),
}; };
} }

View file

@ -2,9 +2,25 @@
import '../app.css'; import '../app.css';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners } from '$lib/stores'; import { isProcessing, agentCount, currentModel, sessionStats, initEventListeners, cleanupEventListeners, currentSessionId, setMessagesFromDb, type DbMessage } from '$lib/stores';
import StopButton from '$lib/components/StopButton.svelte'; import StopButton from '$lib/components/StopButton.svelte';
// Session-Typ vom Backend
interface Session {
id: string;
claude_session_id: string | null;
title: string;
working_dir: string | null;
message_count: number;
token_input: number;
token_output: number;
cost_usd: number;
status: string;
created_at: string;
updated_at: string;
last_message: string | null;
}
onMount(async () => { onMount(async () => {
await initEventListeners(); await initEventListeners();
@ -17,6 +33,32 @@
} catch (err) { } catch (err) {
console.warn('Modell konnte nicht geladen werden:', err); console.warn('Modell konnte nicht geladen werden:', err);
} }
// Aktive Session automatisch laden (falls vorhanden)
try {
const activeSession: Session | null = await invoke('get_active_session');
if (activeSession) {
console.log('📂 Lade aktive Session:', activeSession.title);
$currentSessionId = activeSession.id;
// Session-Stats setzen
$sessionStats = {
totalTokensIn: activeSession.token_input,
totalTokensOut: activeSession.token_output,
totalCost: activeSession.cost_usd,
messageCount: activeSession.message_count,
};
// Nachrichten laden
const messages: DbMessage[] = await invoke('load_messages', { sessionId: activeSession.id });
if (messages.length > 0) {
setMessagesFromDb(messages);
console.log(`💬 ${messages.length} Nachrichten geladen`);
}
}
} catch (err) {
console.warn('Aktive Session konnte nicht geladen werden:', err);
}
}); });
onDestroy(async () => { onDestroy(async () => {