---
title: "Persist LLM chat history in C#"
url: https://recursiveai.org/onellm/cookbook/save-and-resume-a-chat/
description: "Persist LLM chat history in C# and resume it. A complete .NET 10 example that runs offline with OneLLM, real output and a runnable download."
version: "1.0.1"
---

# Persist LLM chat history in C#

**OneLLM** is a free, open-source LLM and agent library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneLlm) · [GitHub](https://github.com/recursive-ai-oss/onellm-csharp) · [Docs](https://recursiveai.org/onellm/docs/)

Save chat messages to disk, then reload them to continue.

```shell
dotnet add package RecursiveAI.OneLlm
```

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using OneLlm;
// runtime and chat: from the full program.
await runtime.Store.SaveAsync(chat.Instance);
var saved = await runtime.Store.LoadAsync(
    chat.InstanceId);
```

Saves and reloads the messages.

**[Download this example (.zip)](https://recursiveai.org/onellm/cookbook/save-and-resume-a-chat/save-and-resume-a-chat.zip)**: unzip, then `dotnet run`.

## The full program

The download runs offline with canned replies, no API key needed.

`gpt-6-luna` is a model name in the shipped OpenAI catalog, not a variable defined by this program ([model names](https://recursiveai.org/onellm/docs/getting-started/#model-names)).

The first runtime saves a chat through `FileInstanceStore("chats")` and writes its ID to `chat-id.txt`. A fresh runtime opens that directory and loads the ID. `AskRequest.History` sends the saved turns before the new question. This is a continuation in a new instance, not a rehydrated `ChatSession` with the original ID.

```csharp
using OneLlm;
using OneLlm.Instances;
using OneLlm.Testing;

var runtime = new Runtime();
runtime.Store = new FileInstanceStore("chats");
var scripted = new ScriptedProvider("openai");
runtime.Providers.Set("openai", scripted);
scripted.EnqueueText("I’ll remember order 1182.");
using var chat = runtime.Chat(new AskRequest
{
    Model = "gpt-6-luna", System = "You help with orders."
});
Console.WriteLine(await chat.AskAsync("My order is 1182."));
await File.WriteAllTextAsync("chat-id.txt", chat.InstanceId);

// A new runtime, as after an application restart.
var resumed = new Runtime();
resumed.Store = new FileInstanceStore("chats");
var next = new ScriptedProvider("openai");
resumed.Providers.Set("openai", next);
next.EnqueueText("Your order is 1182.");
string id = await File.ReadAllTextAsync("chat-id.txt");
var saved = await resumed.Store.LoadAsync(id)
    ?? throw new InvalidOperationException("Chat not found.");
var answer = await resumed.AskAsync(new AskRequest
{
    Model = "gpt-6-luna", System = "You help with orders.",
    History = saved.Messages, Prompt = "What is my order number?"
});
Console.WriteLine(answer.Text);
Console.WriteLine($"Restored messages: {saved.Messages.Count}");
Console.WriteLine($"Messages sent: {next.Requests[0].Messages.Count}");
await File.WriteAllTextAsync("chat-id.txt", answer.Instance.InstanceId);
```

`dotnet run` prints:

```text
I’ll remember order 1182.
Your order is 1182.
Restored messages: 2
Messages sent: 3
```

A missing instance returns null; this program throws `InvalidOperationException("Chat not found.")` rather than silently starting an empty conversation.

### With a real model

The [pinned getting-started docs](https://recursiveai.org/onellm/docs/getting-started/#install-and-keys) name `OPENAI_API_KEY` for OpenAI. Set that environment variable to your own credential outside source code. Remove the `scripted` and `next` provider declarations, their `Providers.Set(...)` registrations, and their `EnqueueText(...)` calls. Also remove the final `Messages sent` print statement, which reads `next.Requests`. Keep both `FileInstanceStore` assignments, the prompts and the history-loading code. Each runtime now uses its built-in provider:

```csharp
using OneLlm;
var runtime = new Runtime();
var resumed = new Runtime();
```

This is the replacement runtime initialization, not an additional pair of declarations. OneLLM reads the environment key when the first OpenAI request is made. The second runtime sends the stored messages to the real model along with the new question.

This live-model path was not run here: no API key was used. The output shown above comes from the scripted providers, not from OpenAI.

## Variations

**Keep the restored conversation open for more turns.** Instead of the single `AskAsync` continuation, use the same history in a new session:

```csharp
using var continued = resumed.Chat(new AskRequest
{
    Model = "gpt-6-luna", History = saved.Messages,
    System = "You help with orders."
});
Console.WriteLine(await continued.AskAsync("What is my order number?"));
```

The same scripted answer prints `Your order is 1182.`. Save `continued.InstanceId` for the next restart.

## Related recipes

- [Require human approval for LLM tool calls in .NET](https://recursiveai.org/onellm/cookbook/approve-agent-tool-calls/)
- [Classify support tickets with an LLM in C#](https://recursiveai.org/onellm/cookbook/classify-support-tickets/)
- [Use IChatClient with Anthropic Claude and Gemini in C#](https://recursiveai.org/onellm/cookbook/ichatclient-for-claude-and-gemini/)
- [Generate a Git commit message with an LLM in C#](https://recursiveai.org/onellm/cookbook/commit-message-from-git-diff/)

---

## The short answer

Set `Runtime.Store` to a `FileInstanceStore`, retain the conversation’s instance ID, and load it on the next application start. Pass its messages as `AskRequest.History` along with the new prompt. Also supply the model and system prompt your application intends to use.

The distinction matters: this recipe resumes the conversation’s context in a new instance. It does not promise a `ChatSession.Load` API or overwrite the old instance. The download updates `chat-id.txt` to the new answer’s ID so another continuation can follow the latest branch rather than the original pair of messages.

## How to save an AI chat conversation?

Use a durable store before the first turn. The default runtime store is in memory, so keeping an ID alone does not make a conversation survive a process exit. OneLLM’s [store documentation](https://recursiveai.org/onellm/docs/stores/) describes `FileInstanceStore` as one YAML file per instance in a directory, written by temp file and rename. The core package includes it; this example needs no database package.

`ChatSession.InstanceId` identifies the conversation. After the successful turn in the download, the file store contains the user’s order number and the assistant’s reply. Disposing the session releases its turn gate; its instance stays in the store. The next runtime loads the same directory using the ID the application kept.

Keep the ID with the user’s conversation record in a real service, rather than a single shared `chat-id.txt`. That small file is just the console example’s way of handing the ID between two runtimes. Use your application’s ownership checks before loading history for a user. A conversation ID is a lookup key, not evidence that the caller owns the conversation.

The saved messages are richer than a concatenated transcript. They can include structured parts and tool results. Passing message objects preserves that shape instead of converting every turn into a user-authored string. This recipe uses text only so the stored history and the next request are easy to inspect.

## How to maintain context in LLM?

Send the earlier messages with the next question. In this run the saved instance has two messages and the resumed request has three: the earlier user message, the earlier assistant answer, and the new user question. The scripted reply is fixed in advance, so it does not demonstrate a model remembering the order; the request count and saved messages show the context path being exercised.

Supply the system prompt again deliberately. `History` is the earlier turns; it is not a blanket restore of every runtime setting. Keep your chosen model, prompt policy, tool configuration and store configuration in application code. The [request API](https://recursiveai.org/onellm/docs/happy-path/) and the pinned source’s `AskRequest` distinguish `History`, `System` and `Prompt`.

Long conversations also need a context policy. Persisting every message does not make an arbitrarily long history fit a model’s context window. Decide which turns the next request actually needs before you send it. Preserve the original stored conversation if you derive a shorter working history, and avoid dropping a tool call without its matching result. This example makes no automatic summarization or retrieval claim: it sends the saved messages as they stand.
