Claude-Session-ID für SDK-Fortsetzung

- events.ts: Session-ID aus claude-result speichern via set_claude_session_id
- claude.rs: load_claude_session_id() lädt ID der aktiven Session
- claude.rs: send_to_bridge_full() mit resumeSessionId Parameter
- claude-bridge.js: sendMessage() akzeptiert resumeSessionId
- Bridge nutzt sessionId in query() Optionen für SDK-Fortsetzung

Ermöglicht nahtlose Konversations-Fortsetzung auf SDK-Ebene.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-14 14:30:28 +02:00
parent 3f600b828e
commit be65dee04a
3 changed files with 78 additions and 14 deletions

View file

@ -119,7 +119,7 @@ function summarizeToolInput(tool, input) {
// ============ Claude Agent SDK ============
async function sendMessage(message, requestId, model = null, contextOverride = null) {
async function sendMessage(message, requestId, model = null, contextOverride = null, resumeSessionId = null) {
// Modell für diese Anfrage (Parameter > State > Default)
const useModel = model || currentModel;
@ -129,31 +129,37 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
currentAgentId = randomUUID();
activeAbort = new AbortController();
const isResuming = !!resumeSessionId;
sendEvent('agent-started', {
id: currentAgentId,
type: 'Main',
task: message.substring(0, 100),
model: useModel,
resuming: isResuming,
});
// Monitor: Agent gestartet
sendMonitorEvent('agent', `Main Agent gestartet (${useModel})`, {
const resumeInfo = isResuming ? ' (Fortsetzung)' : '';
sendMonitorEvent('agent', `Main Agent gestartet (${useModel})${resumeInfo}`, {
agentId: currentAgentId,
model: useModel,
task: message.substring(0, 100),
contextTokens: useContext ? Math.ceil(useContext.length / 4) : 0,
resumeSessionId: resumeSessionId || null,
});
// Monitor: API-Request
const contextInfo = useContext ? ` +${Math.ceil(useContext.length / 4)} ctx` : '';
sendMonitorEvent('api', `${useModel}${contextInfo}`, {
sendMonitorEvent('api', `${useModel}${contextInfo}${resumeInfo}`, {
model: useModel,
promptLength: message.length,
contextLength: useContext?.length || 0,
maxTurns: 25,
resumeSessionId: resumeSessionId || null,
});
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel });
sendResponse(requestId, { agentId: currentAgentId, status: 'gestartet', model: useModel, resuming: isResuming });
// Nachricht mit Context kombinieren
const fullPrompt = useContext
@ -165,13 +171,21 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
let usedModel = useModel;
try {
// Query-Optionen zusammenstellen
const queryOptions = {
model: useModel,
maxTurns: 25,
abortController: activeAbort,
};
// Session-ID für Fortsetzung hinzufügen wenn vorhanden
if (resumeSessionId) {
queryOptions.sessionId = resumeSessionId;
}
const conversation = query({
prompt: fullPrompt,
options: {
model: useModel,
maxTurns: 25,
abortController: activeAbort,
},
options: queryOptions,
});
for await (const event of conversation) {
@ -342,8 +356,8 @@ function handleCommand(msg) {
sendError(msg.id, 'Keine Nachricht angegeben');
return;
}
// Modell und Context können pro Anfrage überschrieben werden
sendMessage(msg.message, msg.id, msg.model, msg.context);
// Modell, Context und Resume-Session-ID können pro Anfrage überschrieben werden
sendMessage(msg.message, msg.id, msg.model, msg.context, msg.resumeSessionId);
break;
case 'set-context':

View file

@ -259,11 +259,22 @@ fn handle_bridge_message(app: &AppHandle, msg: BridgeMessage) {
/// Befehl an Bridge senden
fn send_to_bridge(app: &AppHandle, command: &str, message: &str) -> Result<String, String> {
send_to_bridge_with_context(app, command, message, None)
send_to_bridge_full(app, command, message, None, None)
}
/// Befehl an Bridge senden mit optionalem Context
fn send_to_bridge_with_context(app: &AppHandle, command: &str, message: &str, context: Option<String>) -> Result<String, String> {
send_to_bridge_full(app, command, message, context, None)
}
/// Befehl an Bridge senden mit Context und Resume-Session-ID
fn send_to_bridge_full(
app: &AppHandle,
command: &str,
message: &str,
context: Option<String>,
resume_session_id: Option<String>,
) -> Result<String, String> {
let state = app.state::<Arc<Mutex<ClaudeState>>>();
let mut state = state.lock().unwrap();
@ -289,6 +300,12 @@ fn send_to_bridge_with_context(app: &AppHandle, command: &str, message: &str, co
payload["context"] = serde_json::Value::String(ctx);
}
}
// Resume-Session-ID hinzufügen wenn vorhanden
if let Some(sid) = resume_session_id {
if !sid.is_empty() {
payload["resumeSessionId"] = serde_json::Value::String(sid);
}
}
payload
},
"set-context" | "clear-context" => serde_json::json!({
@ -339,12 +356,29 @@ pub async fn send_message(app: AppHandle, message: String) -> Result<String, Str
println!("📌 Sticky Context geladen (~{} Token)", context.as_ref().map(|c| c.len() / 4).unwrap_or(0));
}
send_to_bridge_with_context(&app, "message", &message, context)?;
// Claude-Session-ID für Fortsetzung laden
let resume_session_id = load_claude_session_id(&app);
if resume_session_id.is_some() {
println!("🔗 Session fortsetzen mit Claude-ID: {:?}", resume_session_id);
}
send_to_bridge_full(&app, "message", &message, context, resume_session_id)?;
// Hinweis: Die eigentliche Antwort kommt über Events
Ok("Nachricht gesendet. Antwort folgt über Events.".to_string())
}
/// Claude-Session-ID der aktiven Session laden
fn load_claude_session_id(app: &AppHandle) -> Option<String> {
if let Some(db_state) = app.try_state::<Arc<Mutex<db::Database>>>() {
let db = db_state.lock().ok()?;
if let Ok(Some(session)) = db.get_active_session() {
return session.claude_session_id;
}
}
None
}
/// Sticky Context aus DB laden und als Prompt-Text rendern
fn load_sticky_context_for_prompt(app: &AppHandle) -> Option<String> {
use crate::context;

View file

@ -235,7 +235,7 @@ export async function initEventListeners(): Promise<void> {
listeners.push(
await listen<ResultEvent>('claude-result', async (event) => {
const { cost, tokens, session_id, model } = event.payload;
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model });
console.log('📊 Ergebnis:', { cost: cost ? `$${cost.toFixed(4)}` : '-', tokens, model, session_id });
// Modell an die Streaming-Nachricht anhängen und speichern
if (streamingMessageId) {
@ -261,6 +261,22 @@ export async function initEventListeners(): Promise<void> {
currentModel.set(model);
}
// Claude Session-ID speichern für Fortsetzung
if (session_id) {
const appSessionId = get(currentSessionId);
if (appSessionId) {
try {
await invoke('set_claude_session_id', {
sessionId: appSessionId,
claudeSessionId: session_id,
});
console.log('🔗 Claude Session-ID gespeichert:', session_id);
} catch (err) {
console.warn('Claude Session-ID konnte nicht gespeichert werden:', err);
}
}
}
// Session-Statistiken aktualisieren
if (tokens || cost) {
sessionStats.update((s) => ({