perf: lazy panel loading + auto-retry + bridge heartbeat [appimage]
Some checks failed
Build AppImage / build (push) Has been cancelled
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:
parent
3f22e735cd
commit
60e426a13d
2 changed files with 127 additions and 43 deletions
|
|
@ -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}` });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { PaneGroup, Pane, PaneResizer } from 'paneforge';
|
||||
// Kritische Panels sofort laden (immer sichtbar)
|
||||
import SessionList from '$lib/components/SessionList.svelte';
|
||||
import ChatPanel from '$lib/components/ChatPanel.svelte';
|
||||
import ActivityPanel from '$lib/components/ActivityPanel.svelte';
|
||||
import AgentView from '$lib/components/AgentView.svelte';
|
||||
import MemoryPanel from '$lib/components/MemoryPanel.svelte';
|
||||
import KnowledgePanel from '$lib/components/KnowledgePanel.svelte';
|
||||
import ContextPanel from '$lib/components/ContextPanel.svelte';
|
||||
import AuditLog from '$lib/components/AuditLog.svelte';
|
||||
import GuardRailsPanel from '$lib/components/GuardRailsPanel.svelte';
|
||||
import SettingsPanel from '$lib/components/SettingsPanel.svelte';
|
||||
import MonitorPanel from '$lib/components/MonitorPanel.svelte';
|
||||
import PerformancePanel from '$lib/components/PerformancePanel.svelte';
|
||||
import HooksPanel from '$lib/components/HooksPanel.svelte';
|
||||
import ProgramsPanel from '$lib/components/ProgramsPanel.svelte';
|
||||
import VoicePanel from '$lib/components/VoicePanel.svelte';
|
||||
|
||||
// Sekundäre Panels: Lazy-Load bei erstem Tab-Wechsel
|
||||
const lazyPanels = {
|
||||
activity: () => import('$lib/components/ActivityPanel.svelte'),
|
||||
monitor: () => import('$lib/components/MonitorPanel.svelte'),
|
||||
perf: () => import('$lib/components/PerformancePanel.svelte'),
|
||||
knowledge: () => import('$lib/components/KnowledgePanel.svelte'),
|
||||
memory: () => import('$lib/components/MemoryPanel.svelte'),
|
||||
audit: () => import('$lib/components/AuditLog.svelte'),
|
||||
programs: () => import('$lib/components/ProgramsPanel.svelte'),
|
||||
agents: () => import('$lib/components/AgentView.svelte'),
|
||||
voice: () => import('$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 activeRightTab = 'agents';
|
||||
|
||||
// Sofort die Default-Tabs vorladen (nach dem kritischen Pfad)
|
||||
$: void getPanel(activeMiddleTab);
|
||||
$: void getPanel(activeRightTab);
|
||||
|
||||
const middleTabs = [
|
||||
{ id: 'activity', label: 'Aktivität', icon: '📋' },
|
||||
{ id: 'monitor', label: 'Monitor', icon: '📊' },
|
||||
|
|
@ -72,21 +91,11 @@
|
|||
{/each}
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
{#if activeMiddleTab === 'activity'}
|
||||
<ActivityPanel />
|
||||
{:else if activeMiddleTab === 'monitor'}
|
||||
<MonitorPanel />
|
||||
{:else if activeMiddleTab === 'perf'}
|
||||
<PerformancePanel />
|
||||
{:else if activeMiddleTab === 'knowledge'}
|
||||
<KnowledgePanel />
|
||||
{:else if activeMiddleTab === 'memory'}
|
||||
<MemoryPanel />
|
||||
{:else if activeMiddleTab === 'audit'}
|
||||
<AuditLog />
|
||||
{:else if activeMiddleTab === 'programs'}
|
||||
<ProgramsPanel />
|
||||
{/if}
|
||||
{#await getPanel(activeMiddleTab) then mod}
|
||||
<svelte:component this={mod.default} />
|
||||
{:catch}
|
||||
<p style="padding:1rem;color:var(--text-secondary)">Panel laden...</p>
|
||||
{/await}
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
|
|
@ -108,19 +117,11 @@
|
|||
{/each}
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
{#if activeRightTab === 'agents'}
|
||||
<AgentView />
|
||||
{:else if activeRightTab === 'voice'}
|
||||
<VoicePanel />
|
||||
{:else if activeRightTab === 'context'}
|
||||
<ContextPanel />
|
||||
{:else if activeRightTab === 'hooks'}
|
||||
<HooksPanel />
|
||||
{:else if activeRightTab === 'guards'}
|
||||
<GuardRailsPanel />
|
||||
{:else if activeRightTab === 'settings'}
|
||||
<SettingsPanel />
|
||||
{/if}
|
||||
{#await getPanel(activeRightTab) then mod}
|
||||
<svelte:component this={mod.default} />
|
||||
{:catch}
|
||||
<p style="padding:1rem;color:var(--text-secondary)">Panel laden...</p>
|
||||
{/await}
|
||||
</div>
|
||||
</Pane>
|
||||
</PaneGroup>
|
||||
|
|
|
|||
Loading…
Reference in a new issue