Commit Graph

133 Commits

Author SHA1 Message Date
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
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
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
699cb7e08e feat(llm): bump model registry to Qwen3.5 + Qwen3.6 family (4 tiers)
Replaces the three older Qwen3 variants with a four-tier ladder spanning
a wider hardware range:

- Qwen3_5_2B_Q4   (Minimal, 8 GB RAM, ~1.3 GB download)
- Qwen3_5_4B_Q4   (Standard, 16 GB RAM / 6 GB VRAM, ~2.7 GB) — DEFAULT
- Qwen3_5_9B_Q4   (High, 32 GB RAM / 12 GB VRAM, ~5.7 GB)
- Qwen3_6_27B_Q4  (Maximum, 64 GB RAM / 24 GB VRAM, ~17 GB)

All four GGUFs sourced from unsloth's HF org with pinned commit SHAs.
Sizes and SHA256 hashes verified against the live X-Linked-Etag /
X-Linked-Size headers on the LFS CDN. Q4_K_M quantisation throughout
(common sweet-spot for cleanup + task extraction).

recommend_tier rewritten to span four bands; default_tier moves from
the old 4B-Instruct-2507 to Qwen3.5 4B. The 27B Maximum tier honestly
needs 64 GB RAM to run without partial offload — surfaced in the
description string so the Settings UI can warn realistically.

In-tree smoke tests (smoke.rs, content_tags_smoke.rs) updated to
reference the new smallest tier so a developer's MAGNOTIA_LLM_TEST_MODEL
points at the cheapest GGUF to download. Crate description in
crates/llm/Cargo.toml refreshed to mention the new family.

NOTE (out of scope; not fixed): the size_bytes / sha256 / hf_url
methods could collapse into a single LlmModelMetadata table to remove
four parallel match arms. Layer 2 cleanup, separate session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:57:21 +01:00
4e16dbefb4 chore(crates/llm): add missing description field per contributing rule 2026-05-01 08:02:29 +01:00
Claude
89c63891fa chore: rebrand from Kon/Corbie to Magnotia
Replace all instances of the legacy product names "Kon" and "Corbie" with
"Magnotia" across user-facing copy, code identifiers, package names, bundle
ids, file paths, and documentation. Preserves the unrelated "konsole" (KDE
terminal) reference and the parent CORBEL company name.

- Renames 10 Rust crates (kon-* → magnotia-*) and the tauri binary
- Updates package.json, tauri.conf.json (productName + identifier)
- Renames CSS classes (kon-rh-* → magnotia-rh-*) and animations
- Renames brand and roadmap docs
- Regenerates Cargo.lock and package-lock.json

Verified: svelte-check passes; pure-rust crates compile under new names.
2026-04-30 13:06:55 +00:00
jars
be5a7146ca Merge pull request #10 from jakejars/claude/android-target
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
Add Android support with platform-specific feature gating
2026-04-25 19:23:29 +01:00
150059e174 fix(rms_vad): correct types and threshold order in flush idempotency test
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
Two issues in flush_is_idempotent_and_leaves_clean_state from
581a098:

1. silence_close_samples and max_chunk_samples were cast `as u64`
   but with_thresholds takes usize — wouldn't compile.
2. enter_threshold was 0.005 and exit_threshold 0.01, which
   violates the hysteresis invariant (enter must be >= exit) and
   panics in debug_assert at runtime. Swap to 0.01 / 0.005 so the
   test actually runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:22:22 +01:00
Claude
bd16c118cc build(android): split GPU acceleration into optional features
Both `kon-transcription` and `kon-llm` previously hardcoded their native
acceleration features in Cargo.toml — `whisper-rs` with `vulkan`,
`llama-cpp-2` with `openmp` + `vulkan`. That worked everywhere desktop
ships (Linux/macOS/Windows all have Vulkan via MoltenVK on Mac), but it
made an Android build structurally impossible: NDK builds against drivers
that vary wildly across SoCs (Adreno OK, Mali patchy, PowerVR worse), and
some older devices have no Vulkan at all.

