+ {/* #002 (FR-010 / US5): extraction coverage. Only meaningful once chunks
+ exist; near-0% with chunks present means the wiki is built but has no
+ structure (the extraction model is failing). */}
+ {!loading && status && status.total_chunks > 0 && status.extraction_coverage != null ? (
+
+ ) : null}
+
{/* Auto-sync toggle row — markup mirrors AIPanel's inline ToggleRow */}
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 77e77ddcb4..4cbac6c841 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -4390,6 +4390,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'رفض التخزين المحلي',
'pages.settings.account.security': 'الأمان',
'pages.settings.account.securityDesc': 'وضع تخزين الأسرار وحالة سلسلة المفاتيح',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'متدهور',
+ 'memoryTree.status.degradedRecall': 'الاسترجاع الدلالي معطّل',
+ 'memoryTree.status.degradedStructure': 'بنية الويكي غير مكتملة',
+ 'memoryTree.status.extractionCoverage': 'تغطية الاستخراج: {pct}% من الأجزاء لها بنية',
+ 'memory.health.remediation.budget_exhausted':
+ 'استنفدت تضمينات الذاكرة الميزانية المُدارة. أعدّ تضمينات Ollama المحلية (الإعدادات → الذكاء الاصطناعي → التضمينات) أو أضف مفتاح API الخاص بك للتضمينات لمواصلة بناء الذاكرة.',
+ 'memory.health.remediation.auth_missing':
+ 'لم يتم العثور على بيانات اعتماد التضمينات. سجّل الدخول إلى OpenHuman، أو أعدّ تضمينات Ollama المحلية في الإعدادات → الذكاء الاصطناعي → التضمينات.',
+ 'memory.health.remediation.auth_invalid':
+ 'تم رفض بيانات اعتماد التضمينات الخاصة بك. أعد المصادقة، أو بدّل إلى تضمينات Ollama المحلية في الإعدادات → الذكاء الاصطناعي → التضمينات.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'لم يتم تكوين أي مزوّد تضمينات، لذا فإن الاسترجاع الدلالي معطّل. أعدّ تضمينات Ollama المحلية (موصى به) أو أضف مفتاح تضمينات في الإعدادات → الذكاء الاصطناعي → التضمينات.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'يعيد نموذج التضمين حجم متجه خاطئًا (تتوقع الذاكرة 1024 بُعدًا). اختر نموذجًا بـ 1024 بُعدًا، أو اطلب 1024 بُعدًا من مزوّدك.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'نموذج محلي مطلوب غير متوفر. ثبّت/شغّل Ollama ونزّل النموذج، أو بدّل هذا الحِمل إلى مزوّد سحابي في الإعدادات → الذكاء الاصطناعي.',
+ 'memory.health.remediation.extraction_timeout':
+ 'يتجاوز نموذج استخراج الذاكرة المهلة الزمنية، لذا فإن بنية الويكي قليلة. بدّل نموذج استخراج الذاكرة إلى نموذج أسرع في الإعدادات → الذكاء الاصطناعي.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'لا يتوفر مزوّد تلخيص لميزة إنشاء أشجار التلخيص. فعّل الذكاء الاصطناعي المحلي (Ollama)، أو فعّل تلخيص السحابة في الإعدادات → الذكاء الاصطناعي → الذاكرة.',
+ 'memory.health.remediation.transient':
+ 'حدث خطأ مؤقت أدى إلى مقاطعة معالجة الذاكرة. ستتم إعادة المحاولة تلقائيًا.',
+ 'memory.health.remediation.unknown':
+ 'واجهت معالجة الذاكرة مشكلة. تحقق من الإعدادات → الذكاء الاصطناعي للتكوين.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'الملف: {title}',
'chat.artifact.generating': 'جارٍ إنشاء {kind}…',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 44d109a6df..1b47ffee82 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -4468,6 +4468,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'স্থানীয় সঞ্চয়স্থান প্রত্যাখ্যান করুন',
'pages.settings.account.security': 'নিরাপত্তা',
'pages.settings.account.securityDesc': 'গোপনীয়তা সঞ্চয়স্থান মোড এবং কিচেন অবস্থা',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'অবনমিত',
+ 'memoryTree.status.degradedRecall': 'সিম্যান্টিক রিকল নিষ্ক্রিয়',
+ 'memoryTree.status.degradedStructure': 'উইকি কাঠামো অসম্পূর্ণ',
+ 'memoryTree.status.extractionCoverage': 'এক্সট্র্যাকশন কভারেজ: {pct}% অংশের কাঠামো আছে',
+ 'memory.health.remediation.budget_exhausted':
+ 'মেমরি এমবেডিং পরিচালিত বাজেটে পৌঁছেছে। স্থানীয় Ollama এমবেডিং সেট আপ করুন (সেটিংস → AI → এমবেডিংস) অথবা মেমরি তৈরি চালিয়ে যেতে আপনার নিজস্ব এমবেডিং API কী যোগ করুন।',
+ 'memory.health.remediation.auth_missing':
+ 'কোনও এমবেডিং শংসাপত্র পাওয়া যায়নি। OpenHuman-এ লগ ইন করুন, অথবা সেটিংস → AI → এমবেডিংস-এ স্থানীয় Ollama এমবেডিং সেট আপ করুন।',
+ 'memory.health.remediation.auth_invalid':
+ 'আপনার এমবেডিং শংসাপত্র প্রত্যাখ্যাত হয়েছে। পুনরায় প্রমাণীকরণ করুন, অথবা সেটিংস → AI → এমবেডিংস-এ স্থানীয় Ollama এমবেডিং-এ স্যুইচ করুন।',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'কোনও এমবেডিং প্রদানকারী কনফিগার করা নেই, তাই সিম্যান্টিক রিকল বন্ধ। স্থানীয় Ollama এমবেডিং সেট আপ করুন (প্রস্তাবিত) অথবা সেটিংস → AI → এমবেডিংস-এ একটি এমবেডিং কী যোগ করুন।',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'এমবেডিং মডেল ভুল ভেক্টর আকার ফেরত দেয় (মেমরি 1024 মাত্রা প্রত্যাশা করে)। 1024-মাত্রার একটি মডেল বেছে নিন, অথবা আপনার প্রদানকারীর কাছে 1024 মাত্রা অনুরোধ করুন।',
+ 'memory.health.remediation.local_model_unavailable':
+ 'একটি প্রয়োজনীয় স্থানীয় মডেল উপলব্ধ নেই। Ollama ইনস্টল/চালু করুন এবং মডেলটি ডাউনলোড করুন, অথবা সেটিংস → AI-তে এই কাজের চাপ একটি ক্লাউড প্রদানকারীতে স্যুইচ করুন।',
+ 'memory.health.remediation.extraction_timeout':
+ 'মেমরি এক্সট্র্যাকশন মডেল টাইম আউট হচ্ছে, তাই উইকিতে সামান্য কাঠামো আছে। সেটিংস → AI-তে মেমরি এক্সট্র্যাকশন মডেল একটি দ্রুততর মডেলে পরিবর্তন করুন।',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'সারাংশ ট্রি তৈরির জন্য কোনও সারাংশ প্রদানকারী উপলব্ধ নেই। স্থানীয় AI (Ollama) সক্ষম করুন, অথবা সেটিংস → AI → মেমরিতে ক্লাউড সারাংশ সক্ষম করুন।',
+ 'memory.health.remediation.transient':
+ 'একটি অস্থায়ী ত্রুটি মেমরি প্রক্রিয়াকরণে বাধা দিয়েছে। স্বয়ংক্রিয়ভাবে পুনরায় চেষ্টা করা হবে।',
+ 'memory.health.remediation.unknown':
+ 'মেমরি প্রক্রিয়াকরণে একটি সমস্যা হয়েছে। কনফিগারেশনের জন্য সেটিংস → AI পরীক্ষা করুন।',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'আর্টিফ্যাক্ট: {title}',
'chat.artifact.generating': '{kind} তৈরি হচ্ছে…',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 2d4a1e8861..f80c69f583 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -4585,6 +4585,32 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Lokalen Speicher ablehnen',
'pages.settings.account.security': 'Sicherheit',
'pages.settings.account.securityDesc': 'Geheimnisspeicher-Modus und Schlüsselbund-Status',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Eingeschränkt',
+ 'memoryTree.status.degradedRecall': 'Semantische Suche deaktiviert',
+ 'memoryTree.status.degradedStructure': 'Wiki-Struktur unvollständig',
+ 'memoryTree.status.extractionCoverage':
+ 'Extraktionsabdeckung: {pct}% der Abschnitte haben Struktur',
+ 'memory.health.remediation.budget_exhausted':
+ 'Die Speicher-Embeddings haben das verwaltete Budget erreicht. Richte lokale Ollama-Embeddings ein (Einstellungen → KI → Einbettungen) oder füge deinen eigenen Embeddings-API-Schlüssel hinzu, um den Speicher weiter aufzubauen.',
+ 'memory.health.remediation.auth_missing':
+ 'Keine Embeddings-Anmeldedaten gefunden. Melde dich bei OpenHuman an oder richte lokale Ollama-Embeddings unter Einstellungen → KI → Einbettungen ein.',
+ 'memory.health.remediation.auth_invalid':
+ 'Deine Embeddings-Anmeldedaten wurden abgelehnt. Authentifiziere dich erneut oder wechsle unter Einstellungen → KI → Einbettungen zu lokalen Ollama-Embeddings.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Es ist kein Embeddings-Anbieter konfiguriert, daher ist die semantische Suche deaktiviert. Richte lokale Ollama-Embeddings ein (empfohlen) oder füge unter Einstellungen → KI → Einbettungen einen Embeddings-Schlüssel hinzu.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'Das Embedding-Modell liefert die falsche Vektorgröße (der Speicher erwartet 1024 Dimensionen). Wähle ein Modell mit 1024 Dimensionen oder fordere 1024 Dimensionen von deinem Anbieter an.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Ein erforderliches lokales Modell ist nicht verfügbar. Installiere/starte Ollama und lade das Modell herunter, oder wechsle diese Arbeitslast unter Einstellungen → KI zu einem Cloud-Anbieter.',
+ 'memory.health.remediation.extraction_timeout':
+ 'Das Modell zur Speicherextraktion überschreitet die Zeit, daher hat das Wiki wenig Struktur. Wechsle das Modell für die Speicherextraktion unter Einstellungen → KI zu einem schnelleren.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'Für „Zusammenfassungsbäume erstellen” ist kein Zusammenfassungsanbieter verfügbar. Aktiviere die lokale KI (Ollama) oder aktiviere die Cloud-Zusammenfassung unter Einstellungen → KI → Speicher.',
+ 'memory.health.remediation.transient':
+ 'Ein vorübergehender Fehler hat die Speicherverarbeitung unterbrochen. Es wird automatisch erneut versucht.',
+ 'memory.health.remediation.unknown':
+ 'Bei der Speicherverarbeitung ist ein Problem aufgetreten. Überprüfe Einstellungen → KI für die Konfiguration.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefakt: {title}',
'chat.artifact.generating': 'Erstelle {kind}…',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 1af41bc9c0..8598d607fb 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -504,7 +504,34 @@ const en: TranslationMap = {
'memoryTree.status.statusSyncing': 'Syncing',
'memoryTree.status.statusError': 'Error',
'memoryTree.status.statusIdle': 'Idle',
+ 'memoryTree.status.statusDegraded': 'Degraded',
'memoryTree.status.never': 'Never',
+ // #002: degraded badges + typed remediation strings. The Rust core sends a
+ // `remediation_key` (one of memory.health.remediation.*) which the status
+ // panel resolves verbatim, so the cause + fix come from one source of truth.
+ 'memoryTree.status.degradedRecall': 'Semantic recall disabled',
+ 'memoryTree.status.degradedStructure': 'Wiki structure incomplete',
+ 'memoryTree.status.extractionCoverage': 'Extraction coverage: {pct}% of chunks have structure',
+ 'memory.health.remediation.budget_exhausted':
+ 'Memory embeddings hit the managed budget. Set up local Ollama embeddings (Settings → AI → Embeddings) or add your own embeddings API key to keep building memory.',
+ 'memory.health.remediation.auth_missing':
+ 'No embeddings credentials found. Log in to OpenHuman, or set up local Ollama embeddings in Settings → AI → Embeddings.',
+ 'memory.health.remediation.auth_invalid':
+ 'Your embeddings credentials were rejected. Re-authenticate, or switch to local Ollama embeddings in Settings → AI → Embeddings.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'No embeddings provider is configured, so semantic recall is off. Set up local Ollama embeddings (recommended) or add an embeddings key in Settings → AI → Embeddings.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'The embedding model returns the wrong vector size (memory expects 1024 dimensions). Pick a 1024-dim model, or request 1024 dimensions for your provider.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'A required local model is not available. Install/run Ollama and pull the model, or switch this workload to a cloud provider in Settings → AI.',
+ 'memory.health.remediation.extraction_timeout':
+ 'The memory extraction model is timing out, so the wiki has little structure. Switch the Memory extraction model to a faster one in Settings → AI.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'No summarization provider is available for Build Summary Trees. Enable local AI (Ollama), or enable cloud summarization in Settings → AI → Memory.',
+ 'memory.health.remediation.transient':
+ 'A temporary error interrupted memory processing. It will retry automatically.',
+ 'memory.health.remediation.unknown':
+ 'Memory processing encountered an issue. Check Settings → AI for configuration.',
'memoryTree.status.fetchError': "Couldn't fetch Memory Tree status",
'memoryTree.status.retry': 'Retry',
'memoryTree.status.toggleFailed': "Couldn't toggle auto-sync",
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index f211536bc4..bdea137c8c 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -4551,6 +4551,32 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Rechazar almacenamiento local',
'pages.settings.account.security': 'Seguridad',
'pages.settings.account.securityDesc': 'Modo de almacenamiento de secretos y estado del llavero',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Degradado',
+ 'memoryTree.status.degradedRecall': 'Recuperación semántica desactivada',
+ 'memoryTree.status.degradedStructure': 'Estructura de la wiki incompleta',
+ 'memoryTree.status.extractionCoverage':
+ 'Cobertura de extracción: {pct}% de los fragmentos tienen estructura',
+ 'memory.health.remediation.budget_exhausted':
+ 'Los embeddings de memoria agotaron el presupuesto gestionado. Configura embeddings locales de Ollama (Configuración → IA → Incrustaciones) o añade tu propia clave de API de embeddings para seguir construyendo la memoria.',
+ 'memory.health.remediation.auth_missing':
+ 'No se encontraron credenciales de embeddings. Inicia sesión en OpenHuman o configura embeddings locales de Ollama en Configuración → IA → Incrustaciones.',
+ 'memory.health.remediation.auth_invalid':
+ 'Tus credenciales de embeddings fueron rechazadas. Vuelve a autenticarte o cambia a embeddings locales de Ollama en Configuración → IA → Incrustaciones.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'No hay ningún proveedor de embeddings configurado, por lo que la recuperación semántica está desactivada. Configura embeddings locales de Ollama (recomendado) o añade una clave de embeddings en Configuración → IA → Incrustaciones.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'El modelo de embeddings devuelve un tamaño de vector incorrecto (la memoria espera 1024 dimensiones). Elige un modelo de 1024 dimensiones o solicita 1024 dimensiones a tu proveedor.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'No hay disponible un modelo local requerido. Instala/ejecuta Ollama y descarga el modelo, o cambia esta carga de trabajo a un proveedor en la nube en Configuración → IA.',
+ 'memory.health.remediation.extraction_timeout':
+ 'El modelo de extracción de memoria está agotando el tiempo de espera, por lo que la wiki tiene poca estructura. Cambia el modelo de extracción de memoria por uno más rápido en Configuración → IA.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'No hay ningún proveedor de resúmenes disponible para Crear árboles de resumen. Activa la IA local (Ollama) o activa el resumen en la nube en Configuración → IA → Memoria.',
+ 'memory.health.remediation.transient':
+ 'Un error temporal interrumpió el procesamiento de la memoria. Se reintentará automáticamente.',
+ 'memory.health.remediation.unknown':
+ 'El procesamiento de la memoria encontró un problema. Comprueba Configuración → IA para la configuración.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefacto: {title}',
'chat.artifact.generating': 'Generando {kind}…',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index b5009d3284..b590c16df6 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -4567,6 +4567,32 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Refuser le stockage local',
'pages.settings.account.security': 'Sécurité',
'pages.settings.account.securityDesc': 'Mode de stockage des secrets et état du trousseau',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Dégradé',
+ 'memoryTree.status.degradedRecall': 'Rappel sémantique désactivé',
+ 'memoryTree.status.degradedStructure': 'Structure du wiki incomplète',
+ 'memoryTree.status.extractionCoverage':
+ "Couverture d'extraction : {pct}% des fragments ont une structure",
+ 'memory.health.remediation.budget_exhausted':
+ "Les embeddings de mémoire ont atteint le budget géré. Configurez des embeddings Ollama locaux (Paramètres → IA → Encastrements) ou ajoutez votre propre clé d'API d'embeddings pour continuer à construire la mémoire.",
+ 'memory.health.remediation.auth_missing':
+ "Aucune information d'identification d'embeddings trouvée. Connectez-vous à OpenHuman ou configurez des embeddings Ollama locaux dans Paramètres → IA → Encastrements.",
+ 'memory.health.remediation.auth_invalid':
+ "Vos informations d'identification d'embeddings ont été rejetées. Authentifiez-vous à nouveau ou passez aux embeddings Ollama locaux dans Paramètres → IA → Encastrements.",
+ 'memory.health.remediation.embeddings_unconfigured':
+ "Aucun fournisseur d'embeddings n'est configuré, le rappel sémantique est donc désactivé. Configurez des embeddings Ollama locaux (recommandé) ou ajoutez une clé d'embeddings dans Paramètres → IA → Encastrements.",
+ 'memory.health.remediation.embedding_dim_mismatch':
+ "Le modèle d'embeddings renvoie une taille de vecteur incorrecte (la mémoire attend 1024 dimensions). Choisissez un modèle à 1024 dimensions ou demandez 1024 dimensions à votre fournisseur.",
+ 'memory.health.remediation.local_model_unavailable':
+ "Un modèle local requis n'est pas disponible. Installez/lancez Ollama et téléchargez le modèle, ou basculez cette charge de travail vers un fournisseur cloud dans Paramètres → IA.",
+ 'memory.health.remediation.extraction_timeout':
+ "Le modèle d'extraction de mémoire dépasse le délai imparti, le wiki a donc peu de structure. Choisissez un modèle d'extraction de mémoire plus rapide dans Paramètres → IA.",
+ 'memory.health.remediation.summarizer_unavailable':
+ "Aucun fournisseur de résumé n'est disponible pour Créer des arbres de résumé. Activez l'IA locale (Ollama) ou activez la synthèse cloud dans Paramètres → IA → Mémoire.",
+ 'memory.health.remediation.transient':
+ 'Une erreur temporaire a interrompu le traitement de la mémoire. Une nouvelle tentative aura lieu automatiquement.',
+ 'memory.health.remediation.unknown':
+ 'Le traitement de la mémoire a rencontré un problème. Vérifiez Paramètres → IA pour la configuration.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefact : {title}',
'chat.artifact.generating': 'Génération de {kind}…',
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index bbe664e1fb..fd4e4fc1e6 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -4475,6 +4475,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'स्थानीय भंडारण अस्वीकार करें',
'pages.settings.account.security': 'सुरक्षा',
'pages.settings.account.securityDesc': 'रहस्य भंडारण मोड और कीचेन स्थिति',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'अवक्रमित',
+ 'memoryTree.status.degradedRecall': 'सिमेंटिक रिकॉल अक्षम',
+ 'memoryTree.status.degradedStructure': 'विकी संरचना अधूरी',
+ 'memoryTree.status.extractionCoverage': 'एक्सट्रैक्शन कवरेज: {pct}% खंडों में संरचना है',
+ 'memory.health.remediation.budget_exhausted':
+ 'मेमोरी एम्बेडिंग प्रबंधित बजट तक पहुँच गई। स्थानीय Ollama एम्बेडिंग सेट करें (सेटिंग्स → AI → एम्बेडिंग्स) या मेमोरी बनाना जारी रखने के लिए अपनी स्वयं की एम्बेडिंग API कुंजी जोड़ें।',
+ 'memory.health.remediation.auth_missing':
+ 'कोई एम्बेडिंग क्रेडेंशियल नहीं मिला। OpenHuman में लॉग इन करें, या सेटिंग्स → AI → एम्बेडिंग्स में स्थानीय Ollama एम्बेडिंग सेट करें।',
+ 'memory.health.remediation.auth_invalid':
+ 'आपके एम्बेडिंग क्रेडेंशियल अस्वीकार कर दिए गए। फिर से प्रमाणित करें, या सेटिंग्स → AI → एम्बेडिंग्स में स्थानीय Ollama एम्बेडिंग पर स्विच करें।',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'कोई एम्बेडिंग प्रदाता कॉन्फ़िगर नहीं किया गया है, इसलिए सिमेंटिक रिकॉल बंद है। स्थानीय Ollama एम्बेडिंग सेट करें (अनुशंसित) या सेटिंग्स → AI → एम्बेडिंग्स में एम्बेडिंग कुंजी जोड़ें।',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'एम्बेडिंग मॉडल गलत वेक्टर आकार लौटाता है (मेमोरी को 1024 आयाम अपेक्षित हैं)। 1024-आयाम वाला मॉडल चुनें, या अपने प्रदाता से 1024 आयाम का अनुरोध करें।',
+ 'memory.health.remediation.local_model_unavailable':
+ 'एक आवश्यक स्थानीय मॉडल उपलब्ध नहीं है। Ollama इंस्टॉल/चलाएँ और मॉडल डाउनलोड करें, या सेटिंग्स → AI में इस वर्कलोड को क्लाउड प्रदाता पर स्विच करें।',
+ 'memory.health.remediation.extraction_timeout':
+ 'मेमोरी एक्सट्रैक्शन मॉडल टाइम आउट हो रहा है, इसलिए विकी में बहुत कम संरचना है। सेटिंग्स → AI में मेमोरी एक्सट्रैक्शन मॉडल को तेज़ मॉडल में बदलें।',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'सारांश ट्री बनाएँ के लिए कोई सारांश प्रदाता उपलब्ध नहीं है। स्थानीय AI (Ollama) सक्षम करें, या सेटिंग्स → AI → मेमोरी में क्लाउड सारांश सक्षम करें।',
+ 'memory.health.remediation.transient':
+ 'एक अस्थायी त्रुटि ने मेमोरी प्रोसेसिंग को बाधित किया। स्वचालित रूप से पुनः प्रयास किया जाएगा।',
+ 'memory.health.remediation.unknown':
+ 'मेमोरी प्रोसेसिंग में एक समस्या आई। कॉन्फ़िगरेशन के लिए सेटिंग्स → AI जाँचें।',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'आर्टिफैक्ट: {title}',
'chat.artifact.generating': '{kind} बना रहा है…',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index d61c8237e1..3448a3a27f 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -4485,6 +4485,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Tolak penyimpanan lokal',
'pages.settings.account.security': 'Keamanan',
'pages.settings.account.securityDesc': 'Mode penyimpanan rahasia dan status keychain',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Terdegradasi',
+ 'memoryTree.status.degradedRecall': 'Recall semantik dinonaktifkan',
+ 'memoryTree.status.degradedStructure': 'Struktur wiki tidak lengkap',
+ 'memoryTree.status.extractionCoverage': 'Cakupan ekstraksi: {pct}% bagian memiliki struktur',
+ 'memory.health.remediation.budget_exhausted':
+ 'Embedding memori mencapai batas anggaran terkelola. Siapkan embedding Ollama lokal (Pengaturan → AI → Sematan) atau tambahkan kunci API embedding Anda sendiri untuk terus membangun memori.',
+ 'memory.health.remediation.auth_missing':
+ 'Kredensial embedding tidak ditemukan. Masuk ke OpenHuman, atau siapkan embedding Ollama lokal di Pengaturan → AI → Sematan.',
+ 'memory.health.remediation.auth_invalid':
+ 'Kredensial embedding Anda ditolak. Autentikasi ulang, atau beralih ke embedding Ollama lokal di Pengaturan → AI → Sematan.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Tidak ada penyedia embedding yang dikonfigurasi, sehingga recall semantik nonaktif. Siapkan embedding Ollama lokal (disarankan) atau tambahkan kunci embedding di Pengaturan → AI → Sematan.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'Model embedding mengembalikan ukuran vektor yang salah (memori mengharapkan 1024 dimensi). Pilih model 1024 dimensi, atau minta 1024 dimensi dari penyedia Anda.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Model lokal yang diperlukan tidak tersedia. Instal/jalankan Ollama dan unduh model, atau alihkan beban kerja ini ke penyedia cloud di Pengaturan → AI.',
+ 'memory.health.remediation.extraction_timeout':
+ 'Model ekstraksi memori kehabisan waktu, sehingga wiki memiliki sedikit struktur. Ganti model ekstraksi memori ke yang lebih cepat di Pengaturan → AI.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'Tidak ada penyedia ringkasan yang tersedia untuk Buat Pohon Ringkasan. Aktifkan AI lokal (Ollama), atau aktifkan ringkasan cloud di Pengaturan → AI → Memori.',
+ 'memory.health.remediation.transient':
+ 'Kesalahan sementara mengganggu pemrosesan memori. Akan dicoba lagi secara otomatis.',
+ 'memory.health.remediation.unknown':
+ 'Pemrosesan memori mengalami masalah. Periksa Pengaturan → AI untuk konfigurasi.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefak: {title}',
'chat.artifact.generating': 'Membuat {kind}…',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 2990b3546a..f440ee059c 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -4543,6 +4543,32 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Rifiuta archiviazione locale',
'pages.settings.account.security': 'Sicurezza',
'pages.settings.account.securityDesc': 'Modalità archiviazione segreti e stato del portachiavi',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Degradato',
+ 'memoryTree.status.degradedRecall': 'Richiamo semantico disattivato',
+ 'memoryTree.status.degradedStructure': 'Struttura del wiki incompleta',
+ 'memoryTree.status.extractionCoverage':
+ 'Copertura di estrazione: {pct}% dei frammenti ha una struttura',
+ 'memory.health.remediation.budget_exhausted':
+ 'Gli embedding della memoria hanno raggiunto il budget gestito. Configura embedding Ollama locali (Impostazioni → IA → Incorporamenti) o aggiungi la tua chiave API per gli embedding per continuare a costruire la memoria.',
+ 'memory.health.remediation.auth_missing':
+ 'Nessuna credenziale per gli embedding trovata. Accedi a OpenHuman o configura embedding Ollama locali in Impostazioni → IA → Incorporamenti.',
+ 'memory.health.remediation.auth_invalid':
+ 'Le tue credenziali per gli embedding sono state rifiutate. Autenticati di nuovo o passa agli embedding Ollama locali in Impostazioni → IA → Incorporamenti.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Nessun provider di embedding è configurato, quindi il richiamo semantico è disattivato. Configura embedding Ollama locali (consigliato) o aggiungi una chiave per gli embedding in Impostazioni → IA → Incorporamenti.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'Il modello di embedding restituisce una dimensione del vettore errata (la memoria prevede 1024 dimensioni). Scegli un modello a 1024 dimensioni o richiedi 1024 dimensioni al tuo provider.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Un modello locale richiesto non è disponibile. Installa/avvia Ollama e scarica il modello, oppure passa questo carico di lavoro a un provider cloud in Impostazioni → IA.',
+ 'memory.health.remediation.extraction_timeout':
+ 'Il modello di estrazione della memoria sta andando in timeout, quindi il wiki ha poca struttura. Passa a un modello di estrazione della memoria più veloce in Impostazioni → IA.',
+ 'memory.health.remediation.summarizer_unavailable':
+ "Nessun provider di riepilogo è disponibile per Crea alberi di riepilogo. Abilita l'IA locale (Ollama) o abilita il riepilogo cloud in Impostazioni → IA → Memoria.",
+ 'memory.health.remediation.transient':
+ "Un errore temporaneo ha interrotto l'elaborazione della memoria. Verrà riprovato automaticamente.",
+ 'memory.health.remediation.unknown':
+ "L'elaborazione della memoria ha riscontrato un problema. Controlla Impostazioni → IA per la configurazione.",
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefatto: {title}',
'chat.artifact.generating': 'Generazione {kind}…',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index b3b8b685b7..a69915d859 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -4432,6 +4432,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': '로컬 저장소 거부',
'pages.settings.account.security': '보안',
'pages.settings.account.securityDesc': '비밀 저장 모드 및 키체인 상태',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': '저하됨',
+ 'memoryTree.status.degradedRecall': '의미 기반 검색 비활성화됨',
+ 'memoryTree.status.degradedStructure': '위키 구조 불완전',
+ 'memoryTree.status.extractionCoverage': '추출 범위: 청크의 {pct}%에 구조가 있음',
+ 'memory.health.remediation.budget_exhausted':
+ '메모리 임베딩이 관리형 예산에 도달했습니다. 로컬 Ollama 임베딩을 설정하거나(설정 → AI → 임베딩) 메모리를 계속 구축하려면 자체 임베딩 API 키를 추가하세요.',
+ 'memory.health.remediation.auth_missing':
+ '임베딩 자격 증명을 찾을 수 없습니다. OpenHuman에 로그인하거나 설정 → AI → 임베딩에서 로컬 Ollama 임베딩을 설정하세요.',
+ 'memory.health.remediation.auth_invalid':
+ '임베딩 자격 증명이 거부되었습니다. 다시 인증하거나 설정 → AI → 임베딩에서 로컬 Ollama 임베딩으로 전환하세요.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ '구성된 임베딩 제공자가 없어 의미 기반 검색이 꺼져 있습니다. 로컬 Ollama 임베딩을 설정하거나(권장) 설정 → AI → 임베딩에서 임베딩 키를 추가하세요.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ '임베딩 모델이 잘못된 벡터 크기를 반환합니다(메모리는 1024차원을 예상함). 1024차원 모델을 선택하거나 제공자에게 1024차원을 요청하세요.',
+ 'memory.health.remediation.local_model_unavailable':
+ '필요한 로컬 모델을 사용할 수 없습니다. Ollama를 설치/실행하고 모델을 다운로드하거나, 설정 → AI에서 이 작업을 클라우드 제공자로 전환하세요.',
+ 'memory.health.remediation.extraction_timeout':
+ '메모리 추출 모델이 시간 초과되어 위키 구조가 거의 없습니다. 설정 → AI에서 메모리 추출 모델을 더 빠른 것으로 변경하세요.',
+ 'memory.health.remediation.summarizer_unavailable':
+ '요약 트리 만들기에 사용할 수 있는 요약 제공자가 없습니다. 로컬 AI(Ollama)를 활성화하거나, 설정 → AI → 메모리에서 클라우드 요약을 활성화하세요.',
+ 'memory.health.remediation.transient':
+ '일시적인 오류로 메모리 처리가 중단되었습니다. 자동으로 다시 시도됩니다.',
+ 'memory.health.remediation.unknown':
+ '메모리 처리 중 문제가 발생했습니다. 설정 → AI에서 구성을 확인하세요.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': '아티팩트: {title}',
'chat.artifact.generating': '{kind} 생성 중…',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 47adfed35f..5585948548 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -4542,6 +4542,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Odmów lokalnego przechowywania',
'pages.settings.account.security': 'Bezpieczeństwo',
'pages.settings.account.securityDesc': 'Tryb przechowywania sekretów i stan pęku kluczy',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Ograniczony',
+ 'memoryTree.status.degradedRecall': 'Wyszukiwanie semantyczne wyłączone',
+ 'memoryTree.status.degradedStructure': 'Struktura wiki niekompletna',
+ 'memoryTree.status.extractionCoverage': 'Pokrycie ekstrakcji: {pct}% fragmentów ma strukturę',
+ 'memory.health.remediation.budget_exhausted':
+ 'Osadzenia pamięci wyczerpały zarządzany budżet. Skonfiguruj lokalne osadzenia Ollama (Ustawienia → AI → Embeddings) lub dodaj własny klucz API osadzeń, aby kontynuować budowanie pamięci.',
+ 'memory.health.remediation.auth_missing':
+ 'Nie znaleziono poświadczeń osadzeń. Zaloguj się do OpenHuman lub skonfiguruj lokalne osadzenia Ollama w Ustawienia → AI → Embeddings.',
+ 'memory.health.remediation.auth_invalid':
+ 'Twoje poświadczenia osadzeń zostały odrzucone. Uwierzytelnij się ponownie lub przełącz na lokalne osadzenia Ollama w Ustawienia → AI → Embeddings.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Nie skonfigurowano dostawcy osadzeń, więc wyszukiwanie semantyczne jest wyłączone. Skonfiguruj lokalne osadzenia Ollama (zalecane) lub dodaj klucz osadzeń w Ustawienia → AI → Embeddings.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'Model osadzeń zwraca nieprawidłowy rozmiar wektora (pamięć oczekuje 1024 wymiarów). Wybierz model o 1024 wymiarach lub poproś dostawcę o 1024 wymiary.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Wymagany model lokalny jest niedostępny. Zainstaluj/uruchom Ollama i pobierz model albo przełącz to zadanie na dostawcę chmurowego w Ustawienia → AI.',
+ 'memory.health.remediation.extraction_timeout':
+ 'Model ekstrakcji pamięci przekracza limit czasu, więc wiki ma niewielką strukturę. Zmień model ekstrakcji pamięci na szybszy w Ustawienia → AI.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'Brak dostępnego dostawcy podsumowań dla funkcji Twórz drzewa podsumowań. Włącz lokalną AI (Ollama) lub włącz podsumowywanie w chmurze w Ustawienia → AI → Pamięć.',
+ 'memory.health.remediation.transient':
+ 'Tymczasowy błąd przerwał przetwarzanie pamięci. Ponowna próba nastąpi automatycznie.',
+ 'memory.health.remediation.unknown':
+ 'Przetwarzanie pamięci napotkało problem. Sprawdź Ustawienia → AI w celu konfiguracji.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefakt: {title}',
'chat.artifact.generating': 'Tworzenie {kind}…',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 48a84e1a0e..9c8a335256 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -4541,6 +4541,32 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Recusar armazenamento local',
'pages.settings.account.security': 'Segurança',
'pages.settings.account.securityDesc': 'Modo de armazenamento de segredos e status do chaveiro',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Degradado',
+ 'memoryTree.status.degradedRecall': 'Recuperação semântica desativada',
+ 'memoryTree.status.degradedStructure': 'Estrutura do wiki incompleta',
+ 'memoryTree.status.extractionCoverage':
+ 'Cobertura de extração: {pct}% dos fragmentos têm estrutura',
+ 'memory.health.remediation.budget_exhausted':
+ 'Os embeddings de memória atingiram o orçamento gerenciado. Configure embeddings locais do Ollama (Configurações → IA → Incorporações) ou adicione sua própria chave de API de embeddings para continuar construindo a memória.',
+ 'memory.health.remediation.auth_missing':
+ 'Nenhuma credencial de embeddings encontrada. Faça login no OpenHuman ou configure embeddings locais do Ollama em Configurações → IA → Incorporações.',
+ 'memory.health.remediation.auth_invalid':
+ 'Suas credenciais de embeddings foram rejeitadas. Autentique-se novamente ou mude para embeddings locais do Ollama em Configurações → IA → Incorporações.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Nenhum provedor de embeddings está configurado, então a recuperação semântica está desativada. Configure embeddings locais do Ollama (recomendado) ou adicione uma chave de embeddings em Configurações → IA → Incorporações.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'O modelo de embeddings retorna o tamanho de vetor errado (a memória espera 1024 dimensões). Escolha um modelo de 1024 dimensões ou solicite 1024 dimensões ao seu provedor.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Um modelo local necessário não está disponível. Instale/execute o Ollama e baixe o modelo, ou mude esta carga de trabalho para um provedor de nuvem em Configurações → IA.',
+ 'memory.health.remediation.extraction_timeout':
+ 'O modelo de extração de memória está expirando o tempo limite, então o wiki tem pouca estrutura. Mude o modelo de extração de memória para um mais rápido em Configurações → IA.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'Nenhum provedor de resumo está disponível para Criar árvores de resumo. Ative a IA local (Ollama) ou ative o resumo na nuvem em Configurações → IA → Memória.',
+ 'memory.health.remediation.transient':
+ 'Um erro temporário interrompeu o processamento da memória. Será repetido automaticamente.',
+ 'memory.health.remediation.unknown':
+ 'O processamento da memória encontrou um problema. Verifique Configurações → IA para a configuração.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Artefato: {title}',
'chat.artifact.generating': 'Gerando {kind}…',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index ab7ba95e13..ae550fdba2 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -4511,6 +4511,31 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': 'Отклонить локальное хранилище',
'pages.settings.account.security': 'Безопасность',
'pages.settings.account.securityDesc': 'Режим хранения секретов и статус связки ключей',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': 'Ухудшено',
+ 'memoryTree.status.degradedRecall': 'Семантический поиск отключён',
+ 'memoryTree.status.degradedStructure': 'Структура вики неполная',
+ 'memoryTree.status.extractionCoverage': 'Охват извлечения: {pct}% фрагментов имеют структуру',
+ 'memory.health.remediation.budget_exhausted':
+ 'Эмбеддинги памяти исчерпали управляемый бюджет. Настройте локальные эмбеддинги Ollama (Настройки → ИИ → Эмбеддинги) или добавьте свой ключ API для эмбеддингов, чтобы продолжить построение памяти.',
+ 'memory.health.remediation.auth_missing':
+ 'Учётные данные для эмбеддингов не найдены. Войдите в OpenHuman или настройте локальные эмбеддинги Ollama в разделе Настройки → ИИ → Эмбеддинги.',
+ 'memory.health.remediation.auth_invalid':
+ 'Ваши учётные данные для эмбеддингов отклонены. Пройдите аутентификацию заново или переключитесь на локальные эмбеддинги Ollama в разделе Настройки → ИИ → Эмбеддинги.',
+ 'memory.health.remediation.embeddings_unconfigured':
+ 'Поставщик эмбеддингов не настроен, поэтому семантический поиск отключён. Настройте локальные эмбеддинги Ollama (рекомендуется) или добавьте ключ эмбеддингов в разделе Настройки → ИИ → Эмбеддинги.',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ 'Модель эмбеддингов возвращает неверный размер вектора (память ожидает 1024 измерения). Выберите модель с 1024 измерениями или запросите 1024 измерения у своего поставщика.',
+ 'memory.health.remediation.local_model_unavailable':
+ 'Требуемая локальная модель недоступна. Установите/запустите Ollama и загрузите модель либо переключите эту задачу на облачного поставщика в разделе Настройки → ИИ.',
+ 'memory.health.remediation.extraction_timeout':
+ 'Модель извлечения памяти превышает время ожидания, поэтому в вики мало структуры. Выберите более быструю модель извлечения памяти в разделе Настройки → ИИ.',
+ 'memory.health.remediation.summarizer_unavailable':
+ 'Нет доступного поставщика суммаризации для «Построить деревья сводок». Включите локальный ИИ (Ollama) или включите облачную суммаризацию в разделе Настройки → ИИ → Память.',
+ 'memory.health.remediation.transient':
+ 'Временная ошибка прервала обработку памяти. Повтор произойдёт автоматически.',
+ 'memory.health.remediation.unknown':
+ 'При обработке памяти возникла проблема. Проверьте конфигурацию в разделе Настройки → ИИ.',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': 'Артефакт: {title}',
'chat.artifact.generating': 'Создание {kind}…',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index e9456e62f9..802395e954 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -4253,6 +4253,29 @@ const messages: TranslationMap = {
'keyring.settings.revokeConsent': '拒绝本地存储',
'pages.settings.account.security': '安全',
'pages.settings.account.securityDesc': '密钥存储模式和密钥链状态',
+ // #002 memory-pipeline-hardening: degraded badges + typed remediation.
+ 'memoryTree.status.statusDegraded': '已降级',
+ 'memoryTree.status.degradedRecall': '语义召回已禁用',
+ 'memoryTree.status.degradedStructure': 'Wiki 结构不完整',
+ 'memoryTree.status.extractionCoverage': '提取覆盖率:{pct}% 的片段具有结构',
+ 'memory.health.remediation.budget_exhausted':
+ '记忆嵌入已达到托管预算上限。请设置本地 Ollama 嵌入(设置 → AI → 向量嵌入),或添加你自己的嵌入 API 密钥以继续构建记忆。',
+ 'memory.health.remediation.auth_missing':
+ '未找到嵌入凭据。请登录 OpenHuman,或在设置 → AI → 向量嵌入 中设置本地 Ollama 嵌入。',
+ 'memory.health.remediation.auth_invalid':
+ '你的嵌入凭据被拒绝。请重新进行身份验证,或在设置 → AI → 向量嵌入 中切换到本地 Ollama 嵌入。',
+ 'memory.health.remediation.embeddings_unconfigured':
+ '未配置嵌入提供方,因此语义召回已关闭。请设置本地 Ollama 嵌入(推荐),或在设置 → AI → 向量嵌入 中添加嵌入密钥。',
+ 'memory.health.remediation.embedding_dim_mismatch':
+ '嵌入模型返回的向量大小不正确(记忆需要 1024 维)。请选择 1024 维的模型,或向你的提供方请求 1024 维。',
+ 'memory.health.remediation.local_model_unavailable':
+ '所需的本地模型不可用。请安装/运行 Ollama 并拉取模型,或在设置 → AI 中将此工作负载切换到云提供方。',
+ 'memory.health.remediation.extraction_timeout':
+ '记忆提取模型超时,因此 Wiki 结构很少。请在设置 → AI 中将记忆提取模型更换为更快的模型。',
+ 'memory.health.remediation.summarizer_unavailable':
+ '没有可用于构建摘要树的摘要提供方。请启用本地 AI(Ollama),或在设置 → AI → 记忆中启用云端摘要。',
+ 'memory.health.remediation.transient': '临时错误中断了记忆处理。将自动重试。',
+ 'memory.health.remediation.unknown': '记忆处理遇到问题。请在设置 → AI 中检查配置。',
// Chat — agent-generated artifacts (#2779)
'chat.artifact.aria': '工件:{title}',
'chat.artifact.generating': '正在生成{kind}…',
diff --git a/app/src/utils/tauriCommands/memoryTree.ts b/app/src/utils/tauriCommands/memoryTree.ts
index 34104c3bc2..27390ec69d 100644
--- a/app/src/utils/tauriCommands/memoryTree.ts
+++ b/app/src/utils/tauriCommands/memoryTree.ts
@@ -753,7 +753,54 @@ export async function memoryTreeBackfillStatus(): Promise {
* verbatim to a colored pill in the status panel — `paused` is the only
* state the toggle directly influences.
*/
-export type MemoryTreePipelineStatusKind = 'running' | 'paused' | 'syncing' | 'error' | 'idle';
+export type MemoryTreePipelineStatusKind =
+ | 'running'
+ | 'paused'
+ | 'syncing'
+ | 'error'
+ | 'idle'
+ | 'degraded';
+
+/**
+ * Stable typed failure codes the Rust `health::FailureCode` emits (#002). The
+ * UI maps each to a localized remediation string; `remediation_key` carries
+ * the i18n key directly so the panel renders the core's guidance verbatim.
+ */
+export type MemoryTreeFailureCode =
+ | 'budget_exhausted'
+ | 'auth_missing'
+ | 'auth_invalid'
+ | 'embeddings_unconfigured'
+ | 'embedding_dim_mismatch'
+ | 'local_model_unavailable'
+ | 'extraction_timeout'
+ | 'summarizer_unavailable'
+ | 'transient';
+
+/**
+ * Typed pipeline failure (#002 FR-004). Mirrors Rust `health::PipelineFailure`.
+ * `remediation_key` is an i18n key (e.g. `memory.health.remediation.*`); the UI
+ * resolves it via `useT()`. `detail` is a short non-localized diagnostic
+ * string (never a secret) for logs/tooltips.
+ */
+export interface MemoryTreePipelineFailure {
+ code: MemoryTreeFailureCode;
+ class: 'transient' | 'unrecoverable';
+ remediation_key: string;
+ detail?: string;
+}
+
+/**
+ * "The pipeline ran but output quality is reduced" (#002 FR-002/FR-005).
+ * Mirrors Rust `health::DegradedState`. `semantic_recall` true when embeddings
+ * were skipped (no usable provider → recall falls back to recency);
+ * `structure` true when extraction yielded nothing across the board.
+ */
+export interface MemoryTreeDegradedState {
+ semantic_recall: boolean;
+ structure: boolean;
+ cause?: MemoryTreePipelineFailure | null;
+}
/**
* Per-state job counters returned in {@link MemoryTreePipelineStatus}. Mirrors
@@ -795,6 +842,24 @@ export interface MemoryTreePipelineStatus {
is_syncing: boolean;
/** Convenience flag: scheduler-gate mode is `off`. */
is_paused: boolean;
+ /**
+ * #002 (FR-002/FR-005): degradation snapshot. Optional for back-compat with
+ * older cores that don't emit it (the Rust field is `#[serde(default)]`);
+ * absent ⇒ treat as not degraded.
+ */
+ degraded?: MemoryTreeDegradedState;
+ /**
+ * #002 (FR-004): the single first blocking/most-significant cause, rendered
+ * verbatim by the panel (resolving `remediation_key`). `null`/absent when
+ * the pipeline is healthy.
+ */
+ first_blocking_cause?: MemoryTreePipelineFailure | null;
+ /**
+ * #002 (FR-010 / US5): fraction of chunks with ≥1 indexed entity, in
+ * `[0.0, 1.0]`. Near 0 with `total_chunks > 0` ⇒ extraction is producing no
+ * structure ("empty-but-built wiki"). Optional for back-compat.
+ */
+ extraction_coverage?: number | null;
}
/**
diff --git a/src/openhuman/about_app/catalog_data.rs b/src/openhuman/about_app/catalog_data.rs
index 96c8fd735a..5fb7e5afeb 100644
--- a/src/openhuman/about_app/catalog_data.rs
+++ b/src/openhuman/about_app/catalog_data.rs
@@ -326,6 +326,16 @@ pub(super) const CAPABILITIES: &[Capability] = &[
status: CapabilityStatus::Beta,
privacy: LOCAL_RAW,
},
+ Capability {
+ id: "intelligence.memory_pipeline_doctor",
+ name: "Memory Pipeline Doctor",
+ domain: "intelligence",
+ category: CapabilityCategory::Intelligence,
+ description: "Diagnose why the memory tree / wiki is empty or stalled. Walks each pipeline stage (embeddings config, scheduler gate, job queue, extraction/recall degradation, summary-tree precondition) and reports the single first blocking cause with an actionable fix, plus counters and extraction coverage. The agent can run it on itself; a typed 'first blocking cause' is surfaced in the Memory status panel, and jobs that failed under a now-fixed config can be requeued on demand via the `memory_tree_retry_failed` RPC.",
+ how_to: "Memory status panel shows the cause + fix; or ask the agent to diagnose memory; or `openhuman-core` RPC `memory_tree_doctor`",
+ status: CapabilityStatus::Beta,
+ privacy: LOCAL_RAW,
+ },
Capability {
id: "intelligence.github_repo_memory_source",
name: "GitHub Repo Memory Source",
diff --git a/src/openhuman/config/schema/load.rs b/src/openhuman/config/schema/load.rs
index 1749a0c3c2..8ca98c2322 100644
--- a/src/openhuman/config/schema/load.rs
+++ b/src/openhuman/config/schema/load.rs
@@ -1976,6 +1976,12 @@ impl Config {
};
}
+ if let Some(raw) = env.get("OPENHUMAN_MEMORY_TREE_CLOUD_SUMMARIZATION") {
+ if let Some(val) = parse_env_bool("OPENHUMAN_MEMORY_TREE_CLOUD_SUMMARIZATION", &raw) {
+ self.memory_tree.cloud_summarization_opt_in = val;
+ }
+ }
+
// Auto-update overrides
if let Some(flag) = env.get("OPENHUMAN_AUTO_UPDATE_ENABLED") {
let normalized = flag.trim().to_ascii_lowercase();
diff --git a/src/openhuman/config/schema/storage_memory.rs b/src/openhuman/config/schema/storage_memory.rs
index 8439c80e07..541bb760e0 100644
--- a/src/openhuman/config/schema/storage_memory.rs
+++ b/src/openhuman/config/schema/storage_memory.rs
@@ -347,6 +347,16 @@ pub struct MemoryTreeConfig {
/// Env override: `OPENHUMAN_MEMORY_TREE_SMART_WALK_MODEL`.
#[serde(default)]
pub smart_walk_model: Option,
+
+ /// Explicit opt-in to cloud-based summarization when local AI is disabled.
+ ///
+ /// Default `false` — "Build Summary Trees" was local-only before #002.
+ /// Enabling this routes workspace memory summaries to the configured cloud
+ /// provider. Set to `true` via Settings → AI → Memory or the env var
+ /// `OPENHUMAN_MEMORY_TREE_CLOUD_SUMMARIZATION=true` to acknowledge that
+ /// memory content will be sent to an external service.
+ #[serde(default)]
+ pub cloud_summarization_opt_in: bool,
}
/// Returns `None` so that existing installs that never opted into Phase 4
@@ -448,6 +458,7 @@ impl Default for MemoryTreeConfig {
llm_backend: default_llm_backend(),
cloud_llm_model: default_cloud_llm_model(),
smart_walk_model: None,
+ cloud_summarization_opt_in: false,
}
}
}
diff --git a/src/openhuman/embeddings/factory.rs b/src/openhuman/embeddings/factory.rs
index 6a2726a3e2..25bb698977 100644
--- a/src/openhuman/embeddings/factory.rs
+++ b/src/openhuman/embeddings/factory.rs
@@ -10,6 +10,16 @@ use super::provider_trait::EmbeddingProvider;
use super::voyage::VoyageEmbedding;
use super::{NoopEmbedding, OllamaEmbedding, OpenAiEmbedding};
+/// Whether to send the OpenAI `dimensions` request-body parameter for this
+/// model. Only the `text-embedding-3-*` family honors it (it's how 3-large is
+/// pinned to 1024 = `EMBEDDING_DIM`). Sending it to other models or to
+/// arbitrary OpenAI-compatible servers (vLLM, text-embeddings-inference,
+/// stricter LocalAI builds) makes those servers 400 on an unknown field, so we
+/// gate on the model id rather than the provider kind. (Reviewer sanil-23, #3076.)
+fn model_supports_dimensions(model: &str) -> bool {
+ model.starts_with("text-embedding-3-")
+}
+
/// Creates an embedding provider based on the specified name and configuration.
///
/// Supported provider names:
@@ -38,16 +48,17 @@ pub fn create_embedding_provider(
let base_url = crate::openhuman::inference::local::ollama_base_url();
Ok(Box::new(OllamaEmbedding::try_new(&base_url, model, dims)?))
}
- "openai" => Ok(Box::new(OpenAiEmbedding::new(
- "https://api.openai.com",
- "",
- model,
- dims,
- ))),
+ "openai" => Ok(Box::new(
+ OpenAiEmbedding::new("https://api.openai.com", "", model, dims)
+ .with_send_dimensions(model_supports_dimensions(model)),
+ )),
"cohere" => Ok(Box::new(CohereEmbedding::new("", model, dims))),
name if name.starts_with("custom:") => {
let base_url = name.strip_prefix("custom:").unwrap_or("");
- Ok(Box::new(OpenAiEmbedding::new(base_url, "", model, dims)))
+ Ok(Box::new(
+ OpenAiEmbedding::new(base_url, "", model, dims)
+ .with_send_dimensions(model_supports_dimensions(model)),
+ ))
}
"none" => Ok(Box::new(NoopEmbedding)),
unknown => Err(anyhow::anyhow!(
@@ -78,20 +89,24 @@ pub fn create_embedding_provider_with_credentials(
let base_url = crate::openhuman::inference::local::ollama_base_url();
Ok(Box::new(OllamaEmbedding::try_new(&base_url, model, dims)?))
}
- "openai" => Ok(Box::new(OpenAiEmbedding::new(
- "https://api.openai.com",
- api_key,
- model,
- dims,
- ))),
+ "openai" => Ok(Box::new(
+ OpenAiEmbedding::new("https://api.openai.com", api_key, model, dims)
+ .with_send_dimensions(model_supports_dimensions(model)),
+ )),
"cohere" => Ok(Box::new(CohereEmbedding::new(api_key, model, dims))),
"custom" => {
let url = custom_endpoint.unwrap_or("");
- Ok(Box::new(OpenAiEmbedding::new(url, api_key, model, dims)))
+ Ok(Box::new(
+ OpenAiEmbedding::new(url, api_key, model, dims)
+ .with_send_dimensions(model_supports_dimensions(model)),
+ ))
}
name if name.starts_with("custom:") => {
let url = custom_endpoint.unwrap_or_else(|| name.strip_prefix("custom:").unwrap_or(""));
- Ok(Box::new(OpenAiEmbedding::new(url, api_key, model, dims)))
+ Ok(Box::new(
+ OpenAiEmbedding::new(url, api_key, model, dims)
+ .with_send_dimensions(model_supports_dimensions(model)),
+ ))
}
"none" => Ok(Box::new(NoopEmbedding)),
unknown => Err(anyhow::anyhow!(
diff --git a/src/openhuman/embeddings/mod.rs b/src/openhuman/embeddings/mod.rs
index 2ccb769252..8e6fadddce 100644
--- a/src/openhuman/embeddings/mod.rs
+++ b/src/openhuman/embeddings/mod.rs
@@ -39,11 +39,14 @@ pub use factory::{
create_embedding_provider, create_embedding_provider_with_credentials,
default_embedding_provider, default_local_embedding_provider,
};
+// #002 FR-015: the memory-tree OpenAI-compat embedder reuses the same key
+// resolution the embeddings RPC uses, so there is one source of truth.
pub use noop::NoopEmbedding;
pub use ollama::{OllamaEmbedding, DEFAULT_OLLAMA_DIMENSIONS, DEFAULT_OLLAMA_MODEL};
pub use openai::OpenAiEmbedding;
pub use provider_trait::{format_embedding_signature, EmbeddingProvider};
pub use rpc::provider_from_config;
+pub(crate) use rpc::resolve_api_key;
pub use schemas::{
all_controller_schemas as all_embeddings_controller_schemas,
all_registered_controllers as all_embeddings_registered_controllers,
diff --git a/src/openhuman/embeddings/openai.rs b/src/openhuman/embeddings/openai.rs
index 0c85acff4d..a3a289432e 100644
--- a/src/openhuman/embeddings/openai.rs
+++ b/src/openhuman/embeddings/openai.rs
@@ -14,6 +14,13 @@ pub struct OpenAiEmbedding {
api_key: String,
model: String,
dims: usize,
+ /// When true, send `"dimensions": dims` in the request body. OpenAI's
+ /// `text-embedding-3-*` models honour this (Matryoshka — e.g. 3-large can
+ /// return 1024 instead of its native 3072). Off by default so providers
+ /// that don't accept the field — Voyage (uses `output_dimension`), Cohere,
+ /// LocalAI/Ollama — keep working unchanged. Set via
+ /// [`Self::with_send_dimensions`] for the OpenAI / custom-OpenAI paths.
+ send_dimensions: bool,
}
impl OpenAiEmbedding {
@@ -24,9 +31,20 @@ impl OpenAiEmbedding {
api_key: api_key.to_string(),
model: model.to_string(),
dims,
+ send_dimensions: false,
}
}
+ /// Opt into sending the OpenAI `dimensions` request parameter so a
+ /// reducible model (`text-embedding-3-large` / `-3-small`) returns exactly
+ /// `dims` floats instead of its native size. Only call this for genuine
+ /// OpenAI / OpenAI-compatible endpoints that implement the parameter —
+ /// see [`Self::send_dimensions`]. Returns `self` for builder chaining.
+ pub fn with_send_dimensions(mut self, send: bool) -> Self {
+ self.send_dimensions = send;
+ self
+ }
+
/// Returns the configured base URL.
pub fn base_url(&self) -> &str {
&self.base_url
@@ -111,10 +129,17 @@ impl EmbeddingProvider for OpenAiEmbedding {
self.model, texts.len(), url
);
- let body = serde_json::json!({
+ let mut body = serde_json::json!({
"model": self.model,
"input": texts,
});
+ // Request a specific output size on OpenAI 3-* models (Matryoshka) so
+ // the vector matches `dims` (e.g. 3-large → 1024 for the memory tree's
+ // fixed EMBEDDING_DIM). Gated by `send_dimensions` because Voyage /
+ // Cohere / LocalAI don't accept this exact field.
+ if self.send_dimensions && self.dims > 0 {
+ body["dimensions"] = serde_json::json!(self.dims);
+ }
// Retry loop: handles 429 Too Many Requests and 503 Service Unavailable
// with Retry-After–aware exponential backoff.
diff --git a/src/openhuman/embeddings/openai_tests.rs b/src/openhuman/embeddings/openai_tests.rs
index f77a28eb93..6b1074d4b2 100644
--- a/src/openhuman/embeddings/openai_tests.rs
+++ b/src/openhuman/embeddings/openai_tests.rs
@@ -186,6 +186,45 @@ async fn embed_sends_auth_header() {
p.embed(&["test"]).await.unwrap();
}
+// #002: the OpenAI `dimensions` request param. Off by default (so Voyage /
+// Cohere / Ollama, which don't accept this exact field, keep working); on when
+// the OpenAI / custom factory branch opts in via `with_send_dimensions(true)`.
+
+#[tokio::test]
+async fn embed_sends_dimensions_when_opted_in() {
+ let app = Router::new().route(
+ "/v1/embeddings",
+ post(|Json(body): Json| async move {
+ assert_eq!(
+ body["dimensions"], 1024,
+ "dimensions must be sent so 3-large returns 1024, not its native 3072"
+ );
+ Json(serde_json::json!({ "data": [{ "embedding": vec![0.0_f32; 1024] }] }))
+ }),
+ );
+ let url = start_mock(app).await;
+ let p =
+ OpenAiEmbedding::new(&url, "k", "text-embedding-3-large", 1024).with_send_dimensions(true);
+ p.embed(&["test"]).await.unwrap();
+}
+
+#[tokio::test]
+async fn embed_omits_dimensions_by_default() {
+ let app = Router::new().route(
+ "/v1/embeddings",
+ post(|Json(body): Json| async move {
+ assert!(
+ body.get("dimensions").is_none(),
+ "dimensions must NOT be sent by default (Voyage/Cohere/Ollama reject it)"
+ );
+ Json(serde_json::json!({ "data": [{ "embedding": [1.0] }] }))
+ }),
+ );
+ let url = start_mock(app).await;
+ let p = OpenAiEmbedding::new(&url, "k", "m", 1); // no with_send_dimensions
+ p.embed(&["test"]).await.unwrap();
+}
+
#[tokio::test]
async fn embed_skips_auth_header_when_key_empty() {
let app = Router::new().route(
diff --git a/src/openhuman/embeddings/rpc.rs b/src/openhuman/embeddings/rpc.rs
index b7df3bb808..c4b5e6ea8f 100644
--- a/src/openhuman/embeddings/rpc.rs
+++ b/src/openhuman/embeddings/rpc.rs
@@ -401,7 +401,7 @@ pub fn provider_from_config(config: &Config) -> anyhow::Result String {
+pub(crate) fn resolve_api_key(config: &Config, provider_name: &str) -> String {
let slug = if provider_name.starts_with("custom:") {
"custom"
} else {
diff --git a/src/openhuman/memory/ingest_pipeline.rs b/src/openhuman/memory/ingest_pipeline.rs
index aa091acd03..cae9e88d01 100644
--- a/src/openhuman/memory/ingest_pipeline.rs
+++ b/src/openhuman/memory/ingest_pipeline.rs
@@ -503,9 +503,15 @@ mod tests {
);
let rows = list_chunks(&cfg, &ListChunksQuery::default()).unwrap();
assert_eq!(rows[0].metadata.source_kind, SourceKind::Chat);
+ // #002 FR-002: `test_config()` configures NO embeddings provider, so the
+ // extract handler correctly SKIPS embedding rather than persisting a
+ // zero-vector that would silently poison semantic recall. The chunk is
+ // written embedding-less and stays re-embeddable once a provider is set
+ // up. (With a provider configured the embedding is present — see the
+ // `build_write_embedder` tests in memory_tree/score/embed/factory.rs.)
assert!(get_chunk_embedding(&cfg, &out.chunk_ids[0])
.unwrap()
- .is_some());
+ .is_none());
}
#[tokio::test]
diff --git a/src/openhuman/memory/schema.rs b/src/openhuman/memory/schema.rs
index b3d7cfc577..710dc83bef 100644
--- a/src/openhuman/memory/schema.rs
+++ b/src/openhuman/memory/schema.rs
@@ -48,6 +48,8 @@ pub fn all_controller_schemas() -> Vec {
schemas("pipeline_status"),
schemas("set_enabled"),
schemas("smart_walk"),
+ schemas("doctor"),
+ schemas("retry_failed"),
]
}
@@ -143,6 +145,14 @@ pub fn all_registered_controllers() -> Vec {
schema: schemas("smart_walk"),
handler: handle_smart_walk,
},
+ RegisteredController {
+ schema: schemas("doctor"),
+ handler: handle_doctor,
+ },
+ RegisteredController {
+ schema: schemas("retry_failed"),
+ handler: handle_retry_failed,
+ },
]
}
@@ -772,10 +782,13 @@ pub fn schemas(function: &str) -> ControllerSchema {
FieldSchema {
name: "status",
ty: TypeSchema::Enum {
- variants: vec!["running", "paused", "syncing", "error", "idle"],
+ variants: vec![
+ "running", "paused", "syncing", "degraded", "error", "idle",
+ ],
},
- comment: "Coarse, UI-shaped status. paused wins over error wins \
- over syncing wins over running wins over idle.",
+ comment: "Coarse, UI-shaped status. Precedence: paused > error > \
+ degraded > syncing > running > idle. `degraded` (#002) = \
+ the pipeline runs but recall/structure is reduced.",
required: true,
},
FieldSchema {
@@ -824,6 +837,41 @@ pub fn schemas(function: &str) -> ControllerSchema {
comment: "True when scheduler-gate mode is `off`.",
required: true,
},
+ FieldSchema {
+ name: "degraded",
+ ty: TypeSchema::Json,
+ comment: "#002 (FR-002/FR-004): object `{ semantic_recall: bool, \
+ structure: bool, cause?: PipelineFailure }`. The pipeline \
+ ran but output quality is reduced — `semantic_recall` when \
+ embeddings were skipped, `structure` when extraction \
+ yielded nothing. `cause` is the single precedence-resolved \
+ failure (structure over semantic_recall) and is OMITTED \
+ when no degradation is active; the recall/structure flags \
+ are tracked independently behind it. The object itself is \
+ always present (serde default). Distinct from a hard `error`.",
+ required: true,
+ },
+ FieldSchema {
+ name: "first_blocking_cause",
+ ty: TypeSchema::Option(Box::new(TypeSchema::Json)),
+ comment: "#002 (FR-004): the single most-urgent typed cause as a \
+ `PipelineFailure` object `{ code, class, remediation_key }`. \
+ A failed job's classified reason wins over a soft \
+ degradation cause. null when healthy. The UI resolves \
+ `remediation_key` and renders it verbatim.",
+ required: false,
+ },
+ FieldSchema {
+ name: "extraction_coverage",
+ ty: TypeSchema::Option(Box::new(TypeSchema::F64)),
+ comment: "#002 (FR-010): fraction [0.0, 1.0] of chunks with ≥1 \
+ indexed entity. Near 0 with total_chunks > 0 means \
+ extraction produces no structure. `null` when the metric \
+ could not be measured (DB read error) — deliberately \
+ distinct from a genuine `0.0` so a broken measurement is \
+ never misreported as a structure failure.",
+ required: false,
+ },
],
},
"set_enabled" => ControllerSchema {
@@ -864,6 +912,73 @@ pub fn schemas(function: &str) -> ControllerSchema {
},
],
},
+ "doctor" => ControllerSchema {
+ namespace: NAMESPACE,
+ function: "doctor",
+ description: "One-shot Memory pipeline diagnostic (#002). Walks each \
+ stage (embeddings config, scheduler gate, job queue, extraction/recall \
+ degradation, summary-tree precondition) and returns per-stage health, \
+ the single first blocking cause (typed code + i18n remediation key), the \
+ degraded snapshot, and counters. Exposed for the agent's self-diagnosis \
+ and the CLI; cheap (config + queue counters + degraded flags, no live \
+ network probe).",
+ inputs: vec![],
+ outputs: vec![
+ FieldSchema {
+ name: "healthy",
+ ty: TypeSchema::Bool,
+ comment: "True when no stage is blocking (first_blocking_cause is null).",
+ required: true,
+ },
+ FieldSchema {
+ name: "stages",
+ ty: TypeSchema::Json,
+ comment: "Ordered array of { stage, ok, failure?, note } — pipeline \
+ order, so the first non-ok stage is the first blocking cause.",
+ required: true,
+ },
+ FieldSchema {
+ name: "first_blocking_cause",
+ ty: TypeSchema::Option(Box::new(TypeSchema::Json)),
+ comment: "Typed { code, class, remediation_key, detail? } of the first \
+ non-ok stage; null when healthy. Mirrors \
+ pipeline_status.first_blocking_cause as an explicit Option.",
+ required: false,
+ },
+ FieldSchema {
+ name: "degraded",
+ ty: TypeSchema::Json,
+ comment: "{ semantic_recall, structure, cause? } degradation snapshot.",
+ required: true,
+ },
+ FieldSchema {
+ name: "counters",
+ ty: TypeSchema::Json,
+ comment: "{ total_chunks, jobs_ready, jobs_running, jobs_failed, \
+ extraction_coverage: number|null }. extraction_coverage \
+ is the fraction [0,1] of chunks with ≥1 indexed entity; \
+ null when the metric could not be measured (DB error).",
+ required: true,
+ },
+ ],
+ },
+ "retry_failed" => ControllerSchema {
+ namespace: NAMESPACE,
+ function: "retry_failed",
+ description: "Requeue every terminally-failed mem_tree_jobs row back to \
+ `ready` (#002 FR-011) so jobs that failed under a now-fixed config \
+ (e.g. after adding an embeddings key) re-run without re-ingesting \
+ source data. Resets the attempt budget and clears the typed failure \
+ reason. Manual, on-demand retry — there is no automatic \
+ requeue-on-sync yet.",
+ inputs: vec![],
+ outputs: vec![FieldSchema {
+ name: "requeued",
+ ty: TypeSchema::U64,
+ comment: "Number of failed jobs flipped back to ready for retry.",
+ required: true,
+ }],
+ },
"memory_backfill_status" => ControllerSchema {
namespace: NAMESPACE,
function: "memory_backfill_status",
@@ -1316,6 +1431,20 @@ fn handle_smart_walk(params: Map) -> ControllerFuture {
})
}
+fn handle_doctor(_params: Map) -> ControllerFuture {
+ Box::pin(async move {
+ let config = config_rpc::load_config_with_timeout().await?;
+ to_json(rpc::doctor_rpc(&config).await?)
+ })
+}
+
+fn handle_retry_failed(_params: Map) -> ControllerFuture {
+ Box::pin(async move {
+ let config = config_rpc::load_config_with_timeout().await?;
+ to_json(rpc::retry_failed_rpc(&config).await?)
+ })
+}
+
fn parse_value(v: Value) -> Result {
serde_json::from_value(v).map_err(|e| format!("invalid params: {e}"))
}
diff --git a/src/openhuman/memory/tools.rs b/src/openhuman/memory/tools.rs
index 1e3e8f0a6d..176a5d598d 100644
--- a/src/openhuman/memory/tools.rs
+++ b/src/openhuman/memory/tools.rs
@@ -1,8 +1,10 @@
+mod doctor;
mod forget;
mod recall;
mod store;
pub use crate::openhuman::memory::query::*;
+pub use doctor::MemoryDoctorTool;
pub use forget::MemoryForgetTool;
pub use recall::MemoryRecallTool;
pub use store::MemoryStoreTool;
diff --git a/src/openhuman/memory/tools/doctor.rs b/src/openhuman/memory/tools/doctor.rs
new file mode 100644
index 0000000000..c1071bdf36
--- /dev/null
+++ b/src/openhuman/memory/tools/doctor.rs
@@ -0,0 +1,96 @@
+//! Agent tool: diagnose the memory pipeline (#002 FR-009).
+//!
+//! Thin wrapper over [`health::run_doctor`] so the agent can self-diagnose an
+//! empty / stalled wiki and tell the user the single first blocking cause +
+//! how to fix it — the same report the `memory_tree_doctor` RPC and CLI
+//! return. Read-only: takes no arguments and mutates nothing, so it carries no
+//! security-gate (matching the read-only memory tools).
+
+use crate::openhuman::config::Config;
+use crate::openhuman::memory_tree::health::async_run_doctor;
+use crate::openhuman::tools::traits::{Tool, ToolResult};
+use async_trait::async_trait;
+use serde_json::json;
+use std::sync::Arc;
+
+/// Let the agent run the one-shot memory-pipeline diagnostic.
+pub struct MemoryDoctorTool {
+ config: Arc,
+}
+
+impl MemoryDoctorTool {
+ pub fn new(config: Arc) -> Self {
+ Self { config }
+ }
+}
+
+#[async_trait]
+impl Tool for MemoryDoctorTool {
+ fn name(&self) -> &str {
+ "memory_doctor"
+ }
+
+ fn description(&self) -> &str {
+ "Diagnose why the memory tree / wiki is empty or stalled. Returns per-stage health \
+ (embeddings config, scheduler gate, job queue, extraction/recall degradation, \
+ summary-tree precondition), the single first blocking cause with a fix, and current \
+ counters. Read-only — takes no arguments."
+ }
+
+ fn parameters_schema(&self) -> serde_json::Value {
+ json!({ "type": "object", "properties": {}, "required": [] })
+ }
+
+ async fn execute(&self, _args: serde_json::Value) -> anyhow::Result {
+ let report = async_run_doctor(&self.config).await;
+ // Serialize the structured report so the model gets the typed stages +
+ // first_blocking_cause + counters verbatim (it can summarize for the
+ // user from there). serde of a plain struct can't fail here.
+ let payload = serde_json::to_string_pretty(&report)
+ .unwrap_or_else(|e| format!("{{\"error\":\"serialize doctor report: {e}\"}}"));
+ Ok(ToolResult::success(payload))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use tempfile::TempDir;
+
+ fn test_config() -> (TempDir, Arc) {
+ let tmp = TempDir::new().unwrap();
+ let mut cfg = Config::default();
+ cfg.workspace_dir = tmp.path().to_path_buf();
+ cfg.memory_tree.embedding_endpoint = None;
+ cfg.memory_tree.embedding_model = None;
+ (tmp, Arc::new(cfg))
+ }
+
+ #[test]
+ fn name_and_schema() {
+ let (_tmp, cfg) = test_config();
+ let tool = MemoryDoctorTool::new(cfg);
+ assert_eq!(tool.name(), "memory_doctor");
+ // No required args.
+ assert_eq!(tool.parameters_schema()["required"], json!([]));
+ }
+
+ #[tokio::test]
+ async fn execute_returns_a_report_for_a_misconfigured_workspace() {
+ let _g = crate::openhuman::memory_tree::health::test_guard();
+ let (_tmp, cfg) = test_config();
+ // No embeddings provider, local AI off → unhealthy with a typed cause.
+ let tool = MemoryDoctorTool::new(cfg);
+ let result = tool.execute(json!({})).await.unwrap();
+ assert!(!result.is_error);
+ let out = result.output();
+ assert!(
+ out.contains("\"healthy\""),
+ "report should serialize: {out}"
+ );
+ assert!(
+ out.contains("embeddings_unconfigured") || out.contains("\"healthy\": false"),
+ "misconfigured workspace should surface a blocking cause: {out}"
+ );
+ }
+}
diff --git a/src/openhuman/memory_queue/handlers/mod.rs b/src/openhuman/memory_queue/handlers/mod.rs
index 2192ef1578..e676a89a95 100644
--- a/src/openhuman/memory_queue/handlers/mod.rs
+++ b/src/openhuman/memory_queue/handlers/mod.rs
@@ -23,7 +23,7 @@ use crate::openhuman::memory_store::content::{
self as content_store, read as content_read, tags as content_tags,
};
use crate::openhuman::memory_tree::score;
-use crate::openhuman::memory_tree::score::embed::{build_embedder_from_config, pack_checked};
+use crate::openhuman::memory_tree::score::embed::{build_write_embedder, pack_checked};
use crate::openhuman::memory_tree::score::store as score_store;
use crate::openhuman::memory_tree::tree::store as summary_store;
use crate::openhuman::memory_tree::tree::{LeafRef, TreeFactory};
@@ -120,32 +120,52 @@ async fn handle_extract(config: &Config, job: &Job) -> Result {
let scoring_cfg = score::ScoringConfig::from_config(config);
let result = score::score_chunk(&chunk_with_body, &scoring_cfg).await?;
let chunk_embedding: Option> = if result.kept {
- match build_embedder_from_config(config) {
- Ok(embedder) => match embedder.embed(&body).await {
- Ok(vector) => match pack_checked(&vector) {
- Ok(_) => Some(vector),
- Err(e) => {
- log::warn!(
- "[memory::jobs] embed dim check failed chunk_id={} err={e:#} — skipping embedding",
- chunk.id
- );
- None
- }
- },
- Err(e) => {
- log::warn!(
- "[memory::jobs] embed failed chunk_id={} err={e:#} — continuing without embedding",
- chunk.id
- );
- None
- }
- },
- Err(e) => {
+ // #002 (FR-002): when no usable embeddings provider is configured the
+ // write path returns None instead of an InertEmbedder — we SKIP
+ // embedding (the chunk is persisted embedding-less and re-embeddable
+ // later) rather than writing a fake all-zero vector that would
+ // silently poison semantic recall. `build_write_embedder` has already
+ // marked the process-global semantic-recall degraded flag with a typed
+ // cause for the status / doctor surface.
+ match build_write_embedder(config).context("build embedder in extract handler")? {
+ None => {
log::warn!(
- "[memory::jobs] build embedder failed err={e:#} — continuing without embedding"
+ "[memory::jobs] extract chunk_id={} — embeddings unavailable, \
+ skipping embed (semantic recall degraded)",
+ chunk.id
);
None
}
+ Some(embedder) => {
+ // Reuse the body already read — avoid a second disk read.
+ let vector = match embedder.embed(&body).await {
+ Ok(v) => v,
+ Err(e) => {
+ // #002: classify the embed failure so the worker can
+ // fail fast on unrecoverable causes (budget/auth/dim)
+ // and surface a typed reason, instead of burning the
+ // retry budget. The typed failure is the outer
+ // (downcast) error; the original chain is context.
+ let failure =
+ crate::openhuman::memory_tree::health::classify_embed_error(&e);
+ return Err(anyhow::Error::new(failure).context(format!(
+ "embed chunk_id={} in extract handler: {e:#}",
+ chunk.id
+ )));
+ }
+ };
+ // Preserve the pre-cutover dimension guard (the job fails fast
+ // on a misconfigured embedder) even though #1574 no longer
+ // persists the packed blob to the legacy
+ // `mem_tree_chunks.embedding` column — the vector now goes to
+ // the per-model sidecar instead.
+ pack_checked(&vector).with_context(|| {
+ format!("validate embedding dims for chunk_id={}", chunk.id)
+ })?;
+ // A real embed succeeded — recall is healthy again.
+ crate::openhuman::memory_tree::health::clear_semantic_recall_degraded();
+ Some(vector)
+ }
}
} else {
None
@@ -687,8 +707,29 @@ async fn handle_reembed_backfill(config: &Config, job: &Job) -> Result e,
+ None => {
+ crate::openhuman::memory_queue::set_backfill_in_progress(false);
+ log::warn!(
+ "[memory::jobs] reembed_backfill: sig={active_sig} — no usable embeddings \
+ provider, skipping backfill (rows stay re-embeddable; semantic recall degraded)"
+ );
+ return Ok(JobOutcome::Done);
+ }
+ };
let mut chunk_vecs: Vec<(String, Vec)> = Vec::new();
for id in &chunk_ids {
match content_read::read_chunk_body(config, id) {
diff --git a/src/openhuman/memory_queue/handlers/mod_tests.rs b/src/openhuman/memory_queue/handlers/mod_tests.rs
index 924fc5df2a..f346ff9203 100644
--- a/src/openhuman/memory_queue/handlers/mod_tests.rs
+++ b/src/openhuman/memory_queue/handlers/mod_tests.rs
@@ -38,6 +38,8 @@ fn mk_running_job(kind: JobKind, payload_json: String) -> Job {
created_at_ms: now_ms,
started_at_ms: Some(now_ms),
completed_at_ms: None,
+ failure_reason: None,
+ failure_class: None,
}
}
@@ -132,7 +134,12 @@ async fn reembed_backfill_repopulates_then_completes() {
chunk_id, Chunk, Metadata, SourceKind, SourceRef,
};
- let (_tmp, cfg) = test_config();
+ let (_tmp, mut cfg) = test_config();
+ // Deliberate "none" opt-out → build_write_embedder yields an InertEmbedder
+ // (correct-dim zero vectors, no network). This test pins backfill
+ // *mechanics* (worklist → sidecar write → Defer/Done), not embed quality;
+ // the no-provider skip path is covered separately.
+ cfg.embeddings_provider = Some("none".to_string());
let ts = chrono::Utc.timestamp_millis_opt(1_700_000_000_000).unwrap();
let chunk = Chunk {
id: chunk_id(SourceKind::Chat, "slack:#eng", 0, "reembed-seed"),
@@ -212,6 +219,85 @@ async fn reembed_backfill_repopulates_then_completes() {
);
}
+/// #002 (FR-002) regression gate: when NO usable embeddings provider is
+/// configured, the re-embed backfill must SKIP (return `Done`) instead of
+/// falling back to an `InertEmbedder` and persisting all-zero vectors that
+/// would silently poison semantic recall — the same hazard the extract and
+/// seal write paths already guard against. The chunk stays embedding-less at
+/// the active signature (re-embeddable once a provider is configured).
+#[tokio::test]
+async fn reembed_backfill_skips_when_no_provider() {
+ use crate::openhuman::memory_store::chunks::store::{
+ get_chunk_embedding_for_signature, tree_active_signature, upsert_chunks,
+ upsert_staged_chunks_tx,
+ };
+ use crate::openhuman::memory_store::chunks::types::{
+ chunk_id, Chunk, Metadata, SourceKind, SourceRef,
+ };
+
+ // Default test config leaves embeddings unconfigured (no endpoint/model,
+ // provider unset) — the no-provider path build_write_embedder guards.
+ //
+ // Hold the shared health test-guard: the no-provider path marks the
+ // process-global semantic-recall degraded flag, so the guard resets it on
+ // entry and keeps the signal from leaking into parallel status tests.
+ let _health_guard = crate::openhuman::memory_tree::health::test_guard();
+ let (_tmp, cfg) = test_config();
+ let ts = chrono::Utc.timestamp_millis_opt(1_700_000_000_000).unwrap();
+ let chunk = Chunk {
+ id: chunk_id(SourceKind::Chat, "slack:#eng", 0, "no-provider-seed"),
+ content: "memory content with no embeddings provider configured".into(),
+ metadata: Metadata {
+ source_kind: SourceKind::Chat,
+ source_id: "slack:#eng".into(),
+ owner: "alice".into(),
+ timestamp: ts,
+ time_range: (ts, ts),
+ tags: vec![],
+ source_ref: Some(SourceRef::new("slack://x")),
+ path_scope: None,
+ },
+ token_count: 12,
+ seq_in_source: 0,
+ created_at: ts,
+ partial_message: false,
+ };
+ upsert_chunks(&cfg, &[chunk.clone()]).unwrap();
+ let content_root = cfg.memory_tree_content_root();
+ std::fs::create_dir_all(&content_root).unwrap();
+ let staged = content_store::stage_chunks(&content_root, &[chunk.clone()]).unwrap();
+ with_connection(&cfg, |conn| {
+ let tx = conn.unchecked_transaction()?;
+ upsert_staged_chunks_tx(&tx, &staged)?;
+ tx.commit()?;
+ Ok(())
+ })
+ .unwrap();
+
+ let sig = tree_active_signature(&cfg);
+ let job = mk_running_job(
+ JobKind::ReembedBackfill,
+ serde_json::to_string(&ReembedBackfillPayload {
+ signature: sig.clone(),
+ })
+ .unwrap(),
+ );
+
+ // No provider → skip the whole backfill (Done), do NOT write a vector.
+ let out = handle_reembed_backfill(&cfg, &job).await.unwrap();
+ assert_eq!(
+ out,
+ JobOutcome::Done,
+ "no usable provider must skip the backfill, not Defer/embed"
+ );
+ assert!(
+ get_chunk_embedding_for_signature(&cfg, &chunk.id, &sig)
+ .unwrap()
+ .is_none(),
+ "no zero/inert vector may be persisted when no provider is configured"
+ );
+}
+
/// #1574 §6 regression gate: a terminal-failure chunk (its body file is
/// missing on disk, despite the metadata row staying staged) is
/// persistently tombstoned by `mark_chunk_reembed_skipped` on the first
@@ -237,7 +323,11 @@ async fn reembed_backfill_tombstones_orphan_and_terminates() {
chunk_id, Chunk, Metadata, SourceKind, SourceRef,
};
- let (_tmp, cfg) = test_config();
+ let (_tmp, mut cfg) = test_config();
+ // Deliberate "none" opt-out → InertEmbedder (zero vectors, no network) so
+ // the backfill reaches the orphan body-read and tombstones it; this test
+ // pins the tombstone-and-terminate mechanics, not embed quality.
+ cfg.embeddings_provider = Some("none".to_string());
let ts = chrono::Utc.timestamp_millis_opt(1_700_000_000_000).unwrap();
let chunk = Chunk {
id: chunk_id(SourceKind::Chat, "slack:#eng", 0, "orphan-seed"),
diff --git a/src/openhuman/memory_queue/store.rs b/src/openhuman/memory_queue/store.rs
index 60fd2fd881..eabafa09b5 100644
--- a/src/openhuman/memory_queue/store.rs
+++ b/src/openhuman/memory_queue/store.rs
@@ -131,7 +131,8 @@ pub fn claim_next(config: &Config, lock_duration_ms: i64) -> Result