From 944dde36079f85badefd9bc7dea39fe2c982bc84 Mon Sep 17 00:00:00 2001
From: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
Date: Thu, 21 May 2026 01:26:48 -0700
Subject: [PATCH 01/67] Update Product Hunt badges in README
---
README.md | 28 +++++++++++++++++++---------
1 file changed, 19 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index 68501bfcad..f34d7a4c18 100644
--- a/README.md
+++ b/README.md
@@ -5,16 +5,26 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
OpenHuman is your Personal AI super intelligence. Private, Simple and extremely powerful.
From a1f1024e3aa1dd2bb55da17ff8bc519b5bd175a6 Mon Sep 17 00:00:00 2001
From: aqilaziz
Date: Fri, 22 May 2026 09:21:38 +0700
Subject: [PATCH 02/67] i18n: polish Indonesian UI translations
---
app/src/lib/i18n/chunks/de-3.ts | 2 +
app/src/lib/i18n/chunks/de-5.ts | 22 ++++++++++
app/src/lib/i18n/chunks/id-1.ts | 30 ++++++-------
app/src/lib/i18n/chunks/id-2.ts | 72 +++++++++++++++---------------
app/src/lib/i18n/chunks/id-3.ts | 30 ++++++-------
app/src/lib/i18n/chunks/id-4.ts | 14 +++---
app/src/lib/i18n/chunks/id-5.ts | 77 +++++++++++++++++----------------
7 files changed, 136 insertions(+), 111 deletions(-)
diff --git a/app/src/lib/i18n/chunks/de-3.ts b/app/src/lib/i18n/chunks/de-3.ts
index 8cbb4e8ae7..e1b209a9b5 100644
--- a/app/src/lib/i18n/chunks/de-3.ts
+++ b/app/src/lib/i18n/chunks/de-3.ts
@@ -104,6 +104,8 @@ const de3: TranslationMap = {
'subconscious.failed': 'gescheitert',
'subconscious.tickInterval': 'Tick-Intervall',
'subconscious.runNow': 'Jetzt ausführen',
+ 'subconscious.providerUnavailableTitle': 'Subconscious ist pausiert',
+ 'subconscious.providerSettings': 'KI-Einstellungen',
'subconscious.approvalNeeded': 'Genehmigung erforderlich',
'subconscious.requiresApproval': 'Erfordert eine Genehmigung',
'subconscious.fixInConnections': 'Fix in Verbindungen',
diff --git a/app/src/lib/i18n/chunks/de-5.ts b/app/src/lib/i18n/chunks/de-5.ts
index c698c292fd..8ec284678d 100644
--- a/app/src/lib/i18n/chunks/de-5.ts
+++ b/app/src/lib/i18n/chunks/de-5.ts
@@ -501,6 +501,28 @@ const de5: TranslationMap = {
'settings.mascot.colorYellow': 'Gelb',
'settings.mascot.libraryUnavailable': 'OpenHuman Bibliothek nicht verfügbar',
'settings.mascot.title': 'OpenHuman',
+ 'settings.developerMenu.mcpServer.title': 'MCP-Server',
+ 'settings.developerMenu.mcpServer.desc':
+ 'Konfiguriere externe MCP-Clients für die Verbindung mit OpenHuman',
+ 'settings.mcpServer.title': 'MCP-Server',
+ 'settings.mcpServer.toolsSectionTitle': 'Verfügbare Tools',
+ 'settings.mcpServer.toolsSectionDesc':
+ 'Tools, die über den MCP-stdio-Server verfügbar sind, wenn openhuman-core mcp ausgeführt wird',
+ 'settings.mcpServer.configSectionTitle': 'Client-Konfiguration',
+ 'settings.mcpServer.configSectionDesc':
+ 'Wähle deinen MCP-Client aus, um den passenden Konfigurationsausschnitt zu erzeugen',
+ 'settings.mcpServer.copySnippet': 'In Zwischenablage kopieren',
+ 'settings.mcpServer.copied': 'Kopiert!',
+ 'settings.mcpServer.openConfigFile': 'Konfigurationsdatei öffnen',
+ 'settings.mcpServer.binaryPathNotFound':
+ 'OpenHuman-Binärdatei nicht gefunden. Wenn du aus dem Quellcode arbeitest, baue sie mit: cargo build --bin openhuman-core',
+ 'settings.mcpServer.openConfigError': 'Konfigurationsdatei konnte nicht geöffnet werden',
+ 'settings.mcpServer.clientClaudeDesktop': 'Claude Desktop',
+ 'settings.mcpServer.clientCursor': 'Cursor',
+ 'settings.mcpServer.clientCodex': 'Codex',
+ 'settings.mcpServer.clientZed': 'Zed',
+ 'settings.mcpServer.configFilePath': 'Konfigurationsdatei',
+ 'settings.mcpServer.clientSelectorAriaLabel': 'MCP-Client-Auswahl',
};
export default de5;
diff --git a/app/src/lib/i18n/chunks/id-1.ts b/app/src/lib/i18n/chunks/id-1.ts
index b2118b9953..6ed09005dd 100644
--- a/app/src/lib/i18n/chunks/id-1.ts
+++ b/app/src/lib/i18n/chunks/id-1.ts
@@ -3,8 +3,8 @@ import type { TranslationMap } from '../types';
// Indonesian (Bahasa Indonesia) chunk 1/5. Translated from chunks/en-1.ts.
const id1: TranslationMap = {
'nav.home': 'Beranda',
- 'nav.human': 'Human',
- 'nav.chat': 'Chat',
+ 'nav.human': 'Manusia',
+ 'nav.chat': 'Obrolan',
'nav.connections': 'Koneksi',
'nav.memory': 'Memori',
'nav.alerts': 'Peringatan',
@@ -14,11 +14,11 @@ const id1: TranslationMap = {
'common.save': 'Simpan',
'common.confirm': 'Konfirmasi',
'common.delete': 'Hapus',
- 'common.edit': 'Edit',
+ 'common.edit': 'Ubah',
'common.create': 'Buat',
'common.search': 'Cari',
'common.loading': 'memuat…',
- 'common.error': 'Error',
+ 'common.error': 'Kesalahan',
'common.success': 'Berhasil',
'common.back': 'Kembali',
'common.next': 'Berikutnya',
@@ -38,7 +38,7 @@ const id1: TranslationMap = {
'common.seeAll': 'Lihat',
'common.dismiss': 'Abaikan',
'common.clear': 'Bersihkan',
- 'common.reset': 'Reset',
+ 'common.reset': 'Atur ulang',
'common.refresh': 'Segarkan',
'common.export': 'Ekspor',
'common.import': 'Impor',
@@ -152,7 +152,7 @@ const id1: TranslationMap = {
'chat.copyResponse': 'Salin respons',
'chat.citations': 'Sitasi',
'chat.toolUsed': 'Alat yang digunakan',
- 'scope.legacy': 'Legacy',
+ 'scope.legacy': 'Lama',
'scope.user': 'Pengguna',
'scope.project': 'Proyek',
'skills.title': 'Koneksi',
@@ -196,7 +196,7 @@ const id1: TranslationMap = {
'onboarding.localAIDesc': 'Siapkan model AI lokal yang berjalan di mesin Anda.',
'onboarding.chatProvider': 'Penyedia Chat',
'onboarding.chatProviderDesc': 'Pilih cara Anda ingin berinteraksi dengan asisten.',
- 'onboarding.referral': 'Referral',
+ 'onboarding.referral': 'Rujukan',
'onboarding.referralDesc': 'Gunakan kode referral jika Anda memilikinya.',
'onboarding.finish': 'Selesaikan Pengaturan',
'onboarding.finishDesc': 'Semua siap! Mulai gunakan OpenHuman.',
@@ -242,7 +242,7 @@ const id1: TranslationMap = {
'onboarding.custom.stepperSearch': 'Pencarian',
'onboarding.custom.stepperMemory': 'Memori',
'onboarding.custom.stepCounter': 'Langkah {n} dari {total}',
- 'onboarding.custom.defaultTitle': 'Default',
+ 'onboarding.custom.defaultTitle': 'Bawaan',
'onboarding.custom.defaultSubtitle': 'Biarkan OpenHuman mengelolanya untuk Anda.',
'onboarding.custom.configureTitle': 'Konfigurasi',
'onboarding.custom.configureSubtitle': 'Saya akan memilih apa yang digunakan.',
@@ -302,14 +302,14 @@ const id1: TranslationMap = {
'channels.addChannel': 'Tambah Kanal',
'channels.status.connected': 'Terhubung',
'channels.status.disconnected': 'Terputus',
- 'channels.status.error': 'Error',
+ 'channels.status.error': 'Kesalahan',
'channels.status.configuring': 'Mengonfigurasi',
'channels.defaultMessaging': 'Kanal Pesan Default',
'webhooks.title': 'Webhook',
'webhooks.create': 'Buat Webhook',
'webhooks.noWebhooks': 'Belum ada webhook yang dikonfigurasi',
'webhooks.url': 'URL',
- 'webhooks.secret': 'Secret',
+ 'webhooks.secret': 'Rahasia',
'webhooks.events': 'Event',
'webhooks.archiveDirectory': 'Direktori Arsip',
'webhooks.todayFile': 'File Hari Ini',
@@ -419,11 +419,11 @@ const id1: TranslationMap = {
'Impor {count} entri ke ruang kerja saat ini?\n\nSumber: {source}\nTujuan: {target}\n\nMemori yang ada akan dicadangkan sebelum impor dimulai.',
'migration.confirmImport.plural':
'Impor {count} entri ke ruang kerja saat ini?\n\nSumber: {source}\nTujuan: {target}\n\nMemori yang ada akan dicadangkan sebelum impor dimulai.',
- // Settings menu: Appearance + Mascot (#2225) — English stubs; native translations welcome
- 'settings.appearance': 'Appearance',
- 'settings.appearanceDesc': 'Pick light, dark, or match your system theme',
- 'settings.mascot': 'Mascot',
- 'settings.mascotDesc': 'Pick the mascot color used across the app',
+ // Settings menu: Appearance + Mascot (#2225)
+ 'settings.appearance': 'Tampilan',
+ 'settings.appearanceDesc': 'Pilih terang, gelap, atau ikuti tema sistem Anda',
+ 'settings.mascot': 'Maskot',
+ 'settings.mascotDesc': 'Pilih warna maskot yang digunakan di seluruh aplikasi',
};
export default id1;
diff --git a/app/src/lib/i18n/chunks/id-2.ts b/app/src/lib/i18n/chunks/id-2.ts
index 636baaea50..ebd782ed43 100644
--- a/app/src/lib/i18n/chunks/id-2.ts
+++ b/app/src/lib/i18n/chunks/id-2.ts
@@ -142,7 +142,7 @@ const id2: TranslationMap = {
'team.failedToSwitch': 'Gagal berpindah tim',
'team.failedToLeave': 'Gagal meninggalkan tim',
'team.role.owner': 'Pemilik',
- 'team.role.admin': 'Admin',
+ 'team.role.admin': 'Administrator',
'team.role.billingManager': 'Manajer Tagihan',
'team.role.member': 'Anggota',
'team.active': 'Aktif',
@@ -198,7 +198,7 @@ const id2: TranslationMap = {
'autocomplete.stylePreset': 'Preset Gaya',
'autocomplete.style.balanced': 'Seimbang',
'autocomplete.style.concise': 'Ringkas',
- 'autocomplete.style.formal': 'Formal',
+ 'autocomplete.style.formal': 'Resmi',
'autocomplete.style.casual': 'Santai',
'autocomplete.style.custom': 'Kustom',
'autocomplete.disabledApps': 'Aplikasi yang Dinonaktifkan (satu bundle/token aplikasi per baris)',
@@ -268,7 +268,7 @@ const id2: TranslationMap = {
'chat.safetyTimeout': 'Tidak ada respons dari agen setelah 2 menit. Coba lagi atau cek koneksi.',
'chat.filter.all': 'Semua',
'chat.filter.work': 'Kerja',
- 'chat.filter.briefing': 'Briefing',
+ 'chat.filter.briefing': 'Ringkasan',
'chat.filter.notification': 'Notifikasi',
'chat.filter.workers': 'Worker',
'chat.selectThread': 'Pilih thread',
@@ -317,11 +317,11 @@ const id2: TranslationMap = {
'memory.sourceFilter.telegram': 'Telegram',
'memory.sourceFilter.aiInsight': 'Insight AI',
'memory.sourceFilter.system': 'Sistem',
- 'memory.sourceFilter.trading': 'Trading',
+ 'memory.sourceFilter.trading': 'Perdagangan',
'memory.sourceFilter.security': 'Keamanan',
'memory.ingestionActivity': 'Aktivitas Ingesti',
- 'memory.events': 'event',
- 'memory.event': 'event',
+ 'memory.events': 'peristiwa',
+ 'memory.event': 'peristiwa',
'memory.overTheLast': 'selama',
'memory.months': 'bulan',
'memory.peak': 'Puncak',
@@ -369,7 +369,7 @@ const id2: TranslationMap = {
'navigator.sources': 'Sumber',
'navigator.email': 'Email',
'navigator.slack': 'Slack',
- 'navigator.chat': 'Chat',
+ 'navigator.chat': 'Obrolan',
'navigator.documents': 'Dokumen',
'navigator.people': 'Orang',
'navigator.topics': 'Topik',
@@ -378,7 +378,7 @@ const id2: TranslationMap = {
'dreams.comingSoon': 'Segera hadir',
'assignment.memoryLlm': 'LLM Memori',
'assignment.memoryLlmAria': 'Pemilihan LLM Memori',
- 'assignment.embedder': 'Embedder',
+ 'assignment.embedder': 'Penyemat',
'assignment.loaded': 'Dimuat',
'assignment.notDownloaded': 'Belum diunduh',
'assignment.usedForExtractSummarise': 'Digunakan untuk ekstraksi dan ringkasan',
@@ -387,40 +387,40 @@ const id2: TranslationMap = {
'insights.relationships': 'Hubungan',
'insights.skills': 'Skill',
'insights.opinions': 'Pendapat',
- // Developer options menu items (#2225) — English stubs; native translations welcome
- 'devOptions.menuAi': 'AI Configuration',
- 'devOptions.menuAiDesc': 'Cloud providers, local Ollama models, and per-workload routing',
- 'devOptions.menuScreenAware': 'Screen Awareness',
- 'devOptions.menuScreenAwareDesc':
- 'Screen capture permissions, monitoring policy, and session controls',
- 'devOptions.menuMessaging': 'Messaging Channels',
+ // Developer options menu items (#2225)
+ 'devOptions.menuAi': 'Konfigurasi AI',
+ 'devOptions.menuAiDesc': 'Penyedia cloud, model Ollama lokal, dan routing per beban kerja',
+ 'devOptions.menuScreenAware': 'Kesadaran Layar',
+ 'devOptions.menuScreenAwareDesc': 'Izin tangkapan layar, kebijakan pemantauan, dan kontrol sesi',
+ 'devOptions.menuMessaging': 'Channel Pesan',
'devOptions.menuMessagingDesc':
- 'Configure Telegram/Discord auth modes and default channel routing',
- 'devOptions.menuTools': 'Tools',
- 'devOptions.menuToolsDesc': 'Enable or disable capabilities OpenHuman can use on your behalf',
- 'devOptions.menuAgentChat': 'Agent Chat',
- 'devOptions.menuAgentChatDesc': 'Test agent conversation with model and temperature overrides',
- 'devOptions.menuCronJobs': 'Cron Jobs',
- 'devOptions.menuCronJobsDesc': 'View and configure scheduled jobs for runtime skills',
- 'devOptions.menuLocalModelDebug': 'Local Model Debug',
+ 'Konfigurasikan mode autentikasi Telegram/Discord dan routing channel bawaan',
+ 'devOptions.menuTools': 'Alat',
+ 'devOptions.menuToolsDesc':
+ 'Aktifkan atau nonaktifkan kemampuan yang dapat digunakan OpenHuman atas nama Anda',
+ 'devOptions.menuAgentChat': 'Obrolan Agen',
+ 'devOptions.menuAgentChatDesc': 'Uji percakapan agen dengan override model dan suhu',
+ 'devOptions.menuCronJobs': 'Pekerjaan Cron',
+ 'devOptions.menuCronJobsDesc': 'Lihat dan konfigurasikan pekerjaan terjadwal untuk skill runtime',
+ 'devOptions.menuLocalModelDebug': 'Debug Model Lokal',
'devOptions.menuLocalModelDebugDesc':
- 'Ollama config, asset downloads, model tests, and diagnostics',
- 'devOptions.menuWebhooksDebug': 'Webhooks',
+ 'Konfigurasi Ollama, unduhan aset, pengujian model, dan diagnostik',
+ 'devOptions.menuWebhooksDebug': 'Webhook',
'devOptions.menuWebhooksDebugDesc':
- 'Inspect runtime webhook registrations and captured request logs',
- 'devOptions.menuIntelligence': 'Intelligence',
- 'devOptions.menuIntelligenceDesc': 'Memory workspace, subconscious engine, dreams, and settings',
- 'devOptions.menuNotificationRouting': 'Notification Routing',
+ 'Periksa pendaftaran webhook runtime dan log permintaan yang ditangkap',
+ 'devOptions.menuIntelligence': 'Kecerdasan',
+ 'devOptions.menuIntelligenceDesc': 'Workspace memori, mesin subconscious, mimpi, dan pengaturan',
+ 'devOptions.menuNotificationRouting': 'Routing Notifikasi',
'devOptions.menuNotificationRoutingDesc':
- 'AI importance scoring and orchestrator escalation for integration alerts',
- 'devOptions.menuComposeIOTriggers': 'ComposeIO Triggers',
- 'devOptions.menuComposeIOTriggersDesc': 'View ComposeIO trigger history and archive',
- 'devOptions.menuComposioRouting': 'Composio Routing (Direct Mode)',
+ 'Skor kepentingan AI dan eskalasi orkestrator untuk alert integrasi',
+ 'devOptions.menuComposeIOTriggers': 'Pemicu ComposeIO',
+ 'devOptions.menuComposeIOTriggersDesc': 'Lihat riwayat dan arsip pemicu ComposeIO',
+ 'devOptions.menuComposioRouting': 'Routing Composio (Mode Direct)',
'devOptions.menuComposioRoutingDesc':
- 'Bring your own Composio API key and route calls directly to backend.composio.dev',
- 'devOptions.menuComposioTriggers': 'Integration Triggers',
+ 'Gunakan API key Composio milik Anda sendiri dan rutekan panggilan langsung ke backend.composio.dev',
+ 'devOptions.menuComposioTriggers': 'Pemicu Integrasi',
'devOptions.menuComposioTriggersDesc':
- 'Configure AI triage settings for Composio integration triggers',
+ 'Konfigurasikan pengaturan triase AI untuk pemicu integrasi Composio',
};
export default id2;
diff --git a/app/src/lib/i18n/chunks/id-3.ts b/app/src/lib/i18n/chunks/id-3.ts
index f776d0c1f5..156ce97ca5 100644
--- a/app/src/lib/i18n/chunks/id-3.ts
+++ b/app/src/lib/i18n/chunks/id-3.ts
@@ -34,14 +34,14 @@ const id3: TranslationMap = {
'workspace.building': 'Membangun...',
'workspace.buildSummaryTrees': 'Bangun Pohon Ringkasan',
'workspace.viewVault': 'Lihat Vault',
- 'workspace.openingVaultTitle': 'Opening vault in Obsidian',
+ 'workspace.openingVaultTitle': 'Membuka vault di Obsidian',
'workspace.openingVaultMessage':
- "If Obsidian doesn't open, install it from obsidian.md or use Reveal Folder. Vault path:",
- 'workspace.openVaultFailedTitle': "Couldn't open vault in Obsidian",
+ 'Jika Obsidian tidak terbuka, instal dari obsidian.md atau gunakan Tampilkan Folder. Path vault:',
+ 'workspace.openVaultFailedTitle': 'Tidak dapat membuka vault di Obsidian',
'workspace.openVaultFailedMessage':
- 'Use Reveal Folder to open the vault directory directly. Vault path:',
- 'workspace.revealVaultFailed': "Couldn't reveal vault folder",
- 'workspace.revealFolder': 'Reveal Folder',
+ 'Gunakan Tampilkan Folder untuk membuka direktori vault secara langsung. Path vault:',
+ 'workspace.revealVaultFailed': 'Tidak dapat menampilkan folder vault',
+ 'workspace.revealFolder': 'Tampilkan Folder',
'workspace.graphLoadFailed': 'Gagal memuat grafik memori',
'workspace.loadingGraph': 'Memuat grafik memori...',
'workspace.graphViewMode': 'Mode tampilan grafik memori',
@@ -51,7 +51,7 @@ const id3: TranslationMap = {
'graph.noMemory': 'Tidak ada memori',
'graph.source': 'Sumber',
'graph.topic': 'Topik',
- 'graph.global': 'Global',
+ 'graph.global': 'Keseluruhan',
'graph.document': 'Dokumen',
'graph.contact': 'Kontak',
'graph.nodes': 'node',
@@ -73,7 +73,7 @@ const id3: TranslationMap = {
'whatsapp.chatSynced': 'obrolan disinkronkan',
'sync.active': 'Aktif',
'sync.recent': 'Terbaru',
- 'sync.idle': 'Idle',
+ 'sync.idle': 'Siaga',
'sync.memorySources': 'Sumber Memori',
'sync.noConnectedSources': 'Tidak ada sumber terhubung',
'sync.chunks': 'chunk',
@@ -109,7 +109,7 @@ const id3: TranslationMap = {
'subconscious.goAhead': 'Lanjutkan',
'subconscious.activeTasks': 'Tugas Aktif',
'subconscious.noActiveTasks': 'Tidak ada tugas aktif',
- 'subconscious.default': 'Default',
+ 'subconscious.default': 'Bawaan',
'subconscious.addTaskPlaceholder': 'Tambahkan tugas baru...',
'subconscious.activityLog': 'Log Aktivitas',
'subconscious.noActivity': 'Belum ada aktivitas',
@@ -228,7 +228,7 @@ const id3: TranslationMap = {
'onboarding.skills.status.available': 'Tersedia',
'onboarding.skills.status.connected': 'Terhubung',
'onboarding.skills.status.connecting': 'Menghubungkan',
- 'onboarding.skills.status.error': 'Error',
+ 'onboarding.skills.status.error': 'Kesalahan',
'onboarding.skills.status.unavailable': 'Tidak tersedia',
'composio.statusUnavailable': 'Status tidak tersedia',
'composio.envVarOverrides': 'diatur, itu menggantikan pengaturan ini.',
@@ -280,9 +280,9 @@ const id3: TranslationMap = {
'app.connectionBadge.messaging': 'Pesan',
'app.connectionIndicator.connected': 'Terhubung ke OpenHuman AI 🚀',
'app.connectionIndicator.connecting': 'Menghubungkan',
- 'app.connectionIndicator.coreOffline': 'Core offline',
+ 'app.connectionIndicator.coreOffline': 'Core tidak online',
'app.connectionIndicator.disconnected': 'Terputus',
- 'app.connectionIndicator.offline': 'Offline',
+ 'app.connectionIndicator.offline': 'Tidak online',
'app.connectionIndicator.reconnecting': 'Menyambung ulang…',
'app.errorFallback.componentStack': 'Stack komponen',
'app.errorFallback.downloadLatest': 'Unduh terbaru',
@@ -295,7 +295,7 @@ const id3: TranslationMap = {
'app.localAiDownload.preparing': 'Mempersiapkan...',
'app.openhumanLink.accounts.continueWith': 'Lanjutkan dengan masuk {label}',
'app.openhumanLink.accounts.done': 'Selesai',
- 'app.openhumanLink.accounts.intro': 'Intro',
+ 'app.openhumanLink.accounts.intro': 'Pengantar',
'app.openhumanLink.accounts.webviewNote': 'Catatan webview',
'app.openhumanLink.billing.openDashboard': 'Buka dashboard',
'app.openhumanLink.billing.stayOnTrial': 'Tetap di trial',
@@ -303,7 +303,7 @@ const id3: TranslationMap = {
'app.openhumanLink.billing.trialDesc': 'Deskripsi trial',
'app.openhumanLink.defaultBody':
't siap di popup belum. Buka halaman pengaturan lengkap jika Anda',
- 'app.openhumanLink.discord.intro': 'Intro',
+ 'app.openhumanLink.discord.intro': 'Pengantar',
'app.openhumanLink.discord.openInvite': 'Buka undangan',
'app.openhumanLink.discord.perk1': 'Keuntungan 1',
'app.openhumanLink.discord.perk2': 'Keuntungan 2',
@@ -317,7 +317,7 @@ const id3: TranslationMap = {
'app.openhumanLink.notifications.blockedStep1': 'Langkah 1 diblokir',
'app.openhumanLink.notifications.blockedStep2': 'Langkah 2 diblokir',
'app.openhumanLink.notifications.blockedStep3': 'Langkah 3 diblokir',
- 'app.openhumanLink.notifications.intro': 'Intro',
+ 'app.openhumanLink.notifications.intro': 'Pengantar',
'app.openhumanLink.notifications.promptHint': 'Petunjuk prompt',
'app.openhumanLink.notifications.retry': 'Coba ulang notifikasi tes',
'app.openhumanLink.notifications.send': 'Kirim notifikasi tes',
diff --git a/app/src/lib/i18n/chunks/id-4.ts b/app/src/lib/i18n/chunks/id-4.ts
index 32a1e0968f..f358752c03 100644
--- a/app/src/lib/i18n/chunks/id-4.ts
+++ b/app/src/lib/i18n/chunks/id-4.ts
@@ -89,13 +89,13 @@ const id4: TranslationMap = {
'home.banners.promoCreditsBody': 'Isi kredit promo',
'home.banners.promoCreditsTitle': '{amount}',
'home.banners.promoCreditsUsage': 'Penggunaan kredit promo',
- 'intelligence.memoryChunk.detail.chunk': 'Chunk',
+ 'intelligence.memoryChunk.detail.chunk': 'Potongan',
'intelligence.memoryChunk.detail.copyChunkId': 'Salin ID chunk',
'intelligence.memoryChunk.detail.embeddingInfo': 'bge-m3 1024dim',
'intelligence.memoryChunk.detail.noEmbedding': 'Tidak ada embedding',
'intelligence.memoryChunk.letterhead.from': 'dari',
'intelligence.memoryChunk.letterhead.to': 'ke',
- 'intelligence.memoryChunk.mentioned.chunkOne': '1 chunk',
+ 'intelligence.memoryChunk.mentioned.chunkOne': '1 potongan',
'intelligence.memoryChunk.mentioned.chunkOther': '{count} chunk',
'intelligence.memoryChunk.mentioned.heading': 'd i s e b u t k a n',
'intelligence.memoryChunk.scoreBars.ariaScore': '{name} skor {pct} persen',
@@ -113,7 +113,7 @@ const id4: TranslationMap = {
'intelligence.screenDebug.captureTest': 'Tes tangkapan',
'intelligence.screenDebug.capturing': 'Menangkap',
'intelligence.screenDebug.frames': 'Frame',
- 'intelligence.screenDebug.idle': 'Idle',
+ 'intelligence.screenDebug.idle': 'Siaga',
'intelligence.screenDebug.lastApp': 'Aplikasi Terakhir',
'intelligence.screenDebug.mode': 'Mode',
'intelligence.screenDebug.permAccessibility': 'Izin aksesibilitas',
@@ -138,7 +138,7 @@ const id4: TranslationMap = {
'intelligence.tasks.failedToLoad': 'Gagal memuat',
'intelligence.tasks.live': 'langsung',
'intelligence.tasks.loadingBoards': 'Memuat papan tugas...',
- 'intelligence.tasks.threadPrefix': 'Thread {thread}',
+ 'intelligence.tasks.threadPrefix': 'Utas {thread}',
'notifications.card.dismiss': 'Abaikan notifikasi',
'notifications.card.importanceTitle': 'Tingkat penting: {pct}%',
'notifications.center.empty': 'Belum ada notifikasi',
@@ -300,9 +300,9 @@ const id4: TranslationMap = {
'settings.ai.modelLabel': 'Model',
'settings.ai.noCustomProviders': 'Tidak ada penyedia kustom',
'settings.ai.providerLabel': 'Penyedia',
- 'settings.ai.routing': 'Routing',
+ 'settings.ai.routing': 'Perutean',
'settings.ai.routingCustom': 'Routing kustom',
- 'settings.ai.routingDefault': 'Default',
+ 'settings.ai.routingDefault': 'Bawaan',
'settings.ai.routingDesc': 'Deskripsi routing',
'settings.ai.saveChanges': 'Menyimpan...',
'settings.ai.saving': 'Menyimpan...',
@@ -326,7 +326,7 @@ const id4: TranslationMap = {
'{count} pelengkapan diterima tersimpan — digunakan untuk mempersonalisasi saran berikutnya.',
'settings.autocomplete.completionStyle.clearHistory': 'Membersihkan...',
'settings.autocomplete.completionStyle.clearing': 'Membersihkan...',
- 'settings.autocomplete.completionStyle.debounce': 'Debounce (ms)',
+ 'settings.autocomplete.completionStyle.debounce': 'Tunda input (ms)',
'settings.autocomplete.completionStyle.enabled': 'Diaktifkan',
'settings.autocomplete.completionStyle.maxChars': 'Maks Karakter',
'settings.autocomplete.completionStyle.noHistory':
diff --git a/app/src/lib/i18n/chunks/id-5.ts b/app/src/lib/i18n/chunks/id-5.ts
index 36aefb885c..fa7301197b 100644
--- a/app/src/lib/i18n/chunks/id-5.ts
+++ b/app/src/lib/i18n/chunks/id-5.ts
@@ -251,7 +251,7 @@ const id5: TranslationMap = {
'settings.memoryWindow.minimal.badge': 'Termurah',
'settings.memoryWindow.minimal.hint':
'Jendela memori terkecil. Termurah, tercepat, kontinuitas paling sedikit antar run.',
- 'settings.memoryWindow.minimal.label': 'Minimal',
+ 'settings.memoryWindow.minimal.label': 'Ringkas',
'settings.memoryWindow.title': 'Jendela memori jangka panjang',
'settings.screenIntel.permissions.accessibility': 'Aksesibilitas',
'settings.screenIntel.permissions.grantHint': 'Petunjuk izin',
@@ -326,12 +326,12 @@ const id5: TranslationMap = {
'skills.resource.preview.failed': 'Pratinjau gagal',
'skills.resource.preview.loading': 'Memuat pratinjau...',
'skills.resource.tree.empty': 'Tidak ada sumber daya bundel.',
- 'skills.search.placeholder': 'Placeholder',
+ 'skills.search.placeholder': 'Teks placeholder',
'skills.setup.autocomplete.acceptKey': 'Kunci terima',
'skills.setup.autocomplete.activeDesc': 'Deskripsi aktif',
'skills.setup.autocomplete.activeTitle': 'Auto-Complete Aktif',
'skills.setup.autocomplete.customizeSettings': 'Sesuaikan pengaturan',
- 'skills.setup.autocomplete.debounce': 'Debounce',
+ 'skills.setup.autocomplete.debounce': 'Tunda input',
'skills.setup.autocomplete.description': 'Deskripsi',
'skills.setup.autocomplete.enableBtn': 'Mengaktifkan...',
'skills.setup.autocomplete.enableError': 'Gagal mengaktifkan pelengkap otomatis',
@@ -423,7 +423,7 @@ const id5: TranslationMap = {
'webhooks.composioHistory.empty': 'Kosong',
'webhooks.composioHistory.metadataId': 'ID Metadata',
'webhooks.composioHistory.metadataUuid': 'UUID Metadata',
- 'webhooks.composioHistory.payload': 'Payload',
+ 'webhooks.composioHistory.payload': 'Muatan',
'webhooks.composioHistory.title': 'Riwayat Pemicu ComposeIO',
'webhooks.tunnels.active': 'Aktif',
'webhooks.tunnels.createFailed': 'Gagal membuat tunnel',
@@ -460,49 +460,50 @@ const id5: TranslationMap = {
'settings.localModel.status.ollamaDocs': 'Dokumentasi Ollama',
'settings.localModel.status.thenRetry':
'untuk instruksi pengaturan, lalu coba lagi setelah runtime Anda dapat dijangkau.',
- 'settings.appearance.title': 'Appearance',
- 'settings.appearance.themeHeading': 'Theme',
- 'settings.appearance.themeAria': 'Theme',
- 'settings.appearance.modeLight': 'Light',
- 'settings.appearance.modeLightDesc': 'Bright surfaces, dark text.',
- 'settings.appearance.modeDark': 'Dark',
- 'settings.appearance.modeDarkDesc': 'Dim surfaces, easier on the eyes after dusk.',
- 'settings.appearance.modeSystem': 'Match system',
- 'settings.appearance.modeSystemDesc': 'Follow your OS appearance setting.',
+ 'settings.appearance.title': 'Tampilan',
+ 'settings.appearance.themeHeading': 'Tema',
+ 'settings.appearance.themeAria': 'Tema',
+ 'settings.appearance.modeLight': 'Terang',
+ 'settings.appearance.modeLightDesc': 'Permukaan terang, teks gelap.',
+ 'settings.appearance.modeDark': 'Gelap',
+ 'settings.appearance.modeDarkDesc': 'Permukaan redup, lebih nyaman untuk malam hari.',
+ 'settings.appearance.modeSystem': 'Ikuti sistem',
+ 'settings.appearance.modeSystemDesc': 'Ikuti pengaturan tampilan OS Anda.',
'settings.appearance.helperText':
- 'Dark mode switches the entire app — chat, settings, panels — to a dim palette. "Match system" follows your OS appearance and updates live.',
- 'settings.mascot.characterPreview': 'Preview',
- 'settings.mascot.characterStates': 'states',
- 'settings.mascot.characterVisemes': 'visemes',
- 'settings.mascot.colorAria': 'OpenHuman color',
- 'settings.mascot.colorBlack': 'Black',
- 'settings.mascot.colorBurgundy': 'Burgundy',
- 'settings.mascot.colorGreen': 'Green',
- 'settings.mascot.colorNavy': 'Navy',
- 'settings.mascot.colorYellow': 'Yellow',
- 'settings.mascot.libraryUnavailable': 'OpenHuman library unavailable',
+ 'Mode gelap mengubah seluruh aplikasi - obrolan, pengaturan, dan panel - ke palet redup. "Ikuti sistem" mengikuti tampilan OS Anda dan diperbarui otomatis.',
+ 'settings.mascot.characterPreview': 'Pratinjau',
+ 'settings.mascot.characterStates': 'status',
+ 'settings.mascot.characterVisemes': 'visem',
+ 'settings.mascot.colorAria': 'Warna OpenHuman',
+ 'settings.mascot.colorBlack': 'Hitam',
+ 'settings.mascot.colorBurgundy': 'Burgundi',
+ 'settings.mascot.colorGreen': 'Hijau',
+ 'settings.mascot.colorNavy': 'Biru tua',
+ 'settings.mascot.colorYellow': 'Kuning',
+ 'settings.mascot.libraryUnavailable': 'Library OpenHuman tidak tersedia',
'settings.mascot.title': 'OpenHuman',
- 'settings.developerMenu.mcpServer.title': 'MCP Server',
- 'settings.developerMenu.mcpServer.desc': 'Configure external MCP clients to connect to OpenHuman',
- 'settings.mcpServer.title': 'MCP Server',
- 'settings.mcpServer.toolsSectionTitle': 'Available Tools',
+ 'settings.developerMenu.mcpServer.title': 'Server MCP',
+ 'settings.developerMenu.mcpServer.desc':
+ 'Konfigurasikan klien MCP eksternal untuk terhubung ke OpenHuman',
+ 'settings.mcpServer.title': 'Server MCP',
+ 'settings.mcpServer.toolsSectionTitle': 'Alat yang tersedia',
'settings.mcpServer.toolsSectionDesc':
- 'Tools exposed via the MCP stdio server when running openhuman-core mcp',
- 'settings.mcpServer.configSectionTitle': 'Client Configuration',
+ 'Alat yang diekspos melalui server stdio MCP saat menjalankan openhuman-core mcp',
+ 'settings.mcpServer.configSectionTitle': 'Konfigurasi Klien',
'settings.mcpServer.configSectionDesc':
- 'Select your MCP client to generate the correct configuration snippet',
- 'settings.mcpServer.copySnippet': 'Copy to Clipboard',
- 'settings.mcpServer.copied': 'Copied!',
- 'settings.mcpServer.openConfigFile': 'Open Config File',
+ 'Pilih klien MCP Anda untuk membuat cuplikan konfigurasi yang tepat',
+ 'settings.mcpServer.copySnippet': 'Salin ke Clipboard',
+ 'settings.mcpServer.copied': 'Tersalin!',
+ 'settings.mcpServer.openConfigFile': 'Buka File Konfigurasi',
'settings.mcpServer.binaryPathNotFound':
- 'OpenHuman binary not found. If running from source, build with: cargo build --bin openhuman-core',
- 'settings.mcpServer.openConfigError': 'Failed to open config file',
+ 'Binary OpenHuman tidak ditemukan. Jika menjalankan dari source, build dengan: cargo build --bin openhuman-core',
+ 'settings.mcpServer.openConfigError': 'Gagal membuka file konfigurasi',
'settings.mcpServer.clientClaudeDesktop': 'Claude Desktop',
'settings.mcpServer.clientCursor': 'Cursor',
'settings.mcpServer.clientCodex': 'Codex',
'settings.mcpServer.clientZed': 'Zed',
- 'settings.mcpServer.configFilePath': 'Config file',
- 'settings.mcpServer.clientSelectorAriaLabel': 'MCP client selector',
+ 'settings.mcpServer.configFilePath': 'File konfigurasi',
+ 'settings.mcpServer.clientSelectorAriaLabel': 'Pemilih klien MCP',
};
export default id5;
From 208a2acdf84df5cb9791e389d804683e692479ae Mon Sep 17 00:00:00 2001
From: JinHyuk Sung <163989462+sjh9714@users.noreply.github.com>
Date: Thu, 21 May 2026 19:49:29 +0900
Subject: [PATCH 03/67] fix(billing): hide budget-complete prompt for free
zero-budget plans (#2300)
---
app/src/hooks/useUsageState.test.ts | 4 ++--
app/src/hooks/useUsageState.ts | 10 ++++------
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/app/src/hooks/useUsageState.test.ts b/app/src/hooks/useUsageState.test.ts
index cf16453f1e..352b7c0a46 100644
--- a/app/src/hooks/useUsageState.test.ts
+++ b/app/src/hooks/useUsageState.test.ts
@@ -114,7 +114,7 @@ describe('useUsageState', () => {
mockLoadAISettings.mockResolvedValue(ALL_OPENHUMAN_AI_SETTINGS);
});
- it('does not treat free users with zero recurring budget as exhausted', async () => {
+ it('does not show the completed-budget message for free users with zero recurring budget', async () => {
const { useUsageState } = await import('./useUsageState');
mockGetCurrentPlan.mockResolvedValue(freePlan());
mockGetTeamUsage.mockResolvedValue(buildUsage());
@@ -127,7 +127,7 @@ describe('useUsageState', () => {
expect(result.current.isFreeTier).toBe(true);
expect(result.current.isBudgetExhausted).toBe(false);
- expect(result.current.shouldShowBudgetCompletedMessage).toBe(true);
+ expect(result.current.shouldShowBudgetCompletedMessage).toBe(false);
expect(result.current.isAtLimit).toBe(false);
expect(result.current.usagePct).toBe(0);
});
diff --git a/app/src/hooks/useUsageState.ts b/app/src/hooks/useUsageState.ts
index 3b4566bc2e..79d4602482 100644
--- a/app/src/hooks/useUsageState.ts
+++ b/app/src/hooks/useUsageState.ts
@@ -165,12 +165,10 @@ export function useUsageState(): UsageState {
? teamUsage.cycleBudgetUsd > 0.01 && teamUsage.remainingUsd <= 0.01
: false;
- // Some users have no included recurring budget at all. They still need the
- // completed-budget warning in chat even though they are not in an exhausted
- // paid cycle — but only when their chat actually flows through OpenHuman.
- const rawShouldShowBudgetCompletedMessage = teamUsage
- ? rawBudgetExhausted || (teamUsage.cycleBudgetUsd <= 0.01 && teamUsage.remainingUsd <= 0.01)
- : false;
+ // Only show the completed-budget warning for an actually exhausted
+ // recurring budget. Free plans with no recurring budget should not look like
+ // they have exhausted a paid/included cycle (#2129).
+ const rawShouldShowBudgetCompletedMessage = rawBudgetExhausted;
const isBudgetExhausted = !isFullyRoutedAway && rawBudgetExhausted;
const shouldShowBudgetCompletedMessage =
From 790d906b116ecad36fc72952565582c01a873adc Mon Sep 17 00:00:00 2001
From: sanil-23
Date: Thu, 21 May 2026 16:31:25 +0530
Subject: [PATCH 04/67] fix(memory_tree,sync_status,scripts): IMMEDIATE-tx
ingest, reembed skip-persistence, sidecar-based sync-status accounting,
Windows dev-script PATH (#2349)
Co-authored-by: Claude Opus 4.7 (1M context)
Co-authored-by: sanil-23
---
scripts/run-dev-win.sh | 42 ++
src/openhuman/memory/sync_status/rpc.rs | 450 ++++++++++++++----
src/openhuman/memory/tree/ingest.rs | 18 +-
.../memory/tree/jobs/handlers/mod.rs | 260 +++++++++-
src/openhuman/memory/tree/jobs/mod.rs | 16 +-
src/openhuman/memory/tree/store.rs | 85 +++-
.../memory/tree/tree_source/store.rs | 25 +
7 files changed, 786 insertions(+), 110 deletions(-)
diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh
index 8492a24da7..aa2bdcef19 100644
--- a/scripts/run-dev-win.sh
+++ b/scripts/run-dev-win.sh
@@ -488,6 +488,29 @@ if [[ -z "$PNPM_EXE" ]]; then
exit 1
fi
echo "[run-dev-win] pnpm resolved to: $PNPM_EXE"
+
+# `cargo tauri dev` runs its beforeDevCommand (`pnpm run dev`) via a native
+# `cmd /S /C` that resolves bare `pnpm` off PATH. This script otherwise only
+# ever calls pnpm by absolute path, so its dir was never on PATH and Tauri
+# dies with "'pnpm' is not recognized". Prepend the resolved pnpm's dir — it
+# ships pnpm.CMD alongside the bash shim, which cmd.exe uses.
+# Split the dirname computation out of the export so a `dirname` failure
+# surfaces with a non-zero exit (SC2155) instead of being swallowed by the
+# enclosing `export`. `dirname` on a validated absolute path is reliable
+# in practice, but the strict-mode posture is worth the extra line.
+PNPM_DIR="$(dirname "$PNPM_EXE")"
+# `dirname` returns `.` for a bare filename (e.g. if PNPM_EXE somehow
+# resolved to just "pnpm" without a path component). Prepending `.` would
+# inject the current working directory into PATH on a Windows dev machine
+# — a privilege-escalation-flavoured surprise. Skip the prepend in that
+# case (and on the also-degenerate empty result); the absolute-path call
+# sites elsewhere in this script still work.
+if [[ -n "$PNPM_DIR" && "$PNPM_DIR" != "." ]]; then
+ export PATH="$PNPM_DIR:$PATH"
+ echo "[run-dev-win] pnpm dir prepended to PATH: $PNPM_DIR"
+else
+ echo "[run-dev-win] pnpm dir not prepended to PATH (PNPM_EXE has no path component: $PNPM_EXE)"
+fi
echo "[run-dev-win] node on bash PATH: $(command -v node 2>/dev/null || echo '')"
echo "[run-dev-win] node.exe on bash PATH: $(command -v node.exe 2>/dev/null || echo '')"
@@ -576,6 +599,25 @@ else
DEV_PORT=1420
fi
+# Tauri spawns beforeDevCommand (`pnpm run dev`) via a native `cmd /S /C`
+# inheriting THIS process's env. By here PATH has the full system PATH stacked
+# several times over (vcvars rebuild + Git-Bash /etc/profile re-runs + pnpm
+# .bin layering); the MSYS→Windows conversion overflows the process
+# environment-block limit, so the child inherits an EMPTY PATH and Tauri dies
+# with "'pnpm' is not recognized" (even `where` is gone). Collapse PATH to
+# first-seen entries (clean POSIX `/c/...` entries, so ':' split is safe).
+_dedup_seen=":"
+_dedup_new=""
+IFS=':' read -ra _dedup_parts <<< "$PATH"
+for _dp in "${_dedup_parts[@]}"; do
+ [[ -z "$_dp" ]] && continue
+ case "$_dedup_seen" in *":$_dp:"*) continue ;; esac
+ _dedup_seen="${_dedup_seen}${_dp}:"
+ _dedup_new="${_dedup_new:+$_dedup_new:}$_dp"
+done
+export PATH="$_dedup_new"
+echo "[run-dev-win] PATH de-duplicated: ${#_dedup_parts[@]} → $(awk -v RS=: 'END{print NR}' <<< "$_dedup_new") entries"
+
if (( DEV_PORT != 1420 )); then
echo "[run-dev-win] OPENHUMAN_DEV_PORT=$DEV_PORT — overriding tauri devUrl"
"$PNPM_EXE" tauri dev -c "{\"build\":{\"devUrl\":\"http://localhost:$DEV_PORT\"}}"
diff --git a/src/openhuman/memory/sync_status/rpc.rs b/src/openhuman/memory/sync_status/rpc.rs
index 9c2a4f2257..9afac862ea 100644
--- a/src/openhuman/memory/sync_status/rpc.rs
+++ b/src/openhuman/memory/sync_status/rpc.rs
@@ -3,8 +3,27 @@
//! Single SQL query against `mem_tree_chunks`. Two layers of metrics:
//!
//! * **Lifetime** — `chunks_synced` (total ingested), `chunks_pending`
-//! (`embedding IS NULL` = still in the extract+embed queue, not
-//! yet appended to the source-tree buffer).
+//! (not yet *resolved* = still in the extract+embed queue, not yet
+//! appended to the source-tree buffer).
+//!
+//! A chunk is "resolved" (i.e. NOT pending) when ANY of:
+//! - it has a row in the per-(chunk,model) sidecar
+//! `mem_tree_chunk_embeddings` (#1574) — embedded under some model;
+//! - `lifecycle_status = 'dropped'` — the admission gate rejected it,
+//! so it is intentionally never embedded (terminal, not waiting);
+//! - it has a `mem_tree_chunk_reembed_skipped` tombstone (#1574 §6) —
+//! embedding failed terminally (missing body / wrong dim / embed
+//! error) and will not be retried (terminal, not waiting).
+//!
+//! NOTE: "embedded" is keyed off the sidecar table, NOT the legacy
+//! inline `mem_tree_chunks.embedding` column. The #1574 §7 migration
+//! copied every vector into the sidecar and stopped writing the inline
+//! column, so it now reads back NULL for every chunk. Keying pending /
+//! processed off the inline column made this RPC report 100% of chunks
+//! as pending and `0` processed forever, regardless of real progress.
+//! Dropped / terminally-skipped chunks have no sidecar row either, so
+//! without the extra terminal predicates they would read as pending
+//! forever and could pin a provider's progress bar below 100%.
//!
//! * **Active sync wave** — `batch_total` / `batch_processed`. The
//! wave is identified by a *time-cluster anchor*: the earliest
@@ -27,6 +46,7 @@
use crate::openhuman::config::Config;
use crate::openhuman::memory::tree::store::with_connection;
use crate::rpc::RpcOutcome;
+use rusqlite::Connection;
use super::types::{FreshnessLabel, MemorySyncStatus, StatusListResponse};
@@ -44,89 +64,8 @@ pub async fn status_list_rpc(config: &Config) -> Result = match tokio::task::spawn_blocking(move || {
with_connection(&config, |conn| -> anyhow::Result> {
- // Provider parsed from `source_id` prefix (substring before
- // first ':'); falls back to `source_kind` when no prefix.
- //
- // `provider_chunks` projects per-row provider + the columns
- // we need. `provider_pending` flags providers that still
- // have at least one chunk waiting for an embedding —
- // `wave_anchors` is gated on this so a fully-drained
- // provider gets `batch_total = batch_processed = 0` (the
- // UI then hides the progress bar instead of rendering a
- // completed one for an idle connection). `wave_anchors`
- // finds the earliest chunk within WAVE_WINDOW_MS of the
- // most recent — the wave's start. The outer SELECT joins
- // back to count both lifetime and in-wave totals.
- let mut stmt = conn.prepare(
- "WITH provider_chunks AS ( \
- SELECT \
- CASE \
- WHEN INSTR(source_id, ':') > 0 \
- THEN SUBSTR(source_id, 1, INSTR(source_id, ':') - 1) \
- ELSE source_kind \
- END AS provider, \
- created_at_ms, \
- embedding, \
- timestamp_ms \
- FROM mem_tree_chunks \
- ), \
- provider_max AS ( \
- SELECT provider, MAX(created_at_ms) AS max_created \
- FROM provider_chunks \
- GROUP BY provider \
- ), \
- provider_pending AS ( \
- SELECT provider, \
- SUM(CASE WHEN embedding IS NULL THEN 1 ELSE 0 END) AS pending \
- FROM provider_chunks \
- GROUP BY provider \
- ), \
- wave_anchors AS ( \
- SELECT p.provider, MIN(p.created_at_ms) AS anchor \
- FROM provider_chunks p \
- JOIN provider_max m ON p.provider = m.provider \
- JOIN provider_pending pp ON p.provider = pp.provider \
- WHERE pp.pending > 0 \
- AND p.created_at_ms >= m.max_created - ?1 \
- GROUP BY p.provider \
- ) \
- SELECT \
- p.provider, \
- COUNT(*) AS chunks_synced, \
- SUM(CASE WHEN p.embedding IS NULL THEN 1 ELSE 0 END) AS chunks_pending, \
- SUM(CASE WHEN w.anchor IS NOT NULL \
- AND p.created_at_ms >= w.anchor \
- THEN 1 ELSE 0 END) AS batch_total, \
- SUM(CASE WHEN w.anchor IS NOT NULL \
- AND p.created_at_ms >= w.anchor \
- AND p.embedding IS NOT NULL \
- THEN 1 ELSE 0 END) AS batch_processed, \
- MAX(p.timestamp_ms) AS last_chunk_at_ms \
- FROM provider_chunks p \
- LEFT JOIN wave_anchors w ON p.provider = w.provider \
- GROUP BY p.provider \
- ORDER BY last_chunk_at_ms DESC",
- )?;
let now_ms = chrono::Utc::now().timestamp_millis();
- let iter = stmt.query_map([WAVE_WINDOW_MS], |row| {
- let provider: String = row.get(0)?;
- let chunks_synced: i64 = row.get(1)?;
- let chunks_pending: i64 = row.get(2)?;
- let batch_total: i64 = row.get(3)?;
- let batch_processed: i64 = row.get(4)?;
- let last_chunk_at_ms: Option = row.get(5)?;
- Ok(MemorySyncStatus {
- provider,
- chunks_synced: chunks_synced.max(0) as u64,
- chunks_pending: chunks_pending.max(0) as u64,
- batch_total: batch_total.max(0) as u64,
- batch_processed: batch_processed.max(0) as u64,
- last_chunk_at_ms,
- freshness: FreshnessLabel::from_age_ms(last_chunk_at_ms, now_ms),
- })
- })?;
- let out = iter.collect::, _>>()?;
- Ok(out)
+ Ok(query_sync_statuses(conn, now_ms)?)
})
})
.await
@@ -159,6 +98,111 @@ pub async fn status_list_rpc(config: &Config) -> Result rusqlite::Result> {
+ // Provider parsed from `source_id` prefix (substring before first ':');
+ // falls back to `source_kind` when no prefix.
+ //
+ // `provider_chunks` projects per-row provider + a `resolved` flag (embedded
+ // OR dropped OR terminally skipped). `provider_pending` flags providers with
+ // at least one unresolved chunk *inside the wave window* (within
+ // WAVE_WINDOW_MS of the provider's most recent chunk) — `wave_anchors` is
+ // gated on this, so a stale unresolved chunk from an older wave can't
+ // resurrect an "active" wave when the recent chunks are all resolved, and a
+ // fully-drained provider gets `batch_total = batch_processed = 0` (the UI
+ // then hides the progress bar instead of rendering a completed one for an
+ // idle connection). `wave_anchors` finds the earliest chunk within
+ // WAVE_WINDOW_MS of the most recent — the wave's start. The outer SELECT
+ // joins back to count both lifetime and in-wave totals.
+ let mut stmt = conn.prepare(
+ "WITH provider_chunks AS ( \
+ SELECT \
+ CASE \
+ WHEN INSTR(source_id, ':') > 0 \
+ THEN SUBSTR(source_id, 1, INSTR(source_id, ':') - 1) \
+ ELSE source_kind \
+ END AS provider, \
+ created_at_ms, \
+ CASE WHEN EXISTS ( \
+ SELECT 1 FROM mem_tree_chunk_embeddings e \
+ WHERE e.chunk_id = c.id \
+ ) \
+ OR c.lifecycle_status = 'dropped' \
+ OR EXISTS ( \
+ SELECT 1 FROM mem_tree_chunk_reembed_skipped s \
+ WHERE s.chunk_id = c.id \
+ ) THEN 1 ELSE 0 END AS resolved, \
+ timestamp_ms \
+ FROM mem_tree_chunks c \
+ ), \
+ provider_max AS ( \
+ SELECT provider, MAX(created_at_ms) AS max_created \
+ FROM provider_chunks \
+ GROUP BY provider \
+ ), \
+ provider_pending AS ( \
+ SELECT p.provider, \
+ SUM(CASE WHEN p.resolved = 0 \
+ AND p.created_at_ms >= m.max_created - ?1 \
+ THEN 1 ELSE 0 END) AS pending \
+ FROM provider_chunks p \
+ JOIN provider_max m ON p.provider = m.provider \
+ GROUP BY p.provider \
+ ), \
+ wave_anchors AS ( \
+ SELECT p.provider, MIN(p.created_at_ms) AS anchor \
+ FROM provider_chunks p \
+ JOIN provider_max m ON p.provider = m.provider \
+ JOIN provider_pending pp ON p.provider = pp.provider \
+ WHERE pp.pending > 0 \
+ AND p.created_at_ms >= m.max_created - ?1 \
+ GROUP BY p.provider \
+ ) \
+ SELECT \
+ p.provider, \
+ COUNT(*) AS chunks_synced, \
+ SUM(CASE WHEN p.resolved = 0 THEN 1 ELSE 0 END) AS chunks_pending, \
+ SUM(CASE WHEN w.anchor IS NOT NULL \
+ AND p.created_at_ms >= w.anchor \
+ THEN 1 ELSE 0 END) AS batch_total, \
+ SUM(CASE WHEN w.anchor IS NOT NULL \
+ AND p.created_at_ms >= w.anchor \
+ AND p.resolved = 1 \
+ THEN 1 ELSE 0 END) AS batch_processed, \
+ MAX(p.timestamp_ms) AS last_chunk_at_ms \
+ FROM provider_chunks p \
+ LEFT JOIN wave_anchors w ON p.provider = w.provider \
+ GROUP BY p.provider \
+ ORDER BY last_chunk_at_ms DESC",
+ )?;
+ let iter = stmt.query_map([WAVE_WINDOW_MS], |row| {
+ let provider: String = row.get(0)?;
+ let chunks_synced: i64 = row.get(1)?;
+ let chunks_pending: i64 = row.get(2)?;
+ let batch_total: i64 = row.get(3)?;
+ let batch_processed: i64 = row.get(4)?;
+ let last_chunk_at_ms: Option = row.get(5)?;
+ Ok(MemorySyncStatus {
+ provider,
+ chunks_synced: chunks_synced.max(0) as u64,
+ chunks_pending: chunks_pending.max(0) as u64,
+ batch_total: batch_total.max(0) as u64,
+ batch_processed: batch_processed.max(0) as u64,
+ last_chunk_at_ms,
+ freshness: FreshnessLabel::from_age_ms(last_chunk_at_ms, now_ms),
+ })
+ })?;
+ iter.collect()
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -195,4 +239,242 @@ mod tests {
assert!(json.get("result").is_none(), "must not be double-wrapped");
assert!(json.get("logs").is_none(), "must not be double-wrapped");
}
+
+ /// Regression for the legacy-column bug: pending / processed must be
+ /// derived from the `mem_tree_chunk_embeddings` sidecar, not the inline
+ /// `mem_tree_chunks.embedding` column (which is always NULL post-#1574).
+ /// A chunk with a sidecar row counts as processed even though its inline
+ /// column is NULL.
+ #[test]
+ fn pending_and_processed_key_off_sidecar_not_inline_column() {
+ use crate::openhuman::memory::tree::store::with_connection;
+ use rusqlite::params;
+ use tempfile::TempDir;
+
+ let tmp = TempDir::new().expect("tempdir");
+ let mut cfg = Config::default();
+ cfg.workspace_dir = tmp.path().to_path_buf();
+
+ let now = chrono::Utc::now().timestamp_millis();
+
+ with_connection(&cfg, |conn| {
+ let insert_chunk = |id: &str, source_id: &str, created: i64| {
+ conn.execute(
+ "INSERT INTO mem_tree_chunks \
+ (id, source_kind, source_id, owner, timestamp_ms, \
+ time_range_start_ms, time_range_end_ms, content, \
+ token_count, seq_in_source, created_at_ms) \
+ VALUES (?1, 'email', ?2, 'me@x.com', ?3, ?3, ?3, 'body', 10, 0, ?3)",
+ params![id, source_id, created],
+ )
+ .unwrap();
+ };
+ let embed = |id: &str| {
+ conn.execute(
+ "INSERT INTO mem_tree_chunk_embeddings \
+ (chunk_id, model_signature, vector, dim, created_at) \
+ VALUES (?1, 'sig', X'00000000', 1, 0.0)",
+ params![id],
+ )
+ .unwrap();
+ };
+
+ // gmail: 3 chunks inside the active wave; 2 embedded (sidecar), 1 not.
+ insert_chunk("g1", "gmail:acct", now - 1_000);
+ insert_chunk("g2", "gmail:acct", now - 2_000);
+ insert_chunk("g3", "gmail:acct", now - 3_000);
+ embed("g1");
+ embed("g2");
+
+ let statuses = query_sync_statuses(conn, now).unwrap();
+ let gmail = statuses
+ .iter()
+ .find(|s| s.provider == "gmail")
+ .expect("gmail provider row");
+
+ assert_eq!(gmail.chunks_synced, 3, "all three ingested");
+ assert_eq!(
+ gmail.chunks_pending, 1,
+ "only g3 lacks a sidecar embedding (inline column is NULL for all)"
+ );
+ assert_eq!(gmail.batch_total, 3, "all three are within the wave window");
+ assert_eq!(
+ gmail.batch_processed, 2,
+ "g1 and g2 have sidecar rows, so they count as processed"
+ );
+ Ok(())
+ })
+ .unwrap();
+ }
+
+ /// A provider with every chunk embedded must report zero wave (the UI
+ /// hides the progress bar): `batch_total = batch_processed = 0`.
+ #[test]
+ fn fully_embedded_provider_reports_no_active_wave() {
+ use crate::openhuman::memory::tree::store::with_connection;
+ use rusqlite::params;
+ use tempfile::TempDir;
+
+ let tmp = TempDir::new().expect("tempdir");
+ let mut cfg = Config::default();
+ cfg.workspace_dir = tmp.path().to_path_buf();
+ let now = chrono::Utc::now().timestamp_millis();
+
+ with_connection(&cfg, |conn| {
+ conn.execute(
+ "INSERT INTO mem_tree_chunks \
+ (id, source_kind, source_id, owner, timestamp_ms, \
+ time_range_start_ms, time_range_end_ms, content, \
+ token_count, seq_in_source, created_at_ms) \
+ VALUES ('s1', 'slack', 'slack:eng', 'me@x.com', ?1, ?1, ?1, 'b', 10, 0, ?1)",
+ params![now - 5_000],
+ )
+ .unwrap();
+ conn.execute(
+ "INSERT INTO mem_tree_chunk_embeddings \
+ (chunk_id, model_signature, vector, dim, created_at) \
+ VALUES ('s1', 'sig', X'00000000', 1, 0.0)",
+ [],
+ )
+ .unwrap();
+
+ let statuses = query_sync_statuses(conn, now).unwrap();
+ let slack = statuses
+ .iter()
+ .find(|s| s.provider == "slack")
+ .expect("slack provider row");
+ assert_eq!(slack.chunks_pending, 0);
+ assert_eq!(slack.batch_total, 0, "no pending chunks ⇒ no active wave");
+ assert_eq!(slack.batch_processed, 0);
+ Ok(())
+ })
+ .unwrap();
+ }
+
+ /// Terminal-but-unembedded chunks must not read as perpetually pending:
+ /// a `dropped` chunk (admission-rejected) and a `reembed_skipped`
+ /// tombstoned chunk both count as resolved even with no sidecar row, so a
+ /// provider whose only leftovers are terminal drains to 0 pending / no wave.
+ #[test]
+ fn dropped_and_skipped_chunks_count_as_resolved_not_pending() {
+ use crate::openhuman::memory::tree::store::with_connection;
+ use rusqlite::params;
+ use tempfile::TempDir;
+
+ let tmp = TempDir::new().expect("tempdir");
+ let mut cfg = Config::default();
+ cfg.workspace_dir = tmp.path().to_path_buf();
+ let now = chrono::Utc::now().timestamp_millis();
+
+ with_connection(&cfg, |conn| {
+ let insert = |id: &str, lifecycle: &str, created: i64| {
+ conn.execute(
+ "INSERT INTO mem_tree_chunks \
+ (id, source_kind, source_id, owner, timestamp_ms, \
+ time_range_start_ms, time_range_end_ms, content, \
+ token_count, seq_in_source, created_at_ms, lifecycle_status) \
+ VALUES (?1, 'slack', 'slack:eng', 'me@x.com', ?2, ?2, ?2, 'b', 10, 0, ?2, ?3)",
+ params![id, created, lifecycle],
+ )
+ .unwrap();
+ };
+
+ // d1: gate-dropped (no embedding, never will be).
+ insert("d1", "dropped", now - 4_000);
+ // sk1: pending_extraction but terminally tombstoned (e.g. body missing).
+ insert("sk1", "pending_extraction", now - 3_000);
+ conn.execute(
+ "INSERT INTO mem_tree_chunk_reembed_skipped \
+ (chunk_id, model_signature, reason, skipped_at_ms) \
+ VALUES ('sk1', 'sig', 'body read failed', ?1)",
+ params![now - 2_000],
+ )
+ .unwrap();
+ // p1: genuinely still in the queue (no embedding, no terminal marker).
+ insert("p1", "pending_extraction", now - 1_000);
+
+ let statuses = query_sync_statuses(conn, now).unwrap();
+ let slack = statuses
+ .iter()
+ .find(|s| s.provider == "slack")
+ .expect("slack provider row");
+
+ assert_eq!(slack.chunks_synced, 3, "all three ingested");
+ assert_eq!(
+ slack.chunks_pending, 1,
+ "only p1 is genuinely pending; d1 (dropped) and sk1 (skipped) are terminal"
+ );
+ // p1 keeps the wave alive; d1+sk1 are in-window but resolved.
+ assert_eq!(slack.batch_total, 3, "all within the wave window");
+ assert_eq!(
+ slack.batch_processed, 2,
+ "d1 and sk1 count as resolved; p1 does not"
+ );
+ Ok(())
+ })
+ .unwrap();
+ }
+
+ /// The active wave must be gated on an unresolved chunk *inside the window*.
+ /// A stale unresolved chunk from an older wave plus a fully-resolved recent
+ /// chunk must NOT resurrect an active wave (no bogus 100%-complete bar):
+ /// `batch_total = batch_processed = 0`, while lifetime `chunks_pending`
+ /// still reflects the old straggler.
+ #[test]
+ fn stale_out_of_window_pending_does_not_open_a_wave() {
+ use crate::openhuman::memory::tree::store::with_connection;
+ use rusqlite::params;
+ use tempfile::TempDir;
+
+ let tmp = TempDir::new().expect("tempdir");
+ let mut cfg = Config::default();
+ cfg.workspace_dir = tmp.path().to_path_buf();
+ let now = chrono::Utc::now().timestamp_millis();
+ // WAVE_WINDOW_MS is 10 min; place the straggler well outside it.
+ let old = now - 30 * 60 * 1000;
+
+ with_connection(&cfg, |conn| {
+ let insert = |id: &str, created: i64| {
+ conn.execute(
+ "INSERT INTO mem_tree_chunks \
+ (id, source_kind, source_id, owner, timestamp_ms, \
+ time_range_start_ms, time_range_end_ms, content, \
+ token_count, seq_in_source, created_at_ms) \
+ VALUES (?1, 'gmail', 'gmail:acct', 'me@x.com', ?2, ?2, ?2, 'b', 10, 0, ?2)",
+ params![id, created],
+ )
+ .unwrap();
+ };
+
+ // old straggler: unresolved, 30 min ago (outside the wave window).
+ insert("old1", old);
+ // recent: resolved (embedded), inside the window.
+ insert("new1", now - 1_000);
+ conn.execute(
+ "INSERT INTO mem_tree_chunk_embeddings \
+ (chunk_id, model_signature, vector, dim, created_at) \
+ VALUES ('new1', 'sig', X'00000000', 1, 0.0)",
+ [],
+ )
+ .unwrap();
+
+ let statuses = query_sync_statuses(conn, now).unwrap();
+ let gmail = statuses
+ .iter()
+ .find(|s| s.provider == "gmail")
+ .expect("gmail provider row");
+
+ assert_eq!(
+ gmail.chunks_pending, 1,
+ "the old straggler is still pending lifetime-wise"
+ );
+ assert_eq!(
+ gmail.batch_total, 0,
+ "no unresolved chunk inside the window ⇒ no active wave"
+ );
+ assert_eq!(gmail.batch_processed, 0);
+ Ok(())
+ })
+ .unwrap();
+ }
}
diff --git a/src/openhuman/memory/tree/ingest.rs b/src/openhuman/memory/tree/ingest.rs
index 5bd08be50b..e026c06ca3 100644
--- a/src/openhuman/memory/tree/ingest.rs
+++ b/src/openhuman/memory/tree/ingest.rs
@@ -227,7 +227,23 @@ async fn persist(
let written = tokio::task::spawn_blocking(move || -> Result