Roadmap step 0 from the Android plan: make the GPU acceleration
opt-in so a CPU-only target compiles. Reuses the existing pattern that
README's "future Windows non-AVX2 build" comment hinted at.

- kon-transcription: new `whisper-vulkan` feature gates `whisper-rs/vulkan`
  via the optional-syntax `whisper-rs?/vulkan`. Default features stay as
  `["whisper", "whisper-vulkan"]` so desktop is unchanged.
- kon-llm: new `gpu-vulkan` and `openmp` features each gate the matching
  `llama-cpp-2` feature. Default stays `["gpu-vulkan", "openmp"]`. They are
  independent so an Android Vulkan build can opt into vulkan without
  openmp (NDK OpenMP linking has known cross-version fragility).

CPU-only build invocations:
  cargo build -p kon-transcription --no-default-features --features whisper
  cargo build -p kon-llm --no-default-features

Verified: all 91 tests in the buildable-in-sandbox crates still pass.
The two crates whose Cargo.toml changed (kon-transcription, kon-llm)
can't be compiled in this sandbox (ort-sys CDN + cmake-built llama.cpp);
CI's Linux/macOS/Windows builders will exercise the default-feature path
exactly as before.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 12:42:44 +00:00
Claude
d5d751c9ad fix(hotkey): exit listener cleanly when event channel is dropped
The evdev listener's run loop did `let _ = event_tx.send(event).await`
inside the trigger-key match arm. If the receiver was dropped without
the explicit shutdown signal (set hotkey to None), the send returned
Err and the loop kept polling — sending into a closed channel forever
until something else terminated the task.

Replace with explicit handling: on Err, log via log::warn! once and
return Ok(()) from `run`. The shutdown-via-None path is unaffected.
kon-hotkey still 4/4.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:38:03 +00:00
Claude
a7fdc96e7b fix(audio): log dropped capture errors instead of silently discarding
The cpal stream-error closure used `let _ = err_tx.try_send(...)` against a
bounded sync_channel(16). If the live session's listener stalled or the
frontend disconnected, runtime stream errors were silently dropped — the
diagnostic bundle showed nothing for a session that mysteriously stopped
working.

- Bump the error channel capacity 16 → 32 (matches AUDIO_CHANNEL_CAPACITY).
- On try_send failure, log to stderr with the device name + a per-session
  drop counter so the symptom is visible in the diagnostic bundle even
  when the typed event never reached the frontend.
- Plumb a new `dropped_errors: Arc<AtomicU64>` through `build_input_stream`
  alongside the existing `dropped_chunks`, mirroring the same pattern.

(kon-audio doesn't build in the audit sandbox: it links against ALSA
which the sandbox lacks. CI cross-platform compiles it.)

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:37:51 +00:00
Claude
581a098508 fix(rms_vad): flush always leaves the chunker in clean state
The earlier audit noted that `flush()` had three exit paths but only two
of them explicitly cleared all state-machine fields:
1. InSpeech with non-empty active_chunk: emit_active_chunk_and_close()
   handled it.
2. InSpeech with empty active_chunk (hit_max-mid-flush): handled inline.
3. Idle (no padded frame, or padded frame closed cleanly): no explicit
   reset — silent_tail_samples / pending_onset_frames / onset_buffer
   could carry stale values from `consume_frame` calls inside the same
   flush.

In the worst case, the first feed of a fresh recording could see leftover
onset bookkeeping and produce a chunk start that doesn't match the new
session's audio. Reusing the same `RmsVadChunker` across stop/start is
the main path that would hit this.

Add a single defence-in-depth reset block at the end of flush — every
exit path lands the chunker in the same fields a fresh chunker has,
except `next_sample_index` (the running total-samples counter, intent-
ionally preserved). Test asserts: a second flush after a full speech →
silence → partial-pending sequence emits zero chunks, and a subsequent
silent feed also emits zero, proving no stale state leaked.

