Skip to content

MandoCode

v0.11.0 Breaking

This release includes breaking changes for platform teams planning a safe upgrade.

✓ No known CVEs patched
Read the diff → Tool health → What is this tool? →

✓ No known CVEs patched in this version

Topics

ai-agents blazor cli-agents c# .net ollama
+1 more
tui

Affected surfaces

auth rbac

ReleasePort's take

Light signal
editorial:auto 1mo

Plan cancellation now instantly halts all subsequent work; Arrow keys at the plan‑approval menu no longer lag or get ignored.

Why it matters: Immediate stop of cancelled plans prevents runaway resource usage; removal of UI lag improves operator efficiency during approvals (severity 40).

Summary

AI summary

Updates webSearchEnabled, CS1998, and context.CancellationToken across a mixed release.

Changes in this release

Security Medium

Treat all compiler warnings as errors; fixed nullability warnings to enforce stricter type safety.

Treat all compiler warnings as errors; fixed nullability warnings to enforce stricter type safety.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Feature Medium

Added in‑app `/config set <key> <value>` command to change settings without leaving the session.

Added in‑app `/config set <key> <value>` command to change settings without leaving the session.

Source: llm_adapter@2026-06-15

Confidence: high

Feature Medium

Added ranged reads: `read_file_contents(relativePath, startLine?, endLine?)` with line‑truncated output.

Added ranged reads: `read_file_contents(relativePath, startLine?, endLine?)` with line‑truncated output.

Source: llm_adapter@2026-06-15

Confidence: high

Feature Medium

Added Tavily search integration with fallback to DuckDuckGo and onboarding prompts.

Added Tavily search integration with fallback to DuckDuckGo and onboarding prompts.

Source: llm_adapter@2026-06-15

Confidence: high

Feature Medium

Default stall watchdog timeout raised from 180 s to 420 s for long model generations.

Default stall watchdog timeout raised from 180 s to 420 s for long model generations.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Feature Medium

`/model` command now instantly switches models and auto‑sizes context window based on model size.

`/model` command now instantly switches models and auto‑sizes context window based on model size.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Feature Medium

Hardware‑tier context auto‑sizing sets Ollama context length based on model parameter count.

Hardware‑tier context auto‑sizing sets Ollama context length based on model parameter count.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Bugfix High

Concurrent approval prompts can no longer deadlock the session; they are serialized via `ApprovalPromptGate`.

Concurrent approval prompts can no longer deadlock the session; they are serialized via `ApprovalPromptGate`.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Bugfix High

Recover from HTTP 413 "request body too large" errors by compacting chat history and retrying.

Recover from HTTP 413 "request body too large" errors by compacting chat history and retrying.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Bugfix High

`edit_file` failures now count toward the N=3 edit‑failure circuit per file path.

`edit_file` failures now count toward the N=3 edit‑failure circuit per file path.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Bugfix Medium

Arrow keys at the plan-approval menu no longer experience lag or being eaten.

Arrow keys at the plan-approval menu no longer experience lag or being eaten.

Source: llm_adapter@2026-06-15

Confidence: high

Bugfix Medium

Edit‑failure errors now include the current file content as a hint.

Edit‑failure errors now include the current file content as a hint.

Source: llm_adapter@2026-06-15

Confidence: high

Bugfix Medium

Plan cancellation now immediately stops all further work.

Plan cancellation now immediately stops all further work.

Source: llm_adapter@2026-06-15

Confidence: low

Bugfix Medium

Changing settings mid‑task can no longer freeze the stall watchdog permanently.

Changing settings mid‑task can no longer freeze the stall watchdog permanently.

Source: llm_adapter@2026-06-15

Confidence: low

Bugfix Medium

Pending‑function count leak that paused the stall watchdog has been fixed.

Pending‑function count leak that paused the stall watchdog has been fixed.

Source: llm_adapter@2026-06-15

Confidence: low

Bugfix Medium

Approval awaits now observe cancellation tokens.

Approval awaits now observe cancellation tokens.

Source: llm_adapter@2026-06-15

Confidence: low

Bugfix Medium

Shell‑based file reads via `execute_command` are now refused, steering users to `read_file_contents`.

Shell‑based file reads via `execute_command` are now refused, steering users to `read_file_contents`.

