diff --git a/conversation.go b/conversation.go index b6bf3c5..0a993c3 100644 --- a/conversation.go +++ b/conversation.go @@ -311,6 +311,16 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st c.mu.Unlock() }() + // 0. Amorçage anti-buffering. Le chat E2E d'app.ajean.link traverse Cloudflare + // (ajean.link, proxy orange) : tant qu'un proxy intermédiaire n'a pas reçu assez + // d'octets, il bufferise la réponse et ne la relaie qu'en retard (symptôme : + // derniers messages qui arrivent 20-30 s après le reste, de façon intermittente + // sur Safari). Envoyer d'emblée un gros événement de padding force le proxy à + // basculer en mode streaming tout de suite. Le client ignore la clé `pad`. + if !emit(map[string]any{"pad": strings.Repeat("·", 2048)}) { + return + } + // 1. Replay coalescé (snapshot hors verrou pour ne pas bloquer la génération). c.mu.Lock() snapshot := append([]LogEvent(nil), c.Log...) @@ -328,7 +338,10 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st last = s } } - if !emit(map[string]any{"caught_up": true}) { + // Padding aussi sur caught_up : force le flush de la QUEUE du replay à travers un + // proxy qui re-bufferise par lots (Cloudflare) → les derniers messages arrivent + // avec le reste, plus 20-30 s après. + if !emit(map[string]any{"caught_up": true, "pad": strings.Repeat("·", 2048)}) { return } diff --git a/e2e.go b/e2e.go index 5e12fa0..0f711d7 100644 --- a/e2e.go +++ b/e2e.go @@ -157,7 +157,9 @@ func handleE2EChat(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") + // no-transform : interdit à Cloudflare (proxy orange sur ajean.link) de + // bufferiser/compresser le flux — sinon les événements arrivent en retard. + w.Header().Set("Cache-Control", "no-cache, no-transform") w.Header().Set("X-Accel-Buffering", "no") flusher, _ := w.(http.Flusher) mu, stop := sseHeartbeat(w, flusher) diff --git a/main.go b/main.go index 8b276a5..581cbf8 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ import ( "strings" ) -const Version = "0.4.2" +const Version = "0.4.3" func main() { // Migration one-shot des anciens skills (SKILLS//SKILL.md) vers la diff --git a/resource_windows_amd64.syso b/resource_windows_amd64.syso index ba9f97a..2971cd3 100644 Binary files a/resource_windows_amd64.syso and b/resource_windows_amd64.syso differ diff --git a/resource_windows_arm64.syso b/resource_windows_arm64.syso index 7ae2f68..21ff86f 100644 Binary files a/resource_windows_arm64.syso and b/resource_windows_arm64.syso differ diff --git a/versioninfo.json b/versioninfo.json index 93211ed..71417b8 100644 --- a/versioninfo.json +++ b/versioninfo.json @@ -3,13 +3,13 @@ "FileVersion": { "Major": 0, "Minor": 4, - "Patch": 2, + "Patch": 3, "Build": 0 }, "ProductVersion": { "Major": 0, "Minor": 4, - "Patch": 2, + "Patch": 3, "Build": 0 }, "FileFlagsMask": "3f", @@ -25,7 +25,7 @@ "LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.", "OriginalFilename": "jean.exe", "ProductName": "Jean", - "ProductVersion": "0.4.2", + "ProductVersion": "0.4.3", "Comments": "https://github.com/nathaninline/jean — projet open source (MIT)" }, "VarFileInfo": { diff --git a/web.go b/web.go index 9ffa2b3..b24cf4a 100644 --- a/web.go +++ b/web.go @@ -830,7 +830,9 @@ func sseHeartbeat(w http.ResponseWriter, flusher http.Flusher) (*sync.Mutex, fun mu := &sync.Mutex{} done := make(chan struct{}) go func() { - t := time.NewTicker(15 * time.Second) + // 4 s (et non 15) : borne le temps qu'un dernier bout de flux peut rester + // coincé dans un buffer proxy (Cloudflare) faute d'octets pour le pousser. + t := time.NewTicker(4 * time.Second) defer t.Stop() for { select { @@ -907,7 +909,7 @@ func handleChat(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "text/event-stream") - w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Cache-Control", "no-cache, no-transform") w.Header().Set("X-Accel-Buffering", "no") flusher, _ := w.(http.Flusher) mu, stop := sseHeartbeat(w, flusher)