Commit Graph

394 Commits

Author SHA1 Message Date
792fb5ea08 agent: dev launcher — own Linux env-var contract, add dev:tauri, doc sweep
The 2026-05-12 engine-slop pass removed runtime std::env::set_var mutation from src-tauri/src/lib.rs and replaced it with warn_if_x11_env_unset_on_wayland. With no launcher owning the contract, Linux Wayland dogfood was about to regress.

run.sh:
- now owns Linux launcher env defaults via case "$(uname -s)"
  LIBCLANG_PATH=/usr/lib64/llvm21/lib64 (user-set wins)
  WEBKIT_DISABLE_DMABUF_RENDERER=1 (always on Linux; user-set wins)
  GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11 (Wayland only; user-set wins)
- 60s Vite readiness timeout
- detects early Vite exit (kill -0 + wait) instead of hanging forever
- trap installed before wait so Ctrl-C cleans up
- args forwarded: ./run.sh --release etc.
- non-exec final Tauri launch preserved so cleanup trap fires

package.json:
- "dev:tauri": "./run.sh" — canonical discoverable dev command

Docs:
- README, dev-setup, architecture-map runtime + launcher pages updated with the new contract; canonical command is npm run dev:tauri (./run.sh as direct equivalent)
- dev-launcher-and-scripts.md replaces the incorrect "kills process group" claim with honest "kills the spawned npm process"
- dev-setup.md path /CORBEL-Projects/magnotia → /CORBEL-Projects/transcription-app
- gpu-tuning/plan.md gets a superseded note rather than rewriting the original rationale
- engine-slop-residuals.md gains Area F (packaged-binary launcher contract) with honest wrapper-vs-.desktop trade-off documented

Verification: bash -n run.sh; shellcheck clean; cargo check -p magnotia green; npm pkg get 'scripts.dev:tauri' returns "./run.sh"; stale-ref sweep clean across living docs.
2026-05-12 22:05:33 +01:00
db654deecc agent: engine slop pass — DSP, typed errors, regex parsing, tracing, audit fixes
External code review on 2026-05-12 rated the codebase 4/10 across audio DSP, error typing, JS injection, env-var safety, ALSA parsing, and async logging. This commit lands the prognosis-level fixes plus three audit follow-ups.

Audio/DSP:
- StreamingResampler/rubato confirmed in the live capture path
- regression test at 12 kHz (rms < 0.01, ~40 dB) catches naive decimation
- near-Nyquist test at 9 kHz (rms < 0.05, ~26 dB) exercises transition band

Core errors:
- Other(String) removed; ProviderNotRegistered introduced
- Io variant restructured as struct with kind/message/raw_os_error
- FileNotFound display quotes paths
- Configuration variant removed (unused)

Core types:
- ModelId, EngineName backed by Cow<'static, str>; const borrowed ctor
- Megabytes::from_gb takes u64 (was f64)
- AudioSamples::sample_rate is NonZeroU32; zero-rate defensive branch removed

Capture:
- /proc/asound/cards parsing rewritten as anchored regex (OnceLock)
- regression test covers product names with embedded colons
- monitor_pattern_detection test restored alongside the regex test
- DEAD_SILENCE_FLOOR promoted to module-level with rationale
- DEVICE_VALIDATION_MS, SILENCE_RMS_FLOOR documented with field-observation rationale
- RMS validation loop made idiomatic
- eprintln! migrated to tracing with structured fields and targets

Tauri startup:
- unsafe std::env::set_var removed; ensure_x11_on_wayland renamed to warn_if_x11_env_unset_on_wayland (launcher/wrapper owns env-var contract)
- DB init + log prune + preferences load collapsed to one block_on
- build_preferences_script rewrites JS injection from JSON.parse string to direct object literal plus malformed-JSON guard and unit tests
- WebKitGTK microphone auto-grant logs warning at startup
- tracing subscriber initialised at top of run() (warn,magnotia=info,... on stderr; honors RUST_LOG); previously eprintln→tracing migration was silent because no subscriber existed

Filename counter:
- RECORDING_COUNTER uses SeqCst

Tests: cargo test --workspace --lib green (322 passed, 0 failed across 10 crates).

Three independent audits (original cleanup → Wren → fresh Codex subagent) concur on no critical findings.

Deferred to docs/superpowers/plans/2026-05-12-engine-slop-residuals.md: storage-layer typed errors, remaining eprintln→tracing sweep, capture actor-model refactor, property-based DSP testing, frontend/backend error boundary cleanup.
2026-05-12 22:03:58 +01:00
b463c32f17 chore: stabilize current head before Phase 10a QC 2026-05-10 23:00:25 +01:00
c95a5da077 docs(roadmap): bookmark SIE + adjacent embeddings/rerank/extract servers for PKM phase
Wyrdnote scales from voice-first dictation today to local-first
semantic PKM workbench tomorrow per the engine architecture spec.
The PKM phase needs a serving stack for embeddings + reranking +
entity extraction; this note bookmarks SIE (Superlinked Inference
Engine, Apache-2.0) as the trigger candidate plus six adjacents
to bake off against when the PKM phase lands.

Deferred because (1) PKM sits post-public-beta and infrastructure
decisions today would generalise poorly to a Q4 2026 selection;
(2) the candidates evolve fast; (3) Wyrdnote's local-first +
single-consumer-GPU constraint rules out a today-style bake-off
without real PKM-shape workload to test against.

