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 { 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}` });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue