feat(v9): deterministischer Memory-Kurator (Dedupe) statt LLM-Cron

- routers/memory.py: POST /api/memory/dedupe — findet Dubletten (exakt/
  enthalten/ähnlich via SequenceMatcher) je Kategorie, behält den
  vollständigsten Eintrag. Dry-Run (apply:false) + Anwenden. Kein LLM.
- MemoryPanel: "🧹 Aufräumen"-Button (Vorschau → Bestätigung → löschen).

Grund: LLM-Cron-Kurator hat Edits nur halluziniert/als Text ausgegeben
statt auszuführen (Hermes 4 + Qwen). Deterministisch = verlässlich.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-24 17:49:18 +02:00
parent 0c7f3104d4
commit dd2938ee53
3 changed files with 123 additions and 34 deletions
+23
View File
@@ -103,6 +103,26 @@
editId = m.id; editContent = m.content; editCategory = m.category
}
// Deterministischer Kurator: Dubletten (exakt/enthalten/ähnlich) je Kategorie zusammenführen
let deduping = $state(false)
async function dedupe() {
deduping = true
try {
const preview = await api('/api/memory/dedupe', { method: 'POST', body: JSON.stringify({ apply: false }) })
if (preview.duplicate_count === 0) { toast('Keine Dubletten gefunden 🎉'); return }
const ok = await confirmModal({
title: `${preview.duplicate_count} Dubletten entfernen?`,
body: `${preview.duplicate_count} doppelte Einträge werden gelöscht; die ${preview.groups.length} vollständigeren bleiben erhalten. Nur exakte/enthaltene/sehr ähnliche Einträge derselben Kategorie.`,
danger: true,
})
if (!ok) return
const res = await api('/api/memory/dedupe', { method: 'POST', body: JSON.stringify({ apply: true }) })
toast(`${res.removed} Dubletten entfernt`)
await load()
} catch (e: any) { toast('Fehler: ' + e.message, true) }
finally { deduping = false }
}
async function runImport() {
if (importLines.length === 0) return
importBusy = true
@@ -132,6 +152,9 @@
<div class="sub">Fakten, Entscheidungen und Kontext — geteilt von allen KI-Tools via MCP.</div>
</div>
<div class="flex gap-2" style="flex-shrink:0;margin-top:4px">
<button class="ghost" onclick={dedupe} disabled={deduping} title="Doppelte Einträge zusammenführen">
{deduping ? '…' : '🧹 Aufräumen'}
</button>
<button class="ghost" onclick={() => { importing = !importing; adding = false; importText = '' }}>
{importing ? 'Abbrechen' : '⇩ Import'}
</button>