Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ If Ollama isn't running, MandoCode shows setup guidance inline instead of a bare
| **AI** | Project-aware assistant | Reads, writes, deletes, and searches your entire codebase |
| **AI** | Web search & fetch | Web search and webpage reading — keyless via DuckDuckGo, or Tavily with a free API key |
| **AI** | MCP server support | Connect to any Model Context Protocol server (stdio or remote HTTP) — Claude-Desktop-compatible config |
| **AI** | Session resume | `--continue` / `-c` reloads your last conversation for the folder — full memory, tool calls included |
| **AI** | Streaming responses | Streams responses to keep long generations alive — no false "stalled" cutoffs |
| **AI** | Task planner | Auto-detects complex requests and breaks them into steps |
| **AI** | Fallback function parsing | Handles models that output tool calls as raw JSON |
Expand Down Expand Up @@ -247,6 +248,7 @@ Type `/` to see the autocomplete dropdown, or `!` to run a shell command.
### CLI flags (outside the chat loop)

```bash
mandocode --continue # resume this folder's most recent conversation (alias: -c)
mandocode --doctor # preflight check: .NET runtime, Ollama status, models, sign-in
mandocode --config show # print current config
mandocode --config init # create a default config file
Expand All @@ -256,6 +258,10 @@ mandocode --config path # show config file location

Run `mandocode --doctor` any time chat is misbehaving — exits 0 if everything's green, 1 if anything's missing, with a clear summary of what's wrong.

Conversations save themselves automatically at the end of every turn (per project folder, under
`~/.mandocode/sessions/`), so `mandocode --continue` picks up exactly where you left off — the model
remembers what it read, said, and did, tools included. `/clear` deletes the saved session too.

---

## How It Works
Expand Down
54 changes: 54 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,60 @@

All notable changes to MandoCode will be documented in this file.

## [0.14.0] - 2026-07-22

**MandoCode remembers.** Close the terminal mid-task, come back tomorrow, run `mandocode --continue` —
and the conversation picks up exactly where it left off. The restore is full fidelity: the model
genuinely remembers what it read, said, and did (tool calls included), not a summary of it. This
release also anchors the model to today's date, ending a class of confusion where genuine current
events were dismissed as fiction.

### Why this matters (plain-language summary)
- **"I lost everything when I closed the terminal."** The CLI's biggest day-to-day friction: every
session started from zero. Now every conversation saves itself automatically at the end of each
turn — nothing to configure, no save command to remember, and even a crash loses nothing already
said. `--continue` (or `-c`) brings it back.
- **"The model called real news fake."** Models ship with training data that ends months in the
past, and nothing told them today's date. Result, observed live: an assistant read genuine search
results about a recently announced product, decided the dates were "in the future," and concluded
its own search tool was fabricating content. The system prompt now carries the current date, so
current events read as current events.
- **Cost/risk: low and additive.** Saving is best-effort and isolated — a failed write or an
unreadable session file can never break the chat; the worst case is starting fresh. Users who
never pass `--continue` see no behavior change beyond the dated prompt.

### Added
- **Session resume — `mandocode --continue` / `-c`** — restores the current folder's most recent
conversation at startup, with a confirmation line showing how many messages came back (or an
honest "starting fresh" when there's nothing to restore). Sessions save automatically at the end
of every turn — including cancelled ones — to `~/.mandocode/sessions/<folder>-<hash>.json`, one
file per project root (path-hashed, so two folders named `api` can't collide). Writes are
write-then-rename, so a crash mid-save can't tear a previously good session. `/clear` deletes the
stored session along with the live one — cleared means cleared. The per-project layout
deliberately leaves room for a future `--resume`-style picker (choosing among several past
conversations, not just the latest). Flag parsing now distinguishes flags from the project-folder
argument in **both** places arguments are read (Program.cs and the console app's own re-parse) —
without that, `mandocode --continue` would have set the project root to a folder named
`--continue`.
- **Conversation export/import API** — `AIService.ExportHistoryJson()` /
`TryRestoreHistoryJson()`: full-fidelity serialization of the conversation (everything except the
system prompt) via Semantic Kernel's own polymorphic content serialization, so assistant turns
carrying function calls and tool results round-trip intact. This is the mechanism behind
`--continue`, and it's shared: MandoCode.Desktop uses the same API for its session restore and
its "keep memory across a model switch" feature — one tested mechanism, two applications.
- **Date grounding in the system prompt** — the prompt now states the current date with explicit
guidance: results dated up to today are real current events; only dates after today are the
future. Refreshed on every session build and settings change.
- **`--continue` launch profile** — a second Visual Studio debug profile so the resume path can be
exercised under the debugger.

### Test coverage
486/486 passing (unchanged — the suite confirms no existing behavior moved). The resume path was
validated live end-to-end rather than unit-mocked: CLI restore across process restarts, and —
through the same API — MandoCode.Desktop's session restore and cross-model memory carry, including
histories containing tool calls moving between different cloud providers. Dedicated unit tests for
`SessionResumeStore` (path hashing, atomic save, delete-on-clear) are a sensible follow-up.

## [0.13.0] - 2026-06-18

This release is about one thing: **long sessions on big work no longer looking broken.** Building real
Expand Down
33 changes: 30 additions & 3 deletions src/MandoCode/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
protected bool _isConnected { get; set; } = false;
private bool _connectionChecked = false;
private bool _hasRendered = false;

/// <summary>`mandocode --continue` (or -c): restore this folder's most recent
/// conversation once the connection is up — see OnAfterRenderAsync.</summary>
private bool _continueRequested = false;
private string? _modelWarning = null;
private bool _modelError = false;

Expand Down Expand Up @@ -292,11 +296,15 @@

protected override void OnInitialized()
{
// Get project root from command line args if available
// Get project root from command line args if available. Flags (--continue/-c, and
// anything future starting with '-') are not folder names — same filtering as
// Program.cs, or `mandocode --continue` would set the project root to "--continue".
var args = Environment.GetCommandLineArgs().Skip(1).ToArray();
if (args.Length > 0 && !args[0].StartsWith("config"))
_continueRequested = args.Any(a => a is "--continue" or "-c");
var positional = args.Where(a => !a.StartsWith('-')).ToArray();
if (positional.Length > 0 && !positional[0].StartsWith("config"))
{
ProjectRoot.ProjectRoot = args[0];
ProjectRoot.ProjectRoot = positional[0];
}

// Subscribe to function invocation events for UI feedback.
Expand Down Expand Up @@ -363,6 +371,19 @@
{
_hasRendered = true;

// --continue: reload the most recent conversation for this project folder,
// full fidelity (tool calls included). Failure degrades to a fresh start with
// an honest note — never an error.
if (_continueRequested)
{
var savedJson = SessionResumeStore.Load(ProjectRoot.ProjectRoot);
var restoredCount = savedJson == null ? 0 : AI.TryRestoreHistoryJson(savedJson);
AnsiConsole.MarkupLine(restoredCount > 0
? $"[green]Conversation restored ({restoredCount} messages) — continuing where you left off.[/]"
: "[dim]No previous session to continue for this folder — starting fresh.[/]");
AnsiConsole.WriteLine();
}

// Initialize imperative input with the shared state machine
CommandAutocomplete.Initialize(StateMachine);

Expand Down Expand Up @@ -926,6 +947,7 @@
{
await AI.ClearHistoryAsync();
StateMachine.ClearHistory();
SessionResumeStore.Delete(ProjectRoot.ProjectRoot); // cleared means cleared — --continue too
Console.Clear();
AnsiConsole.WriteLine("Conversation cleared.");
continue;
Expand Down Expand Up @@ -1783,6 +1805,11 @@
StopCancelKeyListener();
var oldCts = Interlocked.Exchange(ref _requestCts, null);
oldCts?.Dispose();

// Persist the conversation as of this turn for `mandocode --continue`.
// Runs on cancel too — whatever the history holds now is what "continue" means.
try { SessionResumeStore.Save(ProjectRoot.ProjectRoot, AI.ExportHistoryJson()); }
catch { /* persistence must never break the chat */ }
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/MandoCode/MandoCode.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<PackageId>MandoCode</PackageId>

<!-- NuGet metadata -->
<Version>0.13.0</Version>
<Version>0.14.0</Version>
<Authors>Armando Fernandez (DevMando)</Authors>
<Description>Your AI coding assistant — run locally or in the cloud with Ollama. No API keys required. Just you and your code.</Description>
<PackageProjectUrl>https://github.com/DevMando/MandoCode</PackageProjectUrl>
Expand Down
7 changes: 5 additions & 2 deletions src/MandoCode/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,11 @@ static async Task Main(string[] args)
return;
}

// Get project root from args or use current directory
var projectRoot = args.Length > 0 ? args[0] : Environment.CurrentDirectory;
// Get project root from the first POSITIONAL arg (flags like --continue/-c are not
// folder names) or use the current directory. App.razor applies the same filtering
// when it re-reads the raw command line.
var positional = args.Where(a => !a.StartsWith('-')).ToArray();
var projectRoot = positional.Length > 0 ? positional[0] : Environment.CurrentDirectory;

var hostBuilder = Host.CreateDefaultBuilder(args)
.UseRazorConsole<App>();
Expand Down
4 changes: 4 additions & 0 deletions src/MandoCode/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
"profiles": {
"MandoCode": {
"commandName": "Project"
},
"MandoCode --continue": {
"commandName": "Project",
"commandLineArgs": "--continue"
}
}
}
65 changes: 65 additions & 0 deletions src/MandoCode/Services/Ai/AIService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ private void RebuildSystemPrompt()
{
_systemPrompt += "\n\n" + skillIndex;
}

// Date grounding. A model's training data often ends months before "now"; without an
// anchor it misreads current events in search results as fabricated "future" content
// (a session once accused its own search tool of generating fiction over a real
// announcement dated after its cutoff). Day-level staleness is acceptable — the
// failure mode this prevents is being off by a year, not an afternoon. The prompt is
// rebuilt on every settings change and session (re)build, which refreshes the date.
_systemPrompt += $"\n\nCurrent date: {DateTime.Now:dddd, MMMM d, yyyy}. Your training data may predate this. " +
"Treat web results, news, and file timestamps dated up to today as real current events — " +
"not speculation, leaks, or simulated content. Only dates AFTER today are actually the future.";
}

/// <summary>
Expand Down Expand Up @@ -1083,6 +1093,61 @@ private static string Truncate(string? s, int max)
/// last user message. Used when a direct chat turn hits a provider context-window
/// rejection — we compact the conversation so the next retry fits.
/// </summary>
// ============================================================
// History persistence — full-fidelity session export/restore
// ============================================================
// Consumed by hosts that persist sessions across process restarts (MandoCode.Desktop's
// session restore; a future CLI --continue). Uses Semantic Kernel's own polymorphic
// content serialization, so assistant turns that carried function calls/results
// round-trip too — a restored model genuinely remembers what it read and did, rather
// than being briefed about it.

/// <summary>
/// Serializes the conversation — everything except the system prompt — to JSON.
/// Null when there is nothing beyond the system prompt or serialization fails;
/// callers treat null as "nothing to persist".
/// </summary>
public string? ExportHistoryJson()
{
try
{
var messages = _chatHistory.Where(m => m.Role != AuthorRole.System).ToList();
return messages.Count == 0 ? null : JsonSerializer.Serialize(messages);
}
catch
{
return null;
}
}

/// <summary>
/// Restores a previously exported conversation into the live history, after the current
/// system prompt. Intended for a FRESH session right after construction — it appends,
/// never replaces. Returns the number of messages restored; 0 means nothing usable
/// (corrupt/foreign JSON), and callers should fall back to lighter re-brief mechanisms.
/// </summary>
public int TryRestoreHistoryJson(string json)
{
try
{
var messages = JsonSerializer.Deserialize<List<ChatMessageContent>>(json);
if (messages == null) return 0;

var restored = 0;
foreach (var message in messages)
{
if (message?.Role is null || message.Role == AuthorRole.System) continue;
_chatHistory.Add(message);
restored++;
}
return restored;
}
catch
{
return 0;
}
}

private async Task CompactChatHistoryAsync()
{
await _historyLock.WaitAsync();
Expand Down
69 changes: 69 additions & 0 deletions src/MandoCode/Services/SessionResumeStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System.Security.Cryptography;
using System.Text;

namespace MandoCode.Services;

/// <summary>
/// Backing store for `mandocode --continue`: one most-recent-session file per project root
/// under ~/.mandocode/sessions/, holding the JSON that AIService.ExportHistoryJson produces.
/// Written whole-file (write-then-rename, so a crash can't tear a good save) at the end of
/// every turn; reloaded via TryRestoreHistoryJson when the user asks to continue.
///
/// The per-project layout (leaf name + path hash) deliberately mirrors tools like Claude
/// Code, and leaves room for a future --resume picker: multiple files per project instead
/// of one. Best-effort everywhere — persistence must never break the chat.
/// </summary>
public static class SessionResumeStore
{
/// <summary>Safety valve — the harness's own compaction keeps live histories far
/// smaller than this in practice.</summary>
private const int MaxBytes = 8 * 1024 * 1024;

private static string Folder => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".mandocode", "sessions");

/// <summary>Stable file path for a project root: readable leaf + hash of the full,
/// case-normalized path (two folders named "api" must not collide).</summary>
public static string PathFor(string projectRoot)
{
var full = Path.GetFullPath(projectRoot)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var hash = Convert.ToHexString(
SHA256.HashData(Encoding.UTF8.GetBytes(full.ToLowerInvariant())))[..12];
var leaf = new string(Path.GetFileName(full).Where(char.IsLetterOrDigit).Take(24).ToArray());
return Path.Combine(Folder, leaf.Length > 0 ? $"{leaf}-{hash}.json" : $"{hash}.json");
}

public static void Save(string projectRoot, string? historyJson)
{
try
{
if (string.IsNullOrEmpty(historyJson) || historyJson.Length > MaxBytes) return;
Directory.CreateDirectory(Folder);
var path = PathFor(projectRoot);
var tmp = path + ".tmp";
File.WriteAllText(tmp, historyJson);
File.Move(tmp, path, overwrite: true);
}
catch { }
}

public static string? Load(string projectRoot)
{
try
{
var path = PathFor(projectRoot);
return File.Exists(path) ? File.ReadAllText(path) : null;
}
catch
{
return null;
}
}

/// <summary>/clear forgets the stored session too — cleared means cleared.</summary>
public static void Delete(string projectRoot)
{
try { File.Delete(PathFor(projectRoot)); } catch { }
}
}