mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 18:58:43 +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>
136 lines
5.1 KiB
Go
136 lines
5.1 KiB
Go
package loki
|
|
|
|
// tasks.go — les tâches planifiées : des consignes que l'IA exécute toute seule,
|
|
// en arrière-plan, sur une fréquence réglable (intervalle simple ou cron). Chaque
|
|
// tâche tourne ISOLÉE de la conversation partagée (voir RunAutonomous dans
|
|
// tasks_run.go) : elle ne pollue pas le chat et livre son résultat via les propres
|
|
// outils de l'IA (mail via MCP, shell, web…). L'utilisateur peut désactiver une
|
|
// tâche, ou tout suspendre d'un coup via l'interrupteur maître (tasks_paused).
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
// Task est une tâche planifiée persistée dans le bucket bkTasks (clé = ID).
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Prompt string `json:"prompt"`
|
|
Schedule string `json:"schedule"` // "@every 2h" | "@every 1d@23:00" | expr cron 5 champs
|
|
TZ string `json:"tz"` // fuseau IANA du navigateur (ex. "Europe/Paris") pour l'heure
|
|
Preset string `json:"preset"` // id du preset à activer avant l'exécution (vide = preset actif)
|
|
// Project = slug du projet visé (projects.go). L'exécution force ce projet
|
|
// (setProjectOverride), donc la tâche voit LA MÉMOIRE et les trackers du bon
|
|
// chantier — une veille « NAS » n'écrit pas dans la mémoire « recettes ». Vide
|
|
// sur les tâches d'avant les projets : traitées comme « Générale ».
|
|
Project string `json:"project,omitempty"`
|
|
// Accès de la tâche. On stocke la NÉGATION (« pas de… ») pour que le zéro JSON
|
|
// (tâches créées avant ces champs) garde le comportement historique : accès.
|
|
NoMem bool `json:"no_mem"` // true = pas d'accès à la mémoire pour cette tâche
|
|
NoWeb bool `json:"no_web"` // true = pas d'accès au web pour cette tâche
|
|
Enabled bool `json:"enabled"`
|
|
LastRun int64 `json:"last_run"` // epoch ms, 0 = jamais
|
|
NextRun int64 `json:"next_run"` // epoch ms, 0 = à (re)calculer
|
|
LastDurMs int64 `json:"last_dur_ms"` // durée du dernier passage en ms
|
|
LastOK bool `json:"last_ok"`
|
|
LastError string `json:"last_error"`
|
|
LastReport string `json:"last_report"` // dernier texte final de l'IA (compte-rendu court)
|
|
|
|
Running bool `json:"-"` // transitoire : une exécution est en cours
|
|
}
|
|
|
|
// newTaskID génère un identifiant court et unique.
|
|
func newTaskID() string {
|
|
var b [6]byte
|
|
_, _ = rand.Read(b[:])
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// listTasks renvoie toutes les tâches, triées par nom pour un affichage stable.
|
|
func listTasks() []Task {
|
|
var out []Task
|
|
_ = view(bkTasks, func(b *bolt.Bucket) error {
|
|
return b.ForEach(func(_, v []byte) error {
|
|
var t Task
|
|
if json.Unmarshal(v, &t) == nil {
|
|
out = append(out, t)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
sort.Slice(out, func(i, j int) bool {
|
|
if out[i].Name != out[j].Name {
|
|
return out[i].Name < out[j].Name
|
|
}
|
|
return out[i].ID < out[j].ID
|
|
})
|
|
return out
|
|
}
|
|
|
|
// taskProjectOf renvoie le projet d'une tâche, l'absence valant projet par défaut :
|
|
// une tâche d'avant les projets ne doit ni disparaître ni changer de mémoire.
|
|
func taskProjectOf(t Task) string {
|
|
if s := strings.TrimSpace(t.Project); s != "" {
|
|
return s
|
|
}
|
|
return defaultProjectSlug
|
|
}
|
|
|
|
// Le PANNEAU de l'UI et le planificateur restent GLOBAUX : une tâche doit tourner
|
|
// même si son projet n'est pas celui affiché, et masquer les tâches des autres
|
|
// projets ferait croire qu'elles ne sont plus planifiées. Le projet gouverne ce
|
|
// que la tâche VOIT quand elle s'exécute (runTask force setProjectOverride), pas
|
|
// sa visibilité.
|
|
|
|
// tagOrphanTasks rattache au projet donné les tâches qui n'en ont pas encore
|
|
// (migration, une seule fois).
|
|
func tagOrphanTasks(slug string) {
|
|
for _, t := range listTasks() {
|
|
if strings.TrimSpace(t.Project) != "" {
|
|
continue
|
|
}
|
|
t.Project = slug
|
|
_ = saveTask(t)
|
|
}
|
|
}
|
|
|
|
// getTask lit une tâche par ID. Renvoie false si absente.
|
|
func getTask(id string) (Task, bool) {
|
|
var t Task
|
|
ok := getJSON(bkTasks, id, &t)
|
|
return t, ok
|
|
}
|
|
|
|
// saveTask enregistre (crée ou remplace) une tâche.
|
|
func saveTask(t Task) error { return putJSON(bkTasks, t.ID, t) }
|
|
|
|
// deleteTask supprime une tâche par ID.
|
|
func deleteTask(id string) error { return putBytes(bkTasks, id, nil) }
|
|
|
|
// tasksPaused indique si l'interrupteur maître suspend TOUTES les tâches. On
|
|
// stocke « en pause » plutôt que « actif » pour que l'état par défaut (base
|
|
// neuve) soit « actif » sans dépendre d'une écriture : c'est un frein, pas un
|
|
// moteur.
|
|
func tasksPaused() bool { return getBool(bkState, "tasks_paused") }
|
|
|
|
func setTasksPaused(on bool) error { return putBool(bkState, "tasks_paused", on) }
|
|
|
|
// computeNextRun calcule le prochain déclenchement d'une tâche à partir de son
|
|
// Schedule (dans le fuseau tz), relatif à `from`. Renvoie 0 si le schedule est
|
|
// invalide (la tâche ne se déclenchera alors jamais — l'UI refuse déjà un
|
|
// schedule invalide à la sauvegarde, ceci n'est qu'un garde-fou).
|
|
func computeNextRun(schedule, tz string, from time.Time) int64 {
|
|
next, err := nextAfter(schedule, tz, from)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return next.UnixMilli()
|
|
}
|