Unload AI models from memory when AI is disabled
Details
When the user toggles AI off in settings, the ONNX embedding model
(InferenceSession + tokenizer) and local LLM weights (LLamaWeights +
LLamaContext) were staying resident in memory because nothing called
Dispose or released the native resources. This caused ~1.7GB+ of
memory to persist even after AI was disabled.
Changes:
- Add EmbeddingService.UnloadAsync() — disposes the ONNX session and
tokenizer without permanently disposing the service, so it can be
re-initialized later via InitializeAsync()
- Add LocalLlamaProvider.UnloadModelAsync() — same pattern for the
LLama weights and context
- Add IEmbeddingService.UnloadAsync() to the interface contract
(+ HeadlessEmbeddingService no-op stub)
- Wire OnAiEnabledChanged(false) to call UnloadAiModelsAsync() which
releases both models and nudges GC to reclaim native buffers
- Add RagIndexService + EmbeddingService disposal to
MainWindowViewModel.Cleanup() for clean app shutdown
Fix Dashboard blocked by slow API health check (5-min timeout)
Details
Root cause: RefreshAsync populated AllPlugins AFTER awaiting
IsOnlineAsync(), which used PluginInstallService's 5-minute timeout
HttpClient for a simple health check. When privstack.io was slow or
unreachable, the entire dashboard sat on "Loading..." for minutes
with nothing rendered.
Fix: Split RefreshAsync into three phases:
1. PopulateLocalPlugins() — instant, no network. Builds AllPlugins
from filesystem manifests + IPluginRegistry.Plugins (the actually
loaded plugins). Sets IsLoading=false immediately so plugins render.
2. LoadSystemMetricsAsync() — local metrics (disk sizes, memory).
3. TryFetchRemoteRegistryAsync() — non-blocking enrichment with
server data. If it fails, local plugins are already displayed.
Also added a 5-second CancellationToken timeout to IsOnlineAsync()
so the health check doesn't inherit the 5-minute download timeout.
Fix Dashboard showing no plugins when registry is offline
Details
The previous fix handled the API timeout gracefully but the dashboard
still showed 0 plugins because GetInstalledVersions() only scans
~/.privstack/plugins and bundled plugin directories for manifest.json
files. In dev mode (project references) and when plugins are loaded
from non-standard paths, no manifests exist in those directories.
Added a fallback that populates AllPlugins from IPluginRegistry.Plugins
— the actually loaded/running plugins — for any plugin not already
covered by the server registry or filesystem manifests. This ensures
the dashboard always shows all active plugins with their metadata
regardless of how they were loaded.
Fix Dashboard showing empty when plugin registry API times out
Details
The Dashboard RefreshAsync method called AllPlugins.Clear() before
fetching the remote plugin registry, so when the API call timed out
the entire catch block skipped populating locally installed plugins,
system metrics, and workspace activation state — leaving the UI stuck
showing "0 installed · 0 available" with empty metric cards.
Restructured RefreshAsync to:
- Fetch local installed versions first (never fails)
- Wrap the remote registry fetch in its own try/catch so timeouts
are non-fatal — locally installed plugins still appear
- Always load system metrics regardless of registry availability
Also increased PrivStackApiClient timeout from 15s to 30s since the
plugin registry endpoint can be slow.
Remove N+1 individual entity reads from BacklinkService index build
Details
Phase 2 of BuildIndexAsync already calls WikiLinkParser.ExtractContentFromEntity
on each entity from ReadList. The "load individually" block re-fetched entities
without content via individual SdkAction.Read calls, but the individual Read
returns the same JSON shape — ExtractContentFromEntity produces the same empty
result. This caused 21+ unnecessary contact.Read SDK calls at startup.
Removing the block eliminates the N+1 query pattern with no behavioral change,
since entities that genuinely lack parseable text fields won't gain content from
a re-fetch.
Get notified about new releases