Save and Restore Conversation Sessions Across Restarts
LM-Kit.NET lets you persist a MultiTurnConversation to disk or byte array, then restore it later with full context intact. Users pick up exactly where they left off, with no reprocessing and no lost context.
TL;DR
// Save
chat.SaveSession("session.bin");
// Restore
var chat = new MultiTurnConversation(model, "session.bin");
Why This Matters
Two real problems this solves:
- Desktop app resume. A user has a 30-turn conversation with a coding assistant, then closes the app for the night. Without session persistence, they restart with a blank slate the next morning. With
SaveSession, the app restores the full conversation, including the KV cache, so the assistant remembers everything and responds instantly. - Server crash recovery. A long-running service handles multi-turn conversations for multiple clients. If the process crashes or gets restarted for a deployment, all active sessions are lost. Periodic session saves let the server recover each conversation to its last checkpoint without any reprocessing.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| VRAM | 4+ GB |
| Disk | ~3 GB free for model download |
Full Session Save vs. History-Only Save
LM-Kit.NET provides two persistence strategies. Choose the one that fits your scenario.
Full Session (SaveSession) |
History-Only (ChatHistory.Serialize) |
|
|---|---|---|
| What is saved | Model ID, context length, full chat history, KV cache state, controller state | Message history only (roles, content, conversation ID) |
| File size | Larger (includes binary KV cache) | Much smaller (text and metadata only) |
| Restore speed | Instant. No recomputation needed. | Slower. KV cache must be recomputed from the message history on load. |
| Cross-model portability | No. Must restore with the same model. | Yes. Can deserialize and attach to a different model. |
| Best for | Desktop apps, server checkpointing, instant resume | Long-term archival, database storage, migrating conversations between models |
Step 1: Save a Full Session to Disk
After a conversation has at least one turn, call SaveSession with a file path.
using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
// Load model
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// Start a conversation
var chat = new MultiTurnConversation(model)
{
SystemPrompt = "You are a helpful coding assistant.",
MaximumCompletionTokens = 1024
};
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// Have a few turns
Console.Write("Assistant: ");
chat.Submit("My project uses PostgreSQL 16 with a partitioned events table.");
Console.WriteLine("\n");
Console.Write("Assistant: ");
chat.Submit("How should I index the timestamp column on the partitioned table?");
Console.WriteLine("\n");
// Save the full session to disk
string sessionFile = "chat_session.bin";
chat.SaveSession(sessionFile);
Console.WriteLine($"Session saved to {sessionFile}");
chat.Dispose();
The saved file contains the model ID, full message history, and the KV cache state. This means the next restore will be instant.
Step 2: Restore a Session and Continue
Pass the session file path to the MultiTurnConversation constructor to pick up where you left off.
using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// Restore the session from disk
string sessionFile = "chat_session.bin";
var chat = new MultiTurnConversation(model, sessionFile);
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// The assistant already knows about the PostgreSQL project.
// Continue the conversation seamlessly.
Console.Write("Assistant: ");
chat.Submit("What about queries that filter by both timestamp and user_id?");
Console.WriteLine("\n");
chat.Dispose();
Because the KV cache was restored, the first response after reload is just as fast as any mid-conversation turn.
Step 3: Save to Byte Array for Custom Storage
If you need to store sessions in a database, blob storage, or any non-filesystem backend, use the parameterless SaveSession() overload to get a byte[].
using LMKit.Model;
using LMKit.TextGeneration;
// After some conversation turns...
byte[] sessionData = chat.SaveSession();
// Store in your backend of choice
await SaveToDatabase(conversationId, sessionData);
await UploadToBlobStorage($"sessions/{conversationId}.bin", sessionData);
// Later, restore from the byte array
byte[] restored = await LoadFromDatabase(conversationId);
var chat = new MultiTurnConversation(model, restored);
This pattern works well for multi-tenant services where each user's session is stored independently.
Step 4: Save History Only for Lightweight Persistence
When you need smaller files or want to move a conversation between different models, serialize just the ChatHistory. This saves only the messages, not the KV cache.
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
// Save history to file
chat.ChatHistory.Serialize("history.bin");
// Or save to byte array
byte[] historyBytes = chat.ChatHistory.Serialize();
// Or save to a stream
using (var stream = File.Create("history_stream.bin"))
{
chat.ChatHistory.Serialize(stream);
}
Use history-only persistence when:
- Storage space is a concern and you can tolerate slower restores.
- You want to migrate a conversation to a different model.
- You need to archive conversations for compliance or analytics.
Step 5: Restore from History
Deserialize the ChatHistory, then pass it to the MultiTurnConversation constructor.
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// Deserialize from file
ChatHistory history = ChatHistory.Deserialize("history.bin");
// Inspect what was loaded
Console.WriteLine($"Conversation ID: {history.ConversationId}");
Console.WriteLine($"Messages: {history.MessageCount}");
Console.WriteLine($"User messages: {history.UserMessageCount}");
Console.WriteLine($"Tokens: {history.TokenCount}");
// Restore the conversation from history
var chat = new MultiTurnConversation(model, history);
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// Continue chatting. The KV cache will be recomputed from the message
// history on the first submit, so expect a slightly longer initial response.
Console.Write("Assistant: ");
chat.Submit("Can you summarize what we discussed so far?");
Console.WriteLine("\n");
chat.Dispose();
You can also deserialize from a byte array:
using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
// Load model
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// Start a conversation
var chat = new MultiTurnConversation(model)
{
SystemPrompt = "You are a helpful coding assistant.",
MaximumCompletionTokens = 1024
};
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// Have a few turns
Console.Write("Assistant: ");
chat.Submit("My project uses PostgreSQL 16 with a partitioned events table.");
Console.WriteLine("\n");
Console.Write("Assistant: ");
chat.Submit("How should I index the timestamp column on the partitioned table?");
Console.WriteLine("\n");
byte[] data = await LoadFromDatabase(conversationId);
ChatHistory history = ChatHistory.Deserialize(data);
var chat = new MultiTurnConversation(model, history);
Step 6: Implement Auto-Save
For production applications, save the session periodically rather than only at exit. This protects against crashes and unexpected terminations.
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
int turnsSinceLastSave = 0;
const int AutoSaveInterval = 3; // Save every 3 turns
string sessionFile = "chat_session.bin";
var chat = new MultiTurnConversation(model)
{
SystemPrompt = "You are a helpful assistant.",
MaximumCompletionTokens = 1024
};
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You: ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
break;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("Assistant: ");
Console.ResetColor();
chat.Submit(input);
Console.WriteLine("\n");
turnsSinceLastSave++;
if (turnsSinceLastSave >= AutoSaveInterval)
{
chat.SaveSession(sessionFile);
Console.WriteLine($" [Auto-saved after {AutoSaveInterval} turns]\n");
turnsSinceLastSave = 0;
}
}
// Always save on exit
chat.SaveSession(sessionFile);
Console.WriteLine($"Session saved to {sessionFile}");
chat.Dispose();
Complete Example
A desktop-style chat application that auto-saves on exit and restores on startup.
using System.Text;
using LMKit.Model;
using LMKit.TextGeneration;
using LMKit.TextGeneration.Chat;
LMKit.Licensing.LicenseManager.SetLicenseKey("");
Console.InputEncoding = Encoding.UTF8;
Console.OutputEncoding = Encoding.UTF8;
const string SessionFile = "assistant_session.bin";
const int AutoSaveEveryNTurns = 5;
// ──────────────────────────────────────
// 1. Load model
// ──────────────────────────────────────
Console.WriteLine("Loading model...");
using LM model = LM.LoadFromModelID("gemma3:4b",
downloadingProgress: (_, len, read) =>
{
if (len.HasValue) Console.Write($"\r Downloading: {(double)read / len.Value * 100:F1}% ");
return true;
},
loadingProgress: p => { Console.Write($"\r Loading: {p * 100:F0}% "); return true; });
Console.WriteLine("\n");
// ──────────────────────────────────────
// 2. Restore or create session
// ──────────────────────────────────────
MultiTurnConversation chat;
if (File.Exists(SessionFile))
{
Console.WriteLine("Restoring previous session...");
chat = new MultiTurnConversation(model, SessionFile);
Console.WriteLine($" Restored {chat.ChatHistory.MessageCount} messages.\n");
}
else
{
Console.WriteLine("Starting new session.\n");
chat = new MultiTurnConversation(model)
{
SystemPrompt = "You are a helpful, concise assistant. " +
"Remember all details the user shares across the conversation.",
MaximumCompletionTokens = 1024
};
}
chat.AfterTextCompletion += (sender, e) =>
{
if (e.SegmentType == TextSegmentType.UserVisible)
Console.Write(e.Text);
};
// ──────────────────────────────────────
// 3. Chat loop with auto-save
// ──────────────────────────────────────
int turnsSinceLastSave = 0;
Console.WriteLine("Chat with the assistant (type 'quit' to exit):\n");
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You: ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input) || input.Equals("quit", StringComparison.OrdinalIgnoreCase))
break;
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("Assistant: ");
Console.ResetColor();
TextGenerationResult result = chat.Submit(input);
Console.WriteLine($"\n [{result.GeneratedTokenCount} tokens, {result.TokenGenerationRate:F1} tok/s]\n");
turnsSinceLastSave++;
if (turnsSinceLastSave >= AutoSaveEveryNTurns)
{
chat.SaveSession(SessionFile);
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.WriteLine($" [Auto-saved session]\n");
Console.ResetColor();
turnsSinceLastSave = 0;
}
}
// ──────────────────────────────────────
// 4. Save on exit
// ──────────────────────────────────────
chat.SaveSession(SessionFile);
Console.WriteLine($"\nSession saved to {SessionFile}");
Console.WriteLine($" Messages: {chat.ChatHistory.MessageCount}");
Console.WriteLine($" Tokens: {chat.ChatHistory.TokenCount}");
chat.Dispose();
Best Practices
| Practice | Why |
|---|---|
| Save full sessions for instant resume | SaveSession includes the KV cache, so restoring skips all recomputation. This is the fastest restore path. |
| Use history-only for archival and portability | ChatHistory.Serialize produces smaller files and allows restoring with a different model. |
| Auto-save periodically, not just on exit | Crashes and forced terminations can lose unsaved state. Save every N turns as a safety net. |
| Store the session file path per user | In multi-user apps, use a unique file or database key per user to avoid session collisions. |
Call Dispose() when done |
MultiTurnConversation holds native resources. Always dispose after saving. |
Check File.Exists before restoring |
Attempting to restore from a missing file throws an exception. Guard the restore path. |
| Match the model on full session restore | SaveSession binds to a specific model. Restoring with a different model will fail. Use ChatHistory if you need cross-model portability. |
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
SaveSession throws "no conversation turns" |
The session has no messages yet | Submit at least one user message before saving |
| Restored session gives wrong answers | Session was saved with a different model | Use the same model ID when restoring full sessions. For cross-model use, serialize ChatHistory instead. |
| Session file is very large | Long conversation with large KV cache | Use ChatHistory.Serialize for lighter persistence, or call ClearHistory() to trim old turns before saving |
| Slow first response after history restore | KV cache is being recomputed from messages | This is expected with ChatHistory restore. Use full SaveSession if instant resume is needed. |
| File not found on restore | Session file was deleted or path changed | Check File.Exists before attempting restore, and fall back to creating a new conversation |
| Out of memory during restore | Session was saved with a larger context than currently available | Ensure the same context length setting is available when restoring |
Next Steps
- Build a Conversational Assistant with Memory: add long-term memory with
AgentMemoryso your assistant recalls facts across separate conversations. - Handle Long Inputs with Overflow Policies: manage context window limits when conversations grow very long.
- Stream Agent Responses in Real Time: add real-time streaming to your chat interface.