mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Trois apports repris d'AJEAN 0.12.9 → 0.13.5, adaptés au fork. MÉMOIRE LONGUE DE LA CONVERSATION (chat_recall.go) Le compactage résumait, donc perdait. Chaque gros bloc du torse est désormais ARCHIVÉ verbatim sous un identifiant court (r7…) dans bbolt AVANT d'être résumé ; le résumé cite l'id, et le modèle le rappelle avec recall(id) ou le retrouve par mots-clés avec recall_search. Le contexte reste plat, l'archive grossit sur disque. En mode code, un fichier lu ou un diff produit tôt dans la session n'est plus perdu au compactage suivant. Au passage : garde-fou anti-résumé-dégénéré, budget de résumé indexé sur la fenêtre, queue ramenée à 20 % (compacter plus large ne coûte plus de perte), et fin de l'épinglage du 1er message user — le modèle répondait à l'ancienne demande au lieu de continuer la tâche en cours. PROJETS (projects.go) Un projet cloisonne une mémoire, ses discussions et ses trackers. La couture est memoryDir(), qui pointe sur le projet actif : tout le code mémoire en hérite sans le savoir. Migration automatique au premier démarrage (memory/*.md → memory/generale/, discussions et tâches orphelines rattachées). Une tâche planifiée vise un projet et l'exécution le force, pour qu'une veille n'écrive pas dans la mémoire du chantier affiché à l'écran. TRACKERS (tracker.go) 3e type de mémoire : les données datées qui s'accumulent. On ne les lit jamais en entier — consultation par niveaux (vue d'ensemble → année → mois → événements), et la dernière valeur de chaque tracker est donnée d'emblée au modèle, qui répond sans appeler l'outil. Aussi : index MEMORY.md tenu par le CODE et injecté en tête de conversation avec la description du projet (le modèle ne peut plus le désynchroniser) ; prompt système rattaché au PRESET et non plus global — stocké en base, pas dans le .env, qui ne saurait pas porter un texte multiligne. Deux correctifs du lot précédent voyagent ici, faute de pouvoir séparer les fichiers : msgText signale la présence d'une image au compactage, et le garde-fou « pensé sans agir » passe à deux relances (nudgeCount/maxNudges). Le paquet UI est regénéré dans le commit suivant, qui touche les mêmes sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
4.5 KiB
Go
138 lines
4.5 KiB
Go
package loki
|
|
|
|
// web_projects.go — l'API du hub des projets. Un seul endpoint verbe-orienté
|
|
// (/api/projects) plutôt qu'une route par action : le hub fait six choses sur le
|
|
// même objet, six routes n'auraient rien clarifié.
|
|
//
|
|
// Toutes les mutations passent par projects.go, qui porte les invariants (slug
|
|
// unique, dernier projet indestructible, réassignation de l'actif).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
// projectDTO est la vue d'un projet renvoyée à l'UI : le registre, plus ce qui
|
|
// se compte (discussions, pages, trackers) pour que le hub soit lisible sans
|
|
// ouvrir chaque projet.
|
|
type projectDTO struct {
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Desc string `json:"desc,omitempty"`
|
|
Created int64 `json:"created"`
|
|
Convs int `json:"convs"`
|
|
Pages int `json:"pages"`
|
|
Trackers int `json:"trackers"`
|
|
Active bool `json:"active"`
|
|
}
|
|
|
|
func projectDTOs() []projectDTO {
|
|
active := activeProjectSlug()
|
|
// Un seul parcours de l'index des discussions pour tous les projets : compter
|
|
// projet par projet relirait la base autant de fois qu'il y a de projets.
|
|
convs := map[string]int{}
|
|
for _, m := range convIndex() {
|
|
convs[convProjectOf(m)]++
|
|
}
|
|
out := make([]projectDTO, 0, len(listProjects()))
|
|
for _, p := range listProjects() {
|
|
d := projectDTO{
|
|
Slug: p.Slug, Name: p.Name, Desc: p.Desc, Created: p.CreatedAt,
|
|
Convs: convs[p.Slug], Trackers: len(trackerKeysForProject(p.Slug)),
|
|
Active: p.Slug == active,
|
|
}
|
|
// Les pages se comptent dans le dossier du projet : on emprunte memoryDir
|
|
// le temps du comptage plutôt que de dupliquer la lecture du dossier.
|
|
// MEMORY.md est exclu — c'est l'index du projet, pas une page de contenu :
|
|
// l'annoncer ferait afficher « 1 page » sur un projet tout neuf et vide.
|
|
withProject(p.Slug, func() {
|
|
for _, m := range MemList() {
|
|
if !isIndexFile(m.Name) {
|
|
d.Pages++
|
|
}
|
|
}
|
|
})
|
|
out = append(out, d)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handleProjects :
|
|
//
|
|
// GET → {ok, projects, active}
|
|
// POST {action, ...} → create | rename | desc | delete | switch
|
|
func handleProjects(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
sendJSON(w, 200, map[string]any{"ok": true, "projects": projectDTOs(), "active": activeProjectSlug()})
|
|
case http.MethodPost:
|
|
var body struct {
|
|
Action string `json:"action"`
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name"`
|
|
Desc string `json:"desc"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
slug := strings.TrimSpace(body.Slug)
|
|
var err error
|
|
switch strings.ToLower(strings.TrimSpace(body.Action)) {
|
|
case "create":
|
|
_, err = createProject(body.Name)
|
|
case "rename":
|
|
err = renameProject(slug, body.Name)
|
|
case "desc":
|
|
err = setProjectDesc(slug, body.Desc)
|
|
case "delete":
|
|
// Une génération en cours travaille dans la mémoire du projet actif :
|
|
// supprimer sous ses pieds la laisserait écrire dans le vide.
|
|
if conv.isGenerating() {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": "une génération est en cours"})
|
|
return
|
|
}
|
|
err = deleteProject(slug)
|
|
case "switch":
|
|
if conv.isGenerating() {
|
|
sendJSON(w, 409, map[string]any{"ok": false, "error": "une génération est en cours"})
|
|
return
|
|
}
|
|
err = projectSwitch(slug)
|
|
default:
|
|
err = fmt.Errorf("action inconnue : %q", body.Action)
|
|
}
|
|
if err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "projects": projectDTOs(), "active": activeProjectSlug()})
|
|
default:
|
|
sendJSON(w, 405, map[string]any{"ok": false, "error": "méthode non autorisée"})
|
|
}
|
|
}
|
|
|
|
// handleMemMove déplace une page mémoire vers un autre projet. Séparé de
|
|
// /api/projects : c'est une opération sur une PAGE, pas sur un projet.
|
|
func handleMemMove(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
sendJSON(w, 405, map[string]any{"ok": false, "error": "méthode non autorisée"})
|
|
return
|
|
}
|
|
var body struct {
|
|
Name string `json:"name"`
|
|
To string `json:"to"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
if err := moveMemPage(body.Name, activeProjectSlug(), strings.TrimSpace(body.To)); err != nil {
|
|
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true})
|
|
}
|