From 60e426a13d8c74914552ab94e8cb01a8182c67fd Mon Sep 17 00:00:00 2001 From: Eddy Date: Mon, 20 Apr 2026 22:21:51 +0200 Subject: [PATCH] perf: lazy panel loading + auto-retry + bridge heartbeat [appimage] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Panels werden erst bei Tab-Aktivierung geladen (dynamic import mit Cache) → schnellerer App-Start, weniger initiales DOM - Auto-Retry mit Backoff (3 Versuche, 2s/5s/10s) bei transienten Fehlern (Rate-Limit, Netzwerk, 5xx) → keine manuellen Neustarts mehr - Bridge-Heartbeat alle 30s → Rust erkennt tote Bridge sofort Co-Authored-By: Claude Opus 4.6 --- scripts/claude-bridge.js | 87 +++++++++++++++++++++++++++++++++++++++- src/routes/+page.svelte | 83 +++++++++++++++++++------------------- 2 files changed, 127 insertions(+), 43 deletions(-) diff --git a/scripts/claude-bridge.js b/scripts/claude-bridge.js index 5336461..dff7356 100644 --- a/scripts/claude-bridge.js +++ b/scripts/claude-bridge.js @@ -9,8 +9,10 @@ import { query } from '@anthropic-ai/claude-agent-sdk'; import { createInterface } from 'node:readline'; import { randomUUID } from 'node:crypto'; -// Prozess am Leben halten -const keepAlive = setInterval(() => {}, 60000); +// Prozess am Leben halten + Heartbeat an Rust alle 30s +const keepAlive = setInterval(() => { + sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() }); +}, 30000); process.stdin.resume(); // ============ State ============ @@ -403,6 +405,28 @@ async function sendMessage(message, requestId, model = null, contextOverride = n options: queryOptions, }); + // Auto-Retry bei transienten Fehlern (Rate-Limit, Netzwerk, 5xx) + const MAX_RETRIES = 3; + const RETRY_DELAYS = [2000, 5000, 10000]; // Exponentielles Backoff + + function isRetryableError(err) { + const msg = (err?.message || String(err)).toLowerCase(); + return ( + msg.includes('rate limit') || + msg.includes('429') || + msg.includes('overloaded') || + msg.includes('529') || + msg.includes('500') || + msg.includes('502') || + msg.includes('503') || + msg.includes('network') || + msg.includes('econnreset') || + msg.includes('timeout') || + msg.includes('etimedout') || + msg.includes('fetch failed') + ); + } + // Dedupe: Manche Tool-Events kommen sowohl in assistant-Blocks // als auch als standalone tool_use Event. Via toolUseId deduplizieren. const handledTools = new Set(); @@ -603,6 +627,65 @@ async function sendMessage(message, requestId, model = null, contextOverride = n if (err.name === 'AbortError') { // Abgebrochen — kein Fehler sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' }); + } else if (isRetryableError(err) && !activeAbort?.signal?.aborted) { + // Transienter Fehler — Retry mit Backoff + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + const delay = RETRY_DELAYS[attempt] || 10000; + sendMonitorEvent('agent', `Retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms: ${err.message}`, { + attempt: attempt + 1, + delay, + error: err.message, + }); + sendEvent('text', { text: `\n⏳ Netzwerk-Fehler, Retry ${attempt + 1}/${MAX_RETRIES}...` }); + + await new Promise(r => setTimeout(r, delay)); + + if (activeAbort?.signal?.aborted) break; + + try { + conversation = query({ prompt: fullPrompt, options: queryOptions }); + for await (const event of conversation) { + switch (event.type) { + case 'assistant': + if (event.message?.content) { + for (const block of event.message.content) { + if (block.type === 'text' && block.text) { + fullText += block.text; + sendEvent('text', { text: block.text }); + } else if (block.type === 'tool_use') { + handleToolUse(block); + } + } + } + break; + case 'tool_use': handleToolUse(event); break; + case 'tool_result': handleToolResult(event); break; + case 'result': + sendEvent('result', { + text: fullText, + cost: event.total_cost_usd || 0, + tokens: { input: (event.usage?.input_tokens || 0) + (event.usage?.cache_read_input_tokens || 0), output: event.usage?.output_tokens || 0 }, + session_id: event.session_id || '', + duration_ms: Date.now() - startTime, + model: event.message?.model || usedModel, + }); + break; + } + } + // Retry erfolgreich — raus + sendMonitorEvent('agent', `Retry ${attempt + 1} erfolgreich`, {}); + break; + } catch (retryErr) { + if (retryErr.name === 'AbortError') break; + if (attempt === MAX_RETRIES - 1) { + sendEvent('text', { text: `\n\n**Fehler nach ${MAX_RETRIES} Versuchen:** ${retryErr.message || retryErr}` }); + sendMonitorEvent('error', `Endgültig fehlgeschlagen nach ${MAX_RETRIES} Retries: ${retryErr.message}`, { + name: retryErr.name, + attempts: MAX_RETRIES, + }); + } + } + } } else { sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 13266b5..7ac74dd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,24 +1,43 @@