(kon-transcription doesn't build in the audit sandbox because ort-sys's
build script can't reach pyke's CDN; CI cross-platform compiles it.)

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:37:39 +00:00
Claude
c04c719d48 perf(storage): prune error_log on startup with 90-day retention
The error_log table had no retention policy: every backend error was
appended forever, so across months of dogfooding it grew unbounded. That
silently bloats the diagnostic-bundle export and slows the
list_recent_errors query the Settings → About panel runs.

- New `kon_storage::prune_error_log(pool, keep_days)` does a single
  `DELETE FROM error_log WHERE timestamp < datetime('now', '-Nd days')`
  and returns the row count removed.
- src-tauri/src/lib.rs runs it once during setup() with a const
  ERROR_LOG_RETENTION_DAYS = 90. Failure is logged to stderr but does not
  block startup — a prune that fails is strictly less important than the
  app coming up.
- Test: insert three rows at now / -30d / -200d, verify a 90-day prune
  removes only the oldest, and a subsequent 14-day prune removes the
  -30d row. Storage suite at 60/60.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:37:24 +00:00
Claude
3d568148b8 perf(meeting): cache sysinfo System for the meeting-detection poller
`detect_meeting_processes` is called every 15 s when meeting-auto-capture
is enabled. The previous `list_running_process_names` allocated a fresh
`sysinfo::System` per call and walked /proc cold; on a busy host
(~300 processes) that's ~50–100 ms of work, every poll, forever.

Add `kon_core::process_watch::ProcessLister`, a thin wrapper around a
long-lived `System` whose process table is refreshed in place. The Tauri
host holds one behind a `Mutex<ProcessLister>` in a new `MeetingState`
managed at app setup. The free `list_running_process_names` is kept as a
convenience that constructs a fresh `ProcessLister` per call — its only
remaining caller is the existing smoke test.

- ProcessLister + Default in crates/core/src/process_watch.rs.
- MeetingState in src-tauri/src/commands/meeting.rs; the command takes
  it via `tauri::State` and locks for the duration of the snapshot.
- src-tauri/src/lib.rs registers MeetingState alongside the other
  managed states.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 08:46:57 +00:00
Claude
f3fd86185e perf(storage): composite (profile_id, created_at DESC) index on transcripts
Migration v15 adds a composite index covering the dominant transcripts
query path:

    SELECT ... FROM transcripts
    WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?

Previously SQLite had to choose between idx_transcripts_profile_id
(filter by profile, then in-memory sort by date) and idx_transcripts_created
(scan dates and filter on profile). Both work fine at hundreds of rows
and degrade past a few thousand.

`migration_v15_creates_profile_created_index` asserts (a) the index exists
and (b) `EXPLAIN QUERY PLAN` shows the planner picks it for the canonical
profile-scoped, date-ordered list query.

Test count assertions in `test_migrations_run_on_empty_db` and
`test_migrations_idempotent` bumped 14 → 15.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 08:46:45 +00:00
Claude
90f4d9b0fb fix(mcp): open Kon database read-only
The kon-mcp stdio server is documented as "read-only, no auth, local-only"
but until now opened the SQLite store via `kon_storage::init`, which returns
a writable pool and runs migrations. Read-only-ness was enforced only by the
exposed tool surface (list_transcripts, get_transcript, search_transcripts,
list_tasks); a future bug or a malformed dispatch could escape into a write
against the user's primary database.

Add `kon_storage::init_readonly` that opens with `SqliteConnectOptions
::read_only(true)` and `create_if_missing(false)`, no migrations. The
constraint is now structural — SQLite rejects writes at the connection
level regardless of which handler runs.

- New `init_readonly(path)` in crates/storage/src/database.rs.
- Re-exported from kon_storage.
- crates/mcp/src/main.rs switched over and updated startup banner.
- Two tests: writes fail on the read-only pool, reads succeed; opening a
  non-existent DB returns an error instead of silently creating one.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 08:46:35 +00:00
489c066a70 feat(phase9): migration v14 + storage and Tauri command extension for llm_tags
Adds llm_tags TEXT NOT NULL DEFAULT '' to the transcripts table via
new migration v14. SELECT statements + TranscriptRow + transcript_row_from
now carry the column. update_transcript_meta gains a sixth Option for
llm_tags following the existing COALESCE pattern; an
#[allow(too_many_arguments)] keeps clippy happy without inverting the
signature into a struct that would just shift the indirection.

