mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
Portage des v0.9.9 → v0.10.2 de l'amont, la dernière vraie fonctionnalité qui nous manquait. Une consigne, une fréquence (« @every 2h », « tous les jours à 9h », ou une expression cron à 5 champs, dans le fuseau du navigateur), et l'IA l'exécute seule en arrière-plan. Preset épinglé par tâche (bascule de modèle avant l'exécution, attente du rechargement), accès mémoire et web réglables, interrupteur maître pour tout suspendre, bouton « tester maintenant ». Le planificateur est une goroutine, un tic par minute, dans le process qui détient la conversation et parle au moteur. Une seule tâche part par tic : de toute façon une seule inférence tourne à la fois, et étaler les départs évite qu'une rafale monopolise le modèle. Occupé ou modèle en cours de chargement n'est pas un échec — la tâche repasse au tic suivant. Une tâche ne partage QU'UN point avec le chat : le verrou de génération. Ni messages, ni journal d'affichage, ni epoch — elle construit son fil éphémère et le jette, ne gardant que son texte final comme compte-rendu (borné à 4000 caractères, réinjecté au passage suivant pour la continuité). Deux adaptations, parce que loki n'est pas l'amont : — Dossier de travail. Ici il appartient à la DISCUSSION ouverte : une tâche y aurait déposé ses fichiers, et en aurait changé en cours de route si l'utilisateur changeait de discussion — pour disparaître avec elle à la suppression. Chaque tâche a donc le sien (workspace/tasks/<id>/), stable d'un passage à l'autre. La bascule ne touche que les points d'entrée des OUTILS (agentCwd) : le panneau Fichiers, les dépôts et les liens des messages continuent de suivre la discussion de l'utilisateur. — Capacités. Mem est posé explicitement à MemOff quand l'agent est coupé : le zéro de MemMode est la chaîne vide, qu'EnabledTools ne reconnaît pas comme « coupée » — une tâche sans agent se serait vu offrir les outils mem_*. Le mode code reste off : rôles, critères et passe de vérification n'ont pas de sens sans personne en face. Le refus d'un message pendant qu'une tâche tourne dit maintenant LAQUELLE occupe le modèle : « génération en cours » sur un fil vide et immobile n'expliquait rien. Interface : section repliable dans les réglages (liste, état, prochain passage, pastille), modale d'édition bâtie sur le gabarit de l'éditeur de preset, panneau « dernier résultat » en markdown. Vérifié dans un vrai navigateur — création, rendu, réouverture en édition, bascule intervalle/cron, interrupteur maître, suppression — sans une seule erreur JS.
73 lines
2.3 KiB
Go
73 lines
2.3 KiB
Go
package loki
|
|
|
|
// tasks_sched.go — la boucle de planification. Une seule goroutine, un tick par
|
|
// minute, dans le process qui possède la conversation et l'UI (ajean-ui /
|
|
// `ajean link`). Voir StartTaskScheduler, câblée dans newWebMux (web_server.go).
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var taskSchedOnce sync.Once
|
|
|
|
// StartTaskScheduler démarre la boucle de planification (idempotent : sûr à
|
|
// appeler à chaque construction du mux, ne lance qu'une goroutine).
|
|
func StartTaskScheduler() {
|
|
taskSchedOnce.Do(func() { go taskSchedLoop() })
|
|
}
|
|
|
|
// taskSchedLoop réveille les tâches dues, une par tick (elles sont de toute façon
|
|
// sérialisées par le gate de génération). Le tick d'une minute suffit : c'est la
|
|
// résolution de la planification (voir tasks_cron.go).
|
|
func taskSchedLoop() {
|
|
// Premier passage immédiat pour amorcer les NextRun manquants (après un
|
|
// redémarrage du process, ou pour une tâche fraîchement créée sans NextRun).
|
|
primeNextRuns()
|
|
t := time.NewTicker(time.Minute)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
tickTasks(time.Now())
|
|
}
|
|
}
|
|
|
|
// primeNextRuns renseigne NextRun de toute tâche activée qui n'en a pas (0),
|
|
// relativement à maintenant — sinon une tâche à NextRun=0 serait considérée « due »
|
|
// en permanence, ou jamais planifiée selon le sens de la comparaison.
|
|
func primeNextRuns() {
|
|
now := time.Now()
|
|
for _, t := range listTasks() {
|
|
if t.Enabled && t.NextRun == 0 {
|
|
t.NextRun = computeNextRun(t.Schedule, t.TZ, now)
|
|
_ = saveTask(t)
|
|
}
|
|
}
|
|
}
|
|
|
|
// tickTasks lance la première tâche due (si l'interrupteur maître n'est pas en
|
|
// pause). Une seule par tick : lancer plusieurs inférences en parallèle est de
|
|
// toute façon impossible (gate de génération), et étaler leur départ évite qu'une
|
|
// rafale de tâches monopolise le modèle d'un coup.
|
|
func tickTasks(now time.Time) {
|
|
if tasksPaused() {
|
|
return
|
|
}
|
|
nowMs := now.UnixMilli()
|
|
for _, t := range listTasks() {
|
|
if !t.Enabled {
|
|
continue
|
|
}
|
|
if t.NextRun == 0 {
|
|
// Priming raté (schedule invalide) ou tâche neuve : on tente de le poser,
|
|
// sans la déclencher ce tour-ci.
|
|
t.NextRun = computeNextRun(t.Schedule, t.TZ, now)
|
|
_ = saveTask(t)
|
|
continue
|
|
}
|
|
if t.NextRun <= nowMs {
|
|
runTask(t) // met à jour LastRun/NextRun/… et persiste
|
|
return
|
|
}
|
|
}
|
|
}
|