mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 07:28:13 +02:00
Correctifs repris d'AJEAN et d'OpenFox : LSP, shell, clé web, MCP, recall, compactage, relances
- LSP (lsp.go) : un signal resté dans le tampon par une republication tardive faisait rendre « aucun diagnostic » à l'édition suivante, avant même que le serveur ait vu la nouvelle version. Le canal est vidé, une publication d'une version antérieure est ignorée, et check attend 350 ms de calme après la dernière publication (liste vide puis analyse : c'est l'analyse qui compte), toujours borné par lspDiagWait. Au passage : interblocage latent levé (writeMsg → markDead reprenait mu, déjà tenu par check) — dead devient atomique. - run_shell : ErrWaitDelay (commande finie, process laissé en arrière-plan sur les tubes) rend la sortie au lieu de « WaitDelay expired » qui l'effaçait (AJEAN 7403a8a0). - Clé de pilotage web : lue depuis un cache à chaque requête au lieu d'ouvrir la base sous dbMu (AJEAN #120). Lectures réussies seulement, invalidé par toute écriture du bucket d'état, par la date/taille du fichier (autre process) et au bout d'une seconde. - MCP : l'appel suit le contexte du tour (le bouton stop l'annule), délai par serveur (callTimeoutSec) et allongé par l'argument timeout de l'outil, plafonné à 30 min ; un délai dépassé n'est plus relancé (OpenFox 2ef584b8). - Recall : les blocs d'une compaction s'archivent en une seule transaction au lieu d'une par bloc (AJEAN #130). - Compactage en cours de tour : après un essai qui n'a rien réduit, pas de nouvel essai avant +5 % de la fenêtre (sauf à 90 %), pour les tâches et sous-agents aussi ; la cause d'un résumé raté est journalisée (AJEAN 68659e73). - Motifs de relance : un motif vide (qui correspondait à tout) est écarté, une surcharge sans motif utilisable retombe sur l'embarqué, et un changement de retry_patterns agit sans redémarrer (OpenFox 8e9b4869, c52c5bf7). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
00e09a71e3
commit
01c11be4ae
21 files changed
+967
-104
No files matched your search
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -374,7 +375,11 @@ func isMCPTool(name string) bool {
|
||||
|
||||
// mcpCall exécute un outil MCP à partir de son nom exposé et renvoie une chaîne
|
||||
// résultat (texte aplati), cappée. Reconnecte une fois si la session est morte.
|
||||
func mcpCall(name string, args map[string]any) string {
|
||||
//
|
||||
// parent est le contexte DU TOUR, comme pour runShell : l'appel naissait d'un
|
||||
// context.Background(), donc le bouton stop n'arrêtait rien et le tour restait
|
||||
// bloqué jusqu'au bout des 120 s (OpenFox 2ef584b8).
|
||||
func mcpCall(parent context.Context, name string, args map[string]any) string {
|
||||
if facadeMCP() { // le pool est celui de la façade (facade_mcp.go)
|
||||
return facadeMCPCall(name, args)
|
||||
}
|
||||
@@ -384,35 +389,92 @@ func mcpCall(name string, args map[string]any) string {
|
||||
if !ok {
|
||||
return "[erreur] outil MCP inconnu ou serveur non connecté: " + name
|
||||
}
|
||||
servers, _ := LoadMCPConfig()
|
||||
cfg, exists := servers[ref.server]
|
||||
timeout := mcpCallTimeoutFor(cfg, args)
|
||||
|
||||
result, err := mcpCallOnce(ref, args)
|
||||
result, err := mcpCallOnce(parent, ref, args, timeout)
|
||||
if err != nil {
|
||||
if parent.Err() != nil {
|
||||
return "[appel MCP interrompu]"
|
||||
}
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
// Le serveur est vivant mais l'outil est lent : le relancer
|
||||
// doublerait l'attente (et l'effet de l'outil).
|
||||
return fmt.Sprintf("[erreur MCP] pas de réponse après %s (délai de l'appel ; un argument timeout plus grand l'allonge)", timeout.Round(time.Second))
|
||||
}
|
||||
// Session peut-être morte (process stdio tombé) : on l'invalide et on
|
||||
// retente une fois avec une reconnexion fraîche.
|
||||
servers, _ := LoadMCPConfig()
|
||||
cfg, exists := servers[ref.server]
|
||||
if exists && cfg.Enabled {
|
||||
mcpInvalidate(ref.server)
|
||||
mcpMgr.ensure(ref.server, cfg)
|
||||
if result2, err2 := mcpCallOnce(ref, args); err2 == nil {
|
||||
if result2, err2 := mcpCallOnce(parent, ref, args, timeout); err2 == nil {
|
||||
return result2
|
||||
} else {
|
||||
err = err2
|
||||
}
|
||||
if parent.Err() != nil {
|
||||
return "[appel MCP interrompu]"
|
||||
}
|
||||
}
|
||||
return "[erreur MCP] " + err.Error()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mcpCallOnce(ref mcpToolRef, args map[string]any) (string, error) {
|
||||
// mcpCallTimeoutMax plafonne le délai d'un appel, quoi que demandent l'outil
|
||||
// ou la config : au-delà, c'est un job d'arrière-plan, pas un appel d'outil.
|
||||
const mcpCallTimeoutMax = 30 * time.Minute
|
||||
|
||||
// mcpCallTimeoutFor : délai d'un appel. Défaut mcpCallTimeout, remplacé par
|
||||
// CallTimeoutSec du serveur ; un argument « timeout » de l'outil lui-même
|
||||
// (beaucoup en ont un : navigateur, build, requête) l'allonge de 30 s pour que
|
||||
// l'outil ait le temps de rendre SA propre erreur de délai.
|
||||
func mcpCallTimeoutFor(cfg MCPServerConfig, args map[string]any) time.Duration {
|
||||
d := mcpCallTimeout
|
||||
if cfg.CallTimeoutSec > 0 {
|
||||
d = time.Duration(cfg.CallTimeoutSec) * time.Second
|
||||
}
|
||||
if t := mcpArgTimeout(args); t > 0 && t+30*time.Second > d {
|
||||
d = t + 30*time.Second
|
||||
}
|
||||
return min(d, mcpCallTimeoutMax)
|
||||
}
|
||||
|
||||
// mcpArgTimeout lit l'argument « timeout » d'un appel. Les serveurs MCP ne
|
||||
// s'accordent pas sur l'unité : au-delà de 3600 on le lit en millisecondes
|
||||
// (5000 = 5 s), en deçà en secondes (600 = 10 min).
|
||||
func mcpArgTimeout(args map[string]any) time.Duration {
|
||||
var v float64
|
||||
switch x := args["timeout"].(type) {
|
||||
case float64:
|
||||
v = x
|
||||
case int:
|
||||
v = float64(x)
|
||||
case int64:
|
||||
v = float64(x)
|
||||
case json.Number:
|
||||
v, _ = x.Float64()
|
||||
case string:
|
||||
v, _ = strconv.ParseFloat(strings.TrimSpace(x), 64)
|
||||
}
|
||||
if v <= 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 3600 {
|
||||
return time.Duration(v) * time.Millisecond
|
||||
}
|
||||
return time.Duration(v * float64(time.Second))
|
||||
}
|
||||
|
||||
func mcpCallOnce(parent context.Context, ref mcpToolRef, args map[string]any, timeout time.Duration) (string, error) {
|
||||
mcpMgr.mu.Lock()
|
||||
s := mcpMgr.sessions[ref.server]
|
||||
mcpMgr.mu.Unlock()
|
||||
if s == nil || s.sess == nil {
|
||||
return "", fmt.Errorf("serveur %s non connecté", ref.server)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), mcpCallTimeout)
|
||||
ctx, cancel := context.WithTimeout(parent, timeout)
|
||||
defer cancel()
|
||||
res, err := s.sess.CallTool(ctx, &mcpsdk.CallToolParams{Name: ref.tool, Arguments: args})
|
||||
if err != nil {
|
||||
|
||||
Reference in new issue
Block a user