The Tauri-side TranscriptDto + UpdateTranscriptMetaRequest + the
update_transcript_meta_cmd command pass llm_tags through unchanged.
Pre-existing manualTags persistence path now has a sibling for
llmTags ready for the frontend to call.

Phase 8 brittle test fix included: list_recent_completions_uses_local_day_boundary
was anchoring its "-2 days" UTC offset against the local-day spine,
which drifted across UTC midnight. Anchored to the local date directly
so it matches the spine regardless of clock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:10:54 +01:00
7567bede52 feat(phase9): LlmEngine::extract_content_tags + smoke test
Added as a method on LlmEngine alongside cleanup_text and
extract_tasks; same render_chat_prompt -> generate -> parse pattern.
Truncates the transcript to its trailing 2000 chars on a UTF-8 char
boundary, runs at temperature 0.0 with the CONTENT_TAGS_GRAMMAR GBNF,
and re-validates intent against INTENT_CLOSED_SET to catch the
unlikely grammar bypass case. max_tokens 96 is enough for the JSON
envelope. Smoke test gated on KON_LLM_TEST_MODEL like the existing
smoke.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:02:12 +01:00
1b6ad88ead feat(phase9): ContentTags schema, system prompt, and GBNF grammar
ContentTags serde-serialisable. CONTENT_TAGS_SYSTEM is the system
message rendered at extraction time; INTENT_CLOSED_SET is the single
source of truth for the enum values the grammar restricts. Grammar is
strict: lowercase hyphen-joined topic 3+ chars (max enforced by
max_tokens at call site), intent from the closed set, JSON-only
output. Recursive topic-rest matches the existing GBNF style in this
file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:58:36 +01:00
83bd338aff feat(gamification): list_recent_completions query + DailyCompletionCount
Returns a fixed-length, oldest-first series of daily completion counts
for the last N local-time days. Excludes cascade parents and
uncompleted rows. Empty days are explicit zeros, not missing entries,
so the Phase 8 sparkline can render a fixed 7 bars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:25:32 +01:00
839754f4ee feat(gamification): clear auto_completed on uncomplete
uncomplete_task now clears auto_completed alongside done / done_at on
both the target row and the cascaded-parent reopen. Keeps the flag
accurate so a later re-completion via a different path is counted
correctly by the Phase 8 daily-count query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:18:33 +01:00
b992967e50 style(gamification): drop em-dash from cascade comment
Matches the project's style preference for full stops over em/en dashes.
No functional change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:16:31 +01:00
92b32282d9 feat(gamification): flag cascade-completed parents as auto_completed
complete_subtask_and_check_parent now sets auto_completed = 1 on the
parent when it closes via the cascade. The subtask UPDATE itself
remains at the default 0, so explicit user taps still count toward
the Phase 8 daily total.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:13:28 +01:00
729b82cf50 feat(gamification): migration v13 — auto_completed column
Adds a flag on tasks to distinguish manual completions from the
cascade auto-completion performed by complete_subtask_and_check_parent.
Partial index on (done_at, auto_completed) supports the Phase 8
daily-count query without bloating the tasks index footprint.

Forward-only: pre-migration completed rows default to 0 (they count).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:08:18 +01:00
6cd1c22c0f feat(intentions): Phase 7 — if-then rules for task / time / triage triggers
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
Small if-then automation layer. Rules persist in SQLite; the runner
lives on the frontend and binds to the Phase 6 event bus so the rule
pipeline reuses the same delivery primitives (timer events, TTS,
Tasks navigation).

Storage:
- Migration v12 adds implementation_rules (id, enabled, trigger_kind,
  trigger_value, actions_json, last_fired_key, created_at,
  updated_at) with enabled+trigger_kind index for the runner's hot
  path.
- CRUD helpers: insert / list / get / set-enabled / mark-fired /
  delete, plus a round-trip test.

