Persist conversations¶
By default a conversation's history is lost when a ChatClient is discarded.
The persistence API lets you capture conversation state as an immutable
snapshot, store it (optionally encrypted), and restore it into a fresh client
later — enabling multi-turn conversations across process invocations.
claude, openai, openai-compatible, and gemini all support persistence.
claude-local does not (it delegates to an external subprocess with no
accessible internal state).
Step 1 — Check provider support¶
Persistence is discovered via a type assertion, the same pattern as streaming:
client, _ := chat.New(ctx, chat.Settings{Config: cfg})
pc, ok := client.(chat.PersistentChatClient)
if !ok {
// e.g. ProviderClaudeLocal
return errors.New("provider does not support persistence")
}
Step 2 — Save a snapshot¶
After a conversation, capture the current state:
_, _ = client.Chat(ctx, "What is Go's concurrency model?")
_, _ = client.Chat(ctx, "How do channels differ from mutexes?")
snapshot, err := pc.Save()
if err != nil {
return fmt.Errorf("saving conversation: %w", err)
}
fmt.Println("saved conversation:", snapshot.ID) // a canonical UUID
A Snapshot captures the full message history (in the provider's native, opaque
JSON format), the system prompt, tool metadata (name, description, parameter
schema), a timestamp, and a format version. It never captures API tokens
(security) or tool handler functions (not serialisable).
Step 3 — Store with a FileStore¶
FileStore implements ConversationStore over an afero.Fs, writing each
snapshot as <id>.json with 0600 permissions (the directory is created 0700
if absent). Inject the filesystem, so the same code works against a real disk or
an in-memory test FS:
store, err := chat.NewFileStore(fs, filepath.Join(configDir, "conversations"))
if err != nil {
return err
}
if err := store.Save(ctx, snapshot); err != nil {
return fmt.Errorf("storing snapshot: %w", err)
}
// List summaries without loading full histories
summaries, _ := store.List(ctx)
for _, s := range summaries {
fmt.Printf(" %s %s %s (%d messages)\n", s.ID, s.Provider, s.Model, s.MessageCount)
}
loaded, _ := store.Load(ctx, summaries[0].ID)
_ = store.Delete(ctx, summaries[0].ID)
The store operations are Save, Load, List, and Delete. List is
deliberately robust: files whose names are not canonical UUIDs are logged at
DEBUG (wire a logger with chat.WithLogger) and skipped, so one stray file
cannot break enumeration.
Step 4 — Restore into a fresh client¶
Create a new client for the same provider and restore:
client, _ := chat.New(ctx, chat.Settings{Config: cfg})
pc := client.(chat.PersistentChatClient)
if err := pc.Restore(loaded); err != nil {
return fmt.Errorf("restoring conversation: %w", err)
}
// Re-register tools — handlers are not serialised
_ = pc.SetTools(myTools)
reply, _ := pc.Chat(ctx, "Can you summarise what we discussed?")
Two rules to remember:
- Provider must match. Restoring a Claude snapshot into an OpenAI client returns an error — the message format is provider-specific.
- Re-register tool handlers. The model remembers past tool calls from the history, but handlers must be live via
SetToolsfor any new calls.
Step 5 — Encrypt sensitive conversations¶
Enable transparent AES-256-GCM encryption with a 32-byte key. Generate one with
chat.GenerateEncryptionKey, or supply your own:
key, err := chat.GenerateEncryptionKey() // 32 bytes from crypto/rand
if err != nil {
return err
}
store, err := chat.NewFileStore(fs, dir, chat.WithEncryption(key))
if err != nil {
return err // errors if the key is not exactly 32 bytes
}
// Save and Load are unchanged — encryption is transparent
_ = store.Save(ctx, snapshot)
loaded, _ := store.Load(ctx, snapshot.ID)
When encryption is enabled the snapshot JSON is encrypted before it touches disk, a fresh random nonce is prepended per save, the file contents are indistinguishable from random, and loading with the wrong key returns an error.
Key management is yours. The library never stores or manages encryption keys. Use the OS keychain, an env var, or a secrets manager to hold the key securely.
Validate externally-supplied snapshot IDs¶
If a snapshot identifier arrives from outside your process (a CLI flag, HTTP handler, or queue payload), validate it at the boundary before it reaches the store — see snapshot-storage security:
if err := chat.ValidateSnapshotID(id); err != nil {
return err // reject before any filesystem work
}
Snapshots built via chat.NewSnapshot always get a fresh canonical UUID, so the
validator is transparent for library-produced snapshots.
Related¶
- Provider-endpoint & credential security — the snapshot-ID contract.
- pkg.go.dev reference — runnable
Exampletests for the FileStore.