Skip to content

feat(chat): export + rehydrate conversation history for lossless resume#43

Merged
mudler merged 1 commit into
masterfrom
feat/session-history-rehydration
Jul 3, 2026
Merged

feat(chat): export + rehydrate conversation history for lossless resume#43
mudler merged 1 commit into
masterfrom
feat/session-history-rehydration

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

What & why

Adds two capabilities to chat.Session so a downstream app can persist a conversation to disk and later resume it losslessly — meaning the model actually remembers the prior conversation, not a summary. The motivating consumer is Dante Desktop, which keeps multiple persisted chats the user can close and reopen.

Today this is impossible from outside the package: GetMessages() strips messages down to {Role, Content}, and NewSession always starts from an empty fragment with no way to seed prior history.

The two additions

1. Full-fidelity export

func (s *Session) ExportHistory() []openai.ChatCompletionMessage

Returns a copy of the full conversation messages (JSON-serializable, suitable for persistence). Mutating the result never touches the live session. It excludes the system prompts.messages only ever records user/assistant turns; the system prompt lives on the fragment and is regenerated per model/locale each turn. Safe to call from another goroutine while a turn runs: it takes the same lock SendMessage holds while appending.

2. Seed-on-construct (rehydration)

// types.Config
InitialHistory []openai.ChatCompletionMessage

NewSession seeds it into both s.messages (the UI log) and s.fragment (the real model context), so the very next SendMessage continues with full memory — identical to a session that organically reached that state. Round-trips with ExportHistory.

System-prompt correctness

SendMessage prepends the current system prompt at the start of every turn (s.fragment.AddMessage("system", …)), and cogito consolidates all system messages to the front of the request. So the seed is loaded into the fragment WITHOUT a system message, letting that per-turn add supply exactly one. This:

  • mirrors a fresh multi-turn session (whose fragment likewise carries no leading system message before the turn's own add), and
  • guarantees the first resumed request carries the system prompt exactly once — no duplication, no omission. Baking a system message into the seed would have doubled it.

Config.InitialHistory is documented to exclude the system prompt (and ExportHistory never emits one), keeping the contract airtight. A new historyMu guards the parallel fragment/messages state so ExportHistory can snapshot consistently mid-turn.

Tests

New chat/session_history_test.go (drives a real Session against a fake OpenAI endpoint, race-clean):

  • export is a full copy — returns the whole history; mutating/appending to the result leaves the session untouched.
  • seeded history reaches the model — the first resumed request carries the prior turns in order, followed by the new message.
  • system prompt exactly once — with a prompt configured and history seeded, the first resumed request has exactly one system message, base prompt un-duplicated, at the front, with prior turns following.

go build ./..., go vet ./..., and go test ./chat/... ./types/... all pass; history tests verified under -race.

🤖 Generated with Claude Code

Add two capabilities so a downstream app (e.g. Dante Desktop, which keeps
multiple persisted chats) can save a conversation to disk and later resume it
losslessly — the model actually remembers the prior turns, not a summary.

- Session.ExportHistory() []openai.ChatCompletionMessage returns a copy of the
  full conversation messages (JSON-serializable, excludes the system prompt).
  It is a copy, so callers can't mutate live session state, and it takes the
  same lock SendMessage holds while appending, so it is safe to call from
  another goroutine while a turn runs.

- types.Config.InitialHistory []openai.ChatCompletionMessage seeds a new
  session with a prior conversation. NewSession loads it into BOTH s.messages
  (the UI log) and s.fragment (the real model context) so the very next
  SendMessage continues with full memory, behaving identically to a session
  that organically reached that state.

System-prompt correctness: the fragment is seeded WITHOUT a system message.
SendMessage prepends the current system prompt at the start of every turn, and
cogito consolidates system messages to the front — so seeding without one
yields exactly one, un-duplicated system prompt on the first resumed request.
Baking a system message into the seed would have doubled it. This mirrors a
fresh multi-turn session, whose fragment likewise carries no leading system
message before the turn's own add. Config documents that InitialHistory must
not contain the system prompt (ExportHistory never emits one).

A new historyMu guards the parallel fragment/messages state so ExportHistory
can snapshot consistently while a turn mutates them.

Tests (chat package, race-clean): export returns the full history and is a
copy; a seeded session's first request carries the prior turns in order; and
the resumed turn's system prompt appears exactly once (no duplication/omission).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mudler mudler merged commit 785b120 into master Jul 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants