feat: persist chat conversations server-side (#9432)#9902
Open
TLoE419 wants to merge 6 commits into
Open
Conversation
Persist WebUI chat conversations server-side so browser refresh, private windows, or device changes preserve user history (issue mudler#9432). This commit adds the on-disk representation: - Conversation holds id, name, model, opaque history, MCP / sampling settings, and timestamps. - ConversationsFile is the per-user list serialised to JSON. History is stored as json.RawMessage so the server stays agnostic to React message shapes (user / assistant / thinking / tool_call / tool_result mixed with text / image_url / audio_url / file attachments). Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
…#9432) File-backed persister for chat history. Each user's conversations live under {baseDir}/{userID}/conversations.json (anonymous/ when auth is disabled). Design choices: - In-memory cache backed by sync.Mutex so concurrent saves don't interleave writes. - Atomic write via tmp file + os.Rename so a crash mid-save never leaves a corrupted history. - ID validation regex blocks path-traversal payloads at the store boundary, in addition to the auth context constraint. Tests cover round-trip persistence across instances, user isolation, unsafe-ID rejection, bulk replace migration, and the anonymous fallback path. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
Wire the chathistory store into the HTTP layer. New endpoints under /api/conversations: - GET /api/conversations list - POST /api/conversations upsert (id in body) - DELETE /api/conversations delete all - PUT /api/conversations/bulk replace entire set (localStorage migration) - GET /api/conversations/:id fetch one - PUT /api/conversations/:id upsert (id in path; path id wins over body) - DELETE /api/conversations/:id delete one All endpoints scope queries to the authenticated user via getUserID(c) - callers cannot impersonate other users by passing a user_id in the body. The endpoints are gated behind the new chat_history feature permission (default ON in APIFeatures), registered through the existing RouteFeatureRegistry so the unified feature middleware picks them up automatically. Application.ChatHistoryStore() returns nil when DisableWebUI is set or no persistence path is configured, in which case route registration is skipped entirely rather than registering handlers that always 503. Also adds the chat-history instruction entry and Swagger tag so the endpoint surfaces in /api/instructions and /swagger. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
mudler#9432) useChat now probes /api/conversations on mount. When the endpoint responds 200, the server becomes the authoritative source: the hook merges remote conversations into local state and pushes per-chat PUT updates on each debounced save. When the endpoint 404s - older LocalAI deploys or the feature disabled - the hook silently keeps the old localStorage-only behaviour, so the change is backward-compatible. Migration: when the server is reachable but empty and the browser has local conversations with history, the hook fires a single PUT /api/conversations/bulk to seed the server. The atomic bulk endpoint avoids partial-upload states where a retry would skip already-uploaded entries. Delete operations propagate to the server when it's available; failures are silently swallowed so the local UI stays responsive on transient network errors. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
The initial test file used the stdlib testing package (t.Fatalf / t.Errorf), which is forbidden by the forbidigo linter rule in .golangci.yml — LocalAI mandates Ginkgo/Gomega for all Go tests (see .agents/coding-style.md). Restructured the same six scenarios into Describe / Context / It blocks, with a chathistory_suite_test.go bootstrap that registers the Ginkgo fail handler. Test coverage is unchanged: basic CRUD round-trip, cross- instance persistence, user isolation, unsafe ID rejection (now via DescribeTable), bulk replace overwrite, and the anonymous fallback path. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
The new chat-history instruction definition pushes the total instructionDefs entries from 12 to 13. Update the hard-coded length assertion in api_instructions_test.go to match. The presence-level assertion in the sibling \"should include known instruction names\" test already uses ContainElements rather than ConsistOf, so no further edits are needed there. Assisted-by: Claude:claude-opus-4-7 Signed-off-by: TLoE419 <tloemizuchizu@gmail.com>
mudler
reviewed
May 20, 2026
|
|
||
| // userFile returns the on-disk path for a user's conversations file. | ||
| func (s *Store) userFile(userID string) string { | ||
| return filepath.Join(s.userDir(userID), "conversations.json") |
Owner
There was a problem hiding this comment.
this is fine in the case the user does not specify any DB. but IF we are adding this feature we should be consistent here and store the conversations in the DB
mudler
reviewed
May 20, 2026
| baseDir string | ||
|
|
||
| mu sync.Mutex | ||
| cache map[string]map[string]schema.Conversation // userID -> id -> conv |
Owner
There was a problem hiding this comment.
this calls for a TTL or a mechanism to not grow this idenfinitely in memory
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR fixes #9432
Chat conversations in the WebUI previously lived only in browser
localStorage. Switching browsers, opening an incognito window, or clearing site data wiped the user's entire chat history. This PR adds server-side persistence so chats survive across browsers / devices when the WebUI is enabled.What changed
core/services/chathistory— new file-based, per-user persister at{DataPath|DynamicConfigsDir}/chat_history/{userID}/conversations.json(anonymous/when auth is disabled). Atomic writes viatmp file + os.Renameso a crash mid-save never leaves a corrupted history./api/conversationsCRUD endpoints — GET / POST / PUT / DELETE plusPUT /api/conversations/bulkfor first-time localStorage migration. All endpoints are user-scoped viagetUserID(c); callers cannot impersonate other users.chat_historyfeature permission (default ON inAPIFeatures), registered through the existingRouteFeatureRegistryso the unified feature middleware picks it up automatically.useChathook — probes/api/conversationson mount. Server becomes authoritative when reachable; on 404 the hook silently falls back to the old localStorage-only behaviour, so this is fully backward-compatible.api_instructionstest updated for the new entry.chat-historyinstruction entry surfaces in/api/instructions, plus matching Swagger tag.Notes for Reviewers
DataPathresolves — no env vars to set.DisableWebUI=trueor no persistence path resolves,Application.ChatHistoryStore()returnsnil, route registration is skipped, and the React hook detects 404 to fall back to localStorage. Older deployments and disabled feature both keep working unchanged.json.RawMessageso the server stays agnostic to React message shapes —user/assistant/thinking/tool_call/tool_resultroles mixed with text /image_url/audio_url/ file attachments all round-trip lossless./bulkendpoint avoids partial-upload states where a retry would skip already-uploaded entries — important for the localStorage migration which only fires when the server is empty.anonymous/conversations.json. Matches expected single-user-deployment behavior; per-user isolation kicks in automatically once auth is enabled.Test plan
Signed commits