Source: llm_adapter@2026-06-15

Confidence: low

Bugfix Medium

Per‑scope bookkeeping keys are normalized to avoid duplicate tracking across path aliases.

Per‑scope bookkeeping keys are normalized to avoid duplicate tracking across path aliases.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Refactor Low

`AIService` decomposed into smaller services, reducing code size and removing duplicated scaffolding.

`AIService` decomposed into smaller services, reducing code size and removing duplicated scaffolding.

Source: granite4.1:30b@2026-06-15-audit

Confidence: low

Full changelog

Agentic-loop hardening and prompt trust: every fix in this release closes a way a long session
could hang, thrash, silently lose its mind, or roll past the user's "stop" — each found by
dogfooding and verified live. Every interactive menu now speaks one visual language, arrow keys
are deterministic, and plan cancellation is enforced by the app instead of requested of the
model. Under the hood, the largest file in the codebase was decomposed and warnings are now
build errors — so this release also makes the next one safer to build. Web search also stops
depending on DuckDuckGo's goodwill: an optional free Tavily key makes it reliable.

Fixed

  • Models no longer recite their knowledge cutoff instead of using the web search they have — observed live on minimax-m3: asked for current weather, the model claimed "I don't have any tool to fetch live weather feeds" (false — search_web was registered), recommended the user try Google and weather.com, and asked permission to search instead of searching; the same setup on minimax-m2.7 searched fine. Two causes. First, the prompt's web guidance was a passive "use search_web when you need current information" buried at guideline #9 — no match for a trained cutoff-disclaimer reflex, and once a model disclaims live access once, self-consistency keeps it disclaiming for the rest of the conversation (the bad session opened with a from-memory-answerable question, which set the no-search precedent). Second, the static prompt advertised search even when EnableWebSearch was off. SystemPrompts.MandoCodeAssistant is now BuildMandoCodeAssistant(webSearchEnabled): enabled, an assertive LIVE WEB ACCESS section instructs search-FIRST for anything recent (no asking permission), forbids claiming a lack of internet access, and forbids punting the user to Google; disabled, the prompt stops advertising tools that aren't registered and points at /config set websearch true instead. AIService rebuilds the prompt on every settings path, and RefreshSettingsAsync swaps the in-history system message in place so toggling websearch mid-conversation actually reaches the model instead of waiting for the next /clear.

  • "Cancel the plan" now actually stops the work — immediately — cancelling at a diff approval previously only set a flag that the plan runner checked after the whole step finished. A step that wrote three files kept right on presenting file #2 and file #3 for approval after the user had already cancelled at file #1, because Semantic Kernel's auto-invoke loop was still executing the response's remaining tool calls — and the "stop all further work" message returned to the model is a polite request that models demonstrably ignore (the same lesson as the plan-prompt cancel fix below). New plan-cancellation circuit in FunctionInvocationFilter, first in priority above the budget and dedup circuits: once a scope is flagged cancelled, every subsequent tool call — writes, reads, commands — is refused mechanically, before any approval UI is reached. Cancellation went from a stop sign the model could roll through to a stop gate. Covered by regression tests reproducing the exact observed 3-file scenario, including that a fresh turn isn't poisoned by a previously cancelled scope.

  • Arrow keys at the plan-approval menu no longer get eaten ("press twice" lag) — two console readers were racing: Spectre's blocking SelectionPrompt and RazorConsole's framework keyboard pump (KeyboardEventManager polls Console.KeyAvailable every 50ms and reads unconditionally — no focus check, no pause API, and its IConsoleInput seam is internal so it can't be substituted). An arrow pressed while Spectre was mid-repaint sat in the buffer just long enough for the pump to steal it and drop it (no focused VDOM element exists mid-turn). The plan-approval prompt is now a VDOM component (ApprovalSelect) fed by the pump itself — one reader, no race, by construction. The remaining Spectre menus (step-failure prompt, the four DiffApprovalHandler approvals) carry the same latent race at lower traffic and migrate next using the same component.

  • Changing settings mid-task can no longer freeze the stall watchdog permanently/config set rebuilds the kernel; the rebuild detached event handlers from the old FunctionInvocationFilter even when a tool call was still in flight on it, so that call's completion never reached the pending-function tracker. The count stayed pinned > 0 and the stall watchdog — which only resumes at count 0 — stayed paused for the rest of the session (the same failure surface as the pending-count leak below, through a different door). BuildKernel now deliberately leaves the old filter's handlers attached: in-flight calls complete and decrement correctly, nothing fires twice (new calls route through the new kernel), and the old filter becomes collectible once its calls finish.

  • Step-failure "How would you like to proceed?" prompt no longer fires as a fake-async methodHandleProgressEventAsync was declared async with a fully synchronous body (CS1998); now an honest synchronous HandleProgressEvent. Surfaced by the new warnings-as-errors policy below.

  • Assorted hardening from a full-codebase reviewValidateModelAsync disposes its HTTP response; RequestTimeoutMinutes is clamped by ValidateAndClamp like every other numeric setting (a hand-edited "requestTimeoutMinutes": 999 previously loaded unclamped); FileSystemPlugin's grep/search loops only swallow file-access exceptions (IOException/UnauthorizedAccessException/NotSupportedException) instead of everything; MusicPlayerService teardown disposes each NAudio object in its own guarded step so one failing Dispose can't skip the rest.

  • Concurrent approval prompts can no longer deadlock the session — new ApprovalPromptGate (a SemaphoreSlim(1,1) async mutex) serializes every interactive approval surface: diff, delete, command, MCP, and plan approval. The kernel runs with AllowConcurrentInvocation = true, so a model response containing two approval-gated tool calls opened two blocking Spectre prompts at once — both looping on Console.ReadKey, stealing each other's keys. The visible prompt completed; the invisible one blocked its function forever, the turn never ended, and the input prompt never returned. This was the primary cause of the "agent suddenly stops and won't display the prompt" hang on long agentic loops. The plan-approval prompt holds the gate only around the prompt itself, never around execution — plan steps' own approvals take the same gate, so holding it across ExecutePlanAsync would deadlock the first step.

  • Pending-function-count leak permanently paused the stall watchdogFunctionInvocationFilter.OnFunctionInvocationAsync incremented the count and fired OnFunctionStarted ~250 lines before the try/finally that decremented it, with seven hand-rolled cleanup copies on the early-return paths. An exception anywhere in that window (UI event handler, approval callback) skipped the decrement; the filter is long-lived, so PendingFunctionCount stayed pinned > 0 for the rest of the session and the stall watchdog — which only resumes at count 0 — never resumed again. Any later genuine model stall then hung silently until the 15-minute request ceiling. Restructured: one try/finally owns the lifecycle (InvokeCoreAsync split), all seven manual copies deleted. The MCP approval gate also moved inside the lifecycle so the watchdog pauses while the user deliberates at an MCP prompt, like every other approval.

  • Approval awaits now observe cancellation — all five approval awaits (MCP, edit, write, delete, command) are wrapped in WaitAsync(context.CancellationToken). A wedged prompt previously blocked an await that observed no token, which defeated Esc, the stall watchdog, AND the request ceiling simultaneously — the turn could never unwind.

  • "Cancel request" at the plan-approval prompt actually cancels — it previously returned a "stop here" string to the model as a tool result, a polite request that a frontier cloud model was observed simply ignoring (it executed the cancelled plan's steps itself via direct tool calls). Now it cancels _requestCts — the same mechanical path as Esc — so the turn unwinds regardless of what the model thinks of the instruction.

  • One plan per turn, enforced mechanically — observed runaway: a model completed a 5-step plan, immediately started building an uninvited duplicate of the project, then proposed a third round of work. PlanHandoff's recursion guard only covers a plan that's still running. New InvocationScope.PlanAlreadyProcessed circuit: once a real plan (≥1 step) is handled — executed, rejected, or cancelled — any further propose_plan in the same turn is short-circuited with a stop directive. Malformed/empty proposals don't consume the slot, so a legitimate retry with corrected steps still works. The plan-completion summary returned to the model now also ends with an explicit wrap-up directive ("respond with a brief summary; do NOT create more files or propose another plan").

  • Plugin-level edit_file failures now count toward the N=3 edit-failure circuit — the circuit previously only counted preview-level failures. The preview validates old_text against a snapshot captured at interception; when an earlier edit in the same concurrent batch landed in between, the plugin's re-validation failed AFTER the preview passed — and those failures bypassed the circuit entirely. Observed live: 9+ consecutive "Could not find the specified text" misses on one path with no trip. UpdateScopeForCompletedCall now records error-result edit_file calls into the same counter.

  • Per-scope bookkeeping keys are normalized across path aliases — the model was observed addressing one file as both Games/index.html and src/MandoCode/bin/Debug/net8.0/Games/index.html (the plugin's StripRedundantRootPrefix resolves both to the same file). Edit-failure counts, read-dedup keys, and modified-since-read tracking keyed on the raw strings, so an alias switch split the circuit counters and dodged trip thresholds. The plugin's resolution is now extracted as FileSystemPlugin.ResolvePath and the filter normalizes every bookkeeping key through it. Pre-execution content capture (diff previews) uses the same resolution, so aliased edit/write/delete targets now capture the real file instead of silently skipping the preview.

  • cd X && <command> no longer swallowed by the cd interceptorexecute_command's cd handling treated the entire compound string as a directory path and failed with No such directory: "X" && <command>. Compound commands (&&, ;, |) now fall through to the real shell; bare cd <dir> still retargets the project root.

  • qwen3-coder:480b-cloud was misclassified as a local model — every cloud check was Contains(":cloud"), but size-variant cloud tags end in -cloud, not :cloud. Worst impact: CheckCloudSignInAsync told users whose only cloud model was qwen3-coder that they weren't signed in. Centralized into MandoCodeConfig.IsCloudModel (matches both suffix forms) and adopted at every former call site.

  • A completed plan's work can no longer be bulldozed by the model that requested it — plan steps execute in isolated chat histories, so the outer model never sees them run; all it got back was "Successfully completed N of N steps," and models routinely treated that verdict as "not started yet." Observed live: after a 4-step plan finished a working 740-line build, the outer model re-created the project folder and replaced the finished file with a fresh 63-line skeleton — every write auto-approved because the user had picked "don't ask again" during the plan, until a server error killed the turn mid-rewrite. Two layers, same doctrine as the cancellation fixes above (a stop gate, not a stop sign). Persuasion: PlanHandoff now returns an evidence manifest instead of a verdict — per-step statuses with capped result digests (~500 chars each, the whole manifest bounded so it doesn't eat small local context windows), the file operations recorded at the invocation-filter choke point (ground truth that the call ran and succeeded, not a model self-report), and an explicit stop directive. Enforcement: a new post-plan mutation circuit in FunctionInvocationFilter refuses write_file/edit_file/delete_file/delete_folder/create_folder for the rest of the turn once a plan has completed ≥1 step. Reads and shell stay allowed so the model can inspect results; rejected plans never arm the gate (the model must stay free to do the work directly); the gate is turn-scoped, so the next user message edits freely. Verified live: the same scenario now ends with a 278-token summary referencing the manifest and zero post-plan tool calls.

  • Plan steps now see the user's actual request, not the model's summary of it — steps were seeded with only the propose_plan goal, a lossy distillation. Observed live: "in @STarfox/ create a starfox64 inspired game…" became goal "create a starfox64 inspired game in three.js" — the folder reference vanished, every step wrote to the project root (editing a leftover index.html from an earlier run), and the final summary then claimed the game was "in the STarfox folder." ChatStreamAsync now captures the turn's verbatim user message — including the @file/@folder expansions — and every step's context includes it under an "Original Request" header declared authoritative for target folders and paths. Head-capped at 4k chars so a pasted @file can't flood every step's context on small local models (paths and intent lead a prompt, so they always survive truncation). Step-context construction extracted to a static, tested AIService.BuildStepContext. Verified live: the next run's first action was CreateFolder(StarFox) and every diff landed in StarFox/.

  • The done_reason: "length" cutoff warning no longer misdiagnoses — one stop reason has three causes needing different cures, and the old message always said "increase max tokens with /config", which is exactly the wrong knob in two of them. New BuildLengthCutoffNotice: output ≈ maxTokens → genuine response-cap advice; output far below the cap on a local model → the context window filled (points at the Ollama desktop app's Context length slider — which defaults to ~4k — or the /setup daemon restart); same on a cloud model → server-side window, /clear/smaller-request advice with no local-daemon nonsense. Empty-content cases note that thinking models (qwen3, minimax) can spend the whole budget on internal reasoning with nothing visible to show.

Added

  • Tavily web search with DuckDuckGo fallback — DuckDuckGo's free HTML endpoint rate-limits and temporarily blocks IPs under normal agentic use, so search_web could randomly fail with an opaque error. WebSearchPlugin now prefers Tavily (an LLM-optimized search API with a free ~1,000-searches/month tier) whenever a key is configured, keeping DuckDuckGo as the zero-config default and as the fallback when Tavily itself fails. Onboarding is deliberately just-in-time rather than upfront: when DuckDuckGo blocks a search (403/429/503 or its bot-challenge page), the tool result returned to the model explains why, where to get a free key, and the exact /config set tavilyKey <key> command — so the assistant teaches the fix in context at the exact moment search fails, which is when the user actually cares. Setting the key fires one live verification probe against Tavily and reports ✓ verified / ✗ rejected immediately (the key saves either way — the user may be offline). The key is masked everywhere it's displayed (tvly-…7890 in /config show, DescribeKeys, and set confirmations), clearable with /config set tavilyKey clear, and overridable via the TAVILY_API_KEY environment variable — which deliberately bypasses the Program.cs env-override pattern so a later Save() never persists an env-provided secret to disk. The /setup wizard gains a skippable step 7 (default: skip) explaining the trade-off, with keep/replace/remove handling for an existing key. New SetResult.PostSetValidation delegate lets ConfigKeySetter stay synchronous and shared between the CLI and in-app command (below) while both run the async key probe post-save.
  • In-app /config set <key> <value> — change any setting without leaving the session (or losing the conversation). When an error message says "raise the watchdog: /config set modelResponseTimeout 300", the user can now do exactly that, in place. With no arguments it lists every key with its current value and valid range. Backed by a new shared ConfigKeySetter used by BOTH the in-app command and the CLI's --config set, so key names and validation can never drift apart again — they already had: the stall-watchdog error recommended a modelResponseTimeout key the CLI didn't actually have (now it does, with watchdog as an alias). Kernel-baked settings (temperature, maxTokens, toolBudget, plugin toggles) apply via a new history-preserving kernel rebuild (RefreshSettingsAsync); live-read settings take effect on the next message; restart-scoped keys say so explicitly.
  • Ranged reads: read_file_contents(relativePath, startLine?, endLine?) — the ~10K output cap is unchanged, but truncation now cuts at a line boundary and names the exact resume point: [truncated at line 312 of 1051 — call read_file_contents with startLine=313 to continue]. Closes the "editing blind" failure on large generated files: the model could only ever see the first ~third of a 1,000-line file, composed old_text from memory of code it wrote below the truncation horizon, and thrash-looped on "Could not find" errors. The read-dedup key includes the range so paging is never mistaken for a redundant re-read; CRLF files round-trip byte-exact so what the model sees is what edit_file matches. New LARGE FILES system-prompt section teaches the model to page to the section it's editing and never compose old_text from memory.
  • contextLength config → OLLAMA_CONTEXT_LENGTH on daemon spawn — the SK Ollama connector exposes no num_ctx, so MandoCode never set the context window; local models ran at Ollama's ~4k default, silently truncating the system prompt and earlier reads as agentic conversations grew (the root cause behind "model forgot its instructions" and several stall patterns). When MandoCode starts the daemon it now sets OLLAMA_CONTEXT_LENGTH from config (default 8192, 0 = leave Ollama alone, clamped 2048–262144, --config set contextLength <n>). Only applies when MandoCode launches the daemon — desktop-app users are steered to the app's own Context length slider by the new diagnostics.
  • Hardware-tier context auto-sizing — picking a local model in /setup or /model sizes the window from the parameter count in the tag (<3b → 8k, 3–6b → 16k, 7b+ → 32k; unparseable tags → 8k floor): the user self-selected the model knowing their GPU, so the tag is the best hardware signal available without probing VRAM. Cloud picks leave the setting alone (context is managed server-side at the model's full window). MandoCodeConfig.RecommendedContextLength is the single mapping.
  • /learn teaches the context window — new section with the "model's desk" analogy, the silent-forgetting failure mode, the ~0.5–1.5 GB VRAM per 8k rule of thumb, a warning that the Ollama desktop app's slider defaults to a small 4k, and an advanced note on OLLAMA_KV_CACHE_TYPE=q8_0.

Changed

  • /model is now an actual quick switch — pick a model, done. The max-tokens picker step is removed from the flow (response length lives in /config); local picks auto-size the context window per the tier mapping. The command was also mislabeled everywhere as picking "context size" — MaxTokens maps to Ollama's NumPredict, the per-reply output cap, not the context window. All labels now say what the knob does.
  • Max-response-tokens tiers trimmed to 2k–64k — the 128k/200k tiers were context-window numbers wearing the wrong hat; no single reply is 200k tokens, agentic work is chunked across tool calls by design, and on local models a reply can't exceed the context window anyway. MaxMaxTokens lowered to 65536; ValidateAndClamp heals existing configs saved above it. Picker explainer now states explicitly that this is not the context window.
  • Recover from "request body too large" / HTTP 413 errorsRetryPolicy.IsContextOverflowError now matches "request body too large", "request entity too large", "payload too large", "413", and HttpRequestException.StatusCode == 413. Previously these transport-level rejections (Ollama's Go HTTP server enforces a request-body size limit before the model ever sees the prompt) fell through to FormatHttpFailure, which printed "Make sure Ollama is running: ollama serve" — wrong advice since the daemon was fine and the payload was too big. Now they route through CompactChatHistoryAsync and auto-retry the same way model-side context overflows do, so a long agentic session that blows the HTTP body limit recovers transparently instead of dying with a misleading sign-in nag.
  • Shell-based file reads via execute_command are refused — new circuit in FunctionInvocationFilter.OnFunctionInvocationAsync short-circuits commands whose first verb matches type|cat|head|tail|more|less|nl|gc|Get-Content|findstr|grep|sls|Select-String|sed|awk and steers the model back to read_file_contents. Models that hit read_file_contents's 10K cap on large files were falling back to type foo.js | more +555 or findstr /n "x" *.cs, which bypassed the duplicate-read circuit and dumped fresh file content into chat history on every call — a major contributor to runaway request-body growth. The check uses a word-boundary regex so typescript/category/etc. don't trigger, and piped filters like git status | grep modified still pass through (only the leading verb counts).
  • edit_file "Could not find old_text" error includes the current file contentBuildEditPreview in the filter and EditFile in the plugin both attach a content hint (capped at 5K chars with a truncation marker) when the match fails. Previously the model's only recovery path was to fire a separate read_file_contents trip, which contributed to the cascade of redundant reads in long sessions. With the content inline, the model can correct its old_text and retry in the next call without the round trip.
  • Edit-failure content hint is deduped per (scope, path)InvocationScope.HasEmittedEditHint / MarkEditHintEmitted track which paths have already had their content shipped this turn. First failure emits the 5K hint; subsequent failures emit a one-line pointer back ("the content was attached to an earlier failure — re-examine it"). Without this dedup, a model stuck in an exploratory loop (e.g. 20 failed edits on the same file as it hunts for the right region) shipped the same 5K blob 20 times, defeating the cascade-prevention the hint was meant to provide. RecordWrite clears the flag because the file's content has changed and any prior hint is stale.
  • N=3 edit-failure circuit per pathInvocationScope.RecordEditFailure / GetEditFailureCount track consecutive failed edit_file attempts per path. After InvocationScope.EditFailureCircuitThreshold (3) failures without an intervening write, the filter refuses further edits on that path and steers the model toward read_file_contents (with a targeted line range) or write_file (whole-region rewrite). Caps the wasted-call cost of a stuck loop independently of the content-hint dedup — three retries is enough to absorb a normal "fix old_text and try again" pattern, but short enough to short-circuit a thrash loop before it eats the turn.
  • "Cancel the plan" from the step-failure prompt actually cancels the planTaskPlannerService.ExecutePlanAsync's generic exception catch was deciding shouldCancel BEFORE the yield that prompted the user. So a "Cancel" pick set plan.Status = Cancelled after the catch had already committed to "skip and continue", and the loop silently moved to the next step. Decision moved to after the yield; added a plan.Status == Cancelled guard at the foreach loop top as defense in depth (the previous version only checked cancellationToken.IsCancellationRequested, which the user-prompt path doesn't trigger).
  • AIService decomposed (~1,550 → ~1,100 lines) — the ~450-line text-based function-call fallback (for local models that emit tool calls as JSON text) moved to its own FallbackFunctionCallExecutor service, and the duplicated timeout/stall-watchdog/retry/token-recording scaffolding that existed once in the chat-turn path and again in the plan-step path is now a single shared ExecuteModelCallAsync with typed cancellation classification (ModelStallException / ModelCallTimeoutException). The two copies had already started drifting; a watchdog fix now lands once and applies everywhere.
  • Warnings are now build errorsTreatWarningsAsErrors enabled in both projects after fixing all five outstanding nullability warnings properly ([MemberNotNull] on BuildKernel, no null! suppressions). The warning count is locked at zero permanently.
  • Default stall watchdog raised from 180s to 420s — model calls are non-streaming, so the watchdog receives no signal while the model generates and cannot distinguish "healthily generating a long reply" from "hung." With the response cap now at honest values, long single-round generations are possible — and on a busy cloud provider (~10 tok/s observed), a several-thousand-token reply legitimately runs past 3 minutes. Observed live: a plan step composing a design document was killed mid-generation by the 180s watchdog. 420s keeps genuine-stall detection bounded while giving slow generations room; existing configs keep their saved value (/config set modelResponseTimeout 420 to adopt). The durable fix — streaming model calls with per-chunk watchdog resets, so the watchdog measures silence rather than duration — is tracked for a future release.
  • Folder creation reviewed, deliberately left unprompted — audited whether create_folder should gate like writes/deletes. Verdict: it can't overwrite or destroy anything, it's confined to the project root, write_file (which is gated) already creates parent directories anyway, and git doesn't track empty dirs — while prompting for it would erode the attention users give to the approvals that matter. Documented decision, zero code.

UI

  • One palette across every menu in the app — green = proceed, warm gold = redirect/decline, red = destructive, black-on-deepskyblue1 = current selection, deepskyblue1 = prompt titles. Applied to all five approval prompts (diff, command, delete, MCP, plan), the step-failure prompt (gold Skip / red Cancel the plan), /config, all four /setup wizard menus, every onboarding menu, /model, /skill, and the music genre picker. Neutral pickers (models, genres, skills) get the title + highlight only — option colors stay reserved for decision prompts so the green/gold/red semantics keep their meaning. Supersedes 0.10.0's green-highlight convention. Both App.razor and ConfigurationWizard expose a shared SelectionHighlight style so future prompts inherit the standard by reference.
  • New ApprovalSelect Razor component — arrow-key decision menu rendered through the VDOM with per-option colors (something RazorConsole's stock Select can't do), the standard highlight treatment, and a > indicator matching the Spectre look. Awaits observe the cancellation token, so Esc/timeouts unwind a turn stuck at the prompt. Component docs capture the hard-won rendering rule: element structure must stay identical across renders (vary attributes, never swap element shapes) or RazorConsole's VDOM differ collapses the menu.

Test coverage

393/393 tests passing. New: SystemPromptsTests (LIVE WEB ACCESS section present with anti-disclaimer rules when web search is on, no tool advertising when off, core identity intact in both variants), tavilyKey cases in ConfigKeySetterTests (set/clear aliases, masking in every message and listing, the wrong-prefix warning, the post-set validation hook), FallbackFunctionCallExecutorTests (36 cases — all three text-call JSON formats, escaped-string arguments, nested braces, name/parameter normalization), PlanCancellationCircuitTests (the exact observed cancel-leak scenario, driven through a real Kernel), PostPlanMutationGateTests (gate refusal without invocation across all five mutating functions, reads still allowed, fresh-scope reset, rejected plans not arming the gate, manifest content and capping, partial-completion arming), and PlanStepContextTests (verbatim-request inclusion, head-truncation survival of folder references, last-2 previous-results window).

Weekly OSS security release digest.

The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.

No spam, unsubscribe anytime.

Share this release

Track MandoCode

Get notified when new releases ship.

Sign up free

About MandoCode

All releases →

Related context

Beta — feedback welcome: [email protected]