Decision dimensions captured for the future evaluator: self-host
footprint, cold-start latency, throughput, quality on a Wyrdnote
eval set, reranker availability, extract/NER capability,
OEM-licensability against AGPL-3.0+dual-licence stack, telemetry
posture. Re-evaluation triggers documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:27:32 +01:00
6bc8acccce feat(transcription): Phase A — engine protocol + registry + orchestrator (clean-room)
Introduces the architectural spine for the Wyrdnote engine layer per
the spec at outputs/wyrdnote/2026-05-10-engine-architecture-spec.md
in the CORBEL-Main workspace, Codex-reviewed 2026/05/10.

Clean-room derivative of VoiceInk's TranscriptionService /
TranscriptionServiceRegistry / TranscriptionPipeline shape. Built from
the architectural pattern only; no GPL-3.0 code, comments, or
identifiers lifted. Wyrdnote names, control flow, and trait signatures
are original.

What this lands:

crates/cloud-providers/src/provider.rs (new, 184 lines)
  TranscriptionProvider async trait. Object-safe via async_trait so
  Arc<dyn TranscriptionProvider> is the canonical shape. Lives in
  cloud-providers (not transcription) per D7 so an OEM licensee
  implementing the trait depends only on this crate plus magnotia-core
  (no transcription internals leaked through the trait surface).
  Surrounding types: ProviderId (lower-kebab-case), ProviderKind,
  NetworkRequirement, CostClass, ProviderCapabilities, EngineProfile,
  ProviderTranscript. Compile-time object-safety witness in tests.

crates/cloud-providers/Cargo.toml (modified)
  Adds async-trait + serde dependencies.

crates/cloud-providers/src/lib.rs (modified)
  Re-exports the provider surface.

crates/transcription/src/registry.rs (new, 183 lines)
  EngineRegistry: catalogue of providers keyed by ProviderId. Push-style
  registration, default-provider id captured at construction. Read-only
  after boot. Full unit-test coverage: empty default, register/get
  round-trip, default-resolves-after-registration, re-register replaces,
  ids enumeration.

crates/transcription/src/orchestrator.rs (new, 286 lines)
  LocalProviderAdapter wraps Arc<LocalEngine>, presents async
  TranscriptionProvider upward via tokio::task::spawn_blocking. Per D7
  the adapter lives in the orchestrator, NOT as impl TranscriptionProvider
  for LocalEngine — this keeps the dependency edge one-directional and
  avoids leaking async-runtime requirements onto the synchronous
  Transcriber trait.

  Orchestrator: single transcribe() entry point; resolves a provider
  from the registry, derives TranscriptionOptions from the EngineProfile,
  dispatches. Returns a clear error when an unregistered provider is
  named. Unit tests use a mock CannedProvider to validate dispatch,
  error handling, and option routing without booting a real model.

crates/transcription/Cargo.toml (modified)
  Depends on magnotia-cloud-providers + async-trait. Dev-dep tokio
  gains macros + rt-multi-thread features for #[tokio::test].

crates/transcription/src/lib.rs (modified)
  Exports Orchestrator, EngineRegistry, LocalProviderAdapter, and
  re-exports the provider trait surface so downstream crates depend
  only on magnotia-transcription for both local engines and the trait.

KNOWN-ISSUES.md (modified)
  Adds KI-06: existing dictation, live, and meeting commands still call
  LocalEngine::transcribe_sync directly via pick_engine. The orchestrator
  path is dormant until a Phase A.1 follow-up commit migrates the call
  sites. Cloud providers (Phase G) cannot be exercised end-to-end until
  the rewire lands. Phasing rationale: this commit lands the abstractions
  cleanly with full test coverage; the rewire is a separate diff so the
  chunking + post-processing logic in transcribe_file moves without
  inflating this commit beyond review fidelity.

Test results: 9 new tests pass (orchestrator: dispatches, errors-on-
unregistered, routes-initial-prompt, object-safe; registry: empty-default,
round-trip, default-resolves, re-register, ids-list). Pre-existing
59 transcription tests + 5 cloud-providers tests still green.
cargo check --workspace clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 07:49:24 +01:00
jars
d21109ec27 docs: defer dual theme cleanup to KNOWN-ISSUES KI-05
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
audit / cargo audit (push) Has been cancelled
audit / npm audit (push) Has been cancelled
settings.theme and preferences.theme overlap: the legacy localStorage
field still drives two SegmentedButton bindings in SettingsPage.svelte
while preferences.theme is the canonical Tauri-persisted store. A
migration \$effect in the four route +layout files syncs the legacy
value into preferences whenever it changes.

Resolution requires touching SettingsPage.svelte (2 484 LOC),
src/lib/stores/page.svelte.ts, four route layouts, and the localStorage
migration system. That is moderate-risk frontend work, not pre-launch
polish, so it lands in KNOWN-ISSUES with a concrete plan and is
deferred from v0.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:04:56 +01:00
jars
b16fc179b3 chore: remove 11 orphan Tauri commands and downstream dead code
Sweep of registered-but-never-invoked commands surfaced by an audit
against the frontend invoke() call sites. Each was confirmed dead via
grep across src-tauri, src, and crates: no caller anywhere.

Removed commands:
- check_model, count_transcripts_command, get_profile_cmd,
  install_update, list_feedback_examples_cmd (utility/CRUD shapes
  never wired)
- save_audio, start_native_capture, stop_native_capture (native-capture
  path superseded by the live transcription session)
- transcribe_pcm, transcribe_pcm_parakeet (PCM commands superseded by
  live session; no frontend caller)
- close_preview_window (preview window is hidden via the
  core:window:allow-hide capability, not the command)

