mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Trois apports repris d'AJEAN 0.12.9 → 0.13.5, adaptés au fork. MÉMOIRE LONGUE DE LA CONVERSATION (chat_recall.go) Le compactage résumait, donc perdait. Chaque gros bloc du torse est désormais ARCHIVÉ verbatim sous un identifiant court (r7…) dans bbolt AVANT d'être résumé ; le résumé cite l'id, et le modèle le rappelle avec recall(id) ou le retrouve par mots-clés avec recall_search. Le contexte reste plat, l'archive grossit sur disque. En mode code, un fichier lu ou un diff produit tôt dans la session n'est plus perdu au compactage suivant. Au passage : garde-fou anti-résumé-dégénéré, budget de résumé indexé sur la fenêtre, queue ramenée à 20 % (compacter plus large ne coûte plus de perte), et fin de l'épinglage du 1er message user — le modèle répondait à l'ancienne demande au lieu de continuer la tâche en cours. PROJETS (projects.go) Un projet cloisonne une mémoire, ses discussions et ses trackers. La couture est memoryDir(), qui pointe sur le projet actif : tout le code mémoire en hérite sans le savoir. Migration automatique au premier démarrage (memory/*.md → memory/generale/, discussions et tâches orphelines rattachées). Une tâche planifiée vise un projet et l'exécution le force, pour qu'une veille n'écrive pas dans la mémoire du chantier affiché à l'écran. TRACKERS (tracker.go) 3e type de mémoire : les données datées qui s'accumulent. On ne les lit jamais en entier — consultation par niveaux (vue d'ensemble → année → mois → événements), et la dernière valeur de chaque tracker est donnée d'emblée au modèle, qui répond sans appeler l'outil. Aussi : index MEMORY.md tenu par le CODE et injecté en tête de conversation avec la description du projet (le modèle ne peut plus le désynchroniser) ; prompt système rattaché au PRESET et non plus global — stocké en base, pas dans le .env, qui ne saurait pas porter un texte multiligne. Deux correctifs du lot précédent voyagent ici, faute de pouvoir séparer les fichiers : msgText signale la présence d'une image au compactage, et le garde-fou « pensé sans agir » passe à deux relances (nudgeCount/maxNudges). Le paquet UI est regénéré dans le commit suivant, qui touche les mêmes sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
153 lines
4.7 KiB
Go
153 lines
4.7 KiB
Go
package loki
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// L'index est tenu par le CODE : créer une page l'y inscrit, la supprimer l'en
|
|
// retire. C'est tout l'intérêt — un modèle qui oublie de tenir son index ne peut
|
|
// plus le désynchroniser.
|
|
func TestMemIndexSuitLesPages(t *testing.T) {
|
|
newProjectHome(t)
|
|
ensureDefaultProject()
|
|
|
|
if err := MemAdd("docker.md", "# Notes Docker\n\ncontenu"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
idx := MemContent(memIndexFile)
|
|
if !strings.Contains(idx, "](docker.md)") {
|
|
t.Fatalf("page créée absente de l'index :\n%s", idx)
|
|
}
|
|
if !strings.Contains(idx, "Notes Docker") {
|
|
t.Fatalf("l'index doit porter le titre de la page :\n%s", idx)
|
|
}
|
|
|
|
if err := MemDelete("docker.md"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if idx := MemContent(memIndexFile); strings.Contains(idx, "](docker.md)") {
|
|
t.Fatalf("page supprimée toujours indexée — l'IA la chercherait en vain :\n%s", idx)
|
|
}
|
|
}
|
|
|
|
// L'accroche écrite après le titre appartient au MODÈLE : réenregistrer la page
|
|
// ne doit pas l'effacer, sinon tout ce qu'il ajoute disparaît à la prochaine
|
|
// modification.
|
|
func TestMemIndexPreserveLAccroche(t *testing.T) {
|
|
newProjectHome(t)
|
|
ensureDefaultProject()
|
|
if err := MemAdd("nas.md", "# NAS\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Le modèle enrichit la ligne d'index.
|
|
enriched := strings.Replace(MemContent(memIndexFile), "- [NAS](nas.md)", "- [NAS](nas.md) — adresses IP et partages", 1)
|
|
if err := MemSave(memIndexFile, "", enriched); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Une réconciliation ne doit pas repasser dessus.
|
|
reconcileMemIndex()
|
|
memIndexAdd("nas.md")
|
|
|
|
idx := MemContent(memIndexFile)
|
|
if !strings.Contains(idx, "adresses IP et partages") {
|
|
t.Fatalf("l'accroche du modèle a été écrasée :\n%s", idx)
|
|
}
|
|
if strings.Count(idx, "](nas.md)") != 1 {
|
|
t.Fatalf("ligne dupliquée pour la même page :\n%s", idx)
|
|
}
|
|
}
|
|
|
|
// Un nom qui est le SUFFIXE d'un autre ne doit pas emporter sa ligne : c'est ce
|
|
// que garantit le motif `](fichier.md)` plutôt qu'une simple recherche du nom.
|
|
func TestMemIndexNeConfondPasLesNomsProches(t *testing.T) {
|
|
newProjectHome(t)
|
|
ensureDefaultProject()
|
|
if err := MemAdd("notes.md", "# Notes\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MemAdd("mes-notes.md", "# Mes notes\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MemDelete("notes.md"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
idx := MemContent(memIndexFile)
|
|
if !strings.Contains(idx, "](mes-notes.md)") {
|
|
t.Fatalf("la suppression de notes.md a emporté mes-notes.md :\n%s", idx)
|
|
}
|
|
if strings.Contains(idx, "](notes.md)") {
|
|
t.Fatalf("notes.md est toujours indexée :\n%s", idx)
|
|
}
|
|
}
|
|
|
|
// L'index n'est injecté qu'en mode mémoire proactif : en « sur demande » ou
|
|
// « désactivée », le lister à chaque tour contredirait le réglage (et coûterait
|
|
// du contexte pour rien).
|
|
func TestMemIndexMessageSuitLeModeMemoire(t *testing.T) {
|
|
newProjectHome(t)
|
|
ensureDefaultProject()
|
|
if err := MemAdd("x.md", "# X\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := setMemMode(MemAlways); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, ok := memIndexMessage()
|
|
if !ok {
|
|
t.Fatal("aucun index injecté en mode auto")
|
|
}
|
|
if s, _ := m.Content.(string); !strings.HasPrefix(s, memIndexPrefix) || !strings.Contains(s, "](x.md)") {
|
|
t.Fatalf("message d'index mal formé : %q", s)
|
|
}
|
|
|
|
for _, mode := range []MemMode{MemOnDemand, MemOff} {
|
|
if err := setMemMode(mode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := memIndexMessage(); ok {
|
|
t.Fatalf("index injecté en mode %q", mode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// projectSystemMessages est le point d'entrée unique : il doit poser le contexte
|
|
// projet AVANT l'index mémoire (on situe le chantier, puis on liste), et ne rien
|
|
// produire quand il n'y a rien à dire.
|
|
func TestProjectSystemMessagesOrdreEtVide(t *testing.T) {
|
|
newProjectHome(t)
|
|
ensureDefaultProject()
|
|
if err := setMemMode(MemAlways); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if msgs := projectSystemMessages(); len(msgs) != 0 {
|
|
t.Fatalf("%d message(s) alors qu'il n'y a ni description, ni page, ni tracker", len(msgs))
|
|
}
|
|
|
|
if err := setProjectDesc(defaultProjectSlug, "le NAS"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := MemAdd("x.md", "# X\n"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgs := projectSystemMessages()
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("%d message(s), attendu 2 (contexte + index)", len(msgs))
|
|
}
|
|
first, _ := msgs[0].Content.(string)
|
|
second, _ := msgs[1].Content.(string)
|
|
if !strings.HasPrefix(first, projectContextPrefix) {
|
|
t.Fatalf("le contexte projet doit venir en premier : %q", first)
|
|
}
|
|
if !strings.HasPrefix(second, memIndexPrefix) {
|
|
t.Fatalf("l'index mémoire doit suivre le contexte : %q", second)
|
|
}
|
|
for _, m := range msgs {
|
|
if m.Role != "system" {
|
|
t.Fatalf("rôle %q : ces messages doivent être fusionnables en tête par normalizeSystemMessages", m.Role)
|
|
}
|
|
}
|
|
}
|