Cache : PREWARM prépare le prochain tour pendant que l'utilisateur lit, en opt-in

Une compaction, un dernier message rendu autrement au tour suivant ou une
tâche planifiée qui a pris le slot laissent au message suivant un recalcul
inévitable : des secondes sur un 27B, des dizaines sur un MoE aux experts en
RAM, avant le premier mot. Nouvelle clé PREWARM (off par défaut) : dès que le
moteur local est libre, Loki lui envoie la requête du prochain tour suivie
d'un message utilisateur « . », max_tokens 1, sans flux. Le moteur calcule le
préfixe et pose un point de reprise au début de ce message ; le vrai tour ne
calcule plus que le sien. Sans la clé, aucune requête de plus (testé).

- assemblage partagé, contenu vivant : turnViewDry (même code que turnView,
  sans rien toucher à la discussion, décision PROJ_SNAPSHOT via
  projSnapDecide), wireMessages, turnReasoning et buildChatPayload, extraits
  de runChat ; corps identique à l'octet près à celui d'avant (matrice
  d'options et de modes, testée contre une copie de l'ancien code)
- jamais devant un vrai travail : toute requête de Loki l'annule au départ
  (compteur de llm_slots.go, sous son verrou), sauf un tour de chat dont les
  messages sérialisés, outils et arguments du gabarit prolongent exactement
  le préfixe préchauffé
- moteur local, un seul slot (/props total_slots), pas pendant un tour, une
  tâche, un bench, un travail annexe ni une requête en vol ; un seul à la
  fois ; pas si le prochain tour compactera, ni à moins de 10 min de minuit
- requête brute : ni compaction, ni relance, ni effort appris, ni CtxUsed,
  ni stats ; erreurs lâchées (une ligne de journal par statut) ; rien persisté
- déclencheurs : fin de tour sans file d'attente, fin de tâche (projet forcé
  levé, slot effacé) ; full = aussi changement de discussion après 3 s
- capacités du dernier tour gardées en mémoire par discussion ; mode Code =
  rôle d'un message ordinaire, une demande de plan diverge et annule
- télémétrie kind=prewarm ; la vérification du cache RAM après une tâche se
  fait sur lui

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 09:01:57 +02:00
1 parent 4932999d5c
commit 630e84cc08
14 files changed
+1186 -86

No files matched your search

+43 -11
View File
@@ -233,7 +233,7 @@ func projSnapKey(caps Caps, cfg map[string]string) string {
// projSnapIgnoredKeys : réglages changés en cours de discussion sans relancer le
// moteur ni toucher au bloc. Les compter rafraîchirait pour rien.
var projSnapIgnoredKeys = map[string]bool{
"PROJ_SNAPSHOT": true, "REASONING_ECHO": true, "REASONING_EFFORT": true, "TEMP": true,
"PROJ_SNAPSHOT": true, "PREWARM": true, "REASONING_ECHO": true, "REASONING_EFFORT": true, "TEMP": true,
"COMPACT": true, "CRAWL4AI_URL": true, "CRAWL4AI_KEY": true, "WEB_ENGINE": true,
}
@@ -515,6 +515,20 @@ func replaceProjectHead(msgs []Message, block []Message) []Message {
// (sous c.mu, si l'epoch n'a pas changé). pt sert au rafraîchissement en cours
// de tour (runChatTools) ; nil sans la clé.
func (c *Conversation) turnView(caps Caps, epoch int, msgs []Message, inject bool) (sent []Message, tools []Tool, pt *projSnapTurn) {
return c.assembleTurn(caps, epoch, msgs, inject, false)
}
// turnViewDry : la séquence que turnView enverrait pour msgs, SANS rien toucher
// à la conversation — ni instantané pris, ni mise à jour rangée, ni nettoyage.
// Pour le préchauffage (chat_prewarm.go) : contenu vivant, mêmes décisions que
// le vrai tour. La mise à jour qu'il poserait en tête du dernier message n'y est
// pas : ce message-là est la sentinelle, hors du préfixe préchauffé.
func (c *Conversation) turnViewDry(caps Caps, msgs []Message, inject bool) ([]Message, []Tool) {
sent, tools, _ := c.assembleTurn(caps, 0, msgs, inject, true)
return sent, tools
}
func (c *Conversation) assembleTurn(caps Caps, epoch int, msgs []Message, inject, dry bool) (sent []Message, tools []Tool, pt *projSnapTurn) {
cfg := ReadConfig()
on := projSnapEnabled(cfg)
if !inject {
@@ -526,7 +540,7 @@ func (c *Conversation) turnView(caps Caps, epoch int, msgs []Message, inject boo
}
if !on {
c.mu.Lock()
if c.epoch == epoch && (c.ProjSnap != nil || hasContextUpdates(c.Messages)) {
if !dry && c.epoch == epoch && (c.ProjSnap != nil || hasContextUpdates(c.Messages)) {
c.ProjSnap = nil
c.Messages = stripContextUpdates(c.Messages)
}
@@ -559,6 +573,17 @@ func (c *Conversation) turnView(caps Caps, epoch int, msgs []Message, inject boo
block := live.msgs
c.mu.Lock()
if dry {
// Même décision que projSnapApplyLocked, appliquée à la seule copie.
if refresh, _, _ := projSnapDecide(c.ProjSnap, live, key, sysHash, asOf); refresh {
msgs = stripContextUpdates(msgs)
} else {
block = c.ProjSnap.Msgs
}
c.mu.Unlock()
final := append(append(append([]Message(nil), spMsgs...), block...), msgs...)
return InjectSkills(final, caps, tools), tools, nil
}
if c.epoch == epoch {
refresh, delta := c.projSnapApplyLocked(live, key, sysHash, asOf)
if refresh {
@@ -586,15 +611,7 @@ func (c *Conversation) turnView(caps Caps, epoch int, msgs []Message, inject boo
// que l'appelant l'applique à sa propre copie.
func (c *Conversation) projSnapApplyLocked(live projLive, key, sysHash, asOf string) (refresh bool, delta string) {
snap := c.ProjSnap
refresh = snap == nil || snap.Key != key || snap.SysHash != sysHash
tok := 0
if !refresh {
delta = projDelta(snap.State, live, asOf)
tok = len(delta) / 4
if delta != "" && snap.DeltaTok+tok > projSnapDeltaLimit(snap.SnapTok) {
refresh = true
}
}
refresh, delta, tok := projSnapDecide(snap, live, key, sysHash, asOf)
if refresh {
c.Messages = stripContextUpdates(c.Messages)
c.ProjSnap = newProjSnapshot(live, key, sysHash)
@@ -619,6 +636,21 @@ func (c *Conversation) projSnapApplyLocked(live projLive, key, sysHash, asOf str
return false, delta
}
// projSnapDecide : rafraîchir ou non, et la mise à jour à annoncer sinon (tok :
// sa taille estimée). Pur : projSnapApplyLocked l'applique, le préchauffage
// (turnViewDry) s'en sert pour prévoir le tour sans rien toucher.
func projSnapDecide(snap *projSnapshot, live projLive, key, sysHash, asOf string) (refresh bool, delta string, tok int) {
refresh = snap == nil || snap.Key != key || snap.SysHash != sysHash
if !refresh {
delta = projDelta(snap.State, live, asOf)
tok = len(delta) / 4
if delta != "" && snap.DeltaTok+tok > projSnapDeltaLimit(snap.SnapTok) {
refresh = true
}
}
return refresh, delta, tok
}
// --- rafraîchissement en cours de tour -------------------------------------
// projSnapTurn accompagne un tour de discussion avec la clé posée. runChatTools