Cascade in audio.rs (~430 lines removed):
- CaptureWorker, NativeCaptureState struct + impl, stop_worker,
  append_recorded_chunk, MAX_NATIVE_CAPTURE_RETURN_SAMPLES,
  persist_audio_samples
- The two cfg(test) tests that exercised stop_worker (the
  recording_filename tests stay, supporting resolve_recording_path
  which the live session uses)

Cascade elsewhere:
- FeedbackDto struct and its From<FeedbackRow> impl
- Stale storage imports in feedback.rs, profiles.rs, transcripts.rs
- tauri::Emitter import in transcription.rs
- app.manage(NativeCaptureState::new()) in lib.rs setup

generate_handler entries removed for all 11 commands. cargo check passes
cleanly with zero warnings; no tests reference any deleted symbols.

Net: 709 deletions, 17 insertions across 9 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:03:19 +01:00
jars
2a45cb8033 fix: model-loaded guards on transcribe_pcm, power assertion guards on tasks LLM commands
transcribe_pcm and transcribe_pcm_parakeet did not check that their
respective engines were loaded before clone+spawn_blocking. transcribe_file
already calls ensure_model_loaded; these now mirror that posture with a
friendly error when the engine is unloaded, matching what
extract_content_tags_cmd does.

decompose_and_store and extract_tasks_from_transcript_cmd ran multi-second
LLM inference inside spawn_blocking without the PowerAssertion guard that
cleanup_transcript_text_cmd and extract_content_tags_cmd already use. Both
now begin a guard so the macOS App-Nap inhibitor (and the planned
Linux/Windows equivalents per KI-02, KI-03) can pin the process for the
duration of the inference.

HANDOVER.md gets a status note clarifying it captures Phase 9 state. The
schema head referenced (v14) was the head at session time; current head
is v15 (`idx_transcripts_profile_created` composite index) per the
architecture map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:47:48 +01:00
jars
e5e12387e8 docs: add KNOWN-ISSUES.md and rewrite PowerAssertion top doc
PowerAssertion file-level doc previously claimed Linux logind and
Windows SetThreadExecutionState implementations in present tense.
Both are no-ops; the macOS path compiles but is unverified on
Apple Silicon (RB-08). Rewrite top doc to state present vs
planned posture and reference KNOWN-ISSUES.md.

Surfaced as tracked limitations:
- KI-01: macOS App Nap guard pending Apple Silicon verification
- KI-02: Linux power assertion is a no-op
- KI-03: Windows power assertion is a no-op
- KI-04: magnotia-cloud-providers crate not user-exposed in v0.1
  (in-memory keystore needs OS keychain before any save-key UX)

README links to KNOWN-ISSUES.md from Status, Platform support
table, and Project documentation. Platform support table notes
adjusted per OS to reflect actual idle-inhibit posture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:29:50 +01:00
jars
a1f3f3f134 docs: architecture map (initial 5-slice generation, 105 pages)
Five-slice navigable map of the entire codebase under
docs/architecture-map/. Each slice is a self-contained
breadcrumbed sub-tree:

  01-frontend (16)              Svelte/SvelteKit UI
  02-tauri-runtime (26)         src-tauri commands + lifecycle
  03-audio-transcription (16)   audio + transcription crates
  04-llm-formatting-mcp (19)    llm, ai-formatting, mcp, cloud
  05-core-storage-hotkey-build  core, storage, hotkey, workspace,
                          (26) CI, dev glue

Plus master README.md and data-flow-end-to-end.md tracing
audio bytes from microphone to FTS5 search to MCP read.

Generated by 5 parallel subagents on 2026/05/09 against
HEAD 3c47000. Each page has YAML frontmatter, file:line code
refs, sibling cross-links, plain-English summaries.

Aggregated debt surfaced (full lists in master README):
RB-08 macOS power assertion, schema head drift v14 vs v15,
VAD blocked on ort version conflict, streaming primitives
not wired into live.rs, no prompt versioning, MCP has no
auth, cloud-providers in-memory keystore, SettingsPage
2 484 LOC, commands/live.rs 1 737 LOC, dual theme system,
brand rename to Lumenote pending across the codebase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:04:13 +01:00
jars
3c47000ea9 chore(core): add tuning_log_demo example for live heuristic validation
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Run with:
  cargo run -p magnotia-core --example tuning_log_demo

Initialises a tracing-subscriber fmt layer so the inference_thread_count
INFO event is rendered to stderr. Exercises all 8 (workload,
gpu_offloaded) combos under three power scenarios: AC override,
battery override, and the real sysfs probe.

Used to validate the heuristic on 2026-05-09 against the spec's
6c12t truth table — all 6 rows match.

Adds tracing-subscriber as a [dev-dependencies] for the example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:55:00 +01:00
jars
052265b3c2 chore(llm): document trivial-true cross-check as observability gap
Per the final reviewer's suggestion: the gpu_offloaded cross-check
(gpu_layers >= model.n_layer()) is trivially true when use_gpu since
we always pass u32::MAX. The check documents intent and is future-
proofed if we ever pass specific N, but a future reader might
simplify it away as dead code. Inline comment points at the spec's
out-of-scope follow-up for true residency observability.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:47:28 +01:00
jars
4f3c063008 chore(core): tidy review nits — alphabetical lib.rs, import placement, docstring
End-of-phase cleanup of items flagged across review passes for tasks
1.1, 1.2, and 1.3:

1. crates/core/src/lib.rs: pub mod power moved to alphabetical
   position (between paths and process_watch).
2. crates/core/src/power.rs: use statements moved to top of file
   (above the PowerState enum) per Rust convention.
