From 90d6dd4a9ae6dbb40f75b9b033042c08d7f05535 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 04:29:53 +0000 Subject: [PATCH] =?UTF-8?q?Lot=2040=20:=20outils=20m=C3=A9moire=20de=20l'a?= =?UTF-8?q?gent=20ex=C3=A9cut=C3=A9s=20par=20la=20fa=C3=A7ade?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand la façade tient la mémoire (LOKI_MEM=rust), les outils mem_search, mem_read, mem_add, mem_edit et tracker de ses tours s'exécutent chez elle (src/memory/tools.rs, port de chat_memory.go et de l'outil de tracker.go) : mêmes résultats et diffs, mêmes lectures et écritures que les routes de la mémoire (scellées selon le chiffrement, .bak, index tenu par le code, seed au nom du projet). Recherche classée par couverture puis pertinence, extrait coupé à l'octet comme le Go ; trackers vus niveau par niveau. ChatShared porte la même mémoire que les routes ; tracker_add prend l'heure en paramètre (tracker_add_at). Côté Go, l'heure des trackers passe par trackerNow, que le journal fixe. Parité : journal de 57 appels tiré du Go (parity_mem_tools_test.go → tests/parity_mem/outils.json : résultats, diffs, fichiers de la mémoire et trackers), rejoué par tests/memory.rs ; un tour de la façade qui crée une page (tests/lot6.rs). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv --- README.md | 2 +- ...t-migration-lot40-outils-memoire-design.md | 87 + internal/loki/chat_memory.go | 4 + internal/loki/parity_mem_tools_test.go | 222 ++ internal/loki/tracker.go | 8 +- rust/loki-rs/src/chat/generate.rs | 1 + rust/loki-rs/src/chat/host.rs | 25 +- rust/loki-rs/src/main.rs | 29 +- rust/loki-rs/src/memory.rs | 18 +- rust/loki-rs/src/memory/tools.rs | 692 +++++ rust/loki-rs/tests/common/fakego.rs | 1 + rust/loki-rs/tests/lot6.rs | 38 + rust/loki-rs/tests/memory.rs | 146 ++ rust/loki-rs/tests/parity_mem/outils.json | 2319 +++++++++++++++++ 14 files changed, 3578 insertions(+), 14 deletions(-) create mode 100644 docs/superpowers/specs/2026-10-16-rust-migration-lot40-outils-memoire-design.md create mode 100644 internal/loki/parity_mem_tools_test.go create mode 100644 rust/loki-rs/src/memory/tools.rs create mode 100644 rust/loki-rs/tests/parity_mem/outils.json diff --git a/README.md b/README.md index 509e303..cbdd381 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ redémarrages (volume `/data`). Variables d'environnement du conteneur : | `LOKI_SETTINGS` | `rust` : les réglages partagés — préférences d'affichage, prompt système, intensité du raisonnement, adresse publique — sont servis par la façade Rust (expérimental, migration en cours) | Go | | `LOKI_FILES` | `rust` : les fichiers de la discussion — dépôt, panneau Fichiers, archive .zip, téléchargement, images et vignettes — sont servis par la façade Rust (expérimental, migration en cours) | Go | | `LOKI_PUSH` | `rust` : les notifications Web Push (clé VAPID, abonnements, envoi « réponse prête ») sont tenues par la façade Rust (expérimental, migration en cours) | Go | -| `LOKI_MEM` | `rust` : la mémoire (éditeur des pages, index, déplacement entre projets, trackers, mode mémoire, interrupteurs du mode agent) est tenue par la façade Rust ; le chiffrement reste au Go (expérimental, migration en cours) | Go | +| `LOKI_MEM` | `rust` : la mémoire (éditeur des pages, index, déplacement entre projets, trackers, mode mémoire, interrupteurs du mode agent), et les outils mémoire de l'agent (mem_search, mem_read, mem_add, mem_edit, tracker) dans les tours de la façade, sont tenus par la façade Rust ; le chiffrement reste au Go (expérimental, migration en cours) | Go | | `LOKI_SERVE` | `rust` : la ligne de commande de llama-server est composée par la façade Rust (`loki-rs engine serve`) au lieu de `loki serve` ; Strata et l'optimiseur restent au Go (expérimental, migration en cours) | Go | | `LOKI_PRESETS` | `rust` : les presets (liste, éditeur et aperçus, suppression, placement auto, presets externes, bascule) sont tenus par la façade Rust ; l'application au moteur reste au Go (expérimental, migration en cours) | Go | | `LOKI_UPDATE` | `rust` : la mise à jour de Loki (`/api/update`, `/api/update/apply`) est servie par la façade Rust ; en conteneur, l'image se met à jour par Docker (expérimental, migration en cours) | Go | diff --git a/docs/superpowers/specs/2026-10-16-rust-migration-lot40-outils-memoire-design.md b/docs/superpowers/specs/2026-10-16-rust-migration-lot40-outils-memoire-design.md new file mode 100644 index 0000000..de22f16 --- /dev/null +++ b/docs/superpowers/specs/2026-10-16-rust-migration-lot40-outils-memoire-design.md @@ -0,0 +1,87 @@ +# Lot 40 — Outils mémoire de l'agent dans la façade + +## Contexte + +Depuis le lot 13, la façade tient la mémoire quand `LOKI_MEM=rust` : + +- pages ; +- index `MEMORY.md` ; +- trackers ; +- mode mémoire. + +Les outils par lesquels le modèle s'en sert partaient pourtant au Go +(`/internal/v1/tool/exec`) : + +- `mem_search`, `mem_read`, `mem_add`, `mem_edit` ; +- `tracker`. + +Deux processus écrivaient donc les mêmes pages et le même index. + +## Le lot + +`src/memory/tools.rs` porte les outils (`chat_memory.go`, `tracker.go`), +mêmes résultats au caractère près. Il réutilise les lectures et écritures des +routes de la mémoire : + +- pages scellées selon le chiffrement ; +- `.bak` de la version précédente ; +- écriture vérifiée ; +- index tenu par le code. + +Chaque outil : + +- **`mem_search`** : classement par couverture des termes puis par + pertinence (rareté IDF, fréquence amortie, bonus du nom et du titre) ; + extrait coupé à l'octet comme le Go (U+FFFD par octet d'un caractère + coupé) ; limite 8 par défaut, 30 au plus ; +- **`mem_read`** : lignes numérotées, plage, 500 au plus ; +- **`mem_add`** : jamais par-dessus une page existante, index tenu (seed au + nom du projet), diff d'ajout ; +- **`mem_edit`** : `old` unique, édition déjà en place reconnue, diff ; +- **`tracker`** : + - vue d'ensemble ; + - squelette (années, année en cours par mois) ; + - une année par mois ; + - points (60 plus récents au plus) ; + - ajout, modification, suppression d'un point (jamais d'un tracker + entier) ; + - arguments d'un autre type rendus comme `str` du Go. + +`tracker_add` prend désormais l'heure en paramètre (`tracker_add_at`). + +`ChatShared.memory` porte la même mémoire que les routes. `GoTurnHost::exec_tool` +y exécute les outils quand la façade la tient ; sinon l'appel part au Go +comme avant. + +Côté Go, l'heure des trackers (« maintenant », année en cours) passe par +`trackerNow`, que le journal fixe ; rien ne change en service. + +## Parité + +`internal/loki/parity_mem_tools_test.go` tire du Go un journal de 57 appels +(`tests/parity_mem/outils.json`), en fuseau UTC et à heure fixée. Chaque +étape porte : + +- les arguments ; +- le libellé du tour ; +- le résultat, le diff ; +- les fichiers de la mémoire du projet (`.bak` et index compris) ; +- les trackers en base. + +Il couvre : + +- recherche : couverture, rareté, requête vide, limites, extrait coupé ; +- lecture : plage, au-delà, page absente, nom invalide ; +- ajout : existant, nom invalide, majuscules ; +- modification : rejouée, déjà en place, absente, en double, vide ; +- trackers : chaque vue, dates illisibles, nom en nombre, identifiants + inconnus. + +`tests/memory.rs` (`outils_memoire_comme_le_go`) le rejoue contre le port. + +`tests/lot6.rs` (`outil_memoire_par_la_facade`) fait tourner un tour de la +façade qui crée une page. Il vérifie : + +- rien ne passe par le Go ; +- la page est rangée dans le projet actif, l'index tenu ; +- le résultat est celui que le modèle reçoit. diff --git a/internal/loki/chat_memory.go b/internal/loki/chat_memory.go index 30b2d0c..a5f9e5d 100644 --- a/internal/loki/chat_memory.go +++ b/internal/loki/chat_memory.go @@ -15,6 +15,10 @@ import ( // L'IA y range ce qu'elle veut retenir entre les sessions (préférences, // décisions, procédures, infos projet). Quatre outils : mem_search, mem_read, // mem_add, mem_edit. +// +// Port Rust des outils (lot 40) : rust/loki-rs/src/memory/tools.rs, employé +// par les tours de la façade quand elle tient la mémoire (LOKI_MEM=rust) ; +// journal de parité dans parity_mem_tools_test.go. type MemPage struct { Name string // nom de fichier sans le dossier (ex: "docker-notes.md") diff --git a/internal/loki/parity_mem_tools_test.go b/internal/loki/parity_mem_tools_test.go new file mode 100644 index 0000000..97a6911 --- /dev/null +++ b/internal/loki/parity_mem_tools_test.go @@ -0,0 +1,222 @@ +//go:build linux + +package loki + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "testing" + "time" +) + +// parity_mem_tools_test.go — journal de référence des outils mémoire de +// l'agent (lot 40 de la migration Rust) : mem_search, mem_read, mem_add, +// mem_edit et tracker. Chaque appel (arguments tels que le modèle les +// envoie), son résultat et son diff, puis les fichiers de la mémoire du projet +// et les trackers après coup. Heure fixée, fuseau UTC. Rangé dans +// rust/loki-rs/tests/parity_mem/outils.json ; src/memory/tools/tests.rs le +// rejoue. +// +// LOKI_PARITY_UPDATE=1 go test ./internal/loki -run TestPariteOutilsMemoire + +const parityMemToolsFile = "../../rust/loki-rs/tests/parity_mem/outils.json" + +type parityMemToolStep struct { + Name string `json:"name"` + Now string `json:"now,omitempty"` + Tool string `json:"tool"` + Args json.RawMessage `json:"args"` + Label string `json:"label"` + Out string `json:"out"` + Diff any `json:"diff"` + Added int `json:"added"` + Removed int `json:"removed"` + Files map[string]string `json:"files"` + Trackers map[string]any `json:"trackers"` +} + +var parityTrackerIDRe = regexp.MustCompile(`\b[0-9]{18,20}\b`) + +func TestPariteOutilsMemoire(t *testing.T) { + home := testHome(t) + oldLocal, oldNow := time.Local, trackerNow + time.Local = time.UTC + var now time.Time + trackerNow = func() time.Time { return now } + t.Cleanup(func() { time.Local, trackerNow = oldLocal, oldNow }) + activeProjMu.Lock() + activeProjCache = "" + activeProjMu.Unlock() + + ids := map[string]string{} + var order []string + mask := func(s string) string { + return parityTrackerIDRe.ReplaceAllStringFunc(s, func(id string) string { + if _, ok := ids[id]; !ok { + order = append(order, id) + ids[id] = "{{T" + strconv.Itoa(len(order)) + "}}" + } + return ids[id] + }) + } + unmask := func(s string) string { + for id, m := range ids { + s = strings.ReplaceAll(s, m, id) + } + return s + } + dir := projectMemoryDir(defaultProjectSlug) + files := func() map[string]string { + out := map[string]string{} + _ = filepath.WalkDir(dir, func(p string, d os.DirEntry, err error) error { + if err != nil || d.IsDir() { + return nil + } + b, _ := os.ReadFile(p) + rel, _ := filepath.Rel(dir, p) + out[rel] = string(b) + return nil + }) + return out + } + trackers := func() map[string]any { + out := map[string]any{} + kv := allKV(bkTracker) + keys := make([]string, 0, len(kv)) + for k := range kv { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + var v any + _ = json.Unmarshal([]byte(mask(string(kv[k]))), &v) + out[k] = v + } + return out + } + var steps []parityMemToolStep + run := func(st parityMemToolStep) { + if st.Now != "" { + n, err := time.Parse(time.RFC3339, st.Now) + if err != nil { + t.Fatal(err) + } + now = n + } + var args map[string]any + if err := json.Unmarshal([]byte(unmask(string(st.Args))), &args); err != nil { + t.Fatalf("%s : %v", st.Name, err) + } + if args == nil { + args = map[string]any{} + } + st.Label = toolCallLabel(st.Tool, args) + res := execTool(context.Background(), st.Tool, args, st.Label, Caps{Agent: true}) + b, _ := json.Marshal(res.Result) + var out string + _ = json.Unmarshal(b, &out) + st.Out = mask(out) + db, _ := json.Marshal(res.Diff) + _ = json.Unmarshal(db, &st.Diff) + st.Added, st.Removed = res.Added, res.Removed + st.Files = files() + st.Trackers = trackers() + steps = append(steps, st) + } + a := func(s string) json.RawMessage { return json.RawMessage(s) } + + // Pages. + run(parityMemToolStep{Name: "recherche dans une mémoire vide", Now: "2026-07-20T10:00:00Z", Tool: "mem_search", Args: a(`{"query":"docker"}`)}) + run(parityMemToolStep{Name: "ajout", Tool: "mem_add", Args: a(`{"file":"docker-notes","content":"# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n\n\n"}`)}) + run(parityMemToolStep{Name: "ajout d'une page existante", Tool: "mem_add", Args: a(`{"file":"docker-notes.md","content":"x"}`)}) + run(parityMemToolStep{Name: "ajout d'un nom invalide", Tool: "mem_add", Args: a(`{"file":"../x","content":"x"}`)}) + run(parityMemToolStep{Name: "ajout sans nom", Tool: "mem_add", Args: a(`{"content":"x"}`)}) + run(parityMemToolStep{Name: "ajout en majuscules", Tool: "mem_add", Args: a(`{"file":"Recettes.MD","content":"# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte"}`)}) + run(parityMemToolStep{Name: "ajout d'une page bavarde", Tool: "mem_add", Args: a(`{"file":"nathan","content":"## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019."}`)}) + long := strings.Repeat("Un texte de remplissage pour éloigner le terme. ", 4) + "Le mot RARE est là, " + strings.Repeat("puis encore du texte après lui, assez pour couper. ", 4) + lb, _ := json.Marshal(map[string]string{"file": "longue", "content": "# Longue\n" + long}) + run(parityMemToolStep{Name: "ajout d'une longue page", Tool: "mem_add", Args: lb}) + run(parityMemToolStep{Name: "couverture d'abord", Tool: "mem_search", Args: a(`{"query":"copine Nathan"}`)}) + run(parityMemToolStep{Name: "rareté et nom", Tool: "mem_search", Args: a(`{"query":"Nathan nathan TARTE"}`)}) + run(parityMemToolStep{Name: "requête vide : tout", Tool: "mem_search", Args: a(`{"query":" "}`)}) + run(parityMemToolStep{Name: "limite", Tool: "mem_search", Args: a(`{"query":"e","limit":2}`)}) + run(parityMemToolStep{Name: "limite hors bornes", Tool: "mem_search", Args: a(`{"query":"e","limit":99}`)}) + run(parityMemToolStep{Name: "extrait coupé des deux côtés", Tool: "mem_search", Args: a(`{"query":"rare"}`)}) + run(parityMemToolStep{Name: "rien", Tool: "mem_search", Args: a(`{"query":"zzz"}`)}) + run(parityMemToolStep{Name: "lecture", Tool: "mem_read", Args: a(`{"file":"docker-notes"}`)}) + run(parityMemToolStep{Name: "lecture d'une plage", Tool: "mem_read", Args: a(`{"file":"docker-notes.md","offset":3,"limit":1}`)}) + run(parityMemToolStep{Name: "lecture au-delà", Tool: "mem_read", Args: a(`{"file":"docker-notes","offset":40}`)}) + run(parityMemToolStep{Name: "lecture d'une page absente", Tool: "mem_read", Args: a(`{"file":"absente"}`)}) + run(parityMemToolStep{Name: "lecture d'un nom invalide", Tool: "mem_read", Args: a(`{"file":"a/b"}`)}) + run(parityMemToolStep{Name: "modification", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"Le port 8080 est pris.","new":"Le port 8080 est pris.\nLe port 9090 aussi."}`)}) + run(parityMemToolStep{Name: "modification rejouée", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"Le port 8080 est pris.\nLe port 9090 aussi.","new":"Le port 8080 est pris.\nLe port 9090 aussi."}`)}) + run(parityMemToolStep{Name: "modification déjà en place", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"compose v1","new":"Utiliser compose v2."}`)}) + run(parityMemToolStep{Name: "ancien texte absent", Tool: "mem_edit", Args: a(`{"file":"docker-notes","old":"introuvable","new":"x"}`)}) + run(parityMemToolStep{Name: "ancien texte en double", Tool: "mem_edit", Args: a(`{"file":"nathan","old":"Nathan Nathan","new":"N"}`)}) + run(parityMemToolStep{Name: "ancien texte vide", Tool: "mem_edit", Args: a(`{"file":"nathan","old":"","new":"N"}`)}) + run(parityMemToolStep{Name: "modification d'une page absente", Tool: "mem_edit", Args: a(`{"file":"absente","old":"a","new":"b"}`)}) + run(parityMemToolStep{Name: "suppression d'un passage", Tool: "mem_edit", Args: a(`{"file":"recettes","old":"\nTarte aux pommes","new":""}`)}) + + // Trackers. + run(parityMemToolStep{Name: "aucun tracker", Tool: "tracker", Args: a(`{}`)}) + run(parityMemToolStep{Name: "ajout sans nom", Tool: "tracker", Args: a(`{"action":"add","text":"1"}`)}) + run(parityMemToolStep{Name: "ajout sans texte", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","text":" "}`)}) + run(parityMemToolStep{Name: "date illisible", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","when":"hier","text":"72"}`)}) + run(parityMemToolStep{Name: "ajout daté", Tool: "tracker", Args: a(`{"action":"ADD","name":"Poids","when":"2026-07-15","text":" 72.5 "}`)}) + run(parityMemToolStep{Name: "ajout à l'heure", Tool: "tracker", Args: a(`{"action":"add","name":"poids","when":"2026-07-16T08:30","text":"72.1"}`)}) + run(parityMemToolStep{Name: "ajout l'an passé", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","when":"2025-12","text":"73"}`)}) + run(parityMemToolStep{Name: "ajout maintenant", Now: "2026-07-20T10:15:00Z", Tool: "tracker", Args: a(`{"action":"add","name":"Poids","text":"71.9"}`)}) + run(parityMemToolStep{Name: "un autre tracker", Tool: "tracker", Args: a(`{"action":"add","name":"Séances de sport","when":"2024","text":"course"}`)}) + run(parityMemToolStep{Name: "un nom en nombre", Tool: "tracker", Args: a(`{"action":"add","name":42,"when":"2026-01-02 03:04:05","text":7}`)}) + run(parityMemToolStep{Name: "vue d'ensemble", Tool: "tracker", Args: a(`{"action":"view"}`)}) + run(parityMemToolStep{Name: "squelette", Tool: "tracker", Args: a(`{"name":"Poids"}`)}) + run(parityMemToolStep{Name: "une année", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026"}`)}) + run(parityMemToolStep{Name: "une année vide", Tool: "tracker", Args: a(`{"name":"Poids","when":"2027"}`)}) + run(parityMemToolStep{Name: "un mois", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026-07"}`)}) + run(parityMemToolStep{Name: "un jour à l'heure", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026-07-16T08"}`)}) + run(parityMemToolStep{Name: "tracker inconnu", Tool: "tracker", Args: a(`{"name":"Tension"}`)}) + run(parityMemToolStep{Name: "modifier sans id", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","text":"1"}`)}) + run(parityMemToolStep{Name: "modifier", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"{{T1}}","text":"72.4","when":"2026-07-14"}`)}) + run(parityMemToolStep{Name: "modifier une date illisible", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"{{T1}}","when":"demain"}`)}) + run(parityMemToolStep{Name: "modifier un point inconnu", Tool: "tracker", Args: a(`{"action":"edit","name":"Poids","id":"1"}`)}) + run(parityMemToolStep{Name: "modifier un tracker inconnu", Tool: "tracker", Args: a(`{"action":"edit","name":"Tension","id":"1"}`)}) + run(parityMemToolStep{Name: "supprimer sans id", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids"}`)}) + run(parityMemToolStep{Name: "supprimer sans nom", Tool: "tracker", Args: a(`{"action":"delete"}`)}) + run(parityMemToolStep{Name: "supprimer", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids","id":"{{T2}}"}`)}) + run(parityMemToolStep{Name: "supprimer deux fois", Tool: "tracker", Args: a(`{"action":"delete","name":"Poids","id":"{{T2}}"}`)}) + run(parityMemToolStep{Name: "action inconnue", Tool: "tracker", Args: a(`{"action":"purge","name":"Poids"}`)}) + run(parityMemToolStep{Name: "après modifications", Tool: "tracker", Args: a(`{"name":"Poids","when":"2026"}`)}) + + got, err := json.MarshalIndent(steps, "", " ") + if err != nil { + t.Fatal(err) + } + got = append(got, '\n') + if strings.Contains(string(got), home) { + t.Fatal("le journal contient le dossier du test") + } + if os.Getenv("LOKI_PARITY_UPDATE") == "1" { + if err := os.MkdirAll(filepath.Dir(parityMemToolsFile), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(parityMemToolsFile, got, 0o644); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(parityMemToolsFile) + if err != nil { + t.Fatalf("journal de référence absent (%v) : LOKI_PARITY_UPDATE=1 pour le créer", err) + } + if !bytes.Equal(got, want) { + t.Errorf("les outils mémoire ne produisent plus le journal rangé dans %s.\n"+ + "LOKI_PARITY_UPDATE=1 pour le régénérer si le changement est voulu.", parityMemToolsFile) + } +} diff --git a/internal/loki/tracker.go b/internal/loki/tracker.go index 3911d44..a50fad2 100644 --- a/internal/loki/tracker.go +++ b/internal/loki/tracker.go @@ -273,10 +273,14 @@ func trackerMoveToProject(slug, toSlug string) error { // Vide ou « now » = maintenant. Le T de l'ISO est accepté à la place de l'espace. // dateOnly=true quand aucune heure n'a été fournie → le point n'aura pas d'heure // d'affichage, plutôt qu'un faux « minuit ». +// trackerNow : l'heure des trackers (« maintenant », année en cours), +// remplaçable par les tests (journal de parité des outils mémoire). +var trackerNow = time.Now + func parseWhen(when string) (ts int64, dateOnly bool, err error) { w := strings.TrimSpace(when) if w == "" || strings.EqualFold(w, "now") || strings.EqualFold(w, "maintenant") { - return time.Now().UnixMilli(), false, nil + return trackerNow().UnixMilli(), false, nil } w = strings.Replace(w, "T", " ", 1) for _, layout := range []string{"2006-01-02 15:04:05", "2006-01-02 15:04", "2006-01-02 15"} { @@ -402,7 +406,7 @@ func trackerSkeleton(s *Tracker) string { parts = append(parts, fmt.Sprintf("%s (%d)", y, years[y])) } fmt.Fprintf(&b, "Années : %s\n", strings.Join(parts, " · ")) - cur := time.Now().In(time.Local).Format("2006") + cur := trackerNow().In(time.Local).Format("2006") var curEv []TrackerEvent for _, e := range s.Events { if strings.HasPrefix(fmtTS(e.TS), cur) { diff --git a/rust/loki-rs/src/chat/generate.rs b/rust/loki-rs/src/chat/generate.rs index e1b8bcb..aa589da 100644 --- a/rust/loki-rs/src/chat/generate.rs +++ b/rust/loki-rs/src/chat/generate.rs @@ -1321,6 +1321,7 @@ impl ChatShared { perf, mcp: None, tasks: None, + memory: None, go_version: Default::default(), } } diff --git a/rust/loki-rs/src/chat/host.rs b/rust/loki-rs/src/chat/host.rs index 25ab5c5..1c99111 100644 --- a/rust/loki-rs/src/chat/host.rs +++ b/rust/loki-rs/src/chat/host.rs @@ -5,7 +5,8 @@ //! - outils fichiers et shell exécutés ici ([`crate::tools`], état partagé //! tenu par le Go), outils MCP par le pool de la façade quand elle le tient //! (lot 36), outils task_* quand elle tient les tâches (lot 37), critères -//! du mode Code (lot 38) ; les autres +//! du mode Code (lot 38), outils mémoire quand elle tient la mémoire +//! (lot 40) ; les autres //! par le Go (`/internal/v1/tool/exec`), avec //! ce qu'il tire du résultat (capture de la bulle, aperçu du navigateur, //! image relayée) ; @@ -343,6 +344,9 @@ pub struct ChatShared { /// Les tâches, quand la façade les tient (LOKI_TASKS=rust) : les outils /// `task_*` du tour s'exécutent ici (lot 37). pub tasks: Option>, + /// La mémoire, quand la façade la tient (LOKI_MEM=rust) : les outils + /// `mem_*` et `tracker` du tour s'exécutent ici (lot 40). + pub memory: Option>, /// La version du Go, annoncée aux serveurs MCP à une reconnexion ; lue /// une fois. pub go_version: tokio::sync::OnceCell, @@ -675,6 +679,25 @@ impl TurnHost for GoTurnHost { ..Default::default() }; } + // Outils mémoire, quand la façade tient la mémoire (lot 40). + if crate::memory::tools::is_tool(name) + && let Some(mem) = &self.shared.memory + { + let env = crate::memory::tools::MemEnv { + store: &self.shared.store, + mem, + now_ms: crate::memory::now_ms(), + }; + if let Some(o) = crate::memory::tools::exec(&env, name, args, label).await { + return ToolOutcome { + result: o.result, + diff: o.diff, + added: o.added, + removed: o.removed, + ..Default::default() + }; + } + } // Outils task_*, quand la façade tient les tâches (lot 37). if crate::tasks::tools::is_tool(name) && let Some(tasks) = &self.shared.tasks diff --git a/rust/loki-rs/src/main.rs b/rust/loki-rs/src/main.rs index 049cac5..2aa9133 100644 --- a/rust/loki-rs/src/main.rs +++ b/rust/loki-rs/src/main.rs @@ -268,8 +268,14 @@ fn serve() -> ExitCode { .then(|| std::sync::Arc::new(loki_rs::mcp::Mcp::new(engine_paths.home.clone()))); let tasks = tasks_rust .then(|| std::sync::Arc::new(loki_rs::tasks::Tasks::new(engine_paths.home.clone()))); + let mem = mem_rust + .then(|| std::sync::Arc::new(loki_rs::memory::Memory::new(engine_paths.home.clone()))); let chat = if chat_rust { - let held = (mcp.clone(), tasks.clone()); + let held = ChatHeld { + mcp: mcp.clone(), + tasks: tasks.clone(), + mem: mem.clone(), + }; match start_chat(backend, &token, &engine_paths.home, held).await { Ok(c) => Some(c), Err(e) => { @@ -345,7 +351,6 @@ fn serve() -> ExitCode { } }; eprintln!("[loki-rs] façade : http://{addr} → {backend}"); - let mem_home = engine_paths.home.clone(); // Le superviseur reçoit ce que le Go lui passait (rustSvcAction), et de // quoi joindre l'API interne du Go pour « loki-rs engine serve ». let engine_ctl = engine_rust.then(|| { @@ -391,7 +396,7 @@ fn serve() -> ExitCode { settings: settings_rust, files, push: push_rust.then(|| std::sync::Arc::new(loki_rs::push::Push::new())), - mem: mem_rust.then(|| std::sync::Arc::new(loki_rs::memory::Memory::new(mem_home))), + mem, presets: presets_rust, engine_ctl, system: system_rust.then(|| std::sync::Arc::new(loki_rs::system::System::new())), @@ -509,15 +514,20 @@ async fn go_keeps_chat(backend: SocketAddr, token: &str) -> Result>, + tasks: Option>, + mem: Option>, +} + /// Charge la discussion active et prépare le service des tours. async fn start_chat( backend: SocketAddr, token: &str, home: &std::path::Path, - (mcp, tasks): ( - Option>, - Option>, - ), + held: ChatHeld, ) -> Result, String> { use loki_rs::chat::{conv::Conversation, generate::ChatService, host::ChatShared, sessions}; let store = loki_rs::store::Store::new(backend, token.to_string()); @@ -529,8 +539,9 @@ async fn start_chat( s.load().await?; let mut shared = ChatShared::for_sessions(&s, store.clone(), home.join("chatimg")); shared.patterns = loki_rs::chat::generate::retry_patterns(&store).await; - shared.mcp = mcp; - shared.tasks = tasks; + shared.mcp = held.mcp; + shared.tasks = held.tasks; + shared.memory = held.mem; eprintln!("[loki-rs] conversation tenue par la façade (LOKI_CHAT=go pour la rendre au Go)"); Ok(ChatService::new( std::sync::Arc::new(s), diff --git a/rust/loki-rs/src/memory.rs b/rust/loki-rs/src/memory.rs index ca85f87..15089b8 100644 --- a/rust/loki-rs/src/memory.rs +++ b/rust/loki-rs/src/memory.rs @@ -43,6 +43,8 @@ use crate::gojson::{self, Kind}; use crate::proxy::Body; use crate::store::{BK_CONFIG, BK_STATE, CryptError, Store}; +pub mod tools; + /// Index du projet, tenu par le code. pub const MEM_INDEX_FILE: &str = "MEMORY.md"; /// Magic des contenus chiffrés (`memPageMagic`). @@ -1021,6 +1023,20 @@ impl Memory { name: &str, when: &str, text: &str, + ) -> Result { + self.tracker_add_at(store, proj, name, when, text, now_ms()) + .await + } + + /// [`Self::tracker_add`], « maintenant » donné (`when` vide). + async fn tracker_add_at( + &self, + store: &Store, + proj: &str, + name: &str, + when: &str, + text: &str, + now: i64, ) -> Result { let name = go_trim(name).to_string(); if name.is_empty() { @@ -1029,7 +1045,7 @@ impl Memory { if go_trim(text).is_empty() { return Err("contenu vide".into()); } - let (ts, date_only) = parse_when(when, now_ms(), self.tz)?; + let (ts, date_only) = parse_when(when, now, self.tz)?; let mut slug = slugify_ident(&name); if slug.is_empty() { slug = "tracker".into(); diff --git a/rust/loki-rs/src/memory/tools.rs b/rust/loki-rs/src/memory/tools.rs new file mode 100644 index 0000000..e0de95e --- /dev/null +++ b/rust/loki-rs/src/memory/tools.rs @@ -0,0 +1,692 @@ +//! Les outils mémoire de l'agent (lot 40) — port de `chat_memory.go` +//! (`MemSearch`, `MemRead`, `MemAdd`, `MemEdit`) et de l'outil `tracker` +//! (`tracker.go`), exécutés par la façade quand elle tient la mémoire +//! (`LOKI_MEM=rust`). La mémoire est celle du projet actif ; les pages +//! passent par les mêmes lectures et écritures que les routes (scellées +//! selon le chiffrement, `.bak`, index `MEMORY.md` tenu par le code). + +use std::collections::HashMap; + +use serde_json::{Map, Value}; + +use super::{ + Memory, ReadErr, Tracker, TrackerEvent, Tz, active_project, parse_when, slugify_ident, +}; +use crate::chat::types::DiffLine; +use crate::store::Store; + +/// Les outils portés ici. +pub fn is_tool(name: &str) -> bool { + matches!( + name, + "mem_search" | "mem_read" | "mem_add" | "mem_edit" | "tracker" + ) +} + +/// L'issue d'un appel : le texte rendu au modèle et le diff montré. +#[derive(Debug, Default, PartialEq)] +pub struct MemOut { + pub result: String, + pub diff: Vec, + pub added: i64, + pub removed: i64, +} + +impl MemOut { + fn text(s: impl Into) -> Self { + MemOut { + result: s.into(), + ..Default::default() + } + } +} + +/// `strings.TrimSpace`. +fn go_trim(s: &str) -> &str { + s.trim_matches(char::is_whitespace) +} + +/// `unicode.ToLower` rune par rune (`strings.ToLower`). +fn go_lower(s: &str) -> String { + s.chars() + .map(|c| c.to_lowercase().next().unwrap_or(c)) + .collect() +} + +/// Un entier d'argument (`args[k].(float64)` → `int`), 0 sinon. +fn int_arg(args: &Map, k: &str) -> i64 { + args.get(k).and_then(Value::as_f64).map_or(0, |v| v as i64) +} + +/// `str` de `tracker.go` : une chaîne, un nombre arrondi, sinon la forme `%v`. +fn go_str(v: Option<&Value>) -> String { + match v { + None | Some(Value::Null) => String::new(), + Some(Value::String(s)) => s.clone(), + Some(Value::Number(n)) => { + let f = n.as_f64().unwrap_or(0.0); + format!("{f:.0}") + } + Some(v) => go_fmt_v(v), + } +} + +/// `fmt.Sprintf("%v", v)` d'une valeur JSON décodée par le Go. +fn go_fmt_v(v: &Value) -> String { + match v { + Value::Null => "".into(), + Value::Bool(b) => b.to_string(), + Value::Number(n) => { + let f = n.as_f64().unwrap_or(0.0); + if f.fract() == 0.0 && f.abs() < 1e21 { + format!("{f:.0}") + } else { + f.to_string() + } + } + Value::String(s) => s.clone(), + Value::Array(a) => format!("[{}]", a.iter().map(go_fmt_v).collect::>().join(" ")), + Value::Object(m) => { + let mut keys: Vec<&String> = m.keys().collect(); + keys.sort(); + format!( + "map[{}]", + keys.iter() + .map(|k| format!("{k}:{}", go_fmt_v(&m[*k]))) + .collect::>() + .join(" ") + ) + } + } +} + +/// Ce qu'il faut pour exécuter un outil : la base, la mémoire, et l'heure +/// (ms Unix) pour les trackers. +pub struct MemEnv<'a> { + pub store: &'a Store, + pub mem: &'a Memory, + pub now_ms: i64, +} + +/// Un appel d'outil mémoire ; `None` pour un autre outil. `label` : le +/// libellé du tour (`toolCallLabel`), argument principal des outils de page. +pub async fn exec( + env: &MemEnv<'_>, + name: &str, + args: &Map, + label: &str, +) -> Option { + let project = active_project(env.store).await; + let dir = env.mem.project_dir(&project); + Some(match name { + "mem_search" => { + let hits = mem_search(env.store, &dir, label, int_arg(args, "limit")).await; + if hits.is_empty() { + MemOut::text("[aucun résultat]") + } else { + let mut b = String::new(); + for (file, title, snippet) in hits { + b.push_str(&format!("- {file} — {title}\n {snippet}\n")); + } + MemOut::text(b.trim_end_matches('\n')) + } + } + "mem_read" => match mem_read( + env.store, + &dir, + label, + int_arg(args, "offset"), + int_arg(args, "limit"), + ) + .await + { + Ok(c) => MemOut::text(c), + Err(e) => MemOut::text(format!("[erreur] {e}")), + }, + "mem_add" => { + let content = args.get("content").and_then(Value::as_str).unwrap_or(""); + let pname = super::project_name(&super::list_projects(env.store).await, &project); + match mem_add(env.store, &dir, &pname, label, content).await { + Ok(()) => { + let (diff, added) = crate::tools::diff::added_diff(content); + MemOut { + result: format!("[ok] page '{label}' créée"), + diff, + added, + removed: 0, + } + } + Err(e) => MemOut::text(format!("[erreur] {e}")), + } + } + "mem_edit" => { + let s = |k: &str| args.get(k).and_then(Value::as_str).unwrap_or(""); + let (old, new) = (s("old"), s("new")); + match mem_edit(env.store, &dir, label, old, new).await { + Ok(()) => { + let (diff, added, removed) = crate::tools::diff::line_diff(old, new); + MemOut { + result: format!("[ok] page '{label}' modifiée"), + diff, + added, + removed, + } + } + Err(EditErr::Applied) => { + MemOut::text(format!("[ok] page '{label}' {ERR_ALREADY_APPLIED}")) + } + Err(EditErr::Other(e)) => MemOut::text(format!("[erreur] {e}")), + } + } + "tracker" => MemOut::text(tracker(env, &project, args).await), + _ => return None, + }) +} + +// --------------------------------------------------------------------------- +// Pages +// --------------------------------------------------------------------------- + +/// Une page illisible, dite comme le Go. +fn read_err(name: &str, e: &ReadErr) -> String { + match e { + ReadErr::Locked => format!("page '{name}' chiffrée — mémoire verrouillée"), + _ => format!("page '{name}' introuvable"), + } +} + +/// `MemSearch` : les pages classées par couverture des termes, puis par +/// pertinence (rareté du terme, fréquence amortie, bonus du nom et du +/// titre) ; (fichier, titre, extrait). +pub async fn mem_search( + store: &Store, + dir: &std::path::Path, + query: &str, + limit: i64, +) -> Vec<(String, String, String)> { + let limit = if limit <= 0 || limit > 30 { + 8 + } else { + limit as usize + }; + let terms = unique_terms(&go_lower(query)); + struct Doc { + file: String, + title: String, + name: String, + title_low: String, + content: String, + hay: String, + } + let mut docs = Vec::new(); + let mut df: HashMap<&str, i64> = HashMap::new(); + for (file, title) in Memory::list(store, dir).await { + let content = match Memory::read_page(store, dir, &file).await { + Ok(b) => String::from_utf8_lossy(&b).into_owned(), + Err(_) => String::new(), + }; + let hay = go_lower(&format!("{file}\n{content}")); + for t in &terms { + if hay.contains(t.as_str()) { + *df.entry(t.as_str()).or_default() += 1; + } + } + docs.push(Doc { + name: go_lower(&file), + title_low: go_lower(&title), + file, + title, + content, + hay, + }); + } + let n = docs.len() as f64; + let mut ranked: Vec<(i64, f64, (String, String, String))> = Vec::new(); + for d in &docs { + let (mut matched, mut score) = (0i64, 0.0f64); + for t in &terms { + let cnt = d.hay.matches(t.as_str()).count(); + if cnt == 0 { + continue; + } + matched += 1; + let dft = df.get(t.as_str()).copied().unwrap_or(0) as f64; + let idf = ((n + 1.0) / (dft + 1.0)).ln() + 1.0; + let tf = 1.0 + (cnt as f64).ln(); + let mut field = 1.0; + if d.name.contains(t.as_str()) { + field += 4.0; + } + if d.title_low.contains(t.as_str()) { + field += 2.0; + } + score += idf * tf * field; + } + if matched == 0 && !terms.is_empty() { + continue; + } + ranked.push(( + matched, + score, + ( + d.file.clone(), + d.title.clone(), + snippet_around(&d.content, &terms), + ), + )); + } + // Tri stable : couverture d'abord, puis pertinence. + ranked.sort_by(|a, b| b.0.cmp(&a.0).then(b.1.total_cmp(&a.1))); + ranked.into_iter().take(limit).map(|r| r.2).collect() +} + +/// `uniqueTerms` : les mots distincts, dans l'ordre. +fn unique_terms(q: &str) -> Vec { + let mut out: Vec = Vec::new(); + for t in q.split_whitespace() { + if !out.iter().any(|o| o == t) { + out.push(t.to_string()); + } + } + out +} + +/// `snippetAround` : un court extrait autour de la première occurrence d'un +/// terme. Coupé à l'octet comme le Go : un caractère coupé devient U+FFFD +/// par octet, comme à son encodage JSON. +fn snippet_around(content: &str, terms: &[String]) -> String { + let flat = content.split_whitespace().collect::>().join(" "); + let low = go_lower(&flat); + let idx = terms.iter().filter_map(|t| low.find(t.as_str())).min(); + let fb = flat.as_bytes(); + let Some(idx) = idx else { + if fb.len() > 160 { + return format!("{}…", crate::system::go_lossy(&fb[..160])); + } + return flat; + }; + let start = idx.saturating_sub(60).min(fb.len()); + let end = (idx + 100).min(fb.len()); + let mut s = crate::system::go_lossy(&fb[start..end.max(start)]); + if start > 0 { + s = format!("…{s}"); + } + if end < fb.len() { + s.push('…'); + } + s +} + +/// `MemRead` : une plage de lignes numérotées (1-indexé ; 500 au plus). +async fn mem_read( + store: &Store, + dir: &std::path::Path, + name: &str, + offset: i64, + limit: i64, +) -> Result { + let b = Memory::read_page(store, dir, name) + .await + .map_err(|e| read_err(name, &e))?; + let text = String::from_utf8_lossy(&b); + let lines: Vec<&str> = text.split('\n').collect(); + let offset = if offset <= 0 { 1 } else { offset }; + let limit = if limit <= 0 || limit > 500 { + 500 + } else { + limit + }; + let mut out = String::new(); + let mut i = offset - 1; + while (i as usize) < lines.len() && i < offset - 1 + limit { + out.push_str(&format!("{}\t{}\n", i + 1, lines[i as usize])); + i += 1; + } + Ok(out.trim_end_matches('\n').to_string()) +} + +/// `MemAdd` : une nouvelle page, jamais par-dessus une existante ; l'index +/// la reçoit toujours. +async fn mem_add( + store: &Store, + dir: &std::path::Path, + project_name: &str, + name: &str, + content: &str, +) -> Result<(), String> { + let p = Memory::safe_path(dir, name)?; + if std::fs::metadata(&p).is_ok() { + return Err("la page existe déjà — utilise mem_edit pour la modifier".into()); + } + let body = format!("{}\n", content.trim_end_matches('\n')); + Memory::write_page(store, dir, name, body.as_bytes()).await?; + Memory::index_add(store, dir, name, project_name).await; + Ok(()) +} + +/// `errAlreadyApplied`. +const ERR_ALREADY_APPLIED: &str = "déjà à jour — la page contient déjà cette modification"; + +enum EditErr { + /// L'édition est déjà en place : pas un échec. + Applied, + Other(String), +} + +/// `MemEdit` : `old` doit apparaître exactement une fois. +async fn mem_edit( + store: &Store, + dir: &std::path::Path, + name: &str, + old: &str, + new: &str, +) -> Result<(), EditErr> { + let b = Memory::read_page(store, dir, name) + .await + .map_err(|e| EditErr::Other(read_err(name, &e)))?; + let content = String::from_utf8_lossy(&b).into_owned(); + if old.is_empty() { + return Err(EditErr::Other("old vide".into())); + } + let n = content.matches(old).count(); + if n == 0 { + if !new.is_empty() && content.contains(new) { + return Err(EditErr::Applied); + } + return Err(EditErr::Other("old introuvable dans la page".into())); + } + if n > 1 { + return Err(EditErr::Other(format!( + "old apparaît {n} fois — ajoute du contexte pour le rendre unique" + ))); + } + let updated = content.replacen(old, new, 1); + Memory::write_page(store, dir, name, updated.as_bytes()) + .await + .map_err(EditErr::Other) +} + +// --------------------------------------------------------------------------- +// Trackers +// --------------------------------------------------------------------------- + +/// L'heure murale d'un instant (ms) dans le fuseau des trackers. +fn civil_at(tz: Tz, ms: i64) -> crate::tz::Civil { + let sec = ms.div_euclid(1000); + match tz { + Tz::Fixed(off) => crate::tz::civil(sec + off), + Tz::Local => crate::tz::Location::local().civil(sec), + } +} + +/// `fmtTS` : « AAAA-MM-JJ HH:MM ». +fn fmt_ts(tz: Tz, ms: i64) -> String { + civil_at(tz, ms).ymd_hm() +} + +/// `fmtDay` : « AAAA-MM-JJ ». +fn fmt_day(tz: Tz, ms: i64) -> String { + let c = civil_at(tz, ms); + format!("{:04}-{:02}-{:02}", c.year, c.month, c.day) +} + +fn fmt_month(tz: Tz, ms: i64) -> String { + let c = civil_at(tz, ms); + format!("{:04}-{:02}", c.year, c.month) +} + +fn fmt_year(tz: Tz, ms: i64) -> String { + format!("{:04}", civil_at(tz, ms).year) +} + +/// `fmtEvent` : sans heure si le point n'en a pas. +fn fmt_event(tz: Tz, e: &TrackerEvent) -> String { + if e.date_only { + fmt_day(tz, e.ts) + } else { + fmt_ts(tz, e.ts) + } +} + +/// `whenParts` : 0 rien, 1 année, 2 mois, 3 jour, 4 heure. +fn when_parts(when: &str) -> usize { + let w = go_trim(when); + if w.is_empty() { + return 0; + } + let w = w.replacen('T', " ", 1); + let mut date = w.as_str(); + if let Some(sp) = w.find(' ') { + date = &w[..sp]; + if !go_trim(&w[sp..]).is_empty() { + return 4; + } + } + date.split('-').count() +} + +/// `toolTracker` : le texte vu par le modèle. +async fn tracker(env: &MemEnv<'_>, proj: &str, args: &Map) -> String { + let action = go_trim(&go_str(args.get("action"))).to_lowercase(); + let name = go_trim(&go_str(args.get("name"))).to_string(); + let when = go_trim(&go_str(args.get("when"))).to_string(); + let text = go_trim(&go_str(args.get("text"))).to_string(); + let id = go_trim(&go_str(args.get("id"))).to_string(); + let store = env.store; + match action.as_str() { + "" | "view" | "read" | "list" => tracker_view(env, proj, &name, &when).await, + "add" => match env + .mem + .tracker_add_at(store, proj, &name, &when, &text, env.now_ms) + .await + { + Ok(id) => format!("[ok] point ajouté à « {name} » (id {id})."), + Err(e) => format!("[erreur] {e}"), + }, + "edit" => { + if name.is_empty() || id.is_empty() { + return "[erreur] name et id sont requis pour modifier un point".into(); + } + let slug = slugify_ident(&name); + let res = async { + let mut t = Memory::tracker_load(store, proj, &slug) + .await + .ok_or("tracker introuvable")?; + let ev = t.events.get_or_insert_with(Vec::new); + let Some(e) = ev.iter_mut().find(|e| e.id == id) else { + return Err("événement introuvable".to_string()); + }; + if !text.is_empty() { + e.text = text.clone(); + } + if !when.is_empty() { + let (ts, date_only) = parse_when(&when, env.now_ms, env.mem.tz)?; + e.ts = ts; + e.date_only = date_only; + } + Memory::tracker_save(store, proj, &slug, &mut t).await + } + .await; + match res { + Ok(()) => format!("[ok] point {id} de « {name} » modifié."), + Err(e) => format!("[erreur] {e}"), + } + } + "delete" => { + if name.is_empty() { + return "[erreur] name requis".into(); + } + // Sans id, c'est le tracker entier : réservé à l'interface. + if id.is_empty() { + return "[erreur] id requis — supprimer un tracker entier se fait depuis l'interface, pas par l'outil".into(); + } + let slug = slugify_ident(&name); + let res = async { + let mut t = Memory::tracker_load(store, proj, &slug) + .await + .ok_or("tracker introuvable")?; + let ev = t.events.get_or_insert_with(Vec::new); + let n = ev.len(); + ev.retain(|e| e.id != id); + if ev.len() == n { + return Err("événement introuvable".to_string()); + } + Memory::tracker_save(store, proj, &slug, &mut t).await + } + .await; + match res { + Ok(()) => format!("[ok] point {id} de « {name} » supprimé."), + Err(e) => format!("[erreur] {e}"), + } + } + a => format!( + "[erreur] action inconnue {} (view, add, edit, delete)", + crate::gojson::quote(a) + ), + } +} + +/// `trackerView` : vue d'ensemble, ou descente année → mois → points. +async fn tracker_view(env: &MemEnv<'_>, proj: &str, name: &str, when: &str) -> String { + let tz = env.mem.tz; + if go_trim(name).is_empty() { + return tracker_overview(env, proj).await; + } + let slug = slugify_ident(name); + let Some(t) = Memory::tracker_load(env.store, proj, &slug).await else { + return format!( + "[aucun tracker « {name} »] Appelle tracker() sans argument pour la liste, ou ajoute un point avec action:\"add\"." + ); + }; + let ev = t.events.clone().unwrap_or_default(); + if ev.is_empty() { + return format!("Tracker « {} » : vide pour l'instant.", t.name); + } + let prefix = go_trim(when).replacen('T', " ", 1); + let matched: Vec = ev + .iter() + .filter(|e| prefix.is_empty() || fmt_ts(tz, e.ts).starts_with(&prefix)) + .cloned() + .collect(); + match when_parts(when) { + 0 => tracker_skeleton(tz, &t, &ev, env.now_ms), + 1 => { + if matched.is_empty() { + return format!("Tracker « {} » : rien en {when}.", t.name); + } + let example = format!("when:\"{when}-07\""); + format!( + "Tracker « {} » en {when} — {} points :\n{}\nZoom : {example} pour un mois.", + t.name, + matched.len(), + group_line(&matched, |e| fmt_month(tz, e.ts)) + ) + } + _ => tracker_events(tz, &t.name, when, &matched), + } +} + +/// `trackerOverview` : la liste des trackers du projet. +async fn tracker_overview(env: &MemEnv<'_>, proj: &str) -> String { + let tz = env.mem.tz; + let list = Memory::tracker_list(env.store, proj).await; + if list.is_empty() { + return "Aucun tracker dans ce projet. Crée-en un en ajoutant un point : action:\"add\", name:\"…\", text:\"…\" (when facultatif = maintenant).".into(); + } + let mut b = format!("Trackers du projet ({}) :\n", list.len()); + for m in &list { + let count = m["count"].as_i64().unwrap_or(0); + let span = if count > 0 { + format!( + " — {count} points, du {} au {}", + fmt_day(tz, m["first_ts"].as_i64().unwrap_or(0)), + fmt_day(tz, m["last_ts"].as_i64().unwrap_or(0)) + ) + } else { + String::new() + }; + b.push_str(&format!("- {}{span}\n", m["name"].as_str().unwrap_or(""))); + } + b.push_str("Zoom : tracker(name:\"\") pour le détail d'un tracker."); + b +} + +/// Des points regroupés par une clé de date, triés, avec leurs comptes : +/// « 2026-07 (3) · … » (`monthsLine`, `trackerGroup`). +fn group_line(ev: &[TrackerEvent], key: impl Fn(&TrackerEvent) -> String) -> String { + let mut groups: std::collections::BTreeMap = Default::default(); + for e in ev { + *groups.entry(key(e)).or_default() += 1; + } + groups + .into_iter() + .map(|(g, n)| format!("{g} ({n})")) + .collect::>() + .join(" · ") +} + +/// `trackerSkeleton` : les années (avec compte), et l'année en cours par +/// mois. +fn tracker_skeleton(tz: Tz, t: &Tracker, ev: &[TrackerEvent], now_ms: i64) -> String { + let mut b = format!( + "Tracker « {} » — {} points, du {} au {}\n", + t.name, + ev.len(), + fmt_day(tz, ev[0].ts), + fmt_day(tz, ev[ev.len() - 1].ts) + ); + b.push_str(&format!( + "Années : {}\n", + group_line(ev, |e| fmt_year(tz, e.ts)) + )); + let cur = fmt_year(tz, now_ms); + let cur_ev: Vec = ev + .iter() + .filter(|e| fmt_ts(tz, e.ts).starts_with(&cur)) + .cloned() + .collect(); + if !cur_ev.is_empty() { + b.push_str(&format!( + "{cur} (année en cours) : {}\n", + group_line(&cur_ev, |e| fmt_month(tz, e.ts)) + )); + } + b.push_str(&format!( + "Zoom : tracker(name:\"{}\", when:\"2025\") pour une année · when:\"{cur}-07\" pour un mois.", + t.name + )); + b +} + +/// `trackerEventsCap`. +const EVENTS_CAP: usize = 60; + +/// `trackerEvents` : les points (id, date, texte), les plus récents d'abord +/// gardés au-delà du plafond. +fn tracker_events(tz: Tz, name: &str, when: &str, ev: &[TrackerEvent]) -> String { + let trimmed = ev.len() > EVENTS_CAP; + let shown = if trimmed { + &ev[ev.len() - EVENTS_CAP..] + } else { + ev + }; + let mut b = format!("Tracker « {name} »"); + if !go_trim(when).is_empty() { + b.push_str(&format!(" en {when}")); + } + if trimmed { + b.push_str(&format!( + " — {} points (les {EVENTS_CAP} plus récents ; affine le when pour les plus anciens) :\n", + ev.len() + )); + } else { + b.push_str(&format!(" — {} points :\n", ev.len())); + } + for e in shown { + b.push_str(&format!("- [{}] {} — {}\n", e.id, fmt_event(tz, e), e.text)); + } + b.push_str( + "Modifier/supprimer : action:\"edit\"|\"delete\", name, id (l'identifiant entre crochets).", + ); + b +} diff --git a/rust/loki-rs/tests/common/fakego.rs b/rust/loki-rs/tests/common/fakego.rs index c76ae73..4a37412 100644 --- a/rust/loki-rs/tests/common/fakego.rs +++ b/rust/loki-rs/tests/common/fakego.rs @@ -970,6 +970,7 @@ pub async fn start_with( shared.patterns = loki_rs::chat::generate::retry_patterns(&store).await; shared.mcp = opts.mcp.clone(); shared.tasks = opts.tasks.clone(); + shared.memory = opts.mem.clone(); Some(ChatService::new(Arc::new(s), Arc::new(shared))) } else { None diff --git a/rust/loki-rs/tests/lot6.rs b/rust/loki-rs/tests/lot6.rs index 4d0d8b6..68653c3 100644 --- a/rust/loki-rs/tests/lot6.rs +++ b/rust/loki-rs/tests/lot6.rs @@ -670,6 +670,7 @@ async fn tour_complet_par_le_go() { perf: Arc::new(loki_rs::perfring::PerfRing::new(Default::default())), mcp: None, tasks: None, + memory: None, go_version: Default::default(), }); let cfg = std::collections::HashMap::new(); @@ -2708,3 +2709,40 @@ async fn anneau_de_la_facade_pour_le_go() { assert_eq!(code, 405); drop(go); } + +/// Lot 40 : un outil mémoire appelé par un tour de la façade s'exécute ici +/// quand elle tient la mémoire — la page rangée dans le projet actif, l'index +/// tenu, le diff montré, rien par le Go. +#[tokio::test] +async fn outil_memoire_par_la_facade() { + let call = sse(&[ + serde_json::json!({"choices":[{"delta":{"tool_calls":[{"index":0,"id":"m1","type":"function","function":{"name":"mem_add","arguments":"{\"file\":\"chat\",\"content\":\"# Chat\\nIl s'appelle Pixel.\"}"}}]}}]}), + serde_json::json!({"choices":[{"delta":{},"finish_reason":"tool_calls"}]}), + ]); + let (url, seen) = fake_engine(vec![call, answer("noté")]).await; + let dir = tempfile::tempdir().unwrap(); + let mem = Arc::new(loki_rs::memory::Memory::new(dir.path().to_path_buf())); + let (svc, go, _stop, _t) = service_with(Some(&url), |s| s.memory = Some(mem.clone())).await; + *go.crypt_mode.lock().unwrap() = fakego::CryptMode::Plain; + svc.send(serde_json::json!({"message": "retiens le nom du chat", "cid": "m"})) + .await + .unwrap(); + until_done(&svc, 1).await; + assert!(go.tool_log.lock().unwrap().is_empty(), "{:?}", go.tool_log); + let page = dir.path().join("memory/generale/chat.md"); + assert_eq!( + std::fs::read_to_string(&page).unwrap(), + "# Chat\nIl s'appelle Pixel.\n" + ); + let index = std::fs::read_to_string(dir.path().join("memory/generale/MEMORY.md")).unwrap(); + assert!(index.ends_with("- [Chat](chat.md)\n"), "{index}"); + let seen = seen.lock().unwrap().clone(); + let tool = seen[1].1["messages"] + .as_array() + .unwrap() + .iter() + .rfind(|m| m["role"] == "tool") + .unwrap()["content"] + .clone(); + assert_eq!(tool, serde_json::json!("[ok] page 'chat' créée")); +} diff --git a/rust/loki-rs/tests/memory.rs b/rust/loki-rs/tests/memory.rs index 071525d..b8cd29c 100644 --- a/rust/loki-rs/tests/memory.rs +++ b/rust/loki-rs/tests/memory.rs @@ -392,3 +392,149 @@ async fn coffre_et_projets_restent_au_go() { assert_eq!(&b[..], format!("go:{p}").as_bytes()); } } + +/// Lot 40 : les outils mémoire de l'agent (mem_search, mem_read, mem_add, +/// mem_edit, tracker), rejoués contre le journal du Go +/// (`parity_mem_tools_test.go` → `parity_mem/outils.json`) : résultat, +/// diff, fichiers de la mémoire et trackers après chaque appel. +#[tokio::test] +async fn outils_memoire_comme_le_go() { + use loki_rs::memory::tools::{MemEnv, exec, is_tool}; + let fx: Value = serde_json::from_str(include_str!("parity_mem/outils.json")).expect("journal"); + let tmp = tempfile::tempdir().unwrap(); + let home = tmp.path().to_path_buf(); + let (_f, go, _stop) = fakego::start(home.clone()).await; + *go.crypt_mode.lock().unwrap() = CryptMode::Plain; + let backend = go.backend.lock().unwrap().unwrap(); + let store = loki_rs::store::Store::new(backend, fakego::JETON.into()); + let mem = Memory::with_tz(home.clone(), Tz::Fixed(0)); + let dir = home.join("memory").join("generale"); + let mut ids = TrackerIds::default(); + let mut now = 0i64; + for st in fx.as_array().unwrap() { + let name = st["name"].as_str().unwrap(); + if let Some(n) = st["now"].as_str() { + now = rfc3339_ms(n); + } + let tool = st["tool"].as_str().unwrap(); + assert!(is_tool(tool), "{name}"); + // Les identifiants de points déjà vus : {{Tn}} → valeur. + let raw = ids.unmask(&st["args"].to_string()); + let args: serde_json::Map = serde_json::from_str(&raw).unwrap(); + let label = st["label"].as_str().unwrap(); + assert_eq!( + loki_rs::chat::stream::tool_call_label(tool, &args), + label, + "{name} : libellé" + ); + let env = MemEnv { + store: &store, + mem: &mem, + now_ms: now, + }; + let out = exec(&env, tool, &args, label).await.expect("outil mémoire"); + assert_eq!(ids.mask(&out.result), st["out"].as_str().unwrap(), "{name}"); + let diff = serde_json::to_value(&out.diff).unwrap(); + if st["diff"].is_null() { + assert!(out.diff.is_empty(), "{name} : diff {diff}"); + } else { + assert_eq!(diff, st["diff"], "{name} : diff"); + } + assert_eq!( + (out.added, out.removed), + ( + st["added"].as_i64().unwrap(), + st["removed"].as_i64().unwrap() + ), + "{name}" + ); + let mut files = serde_json::Map::new(); + for (rel, content) in walk(&dir) { + files.insert(rel, Value::String(content)); + } + assert_eq!(Value::Object(files), st["files"], "{name} : fichiers"); + let mut trackers = serde_json::Map::new(); + let all = store.all("tracker").await.unwrap_or_default(); + for k in all.keys() { + let b = store.get_open("tracker", k).await.unwrap().unwrap(); + let v: Value = serde_json::from_str(&ids.mask(&String::from_utf8_lossy(&b))).unwrap(); + trackers.insert(k.clone(), v); + } + assert_eq!(Value::Object(trackers), st["trackers"], "{name} : trackers"); + } + assert!( + exec( + &MemEnv { + store: &store, + mem: &mem, + now_ms: now + }, + "bash", + &serde_json::Map::new(), + "" + ) + .await + .is_none() + ); +} + +/// Identifiants de points de trackers → {{T1}}, {{T2}}… dans l'ordre où +/// ils apparaissent (même règle que le journal du Go). +#[derive(Default)] +struct TrackerIds { + ids: Vec<(String, String)>, +} + +impl TrackerIds { + fn mask(&mut self, s: &str) -> String { + let re = regex::Regex::new(r"\b[0-9]{18,20}\b").unwrap(); + re.replace_all(s, |c: ®ex::Captures| { + let id = c[0].to_string(); + if let Some((_, p)) = self.ids.iter().find(|(i, _)| *i == id) { + return p.clone(); + } + let p = format!("{{{{T{}}}}}", self.ids.len() + 1); + self.ids.push((id, p.clone())); + p + }) + .into_owned() + } + + fn unmask(&self, s: &str) -> String { + let mut s = s.to_string(); + // Les plus longs d'abord : {{T10}} avant {{T1}}. + let mut ids: Vec<_> = self.ids.iter().collect(); + ids.sort_by_key(|(_, p)| std::cmp::Reverse(p.len())); + for (id, p) in ids { + s = s.replace(p, id); + } + s + } +} + +fn rfc3339_ms(v: &str) -> i64 { + let p: Vec = v[..10].split('-').map(|x| x.parse().unwrap()).collect(); + let h: Vec = v[11..19].split(':').map(|x| x.parse().unwrap()).collect(); + loki_rs::tz::Location::utc().date(p[0], p[1], p[2], h[0], h[1], h[2]) * 1000 +} + +/// Les fichiers d'un dossier, chemin relatif → contenu. +fn walk(dir: &Path) -> BTreeMap { + let mut out = BTreeMap::new(); + let mut stack = vec![dir.to_path_buf()]; + while let Some(d) = stack.pop() { + let Ok(rd) = std::fs::read_dir(&d) else { + continue; + }; + for e in rd.flatten() { + let p = e.path(); + if p.is_dir() { + stack.push(p); + } else { + let rel = p.strip_prefix(dir).unwrap().to_string_lossy().into_owned(); + out.insert(rel, std::fs::read_to_string(&p).unwrap_or_default()); + } + } + } + out +} diff --git a/rust/loki-rs/tests/parity_mem/outils.json b/rust/loki-rs/tests/parity_mem/outils.json new file mode 100644 index 0000000..4175a27 --- /dev/null +++ b/rust/loki-rs/tests/parity_mem/outils.json @@ -0,0 +1,2319 @@ +[ + { + "name": "recherche dans une mémoire vide", + "now": "2026-07-20T10:00:00Z", + "tool": "mem_search", + "args": { + "query": "docker" + }, + "label": "docker", + "out": "[aucun résultat]", + "diff": null, + "added": 0, + "removed": 0, + "files": {}, + "trackers": {} + }, + { + "name": "ajout", + "tool": "mem_add", + "args": { + "file": "docker-notes", + "content": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n\n\n" + }, + "label": "docker-notes", + "out": "[ok] page 'docker-notes' créée", + "diff": [ + { + "op": "+", + "text": "# Notes Docker" + }, + { + "op": "+", + "text": "" + }, + { + "op": "+", + "text": "Utiliser compose v2." + }, + { + "op": "+", + "text": "Le port 8080 est pris." + }, + { + "op": "+", + "text": "" + }, + { + "op": "+", + "text": "" + } + ], + "added": 6, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n" + }, + "trackers": {} + }, + { + "name": "ajout d'une page existante", + "tool": "mem_add", + "args": { + "file": "docker-notes.md", + "content": "x" + }, + "label": "docker-notes.md", + "out": "[erreur] la page existe déjà — utilise mem_edit pour la modifier", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n" + }, + "trackers": {} + }, + { + "name": "ajout d'un nom invalide", + "tool": "mem_add", + "args": { + "file": "../x", + "content": "x" + }, + "label": "../x", + "out": "[erreur] nom invalide (alphanum, ._-)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n" + }, + "trackers": {} + }, + { + "name": "ajout sans nom", + "tool": "mem_add", + "args": { + "content": "x" + }, + "label": "", + "out": "[erreur] nom vide", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n" + }, + "trackers": {} + }, + { + "name": "ajout en majuscules", + "tool": "mem_add", + "args": { + "file": "Recettes.MD", + "content": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte" + }, + "label": "Recettes.MD", + "out": "[ok] page 'Recettes.MD' créée", + "diff": [ + { + "op": "+", + "text": "# Recettes" + }, + { + "op": "+", + "text": "Tarte aux pommes" + }, + { + "op": "+", + "text": "La copine de Nathan aime la tarte" + } + ], + "added": 3, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n" + }, + "trackers": {} + }, + { + "name": "ajout d'une page bavarde", + "tool": "mem_add", + "args": { + "file": "nathan", + "content": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019." + }, + "label": "nathan", + "out": "[ok] page 'nathan' créée", + "diff": [ + { + "op": "+", + "text": "## Nathan" + }, + { + "op": "+", + "text": "Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan." + }, + { + "op": "+", + "text": "Nathan est né en 2019." + } + ], + "added": 3, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ajout d'une longue page", + "tool": "mem_add", + "args": { + "content": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. ", + "file": "longue" + }, + "label": "longue", + "out": "[ok] page 'longue' créée", + "diff": [ + { + "op": "+", + "text": "# Longue" + }, + { + "op": "+", + "text": "Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. " + } + ], + "added": 2, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "couverture d'abord", + "tool": "mem_search", + "args": { + "query": "copine Nathan" + }, + "label": "copine Nathan", + "out": "- Recettes.MD — Recettes\n # Recettes Tarte aux pommes La copine de Nathan aime la tarte\n- nathan.md — Nathan\n ## Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan. Nathan est né en 2019.\n- MEMORY.md — Index mémoire — Générale\n …Notes Docker](docker-notes.md) - [Recettes](Recettes.MD) - [Nathan](nathan.md) - [Longue](longue.md)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "rareté et nom", + "tool": "mem_search", + "args": { + "query": "Nathan nathan TARTE" + }, + "label": "Nathan nathan TARTE", + "out": "- Recettes.MD — Recettes\n # Recettes Tarte aux pommes La copine de Nathan aime la tarte\n- nathan.md — Nathan\n ## Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan. Nathan est né en 2019.\n- MEMORY.md — Index mémoire — Générale\n …Notes Docker](docker-notes.md) - [Recettes](Recettes.MD) - [Nathan](nathan.md) - [Longue](longue.md)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "requête vide : tout", + "tool": "mem_search", + "args": { + "query": " " + }, + "label": " ", + "out": "- MEMORY.md — Index mémoire — Générale\n # Index mémoire — Générale Une ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la su…\n- Recettes.MD — Recettes\n # Recettes Tarte aux pommes La copine de Nathan aime la tarte\n- docker-notes.md — Notes Docker\n # Notes Docker Utiliser compose v2. Le port 8080 est pris.\n- longue.md — Longue\n # Longue Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un t…\n- nathan.md — Nathan\n ## Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan. Nathan est né en 2019.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "limite", + "tool": "mem_search", + "args": { + "query": "e", + "limit": 2 + }, + "label": "e", + "out": "- longue.md — Longue\n # Longue Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. …\n- MEMORY.md — Index mémoire — Générale\n # Index mémoire — Générale Une ligne par page de ce projet (`- [titre](fichier.md) — accroche`). T…", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "limite hors bornes", + "tool": "mem_search", + "args": { + "query": "e", + "limit": 99 + }, + "label": "e", + "out": "- longue.md — Longue\n # Longue Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. …\n- MEMORY.md — Index mémoire — Générale\n # Index mémoire — Générale Une ligne par page de ce projet (`- [titre](fichier.md) — accroche`). T…\n- Recettes.MD — Recettes\n # Recettes Tarte aux pommes La copine de Nathan aime la tarte\n- docker-notes.md — Notes Docker\n # Notes Docker Utiliser compose v2. Le port 8080 est pris.\n- nathan.md — Nathan\n …an Nathan Nathan Nathan Nathan Nathan Nathan Nathan. Nathan est né en 2019.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "extrait coupé des deux côtés", + "tool": "mem_search", + "args": { + "query": "rare" + }, + "label": "rare", + "out": "- longue.md — Longue\n …me. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, a…", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "rien", + "tool": "mem_search", + "args": { + "query": "zzz" + }, + "label": "zzz", + "out": "[aucun résultat]", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "lecture", + "tool": "mem_read", + "args": { + "file": "docker-notes" + }, + "label": "docker-notes", + "out": "1\t# Notes Docker\n2\t\n3\tUtiliser compose v2.\n4\tLe port 8080 est pris.\n5\t", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "lecture d'une plage", + "tool": "mem_read", + "args": { + "file": "docker-notes.md", + "offset": 3, + "limit": 1 + }, + "label": "docker-notes.md", + "out": "3\tUtiliser compose v2.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "lecture au-delà", + "tool": "mem_read", + "args": { + "file": "docker-notes", + "offset": 40 + }, + "label": "docker-notes", + "out": "", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "lecture d'une page absente", + "tool": "mem_read", + "args": { + "file": "absente" + }, + "label": "absente", + "out": "[erreur] page 'absente' introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "lecture d'un nom invalide", + "tool": "mem_read", + "args": { + "file": "a/b" + }, + "label": "a/b", + "out": "[erreur] page 'a/b' introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "modification", + "tool": "mem_edit", + "args": { + "file": "docker-notes", + "old": "Le port 8080 est pris.", + "new": "Le port 8080 est pris.\nLe port 9090 aussi." + }, + "label": "docker-notes", + "out": "[ok] page 'docker-notes' modifiée", + "diff": [ + { + "op": " ", + "text": "Le port 8080 est pris." + }, + { + "op": "+", + "text": "Le port 9090 aussi." + } + ], + "added": 1, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "modification rejouée", + "tool": "mem_edit", + "args": { + "file": "docker-notes", + "old": "Le port 8080 est pris.\nLe port 9090 aussi.", + "new": "Le port 8080 est pris.\nLe port 9090 aussi." + }, + "label": "docker-notes", + "out": "[ok] page 'docker-notes' modifiée", + "diff": [ + { + "op": " ", + "text": "Le port 8080 est pris." + }, + { + "op": " ", + "text": "Le port 9090 aussi." + } + ], + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "modification déjà en place", + "tool": "mem_edit", + "args": { + "file": "docker-notes", + "old": "compose v1", + "new": "Utiliser compose v2." + }, + "label": "docker-notes", + "out": "[ok] page 'docker-notes' déjà à jour — la page contient déjà cette modification", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ancien texte absent", + "tool": "mem_edit", + "args": { + "file": "docker-notes", + "old": "introuvable", + "new": "x" + }, + "label": "docker-notes", + "out": "[erreur] old introuvable dans la page", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ancien texte en double", + "tool": "mem_edit", + "args": { + "file": "nathan", + "old": "Nathan Nathan", + "new": "N" + }, + "label": "nathan", + "out": "[erreur] old apparaît 4 fois — ajoute du contexte pour le rendre unique", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ancien texte vide", + "tool": "mem_edit", + "args": { + "file": "nathan", + "old": "", + "new": "N" + }, + "label": "nathan", + "out": "[erreur] old vide", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "modification d'une page absente", + "tool": "mem_edit", + "args": { + "file": "absente", + "old": "a", + "new": "b" + }, + "label": "absente", + "out": "[erreur] page 'absente' introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "suppression d'un passage", + "tool": "mem_edit", + "args": { + "file": "recettes", + "old": "\nTarte aux pommes", + "new": "" + }, + "label": "recettes", + "out": "[erreur] page 'recettes' introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "aucun tracker", + "tool": "tracker", + "args": {}, + "label": "", + "out": "Aucun tracker dans ce projet. Crée-en un en ajoutant un point : action:\"add\", name:\"…\", text:\"…\" (when facultatif = maintenant).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ajout sans nom", + "tool": "tracker", + "args": { + "action": "add", + "text": "1" + }, + "label": "add", + "out": "[erreur] nom de tracker vide", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ajout sans texte", + "tool": "tracker", + "args": { + "action": "add", + "name": "Poids", + "text": " " + }, + "label": "add Poids", + "out": "[erreur] contenu vide", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "date illisible", + "tool": "tracker", + "args": { + "action": "add", + "name": "Poids", + "when": "hier", + "text": "72" + }, + "label": "add Poids", + "out": "[erreur] date non comprise : \"hier\" (attendu AAAA, AAAA-MM, AAAA-MM-JJ ou AAAA-MM-JJ HH:MM)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": {} + }, + { + "name": "ajout daté", + "tool": "tracker", + "args": { + "action": "ADD", + "name": "Poids", + "when": "2026-07-15", + "text": " 72.5 " + }, + "label": "ADD Poids", + "out": "[ok] point ajouté à « Poids » (id {{T1}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + } + ], + "name": "Poids" + } + } + }, + { + "name": "ajout à l'heure", + "tool": "tracker", + "args": { + "action": "add", + "name": "poids", + "when": "2026-07-16T08:30", + "text": "72.1" + }, + "label": "add poids", + "out": "[ok] point ajouté à « poids » (id {{T2}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + } + ], + "name": "Poids" + } + } + }, + { + "name": "ajout l'an passé", + "tool": "tracker", + "args": { + "action": "add", + "name": "Poids", + "when": "2025-12", + "text": "73" + }, + "label": "add Poids", + "out": "[ok] point ajouté à « Poids » (id {{T3}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + } + ], + "name": "Poids" + } + } + }, + { + "name": "ajout maintenant", + "now": "2026-07-20T10:15:00Z", + "tool": "tracker", + "args": { + "action": "add", + "name": "Poids", + "text": "71.9" + }, + "label": "add Poids", + "out": "[ok] point ajouté à « Poids » (id {{T4}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + } + } + }, + { + "name": "un autre tracker", + "tool": "tracker", + "args": { + "action": "add", + "name": "Séances de sport", + "when": "2024", + "text": "course" + }, + "label": "add Séances de sport", + "out": "[ok] point ajouté à « Séances de sport » (id {{T5}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "un nom en nombre", + "tool": "tracker", + "args": { + "action": "add", + "name": 42, + "when": "2026-01-02 03:04:05", + "text": 7 + }, + "label": "add 42", + "out": "[ok] point ajouté à « 42 » (id {{T6}}).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "vue d'ensemble", + "tool": "tracker", + "args": { + "action": "view" + }, + "label": "view", + "out": "Trackers du projet (3) :\n- Poids — 4 points, du 2025-12-01 au 2026-07-20\n- 42 — 1 points, du 2026-01-02 au 2026-01-02\n- Séances de sport — 1 points, du 2024-01-01 au 2024-01-01\nZoom : tracker(name:\"\u003cnom\u003e\") pour le détail d'un tracker.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "squelette", + "tool": "tracker", + "args": { + "name": "Poids" + }, + "label": "Poids", + "out": "Tracker « Poids » — 4 points, du 2025-12-01 au 2026-07-20\nAnnées : 2025 (1) · 2026 (3)\n2026 (année en cours) : 2026-07 (3)\nZoom : tracker(name:\"Poids\", when:\"2025\") pour une année · when:\"2026-07\" pour un mois.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "une année", + "tool": "tracker", + "args": { + "name": "Poids", + "when": "2026" + }, + "label": "Poids", + "out": "Tracker « Poids » en 2026 — 3 points :\n2026-07 (3)\nZoom : when:\"2026-07\" pour un mois.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "une année vide", + "tool": "tracker", + "args": { + "name": "Poids", + "when": "2027" + }, + "label": "Poids", + "out": "Tracker « Poids » : rien en 2027.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "un mois", + "tool": "tracker", + "args": { + "name": "Poids", + "when": "2026-07" + }, + "label": "Poids", + "out": "Tracker « Poids » en 2026-07 — 3 points :\n- [{{T1}}] 2026-07-15 — 72.5\n- [{{T2}}] 2026-07-16 08:30 — 72.1\n- [{{T4}}] 2026-07-20 10:15 — 71.9\nModifier/supprimer : action:\"edit\"|\"delete\", name, id (l'identifiant entre crochets).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "un jour à l'heure", + "tool": "tracker", + "args": { + "name": "Poids", + "when": "2026-07-16T08" + }, + "label": "Poids", + "out": "Tracker « Poids » en 2026-07-16T08 — 1 points :\n- [{{T2}}] 2026-07-16 08:30 — 72.1\nModifier/supprimer : action:\"edit\"|\"delete\", name, id (l'identifiant entre crochets).", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "tracker inconnu", + "tool": "tracker", + "args": { + "name": "Tension" + }, + "label": "Tension", + "out": "[aucun tracker « Tension »] Appelle tracker() sans argument pour la liste, ou ajoute un point avec action:\"add\".", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "modifier sans id", + "tool": "tracker", + "args": { + "action": "edit", + "name": "Poids", + "text": "1" + }, + "label": "edit Poids", + "out": "[erreur] name et id sont requis pour modifier un point", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.5", + "ts": 1784073600000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "modifier", + "tool": "tracker", + "args": { + "action": "edit", + "name": "Poids", + "id": "{{T1}}", + "text": "72.4", + "when": "2026-07-14" + }, + "label": "edit Poids", + "out": "[ok] point {{T1}} de « Poids » modifié.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "modifier une date illisible", + "tool": "tracker", + "args": { + "action": "edit", + "name": "Poids", + "id": "{{T1}}", + "when": "demain" + }, + "label": "edit Poids", + "out": "[erreur] date non comprise : \"demain\" (attendu AAAA, AAAA-MM, AAAA-MM-JJ ou AAAA-MM-JJ HH:MM)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "modifier un point inconnu", + "tool": "tracker", + "args": { + "action": "edit", + "name": "Poids", + "id": "1" + }, + "label": "edit Poids", + "out": "[erreur] événement introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "modifier un tracker inconnu", + "tool": "tracker", + "args": { + "action": "edit", + "name": "Tension", + "id": "1" + }, + "label": "edit Tension", + "out": "[erreur] tracker introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "supprimer sans id", + "tool": "tracker", + "args": { + "action": "delete", + "name": "Poids" + }, + "label": "delete Poids", + "out": "[erreur] id requis — supprimer un tracker entier se fait depuis l'interface, pas par l'outil", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "supprimer sans nom", + "tool": "tracker", + "args": { + "action": "delete" + }, + "label": "delete", + "out": "[erreur] name requis", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T2}}", + "text": "72.1", + "ts": 1784190600000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "supprimer", + "tool": "tracker", + "args": { + "action": "delete", + "name": "Poids", + "id": "{{T2}}" + }, + "label": "delete Poids", + "out": "[ok] point {{T2}} de « Poids » supprimé.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "supprimer deux fois", + "tool": "tracker", + "args": { + "action": "delete", + "name": "Poids", + "id": "{{T2}}" + }, + "label": "delete Poids", + "out": "[erreur] événement introuvable", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "action inconnue", + "tool": "tracker", + "args": { + "action": "purge", + "name": "Poids" + }, + "label": "purge Poids", + "out": "[erreur] action inconnue \"purge\" (view, add, edit, delete)", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + }, + { + "name": "après modifications", + "tool": "tracker", + "args": { + "name": "Poids", + "when": "2026" + }, + "label": "Poids", + "out": "Tracker « Poids » en 2026 — 2 points :\n2026-07 (2)\nZoom : when:\"2026-07\" pour un mois.", + "diff": null, + "added": 0, + "removed": 0, + "files": { + "MEMORY.md": "# Index mémoire — Générale\n\nUne ligne par page de ce projet (`- [titre](fichier.md) — accroche`). Tenu à jour automatiquement à la création et à la suppression d'une page ; l'accroche après le titre, elle, est libre.\n- [Notes Docker](docker-notes.md)\n- [Recettes](Recettes.MD)\n- [Nathan](nathan.md)\n- [Longue](longue.md)\n", + "Recettes.MD": "# Recettes\nTarte aux pommes\nLa copine de Nathan aime la tarte\n", + "docker-notes.md": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "docker-notes.md.bak": "# Notes Docker\n\nUtiliser compose v2.\nLe port 8080 est pris.\nLe port 9090 aussi.\n", + "longue.md": "# Longue\nUn texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Un texte de remplissage pour éloigner le terme. Le mot RARE est là, puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. puis encore du texte après lui, assez pour couper. \n", + "nathan.md": "## Nathan\nNathan Nathan Nathan Nathan Nathan Nathan Nathan Nathan.\nNathan est né en 2019.\n" + }, + "trackers": { + "generale/42": { + "events": [ + { + "id": "{{T6}}", + "text": "7", + "ts": 1767323045000 + } + ], + "name": "42" + }, + "generale/poids": { + "events": [ + { + "date_only": true, + "id": "{{T3}}", + "text": "73", + "ts": 1764547200000 + }, + { + "date_only": true, + "id": "{{T1}}", + "text": "72.4", + "ts": 1783987200000 + }, + { + "id": "{{T4}}", + "text": "71.9", + "ts": 1784542500000 + } + ], + "name": "Poids" + }, + "generale/seances-de-sport": { + "events": [ + { + "date_only": true, + "id": "{{T5}}", + "text": "course", + "ts": 1704067200000 + } + ], + "name": "Séances de sport" + } + } + } +]