From 2daea32ade8495502f0ac94718ba6d19ae24bce9 Mon Sep 17 00:00:00 2001 From: nathaninline Date: Tue, 21 Jul 2026 16:18:50 +0200 Subject: [PATCH] compaction: decider sur le VRAI decompte de tokens (ctx_used du client) au lieu de l'estimation L'heuristique ~4 char/token sous-estimait le contexte (elle ne voit ni le prompt systeme injecte ni le gabarit de chat), donc le seuil 75% n'etait jamais franchi. Le client envoie desormais ctx_used (prompt_tokens_total deja affiche) ; repli sur l'estimation quand inconnu (terminal). --- chat.go | 2 +- compact.go | 15 +++++++++++++-- ui/index.html | 2 +- web.go | 6 +++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/chat.go b/chat.go index c7af94d..09e7e4b 100644 --- a/chat.go +++ b/chat.go @@ -69,7 +69,7 @@ func cmdChat(args []string) error { caps := globalCaps() // Compaction proactive (façon Hermes) : on résume les vieux tours quand // l'historique dépasse le seuil, au lieu d'imposer un /reset. - if compacted, changed := MaybeCompact(context.Background(), msgs, caps); changed { + if compacted, changed := MaybeCompact(context.Background(), msgs, caps, 0); changed { msgs = compacted fmt.Println(dim("[contexte compacté pour tenir dans la fenêtre]")) } diff --git a/compact.go b/compact.go index 530bb01..7fde3be 100644 --- a/compact.go +++ b/compact.go @@ -96,11 +96,22 @@ func estimateTokens(msgs []Message) int { // proactif. Renvoie l'historique (compacté ou inchangé) et un booléen indiquant // s'il a changé. À appeler sur l'historique BRUT (avant InjectSkills) pour que // le résultat puisse être renvoyé au client sans le préfixe système injecté. -func MaybeCompact(ctx context.Context, msgs []Message, caps Caps) ([]Message, bool) { +// +// knownTokens = taille RÉELLE du contexte au tour précédent (usage.prompt_tokens +// + tokens générés), telle que rapportée par llama.cpp et affichée par l'UI. On +// la préfère à estimateTokens() car cette dernière n'est qu'une heuristique et, +// surtout, ne « voit » pas le prompt système injecté (machine briefing) ni le +// gabarit de chat — donc elle sous-estime largement le vrai contexte. 0 = inconnu +// (clients sans compteur, ex. terminal) → repli sur l'estimation. +func MaybeCompact(ctx context.Context, msgs []Message, caps Caps, knownTokens int) ([]Message, bool) { if !compactEnabled() { return msgs, false } - if estimateTokens(msgs) < int(float64(ctxWindow())*compactTriggerFrac) { + used := knownTokens + if used <= 0 { + used = estimateTokens(msgs) + } + if used < int(float64(ctxWindow())*compactTriggerFrac) { return msgs, false } return compactMessages(ctx, msgs, caps) diff --git a/ui/index.html b/ui/index.html index 19f2011..bbdcb24 100644 --- a/ui/index.html +++ b/ui/index.html @@ -1445,7 +1445,7 @@ async function send(){ try{ const sys=(document.getElementById('sysprompt').value||'').trim(); const out = sys ? [{role:'system',content:sys}, ...msgs] : msgs; - const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:out}),signal:abortCtrl.signal}); + const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({messages:out,ctx_used:CTX_USED}),signal:abortCtrl.signal}); const reader=r.body.getReader(); const dec=new TextDecoder(); let buf=''; statsTimer = setInterval(updateStats, 250); while(true){ diff --git a/web.go b/web.go index 62b6e98..f17d3cd 100644 --- a/web.go +++ b/web.go @@ -783,6 +783,10 @@ type chatReq struct { Skills *bool `json:"skills"` // Override par requête de l'accès internet (outils web). nil = config machine. Internet *bool `json:"internet"` + // Taille réelle du contexte au tour précédent (usage.prompt_tokens + tokens + // générés), rapportée par le client qui l'affiche déjà. Sert à décider du + // compactage sur le VRAI décompte plutôt qu'une estimation. 0 = inconnu. + CtxUsed int `json:"ctx_used"` } // sseHeartbeat garde la réponse SSE active en écrivant un commentaire (`: ping`, @@ -846,7 +850,7 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any) // le résume AVANT le tour et on renvoie l'historique compacté au client pour // qu'il remplace le sien — évite de re-renvoyer tout à chaque tour. hist := body.Messages - if compacted, changed := MaybeCompact(ctx, hist, caps); changed { + if compacted, changed := MaybeCompact(ctx, hist, caps, body.CtxUsed); changed { hist = compacted emit(map[string]any{"history_replace": hist}) }