3. crates/core/src/power.rs: parse_power_state_from_dir docstring
   tightened to acknowledge that read_dir.flatten() silently skips
   per-entry errors. Theoretical hazard only on world-readable
   sysfs, but the previous wording overstated the guarantee.

No behavioural change. All 40 magnotia-core tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:44:09 +01:00
jars
a58c2f0994 test(transcription): thread_sweep prints power-aware 4-panel table
Extends the JFK thread-count sweep to print four labelled panels
(AC+CPU, AC+GPU, battery+CPU, battery+GPU) driven by
MAGNOTIA_POWER_STATE_OVERRIDE. Each panel shows the helper's
predicted thread count for its (power, gpu) combination alongside
the empirical RTF table for n_threads = [1, 2, 4, physical, logical,
maybe 8].

The actual whisper context is initialised once (Vulkan if compiled
in and resolvable, CPU otherwise), so the RTF rows themselves are
produced by the same backend across panels. The CPU vs GPU axis
controls only the helper-pick column, which is the empirical
question we want answered: is the LLM=2 / Whisper=4 GPU floor right?

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:43:04 +01:00
jars
7eb69e6251 refactor(core): remove old inference_thread_count facade
The MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS, and
inference_thread_count() defined in constants.rs were superseded by
the equivalents in tuning.rs (Task 2.1) and are no longer called by
any production code. Both call sites (whisper backend Task 4.1, LLM
generate Task 5.1) have migrated.

Constants.rs now holds only audio/mel/RAM/VAD/download constants;
inference-tuning concerns live in tuning.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:41:23 +01:00
jars
0a503be38d feat(llm): LLM threads use Workload::Llm + GPU offload detection
Wires the LLM call site through the new tuning helper. gpu_offloaded
reflects intent (use_gpu) cross-checked against the loaded model's
layer count: u32::MAX (when use_gpu) is trivially >= any model's
n_layer, but the explicit comparison is future-proofed if we ever
pass a specific N instead of u32::MAX.

Note: the call site is in generate() not load_model() as the plan
suggested. Context params (and thus thread count) are constructed
per-inference, not per model load, since n_ctx depends on prompt
size. The implementer adapted correctly.

The old magnotia_core::constants::inference_thread_count import is
replaced. Task 6.1 removes the constants helper now that both call
sites have migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:30:34 +01:00
jars
e715da3b54 feat(transcription): whisper threads use Workload::Whisper + GPU detection
Wires the whisper backend through the new tuning helper. gpu_offloaded
combines a compile-time feature check (whisper-vulkan, in default
features) with a runtime libvulkan probe via the hardware module. If
libvulkan1 is missing on Linux, whisper-rs's vulkan backend silently
falls back to CPU, so we should not reduce threads in that case.

The old magnotia_core::constants::inference_thread_count import is
replaced by tuning::{inference_thread_count, Workload}. The constants
module's helper is removed in Task 6.1 once the LLM call site has
also migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:21:33 +01:00
jars
d6bde52d6e refactor(tauri): use magnotia_core::hardware::vulkan_loader_available
Delete the local duplicate fn and libloading dependency from src-tauri;
import the canonical implementation from magnotia-core::hardware instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:08:16 +01:00
jars
b991355cb0 feat(core): port vulkan_loader_available from src-tauri
Adds libloading = "0.8" to magnotia-core dependencies and moves
vulkan_loader_available() into magnotia-core::hardware so non-Tauri
crates (transcription, llm) can probe the Vulkan loader without
depending on the Tauri binary. Test asserts the call completes on any
host regardless of whether libvulkan is installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 12:02:39 +01:00
jars
9c85c25b7f feat(core): per-process INFO log on first thread-count call per tuple
Adds a OnceLock<Mutex<HashSet>> cache in tuning.rs. inference_thread_count
now emits a single tracing::info! line the first time each
(workload, on_battery, gpu_offloaded) tuple is seen in the process,
recording the resolved thread count and which clamps fired (battery, gpu).
Also derives Hash on Workload and tightens the GPU clamp to only record
the "gpu" clamp when it actually reduces chosen. Smoke test added.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:58:32 +01:00
jars
6b456b1766 feat(core): GPU-offload clamp with per-workload floor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:53:03 +01:00
jars
847dc755ac feat(core): inference_thread_count halves on battery
Adds battery-state awareness to the thread-count helper: when
probe_power_state() returns OnBattery, chosen is halved before the
[MIN, MAX] clamp.  Also removes the leading underscore from the
workload/gpu_offloaded parameters (they are silenced via let _ = ...
until the GPU-clamp task).

New test battery_halves_thread_count verifies on_battery <= on_ac and
>= MIN when the host has more than MIN physical cores.  Restructured
to sequential with_override calls (not nested) to avoid re-entrant
deadlock on power::TEST_LOCK.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:49:32 +01:00
jars
02a6cb24ce fix(core): serialise tuning env-var tests behind a lock
Without a lock, env_var_bypasses_clamps (set_var + remove_var) and
matches_existing_clamp_when_no_clamps_apply (remove_var) race under
cargo's parallel test runner. Task 2.2 will add another remove_var
call, compounding the race.