Commands (all main-window-guarded via ensure_main_window):
- list_implementation_rules
- create_implementation_rule — validates HH:MM, checks the target
  task exists at save time for surface-task actions, caps
  speak-line at 240 chars, pins v1 timers to 5 minutes.
- set_implementation_rule_enabled
- mark_implementation_rule_fired — main-thread idempotency shim so
  the runner can atomically claim a fire.
- delete_implementation_rule

Runner (implementationIntentions.svelte.ts):
- Subscribes to kon:task-completed and kon:morning-triage-finished
  (MorningTriageModal now emits on all three exit paths — empty,
  skipped, picked — so skip counts as finishing).
- 30 s poll for time-of-day rules, plus an immediate check on startup
  so a rule whose time has already passed today catches up once.
- Idempotency via last_fired_key composed as YYYY-MM-DD@HH:MM for
  time rules; new time rules whose HH:MM has already passed today
  are pre-seeded so they don't fire retroactively on save.
- Rules are paused when Nudges "Mute for now" is on — a hard mute
  stops all rule delivery in addition to OS notifications.
- Stale-task safety: if a surface-task action's target has been
  deleted, the runner opens Tasks and warns clearly rather than
  pretending to surface something that's gone.

Editor (ImplementationRulesEditor.svelte):
- Lives in Settings under a new "If-then rules" accordion section.
- `If` picker: time of day (with time input), a task completes,
  morning triage finishes.
- `Then` composer: optional surface (inbox / today / all tasks /
  specific task), optional 5-min timer, optional speak-aloud line.
- Saved rules list with enable toggle + delete.

Rules table integration for Phase 10b rename sweep: add
implementation_rules to the kon.db → corbie.db migration shim when
that phase lands.

Gates: fmt, clippy -D warnings, cargo test 265/0, svelte-check
0/0, npm build green. Pre-existing Vite chunk warning on sounds.ts
is unrelated to Phase 7.
2026-04-24 19:27:06 +01:00
b333c6229e chore(hardening): tighten security and footprint defaults 2026-04-24 19:03:57 +01:00
1d4f1070a2 feat(energy): Phase 3 — match-my-energy task sort + tri-state tag column
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
Closes Phase 3 of the 2026-04-23 feature-complete roadmap. Incorporates
the Codex plan-review fixes from this session: profile-free index, tri-
state update command, and de-prioritise-not-hide semantics.

Storage (kon-storage):
- Migration v11 adds `energy TEXT` to `tasks` with a CHECK constraint on
  `high | medium | brain_dead | NULL`. Index `(energy, created_at DESC)`
  — deliberately not per-profile because the tasks table carries no
  profile_id column yet (tracked as a separate gap in HANDOVER).
- `TaskRow.energy: Option<String>` plus `task_row_from` read.
- `insert_task` signature grows by one optional arg (`energy`). Allowed
  `too_many_arguments` with a rationale comment — the positional shape
  matches the column order and flipping to a params struct would have
  rippled through every caller for cosmetic benefit only.
- New `set_task_energy(pool, id, Option<&str>) -> TaskRow`. Lives as its
  own function because `update_task` uses COALESCE to let `None` mean
  "preserve" — which would make clearing the tag impossible.
- Two new tests: round-trip including explicit NULL clear, and CHECK
  constraint rejection of unknown values.
- Tests updated for the v10 → v11 version bump.

Tauri (src-tauri):
- `TaskDto.energy`. `CreateTaskRequest.energy` (optional). Inline
  validation against the allowed set before hitting the DB, so frontend
  bugs surface as friendly errors instead of CHECK-constraint failures.
- New `set_task_energy_cmd` command mirroring the storage tri-state API.

Frontend (svelte):
- `EnergyLevel` type added to `types/app.ts`. `TaskDto`, `TaskEntry`, and
  `TaskDraft` grow an `energy` field.
- `SettingsState.currentEnergy` (persisted) + `matchMyEnergy` (persisted
  toggle). Defaults: null + false — no surface change until user opts in.
- `setTaskEnergy(id, EnergyLevel | null)` action on the task store.
  Calls the dedicated Tauri command, updates local state, broadcasts to
  sibling windows.
