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 <stefan.walcz@walcz.de>
This commit is contained in:
Stefan Walcz authored and GitHub committed 2026-10-20 18:09:02 +02:00
1 parent e684fdf084
commit 6fc2bc4967
3 files changed
+71 -15

No files matched your search

+20 -15
View File
@@ -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.
@@ -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())
})
})
+1
View File
@@ -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