feat(chat): export + rehydrate conversation history for lossless resume#43
Merged
Conversation
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>
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.
What & why
Adds two capabilities to
chat.Sessionso 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}, andNewSessionalways starts from an empty fragment with no way to seed prior history.The two additions
1. Full-fidelity export
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 prompt —
s.messagesonly 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 lockSendMessageholds while appending.2. Seed-on-construct (rehydration)
NewSessionseeds it into boths.messages(the UI log) ands.fragment(the real model context), so the very nextSendMessagecontinues with full memory — identical to a session that organically reached that state. Round-trips withExportHistory.System-prompt correctness
SendMessageprepends 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:Config.InitialHistoryis documented to exclude the system prompt (andExportHistorynever emits one), keeping the contract airtight. A newhistoryMuguards the parallel fragment/messages state soExportHistorycan snapshot consistently mid-turn.Tests
New
chat/session_history_test.go(drives a realSessionagainst a fake OpenAI endpoint, race-clean):go build ./...,go vet ./..., andgo test ./chat/... ./types/...all pass; history tests verified under-race.🤖 Generated with Claude Code