mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 14:08:11 +02:00
Compaction : COMPACT_CONTINUATION, relecture — un refus mémorisé ne vaut que pour le même fil
Le refus mémorisé ne regardait que la taille du contexte. Il n'est évalué qu'au-dessus du seuil : une discussion vidée, éditée ou régénérée qui remontait dans la même plage de jetons voyait sa compaction sautée sur la foi d'un refus qui concernait un autre fil. - le refus garde l'empreinte de l'historique (celle de perfPrefix) ; seul un historique qui prolonge celui d'alors en profite, tout autre retente - testé : historique modifié ou raccourci, refus noté par une vraie compaction, filet réactif jamais bloqué Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2097c9f915
commit
b0c53e32af
5 files changed
+83
-32
No files matched your search
@@ -428,8 +428,9 @@ Ajoutées par ce fork :
|
|||||||
modèle jusqu'au redémarrage (gpt-oss à intensité haute peut épuiser le budget
|
modèle jusqu'au redémarrage (gpt-oss à intensité haute peut épuiser le budget
|
||||||
en réflexion : repli). La clé évite aussi un résumé voué au refus (même vide,
|
en réflexion : repli). La clé évite aussi un résumé voué au refus (même vide,
|
||||||
il ne réduirait pas le contexte de 20 %) et, après un refus faute de réduction,
|
il ne réduirait pas le contexte de 20 %) et, après un refus faute de réduction,
|
||||||
n'en redemande pas avant que le contexte ait grossi de 10 % — sauf à 90 % de la
|
n'en redemande pas avant que le contexte ait grossi de 10 % sur le même fil —
|
||||||
fenêtre. Le filet réactif (prompt refusé), le bouton « compacter », les tâches,
|
sauf à 90 % de la fenêtre ou après une édition, une régénération ou un fil
|
||||||
|
vidé. Le filet réactif (prompt refusé), le bouton « compacter », les tâches,
|
||||||
les sous-agents et un preset externe gardent le chemin d'avant. Visible dans la
|
les sous-agents et un preset externe gardent le chemin d'avant. Visible dans la
|
||||||
télémétrie sous `compact`.
|
télémétrie sous `compact`.
|
||||||
- **Discussions multiples** : historique complet dans la barre latérale, titre
|
- **Discussions multiples** : historique complet dans la barre latérale, titre
|
||||||
|
|||||||
@@ -48,8 +48,9 @@ package loki
|
|||||||
// garantie de réduction (compactHopeless, borne exacte) ;
|
// garantie de réduction (compactHopeless, borne exacte) ;
|
||||||
// - une compaction refusée parce que le résumé obtenu était trop long n'est
|
// - une compaction refusée parce que le résumé obtenu était trop long n'est
|
||||||
// retentée, en début ou fin de tour et entre deux étapes, qu'une fois le
|
// retentée, en début ou fin de tour et entre deux étapes, qu'une fois le
|
||||||
// contexte grossi de 10 % — jamais pour le filet réactif, le bouton
|
// contexte grossi de 10 % sur le même fil (un historique réécrit retente)
|
||||||
// « compacter », une fenêtre pleine à 90 % ou plus.
|
// — jamais pour le filet réactif, le bouton « compacter », une fenêtre
|
||||||
|
// pleine à 90 % ou plus.
|
||||||
//
|
//
|
||||||
// Réactif (prompt refusé par le moteur), fenêtre pleine en pleine génération,
|
// Réactif (prompt refusé par le moteur), fenêtre pleine en pleine génération,
|
||||||
// bouton manuel, tâches, sous-agents, terminal : chemin d'avant, toujours.
|
// bouton manuel, tâches, sous-agents, terminal : chemin d'avant, toujours.
|
||||||
@@ -159,7 +160,7 @@ func compactMessagesNoted(ctx context.Context, msgs []Message, caps Caps, opt co
|
|||||||
if changed {
|
if changed {
|
||||||
compactRefusedClear(opt.conv)
|
compactRefusedClear(opt.conv)
|
||||||
} else if refused {
|
} else if refused {
|
||||||
compactRefusedNote(opt.conv, opt.used)
|
compactRefusedNote(opt.conv, opt.used, msgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, changed
|
return out, changed
|
||||||
@@ -168,10 +169,22 @@ func compactMessagesNoted(ctx context.Context, msgs []Message, caps Caps, opt co
|
|||||||
// --- refus mémorisés --------------------------------------------------------
|
// --- refus mémorisés --------------------------------------------------------
|
||||||
|
|
||||||
// compactRefusal : une compaction refusée faute de réduction, à at jetons de
|
// compactRefusal : une compaction refusée faute de réduction, à at jetons de
|
||||||
// contexte. En mémoire seulement : un redémarrage retente.
|
// contexte, sur un historique de n messages d'empreinte hash. En mémoire
|
||||||
|
// seulement : un redémarrage retente.
|
||||||
type compactRefusal struct {
|
type compactRefusal struct {
|
||||||
at, window int
|
at, window int
|
||||||
model string
|
model string
|
||||||
|
n int
|
||||||
|
hash uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// historyHash : empreinte d'un historique (celle de perfPrefix).
|
||||||
|
func historyHash(msgs []Message) uint64 {
|
||||||
|
if len(msgs) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
p := perfPrefix(msgs)
|
||||||
|
return p[len(p)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
var compactRefused struct {
|
var compactRefused struct {
|
||||||
@@ -181,23 +194,25 @@ var compactRefused struct {
|
|||||||
|
|
||||||
const compactRefusedMax = 64
|
const compactRefusedMax = 64
|
||||||
|
|
||||||
// compactRefusedSkip : la dernière compaction de conv a été refusée et le
|
// compactRefusedSkip : la dernière compaction de conv a été refusée, msgs ne
|
||||||
// contexte n'a pas grossi de 10 % depuis. Jamais à 90 % de la fenêtre ou plus ;
|
// fait que prolonger l'historique d'alors, et le contexte n'a pas grossi de
|
||||||
// oublié dès que le contexte baisse (compaction, réécriture, nouvelle
|
// 10 % depuis. Jamais à 90 % de la fenêtre ou plus ; oublié dès que
|
||||||
// discussion) ou que le modèle ou la fenêtre changent.
|
// l'historique a été réécrit (compaction, édition, régénération, discussion
|
||||||
func compactRefusedSkip(conv string, used int) bool {
|
// vidée), que le contexte baisse, ou que le modèle ou la fenêtre changent.
|
||||||
|
func compactRefusedSkip(conv string, used int, msgs []Message) bool {
|
||||||
if conv == "" || used <= 0 || !compactContinuationOn(ReadConfig()) {
|
if conv == "" || used <= 0 || !compactContinuationOn(ReadConfig()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
model, window := engineMainNow()
|
model, window := engineMainNow()
|
||||||
compactRefused.mu.Lock()
|
compactRefused.mu.Lock()
|
||||||
defer compactRefused.mu.Unlock()
|
|
||||||
r, ok := compactRefused.m[conv]
|
r, ok := compactRefused.m[conv]
|
||||||
|
compactRefused.mu.Unlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if r.model != model || r.window != window || used < r.at || used >= r.at+r.at/10 {
|
if r.model != model || r.window != window || used < r.at || used >= r.at+r.at/10 ||
|
||||||
delete(compactRefused.m, conv)
|
len(msgs) < r.n || historyHash(msgs[:r.n]) != r.hash {
|
||||||
|
compactRefusedClear(conv)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if used*10 >= window*9 {
|
if used*10 >= window*9 {
|
||||||
@@ -207,11 +222,12 @@ func compactRefusedSkip(conv string, used int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func compactRefusedNote(conv string, used int) {
|
func compactRefusedNote(conv string, used int, msgs []Message) {
|
||||||
if conv == "" || used <= 0 {
|
if conv == "" || used <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
model, window := engineMainNow()
|
model, window := engineMainNow()
|
||||||
|
hash := historyHash(msgs)
|
||||||
compactRefused.mu.Lock()
|
compactRefused.mu.Lock()
|
||||||
defer compactRefused.mu.Unlock()
|
defer compactRefused.mu.Unlock()
|
||||||
if compactRefused.m == nil {
|
if compactRefused.m == nil {
|
||||||
@@ -223,7 +239,7 @@ func compactRefusedNote(conv string, used int) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
compactRefused.m[conv] = compactRefusal{at: used, window: window, model: model}
|
compactRefused.m[conv] = compactRefusal{at: used, window: window, model: model, n: len(msgs), hash: hash}
|
||||||
}
|
}
|
||||||
|
|
||||||
func compactRefusedClear(conv string) {
|
func compactRefusedClear(conv string) {
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ func TestCompactContDefautIdentique(t *testing.T) {
|
|||||||
if stamp.seq != 0 {
|
if stamp.seq != 0 {
|
||||||
t.Fatal("tampon de slot posé sans la clé")
|
t.Fatal("tampon de slot posé sans la clé")
|
||||||
}
|
}
|
||||||
if compactContinuationOn(ReadConfig()) || compactRefusedSkip(convActiveID(), 7000) {
|
if compactContinuationOn(ReadConfig()) || compactRefusedSkip(convActiveID(), 7000, nil) {
|
||||||
t.Fatal("continuation ou mémoire des refus active sans la clé")
|
t.Fatal("continuation ou mémoire des refus active sans la clé")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -538,6 +538,25 @@ func TestCompactHopeless(t *testing.T) {
|
|||||||
if changed || !refused || len(m.all()) != 1 {
|
if changed || !refused || len(m.all()) != 1 {
|
||||||
t.Fatalf("sans pré-contrôle : changed=%v refused=%v requêtes=%d", changed, refused, len(m.all()))
|
t.Fatalf("sans pré-contrôle : changed=%v refused=%v requêtes=%d", changed, refused, len(m.all()))
|
||||||
}
|
}
|
||||||
|
// Un refus après résumé est noté ; il n'arrête ni le filet réactif ni le
|
||||||
|
// bouton manuel (compactMessages, sans options), qui redemandent un résumé.
|
||||||
|
if _, changed := compactMessagesNoted(context.Background(), msgs, Caps{}, compactOpts{conv: "c9", used: 7000}); changed {
|
||||||
|
t.Fatal("compaction acceptée")
|
||||||
|
}
|
||||||
|
compactRefused.mu.Lock()
|
||||||
|
_, noted := compactRefused.m["c9"]
|
||||||
|
compactRefused.mu.Unlock()
|
||||||
|
if !noted {
|
||||||
|
t.Fatal("refus non noté")
|
||||||
|
}
|
||||||
|
if err := SetConfigKey("COMPACT_CONTINUATION", "on"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before := len(m.all())
|
||||||
|
compactMessages(context.Background(), msgs, Caps{})
|
||||||
|
if len(m.all()) != before+1 {
|
||||||
|
t.Fatal("le chemin réactif ne doit pas tenir compte des refus mémorisés")
|
||||||
|
}
|
||||||
// Un cas qui réussit n'est jamais déclaré sans issue.
|
// Un cas qui réussit n'est jamais déclaré sans issue.
|
||||||
h := contHistory()
|
h := contHistory()
|
||||||
head, tailStart = compactBounds(h, int(float64(estimateTokens(h))*compactTailFrac))
|
head, tailStart = compactBounds(h, int(float64(estimateTokens(h))*compactTailFrac))
|
||||||
@@ -547,40 +566,55 @@ func TestCompactHopeless(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Mémoire des refus : pas de nouvel essai avant +10 % de contexte, jamais à
|
// Mémoire des refus : pas de nouvel essai avant +10 % de contexte, jamais à
|
||||||
// 90 % de la fenêtre, oubliée quand le contexte baisse ; rien sans la clé.
|
// 90 % de la fenêtre, oubliée quand le contexte baisse ou que l'historique est
|
||||||
|
// réécrit ; rien sans la clé.
|
||||||
func TestCompactRefusedSkip(t *testing.T) {
|
func TestCompactRefusedSkip(t *testing.T) {
|
||||||
testHome(t)
|
testHome(t)
|
||||||
contReset(t)
|
contReset(t)
|
||||||
if err := SetConfigKey("CTX", "65536"); err != nil {
|
if err := SetConfigKey("CTX", "65536"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
compactRefusedNote("c1", 50000)
|
h := contHistory()
|
||||||
if compactRefusedSkip("c1", 52000) {
|
grown := append(append([]Message(nil), h...), um("suite"))
|
||||||
|
compactRefusedNote("c1", 50000, h)
|
||||||
|
if compactRefusedSkip("c1", 52000, grown) {
|
||||||
t.Fatal("sans la clé, aucun essai ne doit être sauté")
|
t.Fatal("sans la clé, aucun essai ne doit être sauté")
|
||||||
}
|
}
|
||||||
if err := SetConfigKey("COMPACT_CONTINUATION", "on"); err != nil {
|
if err := SetConfigKey("COMPACT_CONTINUATION", "on"); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !compactRefusedSkip("c1", 52000) || compactRefusedSkip("c2", 52000) {
|
if !compactRefusedSkip("c1", 52000, grown) || compactRefusedSkip("c2", 52000, grown) {
|
||||||
t.Fatal("refus mal appliqué")
|
t.Fatal("refus mal appliqué")
|
||||||
}
|
}
|
||||||
if compactRefusedSkip("c1", 59000) {
|
if compactRefusedSkip("c1", 59000, grown) {
|
||||||
t.Fatal("essai sauté à 90 % de la fenêtre")
|
t.Fatal("essai sauté à 90 % de la fenêtre")
|
||||||
}
|
}
|
||||||
if compactRefusedSkip("c1", 55000) {
|
if compactRefusedSkip("c1", 55000, grown) {
|
||||||
t.Fatal("essai sauté après +10 %")
|
t.Fatal("essai sauté après +10 %")
|
||||||
}
|
}
|
||||||
if compactRefusedSkip("c1", 52000) {
|
if compactRefusedSkip("c1", 52000, grown) {
|
||||||
t.Fatal("refus non oublié après +10 %")
|
t.Fatal("refus non oublié après +10 %")
|
||||||
}
|
}
|
||||||
compactRefusedNote("c1", 50000)
|
compactRefusedNote("c1", 50000, h)
|
||||||
if compactRefusedSkip("c1", 30000) || compactRefusedSkip("c1", 50000) {
|
if compactRefusedSkip("c1", 30000, grown) || compactRefusedSkip("c1", 50000, grown) {
|
||||||
t.Fatal("refus non oublié quand le contexte baisse")
|
t.Fatal("refus non oublié quand le contexte baisse")
|
||||||
}
|
}
|
||||||
// Une compaction refusée note le refus ; réussie, elle l'efface.
|
// Historique réécrit (édition, régénération, discussion vidée puis
|
||||||
compactRefusedNote("c1", 50000)
|
// regarnie) : même taille de contexte, autre fil — on retente.
|
||||||
|
compactRefusedNote("c1", 50000, h)
|
||||||
|
edited := append([]Message(nil), grown...)
|
||||||
|
edited[3] = um("message modifié")
|
||||||
|
if compactRefusedSkip("c1", 51000, edited) || compactRefusedSkip("c1", 51000, grown) {
|
||||||
|
t.Fatal("refus gardé pour un historique réécrit")
|
||||||
|
}
|
||||||
|
compactRefusedNote("c1", 50000, h)
|
||||||
|
if compactRefusedSkip("c1", 51000, h[:5]) {
|
||||||
|
t.Fatal("refus gardé pour un historique raccourci")
|
||||||
|
}
|
||||||
|
// Une compaction réussie l'efface.
|
||||||
|
compactRefusedNote("c1", 50000, h)
|
||||||
compactRefusedClear("c1")
|
compactRefusedClear("c1")
|
||||||
if compactRefusedSkip("c1", 51000) {
|
if compactRefusedSkip("c1", 51000, grown) {
|
||||||
t.Fatal("refus non effacé")
|
t.Fatal("refus non effacé")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -804,7 +804,7 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
|||||||
// tout de suite, et le résumé peut prolonger le prompt en cache. Sans la
|
// tout de suite, et le résumé peut prolonger le prompt en cache. Sans la
|
||||||
// clé, ni l'un ni l'autre.
|
// clé, ni l'un ni l'autre.
|
||||||
conv := perfTagOf(ctx).conv
|
conv := perfTagOf(ctx).conv
|
||||||
if compactNeeded(msgs, ctxUsed, peak) && !compactRefusedSkip(conv, ctxUsed) {
|
if compactNeeded(msgs, ctxUsed, peak) && !compactRefusedSkip(conv, ctxUsed, msgs) {
|
||||||
opt := c.compactOptsFor(caps, msgs, ctxUsed, conv)
|
opt := c.compactOptsFor(caps, msgs, ctxUsed, conv)
|
||||||
if out, changed := c.compactAndPublish(ctx, epoch, "début-tour", msgs, ctxUsed, caps, opt); changed {
|
if out, changed := c.compactAndPublish(ctx, epoch, "début-tour", msgs, ctxUsed, caps, opt); changed {
|
||||||
msgs = out
|
msgs = out
|
||||||
@@ -1017,7 +1017,7 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
|||||||
// retombe tout de suite et le tour suivant démarre avec de la marge.
|
// retombe tout de suite et le tour suivant démarre avec de la marge.
|
||||||
// Sauf si un Reset est passé (rien à compacter) ou si le tour a été annulé
|
// Sauf si un Reset est passé (rien à compacter) ou si le tour a été annulé
|
||||||
// (bouton stop) : on n'enchaîne pas plusieurs secondes de résumé sur un stop.
|
// (bouton stop) : on n'enchaîne pas plusieurs secondes de résumé sur un stop.
|
||||||
if stale || ctx.Err() != nil || !compactNeeded(msgs, ctxUsed, peak) || compactRefusedSkip(conv, ctxUsed) {
|
if stale || ctx.Err() != nil || !compactNeeded(msgs, ctxUsed, peak) || compactRefusedSkip(conv, ctxUsed, msgs) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// context.Background() et non ctx : le tour est terminé, son contexte peut
|
// context.Background() et non ctx : le tour est terminé, son contexte peut
|
||||||
|
|||||||
@@ -2501,7 +2501,7 @@ func runChatTools(ctx context.Context, messages []Message, tools []Tool, tempera
|
|||||||
if ptag.kind == perfMain {
|
if ptag.kind == perfMain {
|
||||||
mainConv = ptag.conv
|
mainConv = ptag.conv
|
||||||
}
|
}
|
||||||
if compactNeeded(messages, used, peakGen) && !compactRefusedSkip(mainConv, used) {
|
if compactNeeded(messages, used, peakGen) && !compactRefusedSkip(mainConv, used, messages) {
|
||||||
opt := compactOptsMid(chatCfg, ep, mainConv, messages, used, tools, reasoningEffort, reasoningKwargs, pol,
|
opt := compactOptsMid(chatCfg, ep, mainConv, messages, used, tools, reasoningEffort, reasoningKwargs, pol,
|
||||||
disableTools || toolChoiceNone)
|
disableTools || toolChoiceNone)
|
||||||
yes, no := true, false
|
yes, no := true, false
|
||||||
|
|||||||
Reference in new issue
Block a user