- `EnergyChip.svelte` — new component. Cycles unset → High → Medium →
  Brain-Dead → unset on click. Colour tokens: accent / warning /
  text-tertiary (deliberately not danger-red for Brain-Dead — the brief
  is explicit that this state must not feel pathologised).
- Chip rendered on every task row in TasksPage and every row in
  WipTaskList. Hidden-until-hover when energy is unset so untagged rows
  stay calm; always visible once tagged because the colour is the signal.
- Tasks page header gains a "I feel" segmented control and a
  "Match my energy" toggle. When both are active, matching tasks sort
  to the top — unset tasks are treated as Medium-equivalent. Nothing is
  ever hidden; this is a de-prioritisation, not a filter.

Deferred / out of scope:
- LLM-driven surfacing (brief says "The AI surfaces...") — deterministic
  client-side sort is v1; LLM layer is a later phase.
- tasks.profile_id + per-profile energy sort — separate migration.

All green: cargo build + 251 tests + clippy -D warnings (0 warnings)
+ fmt + svelte-check (0/0) + npm run build.
2026-04-24 14:53:19 +01:00
d307722c7a fix(feedback): Phase 2 follow-up — Codex review MAJORs + NIT
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
Independent review surfaced three majors and one nit. All actioned.

MAJOR 1 — profile scoping:
`decompose_and_store` and `extract_tasks_from_transcript_cmd` now
accept an optional `profile_id` (wired from `profilesStore.activeProfileId`
in MicroSteps.svelte and DictationPage.svelte), and thread it into the
feedback-retrieval query so per-profile decomposition styles do not
leak into each other. `record_feedback` gets the same treatment.

MAJOR 2 — prompt-budget regression on long inputs:
New `trim_to_budget` helper + `FEW_SHOT_CHAR_BUDGET = 2000` char cap
in `src-tauri/src/commands/tasks.rs`. Retrieval still pulls up to 5
rows but they are char-counted and truncated against the budget
before being sent to the LLM. Char cost matches the `Input: ...\n
Good output: ...` render path so the budget maps cleanly to ~570
Qwen3 tokens, well inside the 8192-context reserve after the 512-
or 768-token response allocation. Oldest-first drop order (iteration
stops at cost exceeded) preserves the most recent correction which
is the one carrying the user's live preference.

MAJOR 3 — inline edit stale-rollback race:
`saveEdit` in MicroSteps.svelte now stamps a monotonic per-step
`saveToken`. Each edit bumps the token; on failure the rollback
only fires if `saveToken[step.id] === myToken`, so a slow-failing
first save can no longer overwrite a faster successful second save.

NIT — retrieval ordering stability:
`list_feedback_examples` ORDER BY now `created_at DESC, id DESC`.
SQLite timestamp precision is one second; without the secondary
key, bursty feedback within the same second would select
non-deterministically.

Also: malformed `context_json` now warns via eprintln! rather than
disappearing silently — Codex minor.

All green: cargo build + 249 tests + clippy -D warnings + fmt
+ svelte-check (0/0) + npm run build.
2026-04-24 13:32:52 +01:00
46be0a5aca feat(feedback): Phase 2 — HITL thumbs + correction capture with prompt-conditioning loop
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
Closes the human-in-the-loop gap from docs/brief/feature-set.md and
Phase 2 of the 2026-04-23 feature-complete roadmap.

Storage (kon-storage):
- Migration v10 adds the `feedback` table: (target_type, target_id,
  rating, original_text, corrected_text, context_json, profile_id,
  created_at) with CHECK constraints on target_type and rating, plus
  indexes on (target_type, rating, created_at DESC) for prompt-time
  retrieval and (profile_id, target_type, created_at DESC) for
  per-profile scoping.
- New public API: `FeedbackTargetType`, `RecordFeedbackParams`,
  `FeedbackRow`, `record_feedback`, `list_feedback_examples`.
- Tests updated — the RB-02 rollback regression now discovers the
  real max version at runtime instead of hard-coding v10 for its
  poison migration.

