From 1db7fb91770bf6616db27d230c4c8ad8d39bca55 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:54:38 +0200 Subject: [PATCH] fix deadlock in CachedListModels: use dedicated modelsMu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit content() holds sess.mu.RLock, then calls CachedListModels which tried to acquire sess.mu.Lock for cache write — classic RLock/Lock deadlock. Fix by giving the models cache its own mutex independent of the session mu. --- session/session.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/session/session.go b/session/session.go index f9bc1dc..e6f6a99 100644 --- a/session/session.go +++ b/session/session.go @@ -35,6 +35,7 @@ type Session struct { prevPrompt []byte // last submitted prompt; overwritten on each new submission // Cached model list (expensive API call; refreshed every 24h). + modelsMu sync.Mutex modelsCache string modelsCacheAt time.Time } @@ -49,30 +50,32 @@ const modelsCacheTTL = 24 * time.Hour // CachedListModels returns the model list, using a 24h cache to avoid // repeated expensive API calls. +// NOTE: Must NOT be called while holding sess.mu (caller content() holds RLock). +// Uses its own modelsMu to avoid deadlock. func (sess *Session) CachedListModels() string { - sess.mu.RLock() + sess.modelsMu.Lock() if sess.modelsCache != "" && time.Since(sess.modelsCacheAt) < modelsCacheTTL { result := sess.modelsCache - sess.mu.RUnlock() + sess.modelsMu.Unlock() return result } - sess.mu.RUnlock() + sess.modelsMu.Unlock() // Cache miss — fetch and store. result := sess.Core.ListModels() - sess.mu.Lock() + sess.modelsMu.Lock() sess.modelsCache = result sess.modelsCacheAt = time.Now() - sess.mu.Unlock() + sess.modelsMu.Unlock() return result } // InvalidateModelsCache clears the cached model list. func (sess *Session) InvalidateModelsCache() { - sess.mu.Lock() + sess.modelsMu.Lock() sess.modelsCache = "" sess.modelsCacheAt = time.Time{} - sess.mu.Unlock() + sess.modelsMu.Unlock() } func (sess *Session) RunnableID() string { return sess.id }