Adds a #[cfg(test)] static THREAD_ENV_LOCK and a closure-style
with_thread_env_lock helper, mirroring power::with_override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:42:03 +01:00
jars
504ba0a361 feat(core): tuning module with Workload + helper skeleton
Adds crates/core/src/tuning.rs with MIN/MAX_INFERENCE_THREADS consts,
Workload enum (Llm/Whisper), and inference_thread_count() helper matching
the existing constants::inference_thread_count clamp behaviour. Three unit
tests pass. tracing dep added to Cargo.toml (used by future tasks).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:38:07 +01:00
jars
eccc5e9544 feat(core): 10s TTL cache on probe_power_state
Cache sits between override resolution and platform_probe; override
paths (test + env-var) bypass it entirely so they always take effect
immediately. OnceLock<Mutex<Option<CachedState>>> initialised lazily on
first probe. Two test-only helpers (force_clear_cache, force_set_cache)
expose the cache slot for unit tests. Mutex import ungated — now used in
production cache code as well as test override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:33:46 +01:00
jars
3d978e3978 fix(core): cfg-gate test override + restore TEST_OVERRIDE on panic
TEST_OVERRIDE and test_get_override are now #[cfg(test)]-gated and
the read in probe_power_state is wrapped in a #[cfg(test)] block,
so test scaffolding doesn't compile into production builds.

with_override now uses a drop-guard pattern so a panic inside body
still resets TEST_OVERRIDE, preventing stale state from leaking
into subsequent tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:30:33 +01:00
jars
0c761c0be6 feat(core): probe_power_state with Linux dispatch + override paths
Adds the public probe entry point with two override mechanisms:
- In-process TEST_OVERRIDE slot serialised by TEST_LOCK mutex for
  unit tests (avoids races with cargo's parallel runner).
- MAGNOTIA_POWER_STATE_OVERRIDE env var for integration tests set
  externally.
Platform dispatch: Linux reads /sys/class/power_supply; all others
return Unknown. Five new override tests pass alongside the six sysfs
parser tests from Task 1.2 (12 total green).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 11:24:50 +01:00
jars
276e3ca6c1 feat(core): parse_power_state_from_dir reads sysfs power_supply ABI
Pure function takes a directory path and returns OnAc, OnBattery, or
Unknown by reading `type` and `online` files in each supply entry.
Matches the kernel ABI documented at
Documentation/ABI/testing/sysfs-class-power. Failure modes (missing
dir, unreadable files, malformed contents) all fall through to Unknown
rather than panicking.

Seven tests cover: Mains online, battery-only, USB-PD, empty dir,
missing dir, and malformed entries. All pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:19:34 +01:00
jars
1f3496e4ed feat(core): add PowerState skeleton in new power module
Introduces crates/core/src/power.rs with the PowerState enum
(OnAc / OnBattery / Unknown) and a unit test confirming the three
variants are distinct. Registers the module in lib.rs and adds
tempfile = "3" to dev-dependencies for use in later tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:14:24 +01:00
jars
c46c0a5ce9 docs: implementation plan for battery- and GPU-aware thread tuning
Bite-sized TDD plan implementing the design at
docs/superpowers/specs/2026-05-09-battery-gpu-aware-thread-tuning-design.md.

8 phases, ~13 commits:
- Phase 1: power.rs (PowerState enum, sysfs parser, probe + overrides,
  10s TTL cache)
- Phase 2: tuning.rs (Workload enum, helper, battery clamp, GPU clamp,
  per-process logging)
- Phase 3: vulkan_loader_available moves from src-tauri to magnotia-core
- Phase 4: whisper backend wires through new helper
- Phase 5: LLM call site wires through new helper after model load
- Phase 6: remove old constants::inference_thread_count facade
- Phase 7: thread_sweep.rs prints 4-panel power-aware RTF table
- Phase 8: manual battery validation smoke

Each task is TDD: write failing test, run, implement, run, commit.
Tests live in unit-test modules inside power.rs and tuning.rs.

Override design uses an in-process with_override(state, |closure|)
helper for unit tests (cargo runs them in parallel; env-var path
would race) and MAGNOTIA_POWER_STATE_OVERRIDE env var for
integration tests like thread_sweep.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:01:21 +01:00
jars
200c4fb447 docs: battery- and GPU-aware inference thread tuning design
Spec for two new clamps on the existing inference_thread_count helper:
- Battery clamp: drop to physical/2 when on battery (Linux sysfs probe;
  macOS/Windows return Unknown, treated as OnAc).
- GPU-offload clamp: 2 threads for fully-offloaded LLM, 4 for Whisper
  (Whisper keeps mel spectrogram, decoder bookkeeping, and beam search
  on CPU even with full Vulkan offload).

Codex + online-research consult: no prior art in llama.cpp, Ollama,
Jan, or mistral.rs. Apple's Low Power Mode is OS-level (P-core
frequency cap), not a published software API. Lumenote would be first
in the open Rust inference-wrapper space; instrumentation matters,
hence the thread_sweep.rs extension + per-process INFO log.

Architecture (Approach B):
- New crates/core/src/power.rs (PowerState, sysfs probe, 10s TTL).
- New crates/core/src/tuning.rs (Workload enum, helper).
- Move vulkan_loader_available() from src-tauri to magnotia-core
  so crates/transcription can call it without a Tauri dep.
- Existing constants::inference_thread_count() becomes deprecated
  facade for one commit, then removed.