LLM (kon-llm):
- `prompts::FeedbackExample` — local shape for few-shot exemplars so
  kon-llm stays independent of kon-storage.
- `prompts::build_conditioned_system_prompt` — appends a "here is
  the style this user prefers" block to the base system prompt
  when examples are available; returns the base prompt unchanged
  when empty, so new users and early sessions see generic output.
- `LlmEngine::decompose_task_with_feedback` and
  `LlmEngine::extract_tasks_with_feedback` thread examples through
  to the builder. The old one-arg variants are preserved and now
  call through with an empty slice.
- 4 unit tests covering empty, empty-input-skip, correction-wins,
  and thumbs-up-only fallback.

Tauri (src-tauri):
- New commands::feedback module: `record_feedback`,
  `list_feedback_examples_cmd`.
- `decompose_and_store` and `extract_tasks_from_transcript_cmd`
  now fetch the last 5 positive/neutral feedback rows for their
  target type and pass them through to the LLM, wiring the
  learning loop end-to-end.
- Shared `to_llm_examples` helper parses the `context_json.input`
  field (where the recorder stashes the parent task text / transcript
  chunk) back into the exemplar shape.

Frontend (MicroSteps.svelte):
- Thumbs-up and thumbs-down buttons on every micro-step row.
  Hover-revealed; the vote recolours the icon; clicking again
  clears the local highlight (the row itself stays in the audit
  trail).
- Pencil icon + double-click to edit step text. Save flows through
  update_task_cmd for persistence and records a correction feedback
  row with (original_text, corrected_text) — the highest-value
  training signal.
- Parent task text is captured in context_json.input at record time
  so the prompt builder can reconstruct the (input, preferred-output)
  pair on subsequent decompositions.
- Feedback capture is best-effort — a record_feedback failure never
  interrupts the primary action.

What's deferred to a later phase:
- Thumbs + corrections on extracted tasks (same pipeline, different
  surface — probably TasksPage after the AI-extraction path)
- Thumbs on transcript cleanup output
- Semantic retrieval over the feedback corpus (once there is enough
  data to justify embedding infrastructure; the storage shape is
  already ready for it)
2026-04-24 12:53:51 +01:00
4700668df1 docs(readme): refresh test count 136 -> 245
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-04-24 09:53:26 +01:00
fe61661305 chore(lint): clean up clippy warnings across workspace
Auto-applied cargo clippy --fix across 11 files — needless return,
unnecessary cast, map_or simplification, repeat().take() → repeat_n(),
iter().any() → contains(), manual char comparison, lifetime elision,
push_str single-char, reference immediately dereferenced.

Also fixed three lints on file_storage.rs manually: two doc-list-item
overindentations, plus the same needless-return. Baseline main was
not clippy-clean with -D warnings before; after this pass one
needless_range_loop warning remains (live.rs:1089) that clippy's
suggested rewrite would make less readable — left for a dedicated
refactor session.

Build + workspace tests remain green (245 passing, 0 failing, 1
ignored).
2026-04-24 09:43:56 +01:00
9b0067b4c0 Land release blocker fixes and workspace cleanup
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-04-23 00:16:09 +01:00
05d823bf05 fix(rb-02): migrations run inside a single transaction
Every multi-statement migration and its matching schema_version insert
now execute on the same sqlx Transaction. A failure anywhere — a bad
statement, the version insert, or the commit itself — rolls the
database back to its previous state, so the next startup replays the
migration against a clean schema rather than a half-mutated one.

Extracted run_migrations_slice(pool, migrations) as the single apply
path. run_migrations delegates to it with MIGRATIONS; the test helper
run_migrations_up_to now filters MIGRATIONS by target and delegates to
the same code, eliminating the duplicated loop that previously lived
in the test module.

Regression test multi_statement_migration_rolls_back_on_failure
injects a poisoned v9 migration (valid CREATE followed by a bogus
function call) and asserts neither the partial schema change nor the
schema_version row persists after the failure.

