Fix Subsystems tab: use process-level native memory, improve column layout
Details
The "Native Memory" summary card was showing only Rust allocator-tracked
bytes (~126 B), which is misleading when the process uses ~1.4 GB native.
The Rust GlobalAlloc only tracks Rust heap allocations — C/C++ libraries
(SQLCipher, ONNX, Whisper, LLamaSharp) use their own malloc directly.
- Summary "Native Memory" card now uses WorkingSet64 - GC heap (same
calculation as the Overview tab's memory card)
- Per-subsystem memory column renamed to "Tracked Memory" and widened
to accommodate labels like "126 B native" or "7.0 MB managed"
- Memory display now distinguishes native vs managed with explicit labels
- Column widths adjusted (Category 80, Tasks 60, Memory 180, Rate 100)
- Removed broken heartbeat approach for long-running task alloc tracking
(GC.GetAllocatedBytesForCurrentThread can't be queried cross-thread)
Add per-subsystem memory tracker and Dashboard Subsystems tab
Details
Adds visibility into which subsystems consume memory across .NET managed
heap, native Rust allocations, and background task activity. Enables
live per-subsystem metrics in the Dashboard.
Rust tracking allocator (Phase 1):
- New allocator.rs: custom GlobalAlloc with 16-byte header per allocation
storing size + subsystem ID. Per-subsystem atomic counters track bytes
and allocation counts. Thread-local subsystem tag set via with_subsystem()
scope guard.
- Subsystem IDs: untagged(0), storage(1), sync(2), crypto(3), cloud(4),
plugins(5), ffi(6), reserved(7)
- New FFI export: privstack_subsystem_memory() returns JSON snapshots
- Key entry points wrapped: privstack_execute (FFI), auth_initialize/unlock
(Crypto), sync_status (Sync)
C# SubsystemTracker (Phase 2):
- SubsystemTracker: singleton service with AsyncLocal tagging, RunTagged()
for async/sync task wrapping, EnterScope() for synchronous code blocks,
5-sample rolling allocation rate, native counter merging from Rust FFI
- SubsystemDefinitions: static table of 16 built-in subsystems across
UI, AI, Core, Services, and Runtime categories
- Static RunTaggedStatic() convenience for instrumentation without DI
Entry point instrumentation (Phase 3):
- RagIndexService: consumer + deferred init tagged as ai.rag
- IntentEngine: signal consumer tagged as ai.intent
- EmbeddingService: ONNX load + tokenizer tagged as ai.embedding
- LocalLlamaProvider: model load tagged as ai.llm
- WhisperService: model load tagged as ai.whisper
- IpcServer: listener tagged as ipc
- ReminderSchedulerService: initial poll tagged as reminders
- FileEventInboundScanner: poll loop tagged as core.sync
- SyncOutboundService: debounce callback tagged as core.sync
- CloudSyncSettingsViewModel: push + status loop tagged as core.cloud
- UpdateViewModel: startup check tagged as updates
Dashboard Subsystems tab (Phase 4):
- New SubsystemItemViewModel with status dot coloring (active/idle/stopped)
- DashboardViewModel extended with Subsystems tab, summary cards (threads,
managed heap, native memory, active count), per-subsystem item collection
- DashboardView.axaml: third tab button, summary stat cards, table-style
subsystem list with status dot, name, category, task count, memory,
alloc rate columns
- Live refresh on 1-second timer when Subsystems tab is active
Registration wiring (Phase 5):
- SubsystemTracker registered as singleton in CoreServiceRegistration
- Static subsystems registered at startup, eagerly resolved in WireCorePostBuild
- Plugin subsystems registered dynamically in DashboardPlugin.CreateViewModelCore()
- NativeLibrary P/Invoke added for privstack_subsystem_memory
Version bumps:
- Rust workspace: 1.15.3 → 1.15.4
- Desktop app: 1.68.6 → 1.69.0
- Dashboard plugin: 1.4.0 → 1.5.0
Fix disabled plugins not showing in Settings for re-enable
Details
Root cause: when a plugin is disabled, its assembly is deferred (not
loaded) at startup to save memory. But the Settings plugin list only
iterated over loaded plugins (_plugins), so deferred plugins were
invisible — the user could never re-enable them.
Fix:
- Expose DeferredPluginDirs on IPluginRegistry — maps plugin IDs to
their discovered directory paths for plugins that were skipped
- SettingsViewModel.LoadPluginItems() now appends deferred plugins to
the list with IsEnabled=false, allowing the user to toggle them on
- Toggling on triggers the existing EnablePlugin() deferred path which
dynamically loads the assembly from the saved directory
- HeadlessPluginRegistry implements the new property with an empty dict
Fix fresh-install StorageError and update C# paths for SQLite migration
Details
Root cause: on fresh install (no salt file, no existing DB), init_core()
opened an unencrypted on-disk database at data.privstack.db. When
auth_initialize() then tried to create an encrypted DB at the same path,
SQLCipher failed because the file already existed as unencrypted.
Fix (Rust FFI):
- Fresh install now opens in-memory placeholder (same as encrypted mode)
- Only open unencrypted on-disk for legacy databases that already exist
- auth_initialize detects legacy unencrypted DB and skips SQLCipher
creation, just initializes vault on the existing DB
Fix (C# Desktop):
- WorkspaceService: base path changed from "data.duckdb" to "data"
(Rust derives "data.privstack.db" via with_extension)
- SetupWizardViewModel: same path change
- PrivStackService: updated diagnostics to check .privstack.db,
.datasets.db, .privstack.salt instead of old .duckdb files
- App.axaml.cs: updated orphan cleanup patterns and diagnostics
- DashboardViewModel: updated file size checks and compact display
- SystemMetricsService: updated file paths, WAL suffix (.wal → -wal)
Add legacy DuckDB migration detection FFI exports
Details
Since the Rust core no longer includes the DuckDB library, migration
from old .duckdb files must be orchestrated from the C# managed side.
These FFI functions support that workflow:
- privstack_has_legacy_databases(path) → bool: detects if any .duckdb
files exist at the data path
- privstack_list_legacy_databases(path) → JSON array of found files
with names, paths, and sizes
- privstack_archive_legacy_databases(path): renames .duckdb files to
.duckdb.bak after successful migration, removes stale WAL files
The C# side should check for legacy databases on first launch with the
new version, run its migration logic (reading DuckDB via its own driver
and writing to the new SQLite/SQLCipher database), then call archive
to move the old files out of the way.
Get notified about new releases