CI : gofmt + gopls sous Go 1.25 ; MCP : premier lancement npx/uvx patient

- gofmt sur trois fichiers oubliés par le dernier commit (ci/test rouge).
- gopls@latest exige Go ≥ 1.26 alors que l'image de build est en 1.25 avec
  GOTOOLCHAIN=local : l'étape Docker échouait net. GOTOOLCHAIN=auto télécharge
  le toolchain requis, borné à l'étape de build (build-push GHCR rouge).
- Serveurs MCP : npx/uvx téléchargent leur paquet au premier lancement, et le
  délai de connexion de 20 s tombait dessus (« enregistré mais connexion
  échouée : context deadline exceeded » depuis le catalogue). Délai à froid
  de 3 min pour ces deux lanceurs, et erreur de délai réécrite pour dire quoi
  faire (réessayer : le paquet reste en cache).
- README : note sur ce premier lancement, ligne mode Code dans le tableau
  des différences.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-18 22:09:37 +02:00
1 parent 901d518f70
commit 53d23418e9
6 files changed
+55 -10

No files matched your search

+5 -1
View File
@@ -25,7 +25,11 @@ COPY tools/ tools/
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/loki ./cmd/loki
# gopls : serveur LSP Go, consommé par le vérificateur de code du mode code
# (lsp.go). Compilé ici parce que l'image finale n'a pas de toolchain Go.
RUN GOBIN=/out go install golang.org/x/tools/gopls@latest
# GOTOOLCHAIN=auto : gopls@latest exige régulièrement un Go plus récent que
# celui de l'image (v0.23 veut ≥ 1.26 quand l'image est en 1.25) ; en local,
# l'installation échouait net. Auto télécharge le toolchain requis — coût
# limité à cette étape de build.
RUN GOBIN=/out GOTOOLCHAIN=auto go install golang.org/x/tools/gopls@latest
# ── Étape 2 : runtime sur l'image serveur CUDA officielle ───────────────
FROM ${LLAMACPP_IMAGE} AS runtime
+5 -1
View File
@@ -165,7 +165,10 @@ Héritées d'AJEAN :
[Crawl4AI](https://github.com/unclecode/crawl4ai) pour les pages JS.
- **Agent** : shell, fichiers, workspace (`agent on`).
- **Serveurs MCP** : Node.js (`npx`) et uv (`uvx`) sont inclus dans l'image, pour
les serveurs écrits en JavaScript comme en Python.
les serveurs écrits en JavaScript comme en Python. Au **premier** lancement,
`npx`/`uvx` téléchargent le paquet du serveur — Loki attend jusqu'à 3 minutes
ce coup-là (au lieu d'échouer sur « context deadline exceeded ») ; les
lancements suivants partent du cache en quelques secondes.
- **Presets** de configuration par modèle, bench, auto-détection GPU.
- **API OpenAI-compatible** exposable, protégée par clé (voir ci-dessous — ce
fork la sert autrement que l'amont).
@@ -307,6 +310,7 @@ Retirées par ce fork :
| Configuration initiale | `ajean edit` ($EDITOR) | entrypoint + `loki config set` |
| Choix du modèle | lien Hugging Face collé à la main | recherche intégrée + verdict VRAM + projecteur lié |
| Historique de tchat | conversation unique | discussions multiples, titrées et persistées |
| Agent de code | — | mode Code : critères d'acceptation, passe de vérification, LSP, outils git |
| Accès distant | relais chiffré ajean.link | retiré de l'interface |
| Endpoint OpenAI | `:8080/v1` du moteur, ouvert par `network on` | `/v1` servi par Loki sur le port de l'interface, protégé par la clé API |
| Mise à jour | `ajean update` (binaire GitHub) | `docker compose pull` |
+1
View File
@@ -344,6 +344,7 @@ type StreamEvent struct {
// ajouter.
NewHistory []Message
}
// AskEvent : l'outil ask — question structurée posée à l'utilisateur, rendue
// par l'UI comme une carte à boutons. Le tour se termine juste après.
type AskEvent struct {
+38 -2
View File
@@ -3,10 +3,12 @@ package loki
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
@@ -37,6 +39,13 @@ const (
// mcpConnectTimeout borne l'établissement d'une session (handshake initialize
// + tools/list). Un serveur qui rame ne doit pas figer le tour de chat.
mcpConnectTimeout = 20 * time.Second
// mcpConnectTimeoutCold : npx et uvx TÉLÉCHARGENT le paquet du serveur au
// premier lancement (le catalogue les utilise tous les deux). Sur un réseau
// ou un disque lents, ça dépasse largement 20 s, et l'utilisateur voyait
// « enregistré mais connexion échouée : context deadline exceeded » pour un
// serveur parfaitement sain. Les lancements suivants profitent du cache et
// retombent dans le régime normal.
mcpConnectTimeoutCold = 3 * time.Minute
// mcpCallTimeout borne un appel d'outil MCP.
mcpCallTimeout = 120 * time.Second
// mcpMaxOutput cap la sortie renvoyée au modèle (cohérent avec toolMaxOutput).
@@ -174,11 +183,11 @@ func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
m.mu.Unlock()
// Connexion hors-lock (peut être lente).
ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout)
ctx, cancel := context.WithTimeout(context.Background(), mcpTimeoutFor(cfg))
defer cancel()
sess, err := mcpConnect(ctx, name, cfg)
if err != nil {
s.err = err
s.err = mcpFriendlyErr(err, cfg)
} else {
s.sess = sess
lt, lerr := sess.ListTools(ctx, nil)
@@ -509,3 +518,30 @@ func MCPStatus() ([]MCPServerStatus, error) {
}
return out, nil
}
// mcpTimeoutFor choisit le délai de connexion d'un serveur : long pour un
// lanceur qui télécharge son paquet à la volée (npx, uvx), court sinon.
func mcpTimeoutFor(cfg MCPServerConfig) time.Duration {
base := strings.ToLower(filepath.Base(cfg.Command))
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
if base == "npx" || base == "uvx" {
return mcpConnectTimeoutCold
}
return mcpConnectTimeout
}
// mcpFriendlyErr habille l'erreur de connexion la plus vécue : le délai
// dépassé d'un npx/uvx en plein téléchargement. L'erreur brute (« context
// deadline exceeded ») envoyait l'utilisateur chercher un bug de config alors
// qu'il suffit souvent de réessayer une fois le paquet en cache.
func mcpFriendlyErr(err error, cfg MCPServerConfig) error {
if err == nil || !errors.Is(err, context.DeadlineExceeded) {
return err
}
base := strings.ToLower(filepath.Base(cfg.Command))
base = strings.TrimSuffix(strings.TrimSuffix(base, ".exe"), ".cmd")
if base == "npx" || base == "uvx" {
return fmt.Errorf("délai dépassé — %s télécharge le paquet du serveur au premier lancement, ce qui peut être long ; clique « tester » pour réessayer (le paquet reste en cache), et vérifie que le conteneur atteint le registre (npm/PyPI)", base)
}
return fmt.Errorf("délai dépassé — le serveur n'a pas répondu au handshake en %s", mcpConnectTimeout)
}
+2 -2
View File
@@ -61,8 +61,8 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
"port": LLMPort(),
"ctx": ctx,
"version": Version,
"warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien
"load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon
"warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien
"load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon
})
}
+4 -4
View File
@@ -267,10 +267,10 @@ func newWebMux() *http.ServeMux {
api("/api/conversations/delete", handleConvDelete)
api("/api/chat/mode", handleChatMode) // GET mode+critères / POST bascule Chat|Code
api("/api/chat/criteria", handleChatCriteria) // édition manuelle des critères (mode code)
api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte
api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used}
api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json)
api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais)
api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte
api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used}
api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json)
api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais)
return mux
}