SQLite DDL participates in transactions, so this is sufficient. Any
future migration that needs an implicitly-committing statement
(VACUUM / REINDEX / ATTACH — none today) must be its own
non-transactional migration; that's a reviewer responsibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:27:29 +01:00
b48d39bfb1 fix(rb-09): decoder propagates read and decode errors
decode_audio_file's packet loop was `Err(_) => break`, so any non-EOF
read error during playback dropped out silently with whatever samples
had accumulated. Per-packet decode errors were tallied and skipped,
contributing to the same outcome. A corrupt or truncated input
therefore came back as `Ok(partial_samples)` — no way for callers to
distinguish a clean decode from a compromised one.

Every SymphoniaError other than the explicit EOF
(`IoError(UnexpectedEof)`) now maps to `AudioDecodeFailed`. Decoder
errors bubble via `?` rather than being counted. `ResetRequired`
promotes to an error rather than a silent break.

Extracted an internal `decode_media_stream(mss, hint)` so tests can
inject a custom `MediaSource`. Added `FlakyCursor` — a seekable cursor
that returns a synthetic I/O error after N bytes — and a regression
test that confirms mid-stream read failure surfaces as `Err` instead of
returning partial audio. Happy-path and missing-file tests added for
coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:24:18 +01:00
528facfab0 fix(rb-12): hotkey device filter consults configured HotkeyCombo
try_attach_device was rejecting any device that did not report KEY_A or
KEY_R — a leftover heuristic from the whisper-overlay seed. A user whose
binding was anything else (Ctrl+Shift+D is a common default) would see
no hotkey events from that device even though it supports the key.

Replace the hard-coded check with device_supports_combo(supported,
combo), a pure helper that reads the configured trigger key code from
the HotkeyCombo snapshot. Snapshot is taken from hotkey_rx.borrow()
before opening the device; an unconfigured or shutting-down listener
short-circuits to a non-attach.

Four regression tests in linux::tests cover: supported+D → attach,
unsupported → reject, no reported keys → reject, and the explicit
non-A/non-R case that demonstrates the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 10:18:54 +01:00
a5bc45e847 fix(cr-2026-04-22): list_transcripts accepts omitted arguments
Regression surfaced by the batch review: commit 8400128 switched
list_transcripts from unwrap_or_default to map_err(-32602). This
correctly errors on malformed payloads but also rejected the common
case where a client omits the 'arguments' field entirely — which
arrives as Value::Null, and serde_json::from_value does not
deserialise Null into a struct.

Short-circuits the Null case to Args::default() before attempting
deserialisation. Genuine shape mismatches ("limit": "twenty")
still return -32602 as the previous test asserts.

New regression test: tools/call with list_transcripts and no
arguments key must return a successful response.
2026-04-22 09:23:17 +01:00
b376b98f33 refactor(cr-2026-04-22): remove dead code and stale allow(dead_code) suppressions
2026-04-22 review MINORs and NITs:

- crates/core/src/providers.rs: delete entire module. SpeechToText /
  TextProcessor / ProviderRegistry were forward-looking traits that
  never got wired — the Transcriber trait in kon-transcription
  (A.2 #13) has since superseded SpeechToText, and the Registry
  pattern was redundant against LocalEngine. Keeping them as dead
  public surface signalled future direction that is no longer
  accurate.
- crates/core/src/types.rs: delete TranscriptMetadata. Forward-
  looking struct with an unfulfilled TODO; storage has evolved
  independently through v7/v8 migrations without adopting it.
- crates/ai-formatting/src/llm_client.rs: remove #[allow(dead_code)]
  from CLEANUP_PROMPT and format_dictionary_suffix. Both are
  actively called; the suppressions would hide future genuine
  dead-code warnings in this regression-sensitive prompt file.
- src-tauri/src/commands/live.rs: remove #[allow(dead_code)] from
  LiveStatusMessage. Every variant (Warning, Overload, Error,
  Finished) is constructed in the module today.
- README.md: update kon-transcription row from "SpeechToText
  trait" to "Transcriber trait" and mention the new streaming/
  module.

Workspace test gate green (225 lib tests across all crates).
2026-04-22 09:17:05 +01:00