GPU-offload detection is intent-based (use_gpu && requested_layers
>= model.n_layer() for LLM; cfg(whisper-vulkan) && libvulkan
resolvable for Whisper). Residency-based detection deferred:
llama-cpp-2 0.1.145 doesn't expose post-load offload count, so a
true outcome flag would need llama_log_set callback parsing or an
upstream PR. Caveat documented in the spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:36:21 +01:00
jars
4cb954ece4 perf: route whisper + llm n_threads through physical-core helper
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Both inference call sites previously called `num_cpus::get()` (logical
thread count). Established whisper.cpp / llama.cpp guidance is that
SMT siblings contend for shared FPU resources during heavy F16/F32
matmul, so going past physical core count anti-scales. Empirical
sweep on Whisper Tiny / 11s JFK clip / Ryzen 5 4650U (6c12t):

  n_threads | xc_time | RTF    | speedup_vs_1
  ----------|---------|--------|-------------
          1 |   0.33s |  0.030 |   1.00x
          2 |   0.33s |  0.030 |   1.00x
          4 |   0.30s |  0.028 |   1.09x
          6 |   0.32s |  0.029 |   1.04x
          8 |   0.31s |  0.028 |   1.06x
         12 |   0.32s |  0.029 |   1.03x

Tiny doesn't scale (work dominated by overhead) but the larger
Whisper variants and Qwen LLMs do. Sources: whisper.cpp #200, #1033,
#1252, #403; llama.cpp #3167, #572.

Changes:
- crates/core/src/constants.rs:
  * MIN_INFERENCE_THREADS lowered 4 → 2 (research-derived floor)
  * MAX_INFERENCE_THREADS = 8 added (research-derived ceiling)
  * inference_thread_count() rewritten:
      - reads MAGNOTIA_INFERENCE_THREADS env var (override)
      - num_cpus::get_physical() with available_parallelism fallback
      - clamped to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]
- crates/core/Cargo.toml: + num_cpus = "1"
- crates/transcription/src/whisper_rs_backend.rs: call site uses helper.
- crates/llm/src/lib.rs: call site uses helper.
- crates/transcription/Cargo.toml: drop num_cpus from [features] +
  [dependencies] (production no longer needs it). Move to
  [dev-dependencies] for tests/thread_sweep.rs only.
- crates/llm/Cargo.toml: drop num_cpus = "1" (no longer used directly).

Per-machine maps (after this patch):
  Ryzen 5 4650U  6c12t → 6
  big-iron 12c24t       → 8 (clamp; users can override)
  cheap 2c2t laptop     → 2
  1c container/VM       → 2

Adds crates/transcription/tests/thread_sweep.rs — env-gated like
jfk_bench, prints the table above against any model + WAV. Useful for
re-baselining on new hardware or when tuning the clamp values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:26:23 +01:00
jars
fdf27db0a1 perf+fix: DMABUF default on Linux, popout ACL fixes, plugin version sync, JFK bench fixture
Bundled work from a low-end laptop (Ryzen 5 4650U / Vega 6 / Linux Mint
22.2 / X11) profiling pass.

perf: WEBKIT_DISABLE_DMABUF_RENDERER=1 default on all Linux
  Previously only set on Wayland sessions. Empirically it's a
  significant idle-cost win on integrated GPUs in either session type:
  env-var matrix (release binary, 75s settle, 10s jiffies CPU sample)
  showed magnotia idle CPU 12.30% → 2.80% of one core and idle GPU
  17% → 10% on this hardware. Users can opt back in by exporting
  WEBKIT_DISABLE_DMABUF_RENDERER=0. The Wayland-only XWayland
  fallback (GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11) is unchanged.

fix: secondary-windows ACL — allow set-always-on-top
  The float window's pin toggle calls setAlwaysOnTop() but the
  secondary-windows capability didn't permit it, so the popout was
  stuck always-on-top regardless of the pin state. Adds the
  core:window:allow-set-always-on-top permission. Narrow scope.

fix: guard registerGlobalHotkey against non-main webviews
  Cross-window settings sync via localStorage can re-fire the
  $effect(() => settings.globalHotkey) callback inside popout webviews
  where the main layout's registerGlobalHotkey is reachable. Adds an
  early-return when the current window label is not "main", so the
  popout doesn't trigger an ACL-denied register/unregister and the
  user no longer sees a spurious "Hotkey not registered" toast when
  popouts are open. Keeps the global-shortcut perm scoped to main.

build: pin @tauri-apps/api 2.10.1 + @tauri-apps/plugin-dialog 2.7.1
  Match the Rust crate versions tauri-cli's version-mismatch check
  enforces during release builds. Without this, `npm run tauri build`
  exits 0 silently while emitting an Error and never producing
  binaries.

test: add crates/transcription/tests/jfk_bench.rs
  Reproducible RTF regression fixture. Env-gated on
  MAGNOTIA_WHISPER_TEST_MODEL + MAGNOTIA_WHISPER_TEST_AUDIO so it
  never runs in CI without setup. Loads the JFK WAV inline (no hound
  dep), times model load + cold + warm transcribe, prints SUMMARY.
  Baselines on this hardware:
    --release --features whisper:               cold RTF 0.054, warm 0.050, RSS 125 MB
    --release --features whisper,whisper-vulkan: cold RTF 0.029, warm 0.028, RSS 125 MB
  Vulkan on RADV/Vega 6 nearly halves transcription latency for
  Whisper Tiny — useful baseline for Phase 10 hardware-recommendation
  scoring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:09:03 +01:00
