From fde89a7b0fafdac139d03b9644065187c1249ab3 Mon Sep 17 00:00:00 2001 From: DevMando Date: Wed, 22 Jul 2026 00:17:04 -0700 Subject: [PATCH 1/2] Add session resume (--continue), history export/import API, and date grounding - AIService gains ExportHistoryJson/TryRestoreHistoryJson: full-fidelity conversation serialization (function calls included) for hosts that persist sessions - New --continue / -c flag reloads the current folder's most recent conversation at startup; sessions save automatically at every turn end under ~/.mandocode/sessions - /clear also deletes the stored session - System prompt now carries the current date, so models stop misreading current events dated after their training cutoff as fabricated future content - Launch profile added for debugging with --continue --- src/MandoCode/Components/App.razor | 33 +++++++++- src/MandoCode/Program.cs | 7 +- src/MandoCode/Properties/launchSettings.json | 4 ++ src/MandoCode/Services/Ai/AIService.cs | 65 ++++++++++++++++++ src/MandoCode/Services/SessionResumeStore.cs | 69 ++++++++++++++++++++ 5 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/MandoCode/Services/SessionResumeStore.cs diff --git a/src/MandoCode/Components/App.razor b/src/MandoCode/Components/App.razor index 71e55ce..a5064c9 100644 --- a/src/MandoCode/Components/App.razor +++ b/src/MandoCode/Components/App.razor @@ -123,6 +123,10 @@ protected bool _isConnected { get; set; } = false; private bool _connectionChecked = false; private bool _hasRendered = false; + + /// `mandocode --continue` (or -c): restore this folder's most recent + /// conversation once the connection is up — see OnAfterRenderAsync. + private bool _continueRequested = false; private string? _modelWarning = null; private bool _modelError = false; @@ -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. @@ -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); @@ -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; @@ -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 */ } } } diff --git a/src/MandoCode/Program.cs b/src/MandoCode/Program.cs index 4e086e9..1dad660 100644 --- a/src/MandoCode/Program.cs +++ b/src/MandoCode/Program.cs @@ -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(); diff --git a/src/MandoCode/Properties/launchSettings.json b/src/MandoCode/Properties/launchSettings.json index 23cd338..7e01c40 100644 --- a/src/MandoCode/Properties/launchSettings.json +++ b/src/MandoCode/Properties/launchSettings.json @@ -2,6 +2,10 @@ "profiles": { "MandoCode": { "commandName": "Project" + }, + "MandoCode --continue": { + "commandName": "Project", + "commandLineArgs": "--continue" } } } diff --git a/src/MandoCode/Services/Ai/AIService.cs b/src/MandoCode/Services/Ai/AIService.cs index d4b87ce..b995058 100644 --- a/src/MandoCode/Services/Ai/AIService.cs +++ b/src/MandoCode/Services/Ai/AIService.cs @@ -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."; } /// @@ -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. /// + // ============================================================ + // 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. + + /// + /// 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". + /// + 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; + } + } + + /// + /// 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. + /// + public int TryRestoreHistoryJson(string json) + { + try + { + var messages = JsonSerializer.Deserialize>(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(); diff --git a/src/MandoCode/Services/SessionResumeStore.cs b/src/MandoCode/Services/SessionResumeStore.cs new file mode 100644 index 0000000..f8e82f1 --- /dev/null +++ b/src/MandoCode/Services/SessionResumeStore.cs @@ -0,0 +1,69 @@ +using System.Security.Cryptography; +using System.Text; + +namespace MandoCode.Services; + +/// +/// 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. +/// +public static class SessionResumeStore +{ + /// Safety valve — the harness's own compaction keeps live histories far + /// smaller than this in practice. + private const int MaxBytes = 8 * 1024 * 1024; + + private static string Folder => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".mandocode", "sessions"); + + /// Stable file path for a project root: readable leaf + hash of the full, + /// case-normalized path (two folders named "api" must not collide). + 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; + } + } + + /// /clear forgets the stored session too — cleared means cleared. + public static void Delete(string projectRoot) + { + try { File.Delete(PathFor(projectRoot)); } catch { } + } +} From 484f90cd3ec7a6c6f23439f773f4d362eaaf4cea Mon Sep 17 00:00:00 2001 From: DevMando Date: Wed, 22 Jul 2026 00:24:07 -0700 Subject: [PATCH 2/2] Docs and version for 0.14.0: README, CHANGELOG, version bump --- README.md | 6 ++++ docs/CHANGELOG.md | 54 ++++++++++++++++++++++++++++++++++ src/MandoCode/MandoCode.csproj | 2 +- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7bf8ee3..dba9e73 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 @@ -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 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2cf02b2..e247e1a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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/-.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 diff --git a/src/MandoCode/MandoCode.csproj b/src/MandoCode/MandoCode.csproj index 2ae046e..b15a704 100644 --- a/src/MandoCode/MandoCode.csproj +++ b/src/MandoCode/MandoCode.csproj @@ -13,7 +13,7 @@ MandoCode - 0.13.0 + 0.14.0 Armando Fernandez (DevMando) Your AI coding assistant — run locally or in the cloud with Ollama. No API keys required. Just you and your code. https://github.com/DevMando/MandoCode