mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Persistance : le fsync de la discussion ne retarde plus le premier token
StartTurn persistait AVANT de lancer la génération : un marshal, puis deux commits bbolt (contenu, puis index), soit deux fsync et plusieurs ouvertures de base — 26-30 ms mesurés sur un NVMe Windows, bien plus sur un /mnt/user d'Unraid ou un disque de parité, payés à chaque message avant que la requête ne parte vers llama-server. Même facture au milieu d'un tour (vérification du mode Code) et pour chaque résultat d'outil coupé (« voir plus »). - chat_persist.go : un écrivain unique et ordonné. L'instantané reste pris sous c.mu (images → références, marshal, identifiant de la discussion) ; seule l'E/S part en différé. Le dernier instantané de chaque discussion l'emporte (numéro pris sous c.mu), les lots sont fusionnés en UNE transaction : contenu + index ensemble, résultats d'outils compris. - Seuls StartTurn, la persistance en cours de tour et les résultats d'outils sont asynchrones. Fin de tour, reset, bascules, compactage manuel restent synchrones et attendent tout ce qui précède ; la suppression écarte puis attend les écritures qui la visent (plus de discussion ressuscitée). - Discussion active gardée en RAM, créée sous verrou (plus de double identifiant sur base neuve) et changée sous c.mu avec le contenu : un instantané ne peut plus écrire l'ancien fil sous le nouvel identifiant. - « Voir plus » servi depuis la mémoire tant que le résultat n'est pas écrit ; verrouillage vérifié avant de rendre un id. - En plein tour, la base reçoit le journal sous sa forme de fin de tour (compactLog, version pure de compactLogLocked) ; la mémoire n'est pas touchée. - Vidage de l'écrivain avant redémarrage, « Quitter », exec et (dé)chiffrement. Le contexte vu par le modèle ne change pas : il lit c.Messages en mémoire, les résultats complets d'outils et la compaction du journal ne servent qu'à l'UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9b2e974942
commit
c3005f5b56
14 files changed
+1082
-80
No files matched your search
@@ -191,11 +191,15 @@ func LoadConversation() {
|
||||
|
||||
// loadFrom remplace l'état en mémoire par une autre discussion (b vide = fil
|
||||
// neuf) et invalide les abonnés : l'epoch incrémenté leur fait vider l'écran et
|
||||
// rejouer depuis zéro, exactement comme un reset. L'appelant NE doit PAS
|
||||
// détenir mu.
|
||||
func (c *Conversation) loadFrom(b []byte) {
|
||||
// rejouer depuis zéro, exactement comme un reset. id = discussion chargée : le
|
||||
// cache de la discussion active change sous mu, avec le contenu (voir
|
||||
// convActivate). L'appelant NE doit PAS détenir mu.
|
||||
func (c *Conversation) loadFrom(id string, b []byte) {
|
||||
c.Stop()
|
||||
c.mu.Lock()
|
||||
if c == conv {
|
||||
convActiveRemember(id)
|
||||
}
|
||||
c.Messages, c.Log, c.Seq, c.CtxUsed = nil, nil, 0, 0
|
||||
c.ctxUsedLen, c.genPeak = 0, 0
|
||||
c.queued = nil // file de l'ancienne discussion : elle ne suit pas la bascule
|
||||
@@ -224,34 +228,6 @@ func reloadEncryptedStores() {
|
||||
}
|
||||
}
|
||||
|
||||
// persist enregistre l'état (appelé en fin de tour et sur reset, pas à chaque
|
||||
// delta) sous la discussion active, et rafraîchit ses métadonnées (titre déduit
|
||||
// du premier message, date, nombre d'échanges). L'appelant NE doit PAS détenir mu.
|
||||
func (c *Conversation) persist() {
|
||||
c.mu.Lock()
|
||||
// Images en base64 → références (chat_images.go) avant d'écrire : une photo
|
||||
// jointe pesait des Mo, réécrits en entier à CHAQUE fin de tour.
|
||||
refImagesInMessages(c.Messages)
|
||||
b, err := json.Marshal(c)
|
||||
title := convSummary(c.Messages)
|
||||
turns := 0
|
||||
for _, ev := range c.Log {
|
||||
if _, ok := ev.Delta["user"]; ok {
|
||||
turns++
|
||||
}
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
id := convEnsureActive()
|
||||
// Chiffré si la mémoire l'est et qu'elle est déverrouillée. Verrouillée,
|
||||
// l'écriture est REFUSÉE plutôt que de remplacer un blob chiffré par du
|
||||
// clair — le fil de ce tour reste en RAM, rien n'est perdu sur disque.
|
||||
_ = putStoreBytes(bkChat, convKey(id), b)
|
||||
convTouchMeta(id, title, turns)
|
||||
}
|
||||
|
||||
// appendDelta journalise un événement d'affichage et réveille les abonnés.
|
||||
// epoch est celui capturé au début du tour : si un Reset est passé entre-temps,
|
||||
// l'événement appartient à l'ancienne conversation et est jeté (sinon il
|
||||
@@ -303,8 +279,16 @@ func evTS0(d map[string]any, fallback int64) int64 {
|
||||
// On préserve `toks` (somme) et `ts0` (premier) pour que le compteur de vitesse
|
||||
// (tok/s) reste correct au replay. Verrou détenu par l'appelant.
|
||||
func (c *Conversation) compactLogLocked() {
|
||||
if len(c.Log) < 2 {
|
||||
return
|
||||
c.Log = compactLog(c.Log)
|
||||
}
|
||||
|
||||
// compactLog est la compaction de compactLogLocked sous forme PURE : elle rend
|
||||
// un nouveau journal sans toucher à celui qu'on lui passe (les événements non
|
||||
// fusionnés sont partagés, pas recopiés). snapshot s'en sert pour écrire, en
|
||||
// plein tour, la forme qu'aura le journal à la fin du tour.
|
||||
func compactLog(log []LogEvent) []LogEvent {
|
||||
if len(log) < 2 {
|
||||
return log
|
||||
}
|
||||
textKey := func(d map[string]any) string {
|
||||
if _, ok := d["content"].(string); ok {
|
||||
@@ -315,7 +299,7 @@ func (c *Conversation) compactLogLocked() {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
out := make([]LogEvent, 0, len(c.Log))
|
||||
out := make([]LogEvent, 0, len(log))
|
||||
var buf strings.Builder
|
||||
bufKey := ""
|
||||
var cur LogEvent
|
||||
@@ -332,7 +316,7 @@ func (c *Conversation) compactLogLocked() {
|
||||
buf.Reset()
|
||||
bufKey, toks, ts0, seq0 = "", 0, 0, 0
|
||||
}
|
||||
for _, ev := range c.Log {
|
||||
for _, ev := range log {
|
||||
// Outils : un appel s'écrit en de nombreux événements (annonce done=false,
|
||||
// frappe du corps, streaming des arguments) jusqu'au done=true, qui porte
|
||||
// déjà l'état FINAL (résultat + diff). Les intermédiaires ne servent qu'à
|
||||
@@ -368,7 +352,7 @@ func (c *Conversation) compactLogLocked() {
|
||||
toks += evToks(ev.Delta)
|
||||
}
|
||||
flush()
|
||||
c.Log = out
|
||||
return out
|
||||
}
|
||||
|
||||
// compactAndPublish exécute UNE compaction et en publie tout le cycle de vie :
|
||||
@@ -511,9 +495,10 @@ func (c *Conversation) StartTurn(text string, files []attachInfo, caps Caps, tem
|
||||
epoch := c.epoch
|
||||
c.mu.Unlock()
|
||||
|
||||
// Borne de tour + bulle utilisateur (rejouables). Persistée tout de suite :
|
||||
// si le process meurt en pleine génération (crash, restart après MAJ), le
|
||||
// message de l'utilisateur survit au lieu de disparaître avec le tour.
|
||||
// Borne de tour + bulle utilisateur (rejouables). Persistée tout de suite
|
||||
// (sur disque quelques ms plus tard, voir persistAsync) : si le process
|
||||
// meurt en pleine génération (crash, restart après MAJ), le message de
|
||||
// l'utilisateur survit au lieu de disparaître avec le tour.
|
||||
delta := map[string]any{"user": text}
|
||||
// Nom du modèle qui va produire la réponse : journalisé avec la borne de
|
||||
// tour, donc rejoué au chargement — la carte de réponse garde son modèle
|
||||
@@ -531,7 +516,9 @@ func (c *Conversation) StartTurn(text string, files []attachInfo, caps Caps, tem
|
||||
if !caps.Code {
|
||||
maybeCodeHint(c, epoch, text)
|
||||
}
|
||||
c.persist()
|
||||
// Instantané pris ici, écriture laissée à l'écrivain (chat_persist.go) : le
|
||||
// fsync ne retarde plus le départ de la requête vers le modèle.
|
||||
c.persistAsync()
|
||||
if temperature == 0 {
|
||||
temperature = 0.7
|
||||
}
|
||||
@@ -750,7 +737,7 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
||||
}()
|
||||
// Télémétrie : tout ce que ce tour envoie au moteur (étapes, compaction,
|
||||
// sous-agents, vérification) est rattaché à la discussion active.
|
||||
ctx = withPerf(ctx, perfMain, getStr(bkChat, ckActive))
|
||||
ctx = withPerf(ctx, perfMain, convEnsureActive())
|
||||
|
||||
// llama-server local seulement : le preset externe garde le seul seuil, et
|
||||
// ses complétions ne nourrissent pas la garde de marge (compactNeeded).
|
||||
|
||||
Reference in new issue
Block a user