2dfd7167fd fix(a11y): Toggle aria-invalid briefly when async onChange rejects
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
When an async onChange handler rejects, the toggle snapped back to its
previous value silently — screen reader users had no signal that the
action failed, only that the toggle suddenly returned to its original
state. Now aria-invalid="true" exposes the failure on the role=switch
button for 1.5s after rejection, so assistive tech announces the error.
Self-clears via setTimeout; no API change for callers.
2026-05-07 11:31:47 +01:00
aecb191705 refactor(theme): tokenise hard-coded amber shadows and reconcile duplicate @font-face declarations
Hard-coded rgba(214,132,80,X) shadows were tied to the dark-theme accent
and stayed off-hue in light mode (where the accent is a darker #a3683a).
Six tokens added to app.css @theme and mirrored in the light-theme block
plus design-system/colors_and_type.css:

  --shadow-accent-sm     0 0 8px   0.25 alpha  Toggle on-state
  --shadow-accent-md     0 4px 16px 0.3       ModelDownloader card
  --shadow-accent-lg     0 4px 20px 0.3       DictationPage record button
  --shadow-accent-glow   0 0 8px   0.4        progress-bar glow
  --shadow-accent-pill   0 1px 4px 0.3        SegmentedButton pill
  --shadow-accent-raised 0 2px 8px 0.2        FilesPage browse tile

Migrated seven sites: Toggle, SegmentedButton, ModelDownloader (x2),
FilesPage (x2), DictationPage record button, SettingsPage locale pill.
Focus-ring shadows (0_0_0_3px_rgba(...,0.1)) left alone — handled in
parallel via the --accent-shadow-focus token.

@font-face: app.css and colors_and_type.css both declare the same five
fonts. Duplication is unavoidable because preview/*.html loads the
design-system file directly without Vite, so it cannot share the
runtime declarations. font-weight ranges and font-style reconciled to
match exactly (Atkinson 200-800 not 400-700, JetBrains gains font-style:
normal); src URLs intentionally differ (root-absolute vs relative).
Comments added to both copies explaining the contract.
2026-05-07 11:31:14 +01:00
f4c0635549 fix(a11y): guard sidebar-toggle keypress against custom widgets and scope transcript font-size effect
- Extend isInputFocused to also bail when document.activeElement sits
  inside a custom widget with role combobox/listbox/radio/switch/menuitem/tab
  so single-letter shortcuts like '[' do not collapse the sidebar while
  focused on SegmentedButton, ZonePicker, etc.
- Move the transcript font-size CSS variable write from documentElement
  to document.body. Consumers inherit body styles, and scoping the write
  there avoids invalidating root-level styles every time fontSize changes.
2026-05-07 11:27:42 +01:00
736896c2b8 fix(a11y): announce HistoryPage inline-confirm armed state and tag-chip pressed state 2026-05-07 11:24:53 +01:00
9b17425bc9 fix(a11y): name FilesPage drop zone region and denest Card 2026-05-07 11:23:55 +01:00
d3554951e0 fix(a11y): privacy badge role/label and stronger search focus ring in light theme 2026-05-07 11:23:23 +01:00
47b005ad70 fix(a11y): bump record button to 48x48 and announce Recording stopped to screen readers
Record button bumped from 40x40 to 48x48 (WCAG 2.5.5 AA requires 44x44; 48 chosen as forgivable oversize for the most-touched control). Inner stop-square and record-circle bumped 14x14 to 16x16, Loader2 16 to 18, to keep the visual ratio.

Recording-status sr-only live region now announces both transitions: "Recording started" on start and "Recording stopped" on stop, with the stop message clearing after 1200ms so the live region does not hold stale text. Transition tracked via a plain let prevRecording (not $state) since it is bookkeeping and must not retrigger the effect. aria-atomic="true" added so the whole region re-reads on update.
2026-05-07 11:20:35 +01:00
ca3a55320b fix(a11y): bump all text below 12px to 12px floor and promote tertiary-on-body to secondary
Brand guidelines (docs/brand/magnotia-brand-guidelines.md §4) say "Never go
below 12px for any text" and "tertiary text must be ≥18px bold or ≥24px
regular". Audit found 246 sub-12px className sites and 166 cases of
text-text-tertiary paired with body sizes.

Bumped text-[9/10/11px] → text-[12px] across 26 .svelte files in src/lib
and src/routes. Promoted text-text-tertiary → text-text-secondary at body
sizes (12-14px) where context allowed; left placeholder pseudo-states,
ternary-conditional inactive states, and line-through "done" states alone
(8 residual pairings, all decorative).

Affects neurodivergent users on 1366×768 budget hardware most directly.
svelte-check: 0 errors, 0 warnings.
2026-05-07 11:18:10 +01:00
6da15d1ac8 feat(ui): wire deferred Card tone and elevation variants
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
2026-05-07 10:53:30 +01:00
9ba7babcb0 feat(settings): add quick-settings row for theme, font size, and hotkey 2026-05-07 10:52:51 +01:00
5778696140 feat(dictation): surface "Talk now, think later" empty state and hide post-transcript actions
Reduces first-run cognitive load. Save / Copy / Clear / Export now appear only
once the user has a transcript or is recording, sliding in via animate-fade-in
(brand --duration-ui + ease-out-quart). Template and Extract Tasks remain
always-visible because they shape transcription rather than acting on completed
content.

Empty state now leads with the catchphrase as a brand statement
(font-display italic 28px) with the hotkey hint demoted to body-font tertiary.
EmptyState gains an optional headline prop; callers without it render unchanged.
2026-05-07 10:49:37 +01:00
d2f64a231d feat(theme): bump accent chroma, push sensory zones apart, richer status tokens
Phase 10b colour push: theme reads as Magnotia, not "subdued generic dark".
Same lightness band as Phase 10a (AA preserved), more chroma across accent,
status tokens, and zone surfaces.

Dark accent: #c98555 -> #d68450 (subtle/hover/glow recomputed).
Light accent: #a06a3e -> #a3683a.
Dark status: success #7ec89a -> #5fc28a, danger #e87171 -> #e85f5f,
warning #e8c86e -> #e8be4a.
Light status: success #2f7549 -> #1f7344, danger #a83838 -> #b32626,
warning #b89a3e -> #a08a1f.

Sensory zones pushed further apart so each zone reads as its own
environment: cave cools toward blue-grey, energy warms toward red-orange,
reset cools toward green. AA on body text verified for every Card surface
in both themes.

Stale accent rgba (201,133,85) replaced with new (214,132,80) across nine
files. --shadow-accent in colors_and_type.css mirrored.

No secondary tint token added: Magnotia's identity is amber-as-the-only-
meaningful-colour. Cheaper to add a second meaningful colour later if a
real need appears than to dilute the discipline now.

Default surfaces, text tokens, and borders untouched per scope.
2026-05-07 10:46:35 +01:00
ec09f8ffdb feat(settings): finalise polish — autostart Toggle dedupe, AI Default/Advanced split, vocab consistency, model ETA, privacy badge, group icons
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Six changes, one coherent polish pass on SettingsPage:

1. Autostart toggle dedupe. The 27-line custom button that mirrored
   Toggle's markup to dodge a bind:checked race is gone. Toggle's new
   async onChange + snap-back covers the OS round-trip; setLaunchAtLogin
   re-throws on failure so Toggle reverts checked. autostartSyncing
   state retired.

2. AI Assistant Default/Advanced split. Cleanup preset and GPU
   concurrency moved into a nested SettingsGroup title="Advanced",
   closed by default. The Assistant page now leads with four
   first-decision controls (tier, model picker, status, prewarm) and
   parks the tuning knobs behind a disclosure. Search keywords cascade
   from the Advanced group up to AI Assistant up to AI & Processing.

3. Vocabulary disclosure consistency. The hand-rolled Profiles and
   Templates sub-panels (with their own ChevronRight buttons +
   showProfiles/showTemplates state) are now nested SettingsGroups.
   The count badges live in the description slot ("3 profiles ·
   custom vocabulary..."). showProfiles, showTemplates, and the
   ChevronRight import retired.

4. Model-download ETA in Settings. Both whisper and LLM download
   chips now show "{percent}% · {bytes} / {total} · {duration} left",
   reusing formatDuration from utils/time.ts. New formatBytes +
   etaSecondsFromPercent helpers in SettingsPage; bytes pulled from
   the existing emit payload (DownloadProgress snake_case for whisper,
   done/total for the LLM event). FirstRunPage already does the
   timestamp-based ETA; we mirror its shape.

5. Privacy badge in sticky header. Single chip "100% local · no
   telemetry · no cloud" added next to the Settings heading using
   bg-accent-subtle + text-text-tertiary. The longer About list
   stays put as the deep-dive.

6. SettingsGroup icons on the seven top-level groups. Mic, BookOpen,
   Type, Sparkles, SquareCheck, Clipboard, Sliders — paired with the
   existing literal text titles per the icon-with-label brand rule.
   Inner nested groups stay icon-free.

Verification: npm run check returns 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:19:26 +01:00
aa1ba48e07 feat(card): add tone (subtle, danger) and elevation (raised) variant props
Both props default to current behaviour, so all existing call sites render
unchanged. Subtle uses bg-bg-elevated for quieter empty-state contexts;
danger uses bg-danger/5 + border-danger/30 for calm caution panels (not
red-alert loud, matching brand). Raised layers shadow-md on top of the
existing low-contrast shadow for floating or detached surfaces.
2026-05-07 10:09:14 +01:00
3a27031eba feat(settings-group): add optional icon prop and tighten title/description rhythm
Title bumped to font-semibold so it scans first; description gets tracking-wide
plus mt-1 for cleaner rhythm. Chevron alignment shifted from mt-0.5 to mt-1 to
match the new spacing. Optional `icon` prop renders a Lucide component between
the chevron and the title block at 16x16, text-tertiary, no colour change on
open. Layout is preserved exactly when no icon is passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:07:39 +01:00
aebf6cd149 feat(toggle): add disabled, loading, and async onChange props
Extends the shared Toggle with three optional props while keeping the
existing bind:checked path untouched for current call sites.

- disabled: dims the control, sets aria-disabled, and early-returns on click.
- loading: forces a Loader2 spinner inside the thumb, applies cursor-wait
  and aria-busy, and blocks interaction. Cursor-wait wins over disabled's
  not-allowed when both are set, matching loading-precedence guidance.
- onChange(next): when supplied, replaces the immediate flip. Promise
  returns drive an internal pending flag so the toggle renders loading
  while the handler resolves; on resolve checked flips, on reject the
  toggle snaps back. Synchronous handlers flip immediately after the call.

Brand motion preserved: 150ms duration, cubic-bezier(0.2, 0.8, 0.2, 1),
no transition-all.
2026-05-07 10:06:06 +01:00
5e3a9f9f42 fix(ui): promote modal overlay to token, lift modal above grain, dedupe FilesPage drop-zone
- Add --color-overlay-dim token to app.css @theme (rgba(15,14,12,0.7) dark, rgba(26,24,22,0.55) light); mirror as --overlay-dim in design-system/colors_and_type.css.
- MorningTriageModal: replace inlined rgba scrim with var(--color-overlay-dim), drop backdrop-blur-sm (brand opposes glassmorphism by default), bump container to z-[60] so it sits above the .grain z-50 overlay.
- FilesPage: collapse two near-identical drop-zone blocks into one bordered region with conditional inner content; preserves isDragOver hover behaviour and aria region.
2026-05-07 10:04:30 +01:00