Table of Contents

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:

  1. 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.
  2. 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