From 8f8ab15782497549506f9ceb9b71ec57aa8583d2 Mon Sep 17 00:00:00 2001 From: ShaerWare Date: Tue, 31 Mar 2026 21:57:50 +0500 Subject: [PATCH 1/2] fix: prevent bridge from dying on failed backend switch + add auto-restart Bridge was killed BEFORE verifying the target backend (vLLM) is available. If vLLM failed to start, bridge stayed dead with no auto-recovery. Now bridge is stopped only AFTER successful switch, and a periodic health check (60s) auto-restarts it if it crashes. Co-Authored-By: Claude Opus 4.6 (1M context) --- modules/llm/router.py | 22 ++++++++++++++-------- modules/llm/startup.py | 26 ++++++++++++++++++++++++++ orchestrator.py | 5 ++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/modules/llm/router.py b/modules/llm/router.py index 697af37..ace4a45 100644 --- a/modules/llm/router.py +++ b/modules/llm/router.py @@ -392,14 +392,8 @@ async def admin_set_llm_backend( """Переключить LLM backend с горячей перезагрузкой сервиса""" container = get_container() - # Stop bridge if currently running and switching away from it - current_service = container.llm_service - if current_service and getattr(current_service, "provider_type", None) == "claude_bridge": - from bridge_manager import bridge_manager - - if bridge_manager.is_running: - logger.info("🛑 Stopping bridge (switching backend)...") - await bridge_manager.stop() + # NOTE: bridge is stopped AFTER successful switch (not before), + # so if the target backend fails, bridge remains running. # Auto-convert "gemini" to default cloud Gemini provider if request.backend == "gemini": @@ -513,6 +507,18 @@ async def check_vllm_health() -> bool: except ImportError: raise HTTPException(status_code=503, detail="VLLMLLMService не доступен") + # Stop bridge only after vLLM is confirmed working + current_service = container.llm_service + if ( + current_service + and getattr(current_service, "provider_type", None) == "claude_bridge" + ): + from bridge_manager import bridge_manager + + if bridge_manager.is_running: + logger.info("🛑 Stopping bridge (switching to vLLM)...") + await bridge_manager.stop() + container.llm_service = new_service os.environ["LLM_BACKEND"] = "vllm" diff --git a/modules/llm/startup.py b/modules/llm/startup.py index ef71033..5675f28 100644 --- a/modules/llm/startup.py +++ b/modules/llm/startup.py @@ -255,3 +255,29 @@ async def auto_start_bridge() -> None: logger.warning(f"🌉 Bridge auto-start failed: {result.get('error', 'unknown')}") except Exception as e: logger.error(f"🌉 Error during bridge auto-start: {e}") + + +async def bridge_health_check() -> None: + """Periodic health check for bridge — auto-restart if crashed.""" + from bridge_manager import bridge_manager + from db.integration import async_cloud_provider_manager + + try: + bridge_providers = await async_cloud_provider_manager.get_by_type( + "claude_bridge", enabled_only=True + ) + if not bridge_providers: + return + + if bridge_manager.is_running: + return + + # Bridge should be running but isn't — restart it + logger.warning("🌉 Bridge not running, auto-restarting...") + result = await bridge_manager.start() + if result.get("status") == "ok": + logger.info(f"🌉 Bridge auto-restarted on port {result.get('port', 8787)}") + else: + logger.warning(f"🌉 Bridge auto-restart failed: {result.get('error', 'unknown')}") + except Exception as e: + logger.error(f"🌉 Bridge health check error: {e}") diff --git a/orchestrator.py b/orchestrator.py index 48e4567..e74ac76 100644 --- a/orchestrator.py +++ b/orchestrator.py @@ -247,7 +247,7 @@ async def startup_event(): # Auto-start bots and bridge from modules.channels.telegram.startup import auto_start_bots as auto_start_telegram from modules.channels.whatsapp.startup import auto_start_bots as auto_start_whatsapp - from modules.llm.startup import auto_start_bridge + from modules.llm.startup import auto_start_bridge, bridge_health_check await auto_start_telegram() await auto_start_whatsapp() @@ -260,6 +260,9 @@ async def startup_event(): from modules.kanban.tasks import sync_kanban_issues task_registry.register("session-cleanup", cleanup_expired_sessions, interval=3600) + task_registry.register( + "bridge-health-check", bridge_health_check, interval=60, initial_delay=30 + ) task_registry.register( "periodic-vacuum", periodic_vacuum, interval=7 * 24 * 3600, initial_delay=24 * 3600 ) From 0029cb71f275f46749133188ee3dbc4e19f2ac4e Mon Sep 17 00:00:00 2001 From: ShaerWare Date: Tue, 31 Mar 2026 23:39:00 +0500 Subject: [PATCH 2/2] feat: replace CC toggle with orchestra panel in chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Claude Code toggle button in focused chat mode with a sliding right panel (same UX as branch tree / settings panels). Panel has two tabs: "Orchestras" (lists CC sessions linked to the current chat) and "Roadmap" (placeholder for future roadmap view). Relates to #285 ## NEWS 🎼 **Панель оркестров Claude Code в чате** Кнопка Claude Code в чат-режиме теперь открывает выезжающую панель справа — как у дерева веток. Во вкладке «Оркестры» видны все запущенные сессии Claude Code для текущего чата со статусами и моделями. Вкладка «Дорожная карта» — скоро! Co-Authored-By: Claude Opus 4.6 --- admin/src/components/CcOrchestraPanel.vue | 171 ++++++++++++++++++++++ admin/src/plugins/i18n.ts | 24 +++ admin/src/views/ChatView.vue | 41 ++++-- 3 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 admin/src/components/CcOrchestraPanel.vue diff --git a/admin/src/components/CcOrchestraPanel.vue b/admin/src/components/CcOrchestraPanel.vue new file mode 100644 index 0000000..fd8709e --- /dev/null +++ b/admin/src/components/CcOrchestraPanel.vue @@ -0,0 +1,171 @@ + + + diff --git a/admin/src/plugins/i18n.ts b/admin/src/plugins/i18n.ts index d3b98ca..59d44a5 100644 --- a/admin/src/plugins/i18n.ts +++ b/admin/src/plugins/i18n.ts @@ -390,6 +390,14 @@ const messages = { attachFiles: "Файлы из чата", workDir: "Рабочая папка", }, + ccPanel: { + orchestras: "Оркестры", + roadmap: "Дорожная карта", + noChat: "Выберите чат", + noOrchestras: "Нет запущенных оркестров", + roadmapPlaceholder: "Дорожная карта (скоро)", + title: "Оркестры Claude Code", + }, }, // Auth auth: { @@ -1747,6 +1755,14 @@ const messages = { attachFiles: "Files from chat", workDir: "Working directory", }, + ccPanel: { + orchestras: "Orchestras", + roadmap: "Roadmap", + noChat: "Select a chat", + noOrchestras: "No orchestras running", + roadmapPlaceholder: "Roadmap (coming soon)", + title: "Claude Code Orchestras", + }, }, // Auth auth: { @@ -3104,6 +3120,14 @@ const messages = { attachFiles: "Чаттан файлдар", workDir: "Жұмыс қалтасы", }, + ccPanel: { + orchestras: "Оркестрлер", + roadmap: "Жол картасы", + noChat: "Чат таңдаңыз", + noOrchestras: "Іске қосылған оркестрлер жоқ", + roadmapPlaceholder: "Жол картасы (жақында)", + title: "Claude Code оркестрлері", + }, }, // Auth auth: { diff --git a/admin/src/views/ChatView.vue b/admin/src/views/ChatView.vue index 2b03308..f4f1e81 100644 --- a/admin/src/views/ChatView.vue +++ b/admin/src/views/ChatView.vue @@ -28,6 +28,7 @@ import { chatApi, ttsApi, llmApi, sttApi, wikiRagApi, type ChatSession, type Cha import BranchTree from '@/components/BranchTree.vue' import ChatShareDialog from '@/components/ChatShareDialog.vue' import ArtifactPanel, { type Artifact } from '@/components/ArtifactPanel.vue' +import CcOrchestraPanel from '@/components/CcOrchestraPanel.vue' import { useConfirmStore } from '@/stores/confirm' import { useToastStore } from '@/stores/toast' import { useAuthStore } from '@/stores/auth' @@ -192,6 +193,7 @@ const { width: sidebarWidth, startResize: startSidebarResize, startTouchResize: const { width: branchTreeWidth, startResize: startBranchResize, startTouchResize: startBranchTouchResize } = useResizablePanel('chat-branch-width', 208, 160, 400, 'left') const { width: settingsWidth, startResize: startSettingsResize, startTouchResize: startSettingsTouchResize } = useResizablePanel('chat-settings-width', 500, 300, 800, 'left') const { width: artifactWidth, startResize: startArtifactResize, startTouchResize: startArtifactTouchResize } = useResizablePanel('chat-artifact-width', 500, 300, 800, 'left') +const { width: ccPanelWidth, startResize: startCcPanelResize, startTouchResize: startCcPanelTouchResize } = useResizablePanel('chat-cc-panel-width', 320, 220, 500, 'left') // Pasted content blocks const pastedBlocks = ref([]) @@ -402,9 +404,13 @@ function renderMarkdown(content: string, messageId?: string): string { // Branch tree toggle const showBranchTree = ref(false) +// CC Orchestra panel toggle +const showCcPanel = ref(false) + // Mutual exclusivity: panel watchers -watch(showBranchTree, (v) => { if (v) closeArtifact() }) -watch(showSettings, (v) => { if (v) closeArtifact() }) +watch(showBranchTree, (v) => { if (v) { closeArtifact(); showCcPanel.value = false } }) +watch(showSettings, (v) => { if (v) { closeArtifact(); showCcPanel.value = false } }) +watch(showCcPanel, (v) => { if (v) { closeArtifact(); showBranchTree.value = false } }) // File attachment state const attachedFiles = ref<{ name: string; content: string }[]>([]) @@ -2305,15 +2311,15 @@ class="w-4 h-4 rounded border flex items-center justify-center shrink-0" - + @@ -2840,15 +2846,15 @@ class="w-4 h-4 rounded border flex items-center justify-center shrink-0" - + @@ -3809,6 +3815,21 @@ class="w-4 h-4 rounded border flex items-center justify-center shrink-0" + + +