From 6fc2bc4967aa8df286d0e8b55b0ad478bc8405f3 Mon Sep 17 00:00:00 2001 From: Stefan Walcz Date: Tue, 20 Oct 2026 18:09:02 +0200 Subject: [PATCH] fix(chat-history): create the store after the auth database is opened (#12788) The conversation routes are only registered when the chat history store exists, and the store needs the auth database. It was created in start(), which New() runs before it opens the auth database, so the store was always nil and /api/conversations answered 404 with auth enabled; the React UI fell back to localStorage and nothing was synced between devices. The route tests register the routes with a store directly and never went through New(). Create the store right after the auth database is opened, and cover it through New(): with auth the store exists, without auth or with the WebUI disabled it stays nil. Docs-Not-Needed: bug fix; the documented server-side chat history now works as described Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Stefan Walcz --- core/application/application.go | 35 ++++++++------- core/application/chat_history_store_test.go | 50 +++++++++++++++++++++ core/application/startup.go | 1 + 3 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 core/application/chat_history_store_test.go diff --git a/core/application/application.go b/core/application/application.go index 2efe929fb..5e96521d6 100644 --- a/core/application/application.go +++ b/core/application/application.go @@ -676,24 +676,29 @@ func (a *Application) start() error { a.agentJobService = agentJobService - // Initialize chat history store for the WebUI (issue #9432). Reuses the - // shared auth database so per-user chat history sits alongside agent - // configs and jobs in one place — mudler's review on #9902 called out - // the file-based store as inconsistent with the rest of LocalAI's - // per-user state. When auth is disabled the store stays nil and the - // React UI falls back to localStorage. - if !a.applicationConfig.DisableWebUI && a.authDB != nil { - store, err := chathistory.New(a.authDB) - if err != nil { - xlog.Warn("Chat history persistence disabled: failed to initialise store", "error", err) - } else { - a.chatHistoryStore = store - } - } - return nil } +// initChatHistoryStore initializes the chat history store for the WebUI +// (issue #9432). It reuses the shared auth database so per-user chat history +// sits alongside agent configs and jobs in one place — mudler's review on +// #9902 called out the file-based store as inconsistent with the rest of +// LocalAI's per-user state. When auth is disabled the store stays nil and the +// React UI falls back to localStorage. It must run after the auth database is +// opened: start() runs before that, so a store set up there was always nil +// and the conversation routes were never registered. +func (a *Application) initChatHistoryStore() { + if a.applicationConfig.DisableWebUI || a.authDB == nil { + return + } + store, err := chathistory.New(a.authDB) + if err != nil { + xlog.Warn("Chat history persistence disabled: failed to initialise store", "error", err) + return + } + a.chatHistoryStore = store +} + // agentPoolOptions builds the agent pool's dependencies. WorkQueue stays a nil // interface without distributed services, because the pool reads a non-nil // WorkQueue as distributed mode. diff --git a/core/application/chat_history_store_test.go b/core/application/chat_history_store_test.go new file mode 100644 index 000000000..feaf22d3b --- /dev/null +++ b/core/application/chat_history_store_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +//go:build auth + +package application + +import ( + "context" + "path/filepath" + + "github.com/mudler/LocalAI/core/config" + "github.com/mudler/LocalAI/pkg/system" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// The conversation routes are registered only when the store exists, so a +// store that New() never creates means /api/conversations answers 404 even +// with auth on. Built through New(), the same path the server takes. +var _ = Describe("chat history store at startup", func() { + newApp := func(opts ...config.AppOption) *Application { + ctx, cancel := context.WithCancel(context.Background()) + DeferCleanup(cancel) + state, err := system.GetSystemState(system.WithModelPath(GinkgoT().TempDir()), system.WithBackendPath(GinkgoT().TempDir())) + Expect(err).NotTo(HaveOccurred()) + base := []config.AppOption{config.WithContext(ctx), config.WithSystemState(state), config.DisableMetricsEndpoint, + config.WithDisableLocalAIAssistant(true), config.WithDisableStats(true), config.WithDataPath(GinkgoT().TempDir())} + app, err := New(append(base, opts...)...) + if app != nil { + DeferCleanup(func() { Expect(app.Shutdown()).To(Succeed()) }) + } + Expect(err).NotTo(HaveOccurred()) + return app + } + withAuth := func() []config.AppOption { + return []config.AppOption{config.WithAuthEnabled(true), + config.WithAuthDatabaseURL(filepath.Join(GinkgoT().TempDir(), "auth.db"))} + } + + It("is created when auth is enabled", func() { + Expect(newApp(withAuth()...).ChatHistoryStore()).NotTo(BeNil()) + }) + + It("stays nil with the WebUI disabled", func() { + Expect(newApp(append(withAuth(), config.DisableWebUI)...).ChatHistoryStore()).To(BeNil()) + }) + + It("stays nil without auth, so the UI keeps localStorage", func() { + Expect(newApp().ChatHistoryStore()).To(BeNil()) + }) +}) diff --git a/core/application/startup.go b/core/application/startup.go index b1be5f751..90c1f57ed 100644 --- a/core/application/startup.go +++ b/core/application/startup.go @@ -174,6 +174,7 @@ func New(opts ...config.AppOption) (*Application, error) { } }() } + application.initChatHistoryStore() // Initialize the OTel + Prometheus metric pipeline before any // counter is created. monitoring.NewLocalAIMetricsService calls