perf: lazy panel loading + auto-retry + bridge heartbeat [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled

- 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 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-04-20 22:21:51 +02:00
parent 3f22e735cd
commit 60e426a13d
2 changed files with 127 additions and 43 deletions

View file

@ -9,8 +9,10 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
import { createInterface } from 'node:readline'; import { createInterface } from 'node:readline';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
// Prozess am Leben halten // Prozess am Leben halten + Heartbeat an Rust alle 30s
const keepAlive = setInterval(() => {}, 60000); const keepAlive = setInterval(() => {
sendEvent('heartbeat', { ts: Date.now(), uptime: process.uptime() });
}, 30000);
process.stdin.resume(); process.stdin.resume();
// ============ State ============ // ============ State ============
@ -403,6 +405,28 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
options: queryOptions, 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 // Dedupe: Manche Tool-Events kommen sowohl in assistant-Blocks
// als auch als standalone tool_use Event. Via toolUseId deduplizieren. // als auch als standalone tool_use Event. Via toolUseId deduplizieren.
const handledTools = new Set(); const handledTools = new Set();
@ -603,6 +627,65 @@ async function sendMessage(message, requestId, model = null, contextOverride = n
if (err.name === 'AbortError') { if (err.name === 'AbortError') {
// Abgebrochen — kein Fehler // Abgebrochen — kein Fehler
sendMonitorEvent('agent', 'Abgebrochen (User)', { reason: 'abort' }); 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 { } else {
sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` }); sendEvent('text', { text: `\n\n**Fehler:** ${err.message || err}` });

View file

@ -1,24 +1,43 @@
<script lang="ts"> <script lang="ts">
import { PaneGroup, Pane, PaneResizer } from 'paneforge'; import { PaneGroup, Pane, PaneResizer } from 'paneforge';
// Kritische Panels sofort laden (immer sichtbar)
import SessionList from '$lib/components/SessionList.svelte'; import SessionList from '$lib/components/SessionList.svelte';
import ChatPanel from '$lib/components/ChatPanel.svelte'; import ChatPanel from '$lib/components/ChatPanel.svelte';
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
import AgentView from '$lib/components/AgentView.svelte'; // Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel
import MemoryPanel from '$lib/components/MemoryPanel.svelte'; const lazyPanels = {
import KnowledgePanel from '$lib/components/KnowledgePanel.svelte'; activity: () => import('$lib/components/ActivityPanel.svelte'),
import ContextPanel from '$lib/components/ContextPanel.svelte'; monitor: () => import('$lib/components/MonitorPanel.svelte'),
import AuditLog from '$lib/components/AuditLog.svelte'; perf: () => import('$lib/components/PerformancePanel.svelte'),
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte'; knowledge: () => import('$lib/components/KnowledgePanel.svelte'),
import SettingsPanel from '$lib/components/SettingsPanel.svelte'; memory: () => import('$lib/components/MemoryPanel.svelte'),
import MonitorPanel from '$lib/components/MonitorPanel.svelte'; audit: () => import('$lib/components/AuditLog.svelte'),
import PerformancePanel from '$lib/components/PerformancePanel.svelte'; programs: () => import('$lib/components/ProgramsPanel.svelte'),
import HooksPanel from '$lib/components/HooksPanel.svelte'; agents: () => import('$lib/components/AgentView.svelte'),
import ProgramsPanel from '$lib/components/ProgramsPanel.svelte'; voice: () => import('$lib/components/VoicePanel.svelte'),
import VoicePanel from '$lib/components/VoicePanel.svelte'; context: () => import('$lib/components/ContextPanel.svelte'),
hooks: () => import('$lib/components/HooksPanel.svelte'),
guards: () => import('$lib/components/GuardRailsPanel.svelte'),
settings: () => import('$lib/components/SettingsPanel.svelte'),
} as Record<string, () => Promise<{ default: any }>>;
// Cache für bereits geladene Module (kein Re-Import bei Tab-Wechsel)
const loadedPanels: Record<string, Promise<{ default: any }>> = {};
function getPanel(id: string): Promise<{ default: any }> {
if (!loadedPanels[id]) {
loadedPanels[id] = lazyPanels[id]();
}
return loadedPanels[id];
}
let activeMiddleTab = 'activity'; let activeMiddleTab = 'activity';
let activeRightTab = 'agents'; let activeRightTab = 'agents';
// Sofort die Default-Tabs vorladen (nach dem kritischen Pfad)
$: void getPanel(activeMiddleTab);
$: void getPanel(activeRightTab);
const middleTabs = [ const middleTabs = [
{ id: 'activity', label: 'Aktivität', icon: '📋' }, { id: 'activity', label: 'Aktivität', icon: '📋' },
{ id: 'monitor', label: 'Monitor', icon: '📊' }, { id: 'monitor', label: 'Monitor', icon: '📊' },
@ -72,21 +91,11 @@
{/each} {/each}
</div> </div>
<div class="panel-content"> <div class="panel-content">
{#if activeMiddleTab === 'activity'} {#await getPanel(activeMiddleTab) then mod}
<ActivityPanel /> <svelte:component this={mod.default} />
{:else if activeMiddleTab === 'monitor'} {:catch}
<MonitorPanel /> <p style="padding:1rem;color:var(--text-secondary)">Panel laden...</p>
{:else if activeMiddleTab === 'perf'} {/await}
<PerformancePanel />
{:else if activeMiddleTab === 'knowledge'}
<KnowledgePanel />
{:else if activeMiddleTab === 'memory'}
<MemoryPanel />
{:else if activeMiddleTab === 'audit'}
<AuditLog />
{:else if activeMiddleTab === 'programs'}
<ProgramsPanel />
{/if}
</div> </div>
</Pane> </Pane>
@ -108,19 +117,11 @@
{/each} {/each}
</div> </div>
<div class="panel-content"> <div class="panel-content">
{#if activeRightTab === 'agents'} {#await getPanel(activeRightTab) then mod}
<AgentView /> <svelte:component this={mod.default} />
{:else if activeRightTab === 'voice'} {:catch}
<VoicePanel /> <p style="padding:1rem;color:var(--text-secondary)">Panel laden...</p>
{:else if activeRightTab === 'context'} {/await}
<ContextPanel />
{:else if activeRightTab === 'hooks'}
<HooksPanel />
{:else if activeRightTab === 'guards'}
<GuardRailsPanel />
{:else if activeRightTab === 'settings'}
<SettingsPanel />
{/if}
</div> </div>
</Pane> </Pane>
</PaneGroup> </PaneGroup>