Commit Graph

143 Commits

Author SHA1 Message Date
ff8dda06d0 agent: lumotia — Phase A.7 fix startup-order race that silently orphaned legacy data
Critical bug surfaced by the dogfood drill: every upgrading Magnotia user
would silently keep a fresh empty Lumotia install while their Magnotia
data sat orphaned next to it. Drill caught it on the first real run
under sandboxed HOME.

ROOT CAUSE

src-tauri/src/lib.rs::run() previously called the migrations from inside
the Tauri setup hook (post `tauri::Builder::default()`). But three
sequential actions BEFORE the setup hook had already created the
destination directories:

  1. init_tracing() -> logs_dir() -> create_dir_all(app_data_dir/logs)
     creates the lumotia/ root.
  2. install_panic_hook() -> crashes_dir() -> create_dir_all() ditto.
  3. Tauri's WebKitGTK runtime / plugin chain creates the bundle-id-keyed
     consulting.corbel.lumotia/ dir eagerly when the WebContext spins up
     (mediakeys, storage, WebKitCache subdirs appeared even without our
     hook explicitly creating them).

By the time the setup-hook migrations fired, every legacy candidate
returned `TargetAlreadyExists` (paths.rs) or `BothExistLegacyPreserved`
(tauri_app_data_migration.rs) — both silent no-op codepaths. Legacy
data was left untouched, fresh Lumotia install gained no transcripts,
settings, or window state.

FIX

Migrate BEFORE any other code touches app_data_dir().

src-tauri/src/tauri_app_data_migration.rs:
  - NEW_BUNDLE_ID const ("consulting.corbel.lumotia"). MUST agree with
    tauri.conf.json#identifier; reviewer-enforced invariant.
  - Renamed private `legacy_tauri_app_data_dir_for` -> public
    `tauri_app_data_dir_for(identifier)`. Function is parameterised by
    bundle id; the "legacy" name was misleading after this change.
  - New `current_tauri_app_data_dir()` resolves the NEW bundle path
    from platform env vars (same convention Tauri 2 uses), so the
    pre-runtime migration can address its destination without
    needing an AppHandle.

src-tauri/src/lib.rs:
  - New `migrate_user_data_pre_runtime()` orchestrates the two
    migrations + ambiguity guard. Uses `eprintln!` for surface events
    (tracing not yet initialised at this stage; stderr lands in
    journald / foreground terminal which is the right transport for
    boot-phase output). FATAL errors call process::exit(1) — the
    setup-hook version returned Err from the closure, equivalent
    effect.
  - run() now calls migrate_user_data_pre_runtime() as its first line,
    BEFORE init_tracing(), install_panic_hook(), and the Tauri
    builder.
  - Setup-hook migration blocks deleted (~90 lines). Setup hook now
    starts with a one-line comment pointing at the pre-runtime fn.

VERIFICATION

Re-ran the dogfood drill (scripts/dogfood-rebrand-drill.sh) — 8/8 probes
pass after the fix (was 4/8). Both stderr lines fire:

  [lumotia-startup] migrated legacy magnotia data dir to lumotia:
      .../magnotia -> .../lumotia (renamed_db=true, elapsed_ms=0)
  [lumotia-startup] migrated Tauri app_data_dir from legacy bundle
      identifier: .../uk.co.corbel.magnotia ->
      .../consulting.corbel.lumotia (elapsed_ms=0)

On-disk post-state confirms: magnotia/ gone, lumotia/ has migrated db
+ recordings, uk.co.corbel.magnotia/ preserved as backup,
consulting.corbel.lumotia/localStorage/leveldb/ has migrated data.

- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test --workspace: 409/0 (no regression)
2026-05-14 13:59:08 +01:00
27661c816e agent: lumotia — pin rust toolchain + workspace clippy/fmt sweep
rust-toolchain.toml pins to stable 1.94.1 so contributors and CI runners
share the exact rustc / rustfmt / clippy versions. Without the pin, every
machine surfaces a different lint set depending on its local install — six
pre-existing lints showed up on 1.94.1 that 1.93-era HANDOVER reported clean.

Clippy fixes (all pre-existing, not introduced by feature work):

- crates/storage/src/database.rs: std::iter::repeat().take() -> repeat_n()
- crates/llm/src/lib.rs (docs): "+ frontends" was parsed as a markdown bullet
  continuation by rustdoc, breaking doc-lazy-continuation. Reworded to "and".
- crates/llm/src/lib.rs (loop): while-let-on-iterator -> for-loop.
- src-tauri/src/commands/security.rs: .iter().any(|a| *a == x) -> .contains(&x).
- src-tauri/src/lib.rs: io::Error::new(Other, e) -> io::Error::other(e).
- src-tauri/src/tauri_app_data_migration.rs: drop function-tail `return`s
  inside cfg blocks; each platform's block now ends with a tail expression.

cargo fmt sweep across the workspace. Mechanical layout-only changes;
no semantics affected.

Workspace gates after this commit:
- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test --workspace: 405/0 (will become 409/0 with Phase A.1+A.2)
2026-05-14 07:19:59 +01:00
65abfa2ed9 agent: code-atomiser-fix — span propagation across live + model-load spawns (Obs-3)
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
Before this commit `grep -rIn '#\[instrument\|.instrument(\|in_current_span()'`
returned zero matches across the entire workspace. Every tokio::spawn
and thread::spawn lost its parent span, so structured fields recorded
at the call site (session_id, chunk_id, model_id) did not propagate to
log lines emitted inside the spawn. During concurrent-session incidents
the operator could not correlate a runaway log line back to the request
that started it.

Targeted four highest-value join points:

  * src-tauri/src/commands/live.rs::run_live_session
    #[tracing::instrument(skip_all, fields(session_id, engine, language))]
    Attaches the span to the spawn_blocking worker so every per-chunk
    warning carries the session id that owns it.

  * src-tauri/src/commands/live.rs::maybe_dispatch_chunk
    Manual span attach pattern (#[instrument] can't decorate a closure):
    capture the parent span before thread::spawn, .enter() it on the new
    OS thread, then open an "inference" child span with chunk_id +
    duration_secs. Without this, whisper backend warnings appear
    unparented and a runaway chunk can't be traced back to its session.

  * src-tauri/src/commands/models.rs::ensure_model_loaded
    #[instrument(skip_all, fields(model_id, engine, concurrent))]
    Multi-second load + sequential-GPU guard logs now carry the model
    in flight as a structured field.

  * crates/llm/src/lib.rs::load_model
    #[instrument(skip_all, fields(model_id, use_gpu))]
    Same rationale for LLM loads. Tags llama-backend init lines and
    GPU sequential-guard events with the model identifier.

Storage/audio/hotkey/MCP crates left uninstrumented in this commit —
future sweep. The four sites above are the canonical concurrent-load
correlation points; everything else fans out from them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:18:12 +01:00
d1391b34ac agent: code-atomiser-fix — drop lumotia_live custom target in live.rs (Obs-1, Obs-2)
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
The drain-timeout warning in LiveSessionRuntime emitted with
`target: "lumotia_live"`, which EnvFilter treats as the literal
target string and not as a substring of `lumotia_lib::commands::live`.
The operator's documented triage filter
(`RUST_LOG=info,lumotia=debug,lumotia_lib::commands::live=debug`,
per docs/superpowers/audits/2026-05-10-phase10a-dogfood-notes.md)
therefore silenced the only warning that surfaces a wedged inference
worker. Drop the explicit `target:` so the emit picks up its
module-path target and falls under the existing filter directive.

`lumotia_startup`, `lumotia_storage`, `lumotia_hotkey`, etc. remain
deliberately custom targets — each is a separate semantic phase
with its own dedicated EnvFilter directive.

Regression test asserts no `target: "lumotia_live"` literal remains
in live.rs by scanning the file's own source. Skips comment lines
so the rationale prose does not self-trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:15:08 +01:00
12b413d645 agent: code-atomiser-fix — broaden clipboard/paste guards to documented secondary windows
The Trust-3/Trust-6 fix in commit 7aee534 added ensure_main_window to
copy_to_clipboard, paste_text, and paste_text_replacing. That was over-
broad: the transcript-viewer and transcription-preview windows
legitimately call copy_to_clipboard (their "copy raw" / "copy
transcript" buttons), and the transcription-preview window legitimately
calls paste_text_replacing (the preview paste-to-foreground flow).

The original Trust-3/Trust-6 finding was about asymmetric exposure to
arbitrary windows, not about banning the documented secondary windows.
Fix: add ensure_window_in_set helper in commands::security, mirror the
allow-list against src-tauri/capabilities/secondary-windows.json so the
IPC trust boundary and the permission grant stay in lock-step.

  copy_to_clipboard         -> main + transcript-viewer + transcription-preview
  paste_text_replacing      -> main + transcription-preview
  paste_text                -> main only (unchanged; no secondary caller)

Adds two unit tests for ensure_window_in_set_label covering the
listed-label accept path and the unlisted-label reject path. The
existing Trust-3/Trust-6 tests remain in place and continue to assert
the size cap and the constants-equality invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:09:26 +01:00
9653e25e32 agent: code-atomiser-fix — extension allowlist + size cap on transcribe_file (Trust-5)
`transcribe_file` already had `ensure_main_window`, but accepted an
arbitrary `path: String` and fed it straight to
`lumotia_audio::decode_audio_file_limited`. The OS file picker
typically constrains the user's path, but the IPC surface itself never
checked: a compromised webview could point the decoder at a 50 GiB
sparse file (OOM the worker), or a deliberately-malformed blob with an
extension chosen to provoke a parser bug in Symphonia.

This change adds defence-in-depth:

- extension allowlist (`wav`, `mp3`, `m4a`, `mp4`, `flac`, `ogg`,
  `opus`, `webm`, `aac`) matched case-insensitively. Anything else,
  including no extension at all, is rejected with a clear error;
- 1 GiB ceiling on the input file. Stats via `std::fs::metadata`
  (which resolves symlinks) so the cap sees the real blob, not a
  symlink-target lie. The 2-hour duration cap still runs after decode
  for the realistic-audio case.

The validation lives in a pure helper, `validate_transcribe_input`, so
the rule can be unit-tested without spawning Tauri or hitting the
decoder.

Eight unit tests cover: accepts plain `.wav`, accepts uppercase `.MP3`,
accepts every allowlisted extension, rejects `.so` payload, rejects
missing extension, rejects oversize file, accepts exactly-at-cap file,
rejects path-traversal with disallowed extension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:05:07 +01:00
f7af7b07bb agent: code-atomiser-fix — main-window guard on extract_content_tags_cmd (Trust-4)
`extract_content_tags_cmd` was the only LLM command in `commands/llm.rs`
that did not call `ensure_main_window`. Every sibling (`load_llm_model`,
`unload_llm_model`, `delete_llm_model`, `test_llm_model`,
`cleanup_transcript_text_cmd`, `download_llm_model`) gates on it.
Without the guard a secondary-window webview could trigger a multi-
second llama.cpp inference run, blocking the LLM engine for the main
window and leaking model-inferred tags out of the History page's trust
boundary.

This change:

- adds a `window: tauri::WebviewWindow` parameter (Tauri injects it
  automatically — `HistoryPage.svelte`'s `invoke("extract_content_tags_cmd",
  …)` call site is unchanged and `npm run check` is clean);
- calls `ensure_main_window(&window)?` before the engine check so the
  rejection is fast and the cap mirrors the rest of the surface.

Behaviour is otherwise identical: same engine path, same spawn_blocking,
same App-Nap power assertion.

The shared `ensure_main_window_label` test in `commands::security`
already covers the secondary-window rejection behaviour; no
command-level scaffolding for handler-style tests exists in this
codebase, so introducing one for a single new line was out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:02:23 +01:00
7aee5348bc agent: code-atomiser-fix — main-window guard + size cap for clipboard surface (Trust-3, Trust-6)
`paste_text`, `paste_text_replacing`, and `copy_to_clipboard` previously
exposed asymmetric trust against the rest of the Tauri command surface:
no `ensure_main_window` guard and no payload-size cap. A compromised
webview could synthesise an arbitrary Ctrl+V into the foreground
application or write multi-megabyte payloads into the system clipboard
without restriction. `paste_text*` is particularly hot because it also
synthesises keystrokes into whatever app currently has focus.

This change:

- adds `ensure_main_window(&window)?` to all three commands. Each now
  takes a `tauri::WebviewWindow` parameter that Tauri injects
  automatically — frontend invoke call sites are unchanged in their
  TypeScript signatures and `npm run check` is green;
- introduces a shared 1 MiB cap (`MAX_CLIPBOARD_BYTES` /
  `MAX_PASTE_BYTES`) that both surfaces enforce identically. Drift
  between the two caps would let an attacker copy a >1 MiB payload via
  one command and paste it via the other; a unit test asserts the
  constants stay in lock-step.

Tests added:
- `commands::clipboard::tests` — accepts normal payload, accepts
  exactly-at-cap, rejects above-cap.
- `commands::paste::tests_paste_size_cap` — accepts typical dictation
  payload, rejects above-cap, asserts paste cap matches clipboard cap.

Note: `copy_to_clipboard` is currently invoked from the preview
(`/preview`) and viewer (`/viewer`) routes (HistoryPage and DictationPage
too, but those run in the main window). After this change the preview
and viewer invocations will surface a "main window only" error at
runtime. `npm run check` cannot catch this — flagged for follow-up; the
fix is to refactor those routes to delegate the copy through the main
window via an event.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:01:02 +01:00
b3da58cd6b agent: code-atomiser-fix — write_text_file_cmd path scope (Trust-1, redo)
The earlier Trust-1 commits a2b47db and a48653c carried the wrong
files due to parallel-agent races on the index. This commit re-applies
the fs.rs change via explicit pathspec so the working-tree edit is
finally landed in HEAD.

The Tauri command `write_text_file_cmd` previously took an arbitrary
`path: String` and flowed it straight into `tokio::fs::write` with no
main-window guard, no canonicalisation, and no scope check. A
compromised webview could write anywhere the process had write access
— overwriting shell init files, dropping a runner into
`~/.config/autostart`, etc.

This change:

- adds `ensure_main_window(&window)?` so only the main webview can
  invoke the command;
- canonicalises the requested path's parent (rejecting nonexistent
  parents and resolving symlinks) before joining the filename;
- asserts the canonical target sits inside an allowlisted base
  (app data, app local data, downloads, documents, desktop), so a
  `"../../etc/passwd"` payload — even one obtained via symlink trickery
  in the chosen save dir — is refused with a clear error.

Six unit tests cover: outside-allowlist rejection, path-traversal
rejection, nested inside-allowlist acceptance, plain inside-allowlist
acceptance, nonexistent-parent rejection, and the pure
`is_inside_any_base` prefix check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:00:33 +01:00
a48653c93c agent: code-atomiser-fix — write_text_file_cmd path scope (Trust-1, corrective)
Corrective re-apply of Trust-1. The original commit a2b47db carried
the wrong staged file due to a parallel-agent race that swapped
the staged path between `git add` and `git commit` — fs.rs was
never actually changed by a2b47db despite the message. This commit
applies the Trust-1 fix properly.

The Tauri command `write_text_file_cmd` previously took an arbitrary
`path: String` and flowed it straight into `tokio::fs::write` with no
main-window guard, no canonicalisation, and no scope check. A
compromised webview could write anywhere the process had write access
— overwriting shell init files, dropping a runner into
`~/.config/autostart`, etc.

This change:

- adds `ensure_main_window(&window)?` so only the main webview can
  invoke the command;
- canonicalises the requested path's parent (rejecting nonexistent
  parents and resolving symlinks) before joining the filename;
- asserts the canonical target sits inside an allowlisted base
  (app data, app local data, downloads, documents, desktop), so a
  `"../../etc/passwd"` payload — even one obtained via symlink trickery
  in the chosen save dir — is refused with a clear error.

Six unit tests cover: outside-allowlist rejection, path-traversal
rejection, nested inside-allowlist acceptance, plain inside-allowlist
acceptance, nonexistent-parent rejection, and the pure
`is_inside_any_base` prefix check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:59:26 +01:00
99f4ecdecc agent: code-atomiser-fix — Tauri commands for trash list + restore
Adds two `#[tauri::command]` wrappers around the soft-delete pair that
landed with migration v16 in commit 15b74db:

  - `list_trashed_transcripts(limit, offset)` mirrors the existing
    `list_transcripts` shape (default 50, clamp 1..=500, offset >= 0)
    so the Trash view can paginate the `deleted_at IS NOT NULL`
    partition with the same UX as the live history list.

  - `restore_transcript(id)` clears `deleted_at`. Idempotent on a live
    row, per the storage-layer contract. Audio at `audio_path` may
    already have been removed by `delete_transcript`'s best-effort
    filesystem cleanup; the text + metadata recovery is what
    restoration actually buys you.

Both are registered in the `tauri::generate_handler!` block in
`src-tauri/src/lib.rs`, alongside the existing transcripts surface.
Mirrors the signature shape of `delete_transcript` — no
`ensure_main_window` because the rest of the transcripts command
surface does not gate on it; introducing that here would be
inconsistent and out of scope for this fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:58:13 +01:00
ed449ccc1f agent: code-atomiser-fix — validate output_folder against recordings_dir (Trust-2)
resolve_recording_path() previously joined the webview-supplied
output_folder string verbatim into a PathBuf and then mkdir -p'd +
WAV-wrote at that path. The webview is a (mostly-)trusted surface in
Tauri, but the live-transcription command's input is JSON from the
frontend with no schema enforcement on the path field — so a
compromised page, a Tauri IPC sender on an OEM build, or a future
plugin reaching the same command can pipe through paths like `/etc`,
`/var/log`, or anywhere else the Lumotia process can write.

The new flow:
- None / empty → fall back to the default `app_local_data_dir/recordings`
  base. Always safe.
- Non-empty → call validate_output_folder, which:
  1. Ensures the default base exists (so canonicalise can succeed on
     first launch).
  2. Creates the requested path if it doesn't exist (so canonicalise
     can resolve it; an empty directory outside the base is the only
     side effect of an attempted escape, which is acceptable for the
     trust gain).
  3. Canonicalises both base and requested paths (resolves `..` and
     symlinks).
  4. Requires the canonical requested path to start_with the canonical
     base. Reject otherwise with a message naming the trust boundary.

A user who legitimately wants recordings elsewhere routes through
Settings (a separately-validated persisted-preferences boundary). The
command surface stays constrained.

Regression tests (commands::audio::tests):
- validate_output_folder_accepts_base_itself
- validate_output_folder_accepts_descendant
- validate_output_folder_rejects_etc — `/etc` attack shape
- validate_output_folder_rejects_parent_escape — `..`-walk attack
- validate_output_folder_rejects_sibling_dir — prefix-overlap attack
  (`recordings-backdoor` vs `recordings`). Canonical starts_with on
  PathBufs correctly rejects this; a naive string-prefix check would
  have let it through.
- validate_output_folder_rejects_symlink_pointing_out (Unix only) —
  in-base symlink to outside path must be rejected after canonicalise
  follows the link.

cargo test -p lumotia --lib commands::audio::tests: 8 passed.
cargo test --workspace: all green.
npm run check: 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:56:49 +01:00
07f6755961 agent: code-atomiser-fix — drain_inference deadline from task.duration_secs (Time-bomb-1)
F3 derived the drain_inference timeout from the CHUNK_SAMPLES constant
and capped it near 6s. That breaks on the realistic worst-case
configuration (slow CPU + Whisper large-v3, 3-5x realtime): a 4-second
chunk legitimately takes ~20s to clear the decoder, but the F3 budget
aborted it after 6s and treated healthy work as a wedge — the lifecycle
keeps surviving, but every long-tail chunk gets cancelled and the user
sees their final stretch of dictation get dropped.

The deadline now derives from the in-flight task's own duration_secs
multiplied by a REALTIME_SAFETY_MULTIPLIER constant (5x — the
documented upper bound for the slowest supported backend), with a
DRAIN_TIMEOUT_FLOOR of 2s so sub-second tail chunks still get enough
wall-clock to amortise model load, OS scheduling jitter, and the
abort-callback's own poll cadence. Both constants sit next to
CHUNK_SAMPLES with doc comments explaining the rationale.

Defensive: non-finite or non-positive durations fall back to the floor
so a malformed task can't produce a NaN/overflow budget.

Regression tests (commands::live::tests):
- drain_timeout_scales_with_inflight_chunk_duration_secs: 4.0s chunk
  must get 20s budget (5x), not the old 6s cap.
- drain_timeout_honours_floor_for_short_chunks: 0.3s chunk produces
  1.5s scaled value, must be clamped to the 2s floor.
- drain_timeout_uses_floor_when_no_inflight_task: well-defined fallback
  for the (in practice unreachable) None branch.
- drain_timeout_rejects_non_finite_duration: NaN / inf / 0 / negative
  fall back to floor.

cargo test -p lumotia --lib commands::live::tests::drain: 4 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:54:05 +01:00
15b74db747 agent: code-atomiser-fix — soft-delete transcripts with audio cleanup (Rev-2, Rev-3)
Two interlocking reversibility kills, fixed as one bundle:

Rev-2 (hard-DELETE transcripts, no trash) — delete_transcript previously
issued DELETE FROM transcripts WHERE id = ?, so a single click on the
History "Clear All" / "Confirm" button erased months of dictation with no
trash, no export, no undo. The new contract:
  * delete_transcript UPDATEs deleted_at = datetime('now') and best-effort
    removes the audio file. Idempotent on repeat call.
  * Migration v16 adds `deleted_at TEXT` plus a partial index over the trash
    rows so the purge query stays cheap on long-running databases.
  * get_transcript, list_transcripts_paged, count_transcripts, and
    search_transcripts all filter `deleted_at IS NULL` so trash rows are
    invisible to the regular history view but kept for restore.
  * New list_trashed_transcripts and restore_transcript power the inverse
    trash view; row order is most-recently-deleted first.
  * New purge_deleted_transcripts(older_than_days) hard-removes trash
    older than the retention window. Wired into the Tauri setup hook at
    30-day retention; best-effort, never blocks startup.

Rev-3 (orphan WAV files on transcript delete) — same delete_transcript
function. Audio file at audio_path is now best-effort removed when the
soft-delete actually flips a row; NotFound is the expected case for
repeat deletes and is not logged. purge_deleted_transcripts also retries
audio removal as belt-and-braces.

Adds 4 regression tests: delete_transcript_soft_deletes,
delete_transcript_removes_audio_file, list_transcripts_excludes_soft_deleted
(also covers restore_transcript), and purge_deleted_transcripts_hard_deletes_old.
Plus migration_v16_adds_deleted_at_column_and_index already in place.

Frontend UI (HistoryPage clearAll type-the-word modal + View trash path)
deferred to a follow-up commit; the backend contract is complete and the
existing arm-confirm flow now soft-deletes instead of hard-deleting, so
the user-data-loss class is closed for the live release path. Rev-4
(SettingsPage deleteProfile routing) also deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:39:03 +01:00
1068ad9c7d agent: code-atomiser-fix — hotkey supervisor rearchitecture (Race-1, Race-2, TOCTOU)
Fixes three interlocking concurrency leaks in the evdev hotkey listener
flagged by the atomiser full-sweep. Every spawned task is now owned by a
SupervisorHandle that broadcasts cooperative shutdown and joins every
JoinHandle with a 2s per-task timeout on stop(). Per-device attachment is
now insert-before-spawn under one mutex hold, closing the TOCTOU window.
The Tauri command layer now stores the forwarder JoinHandle alongside
the listener so reconfigures join it cleanly instead of leaking one
permanent forwarder per hotkey change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:21:50 +01:00
afbd33d33e agent: code-atomiser-fix — test_llm_model respects caller GPU preference (Race-8)
Add an Option<bool> use_gpu parameter to test_llm_model with the same
default-true semantics as load_llm_model. Hard-coding true triggered an
engine tear-down/rebuild when a parallel load_llm_model(use_gpu=false)
was in flight (LlmEngine::load_model triples on id+path+use_gpu) and
silently flipped the user's GPU mode underneath the test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:12:24 +01:00
094b533ef2 agent: code-atomiser-fix — surface capture-thread drops + bypass validation requeue cap
dropped_chunks was incremented on cpal-callback channel-full and
validation requeue overflow but never read by the live session, so
the UI's dropped_audio_ms missed callback-level losses entirely.
Architecture doc had flagged this as a TODO. Also: the 350ms
validation buffer was requeued via try_send into the same 32-slot
channel, silently dropping past the cap on small-buffer audio hosts
(WASAPI exclusive, low-latency ALSA at 256 frames -> ~65 chunks).

Fix: live runtime reads MicrophoneCapture::dropped_chunks() on each
recv_audio tick (LiveSessionRuntime::poll_capture_drops) and converts
the per-chunk-duration delta into the dropped_audio_ms surfaced to
the UI overload status. Per-chunk duration is derived from the most
recent AudioChunk's sample_rate + samples-per-channel so it adapts
to whatever rate cpal is delivering at. Validation requeue moved
from try_send into the bounded channel onto a VecDeque<AudioChunk>
returned alongside the Receiver; ActiveCapture drains the replay
buffer before reading rx in recv_audio, bypassing the 32-slot cap
entirely. Architecture doc updated to remove the TODO and document
the new pre-roll path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:39:14 +01:00
5725836f40 agent: code-atomiser-fix — cancellable Whisper inference + bounded drain + lock-over-await
Three findings chain: thread::spawn discarded the JoinHandle and gave
no cancellation route into the whisper backend; drain_inference busy-
polled with no timeout; stop_live_transcription_session held the
lifecycle AsyncMutex across the await of a non-cancellable
spawn_blocking handle. Together: a single wedged inference (ggml
deadlock, GPU stall) bricked every future start/stop until app restart.

Fix: each inference task carries an Arc<AtomicBool> abort_flag; the
flag is wired into whisper-rs::FullParams::set_abort_callback_safe so
the spawned blocking thread checks it and exits cleanly. drain_inference
is bounded by a deadline (3 x chunk_duration, min 2s); on expiry the
flag is set, the receiver is dropped, and a typed Error::InferenceTimeout
status is surfaced. Drop for InferenceTask asserts the abort flag so
any '?'-propagation or panic unwind closes the cancellation route
without relying on the explicit drain path. stop_live_transcription_session
restructured so the lifecycle guard is dropped BEFORE the JoinHandle
is awaited; start_live_transcription_session releases the guard
explicitly after installing the RunningLiveSession.

Unit test added: dropping_inference_task_sets_abort_flag covers the
Race-A regression directly. A real Race-B drain-timeout test would
require a wedged whisper-rs, which is hard to fixture; that path is
covered by the SAFETY comment and cargo build + manual smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:38:22 +01:00
3f4e5cc9a4 agent: code-atomiser-fix — migrate Tauri app_data_dir on bundle-identifier change
Commit 14313cf changed the Tauri bundle identifier from
uk.co.corbel.magnotia to consulting.corbel.lumotia. Tauri 2 keys both
app_data_dir and the webview's data store (localStorage, IndexedDB,
cookies, service worker, cache) plus all Tauri-plugin state files
(window-state geometry, autostart enable flag) by the identifier.
Without an explicit migration, every user's webview state was
silently orphaned on first launch under the new identifier; the
JS-side migrateLocalStorageKey helper introduced in 1608109 ran
against an empty store and no-op'd.

Fix: in the Tauri setup hook, before webview navigation, resolve the
legacy app_data_dir under the OLD bundle identifier and recursively
copy it to the new identifier's app_data_dir via an atomic staging
rename. Idempotent: legacy preserved as a backup; if both paths
exist post-rename, a warning is logged and the new path is used
unchanged.

Regression tests cover the migrate-only, both-exist, no-legacy, and
idempotent (run twice) cases against synthetic legacy/new paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:35:42 +01:00
6ca94cbff0 agent: code-atomiser-fix — paths.rs multi-legacy-candidate migration + copy_dir_recursive symlink loop
Two reversibility defects in `crates/core/src/paths.rs`:

Defect A (multi-legacy-candidate orphan):
`resolve_app_data_dir` and `legacy_and_target_paths` short-circuited
on the first legacy candidate, allowing two reachable orphan scenarios
on Linux. With both `~/.magnotia` and `~/.local/share/magnotia` the
shim migrated only the dot-home variant, leaving the XDG legacy
invisible forever. With a stray `~/.lumotia` alongside a freshly
migrated `~/.local/share/lumotia`, the resolver kept returning the
dot-home path, orphaning the XDG target.

`legacy_and_target_paths` now returns `Vec<(legacy, target)>`,
probing every legacy variant the platform supports. The migration
driver in `src-tauri/src/lib.rs` loops over the Vec and emits
per-candidate tracing. A new `resolve_app_data_dir_strict` +
`check_target_ambiguity` API refuses to start when more than one
target candidate exists post-migration, surfacing both paths to the
user via the setup hook instead of silently picking one.

Regression tests: `migrate_handles_both_dot_home_and_xdg`,
`resolve_app_data_dir_refuses_on_multiple_targets`.

Defect B (copy_dir_recursive symlink loop on EXDEV migration):
`entry.metadata()` follows symlinks, so a directory symlink reported
is_dir==true and recursed unconditionally. A self-referential or
ancestor-targeting directory symlink would loop until the disk
filled. Switched to `entry.file_type()` (symlink-aware), re-ordered
branches so `is_symlink()` is checked first, and routed all
symlinks through symlink-creation (Unix + Windows) rather than
recursive copy.

Regression tests:
`copy_dir_recursive_does_not_loop_on_self_referential_dir_symlink`,
`copy_dir_recursive_preserves_directory_symlinks`.

14/14 paths tests green. Full workspace cargo test green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:29:28 +01:00
ab5f6ab995 agent: code-atomiser-fix — repair sed-scar provenance from 26c7307
The Magnotia->Lumotia rebrand commit 26c7307 ran a too-greedy
s/magnotia/lumotia/g across comments that already had the correct
legacy/target distinction. The commit author caught one case in
src-tauri/src/lib.rs ("magnotia era" -> "lumotia era" restored) but
missed four others, plus a duplicated word in a Cargo description
from an earlier two-name sed.

Fixed sites:
  * crates/storage/src/database.rs        — migrate_legacy_setting_keys docstring
  * src/lib/utils/localStorageMigration.ts — file-level docstring
  * src/lib/utils/settingsMigrations.ts    — historical-key comment
  * crates/cloud-providers/Cargo.toml      — description duplication
  * src-tauri/src/lib.rs                   — data-dir migration log + doc

None of the underlying code paths were wrong; the lies were
docstring-only. Risk: future maintainer reading any of these
comments at incident time could invert the migration direction or
conclude no migration ran.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:27:24 +01:00
26c7307607 agent: lumotia-rebrand — docs, scripts, root config, residuals
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
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00
681a9b26dc agent: lumotia-rebrand — frontend strings (svelte + i18n + design-system)
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
Phase 8 of the rebrand cascade. Every rendered string is now Lumotia;
no Magnotia surface visible in the UI.

Sweep replaced \bmagnotia\b -> lumotia and \bMagnotia\b -> Lumotia
across all .svelte / .ts / .js / .css / .html / .json (excluding
package-lock.json which regenerates, target/, build/, node_modules/).

Surfaces touched:
- src/app.css — design-token comment header and .magnotia-rh-* CSS
  resize-handle class selectors (also the consuming elements in
  components/ResizeHandles.svelte and src/routes/*/+layout.svelte).
- src/lib/i18n/locales/{en,de,es}.json — brand name in translations.
- src/lib/i18n/index.ts — header comment.
- src/lib/Sidebar.svelte and most pages under src/lib/pages/ +
  src/lib/components/ — title bars, document titles, default
  filenames (lumotia-YYYY-MM-DD.* etc), toast strings, error
  messages, dialog headers.
- src/routes/+layout.svelte, +page.svelte, viewer/, float/, preview/.
- src/app.html page <title>.
- src/lib/utils/settingsMigrations.ts — fallback toast copy.
- src/design-system/{colors_and_type.css,SKILL.md,README.md,
  ui_kits/{Sidebar.jsx,index.html}} — design-tokens, doc strings,
  preview wordmark in the kit.
- package.json — name + description.

NOT touched (deferred / immutable):
- package-lock.json — regenerates on next npm install.
- The two migration-call sites in stores reference the legacy magnotia
  keys deliberately; restored after the sweep clobbered them.
- docs/, README.md, HANDOVER.md — Phase 9 scope.

npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:29:37 +01:00
16081095e0 agent: lumotia-rebrand — localStorage keys + event channels migration
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
Phase 7 of the rebrand cascade. Persisted UI state + inter-window event
channels migrated from magnotia to lumotia naming, with one-shot
localStorage key migration so dogfooded UI state survives the rename.

src/lib/utils/localStorageMigration.ts (new):
- migrateLocalStorageKey(old, new): idempotent + crash-safe shim.
  - If new key exists, removes old (lumotia value is authoritative).
  - If only old exists, copies value to new key, removes old.
  - If neither, no-op.
- migrateLocalStorageKeys(pairs): batch wrapper.

src/lib/stores/page.svelte.ts:
- 4 key constants renamed to lumotia_settings / lumotia_profiles /
  lumotia_task_lists / lumotia_templates.
- BroadcastChannel name renamed to lumotia_task_lists.
- migrateLocalStorageKeys() called at module load before any read.

src/lib/stores/focusTimer.svelte.ts:
- STORAGE_KEY renamed to lumotia.focusTimer.v1.
- migrateLocalStorageKey() called at module load.

Event channels (magnotia: -> lumotia:) renamed across frontend + Rust:
- magnotia:toggle-recording (src/routes/+layout.svelte)
- magnotia:hotkey-pressed / -released (src-tauri/src/commands/hotkey.rs +
  consumers)
- magnotia:open-wind-down (src-tauri/src/tray.rs + consumer)
- magnotia:llm-download-progress (src-tauri/src/commands/llm.rs)
- magnotia:preferences-changed (src/lib/stores/preferences.svelte.ts +
  consumers)
- magnotia:start-timer (nudgeBus + dispatch sites)
- magnotia:focus-timer-{complete,cancelled} (focusTimer + nudgeBus)
- magnotia:microstep-generated (nudgeBus + dispatch sites)
- magnotia:step-completed (nudgeBus + dispatch sites)
- magnotia:task-{completed,uncompleted,deleted} (page.svelte.ts +
  nudgeBus + consumers)

Storage-event filters in src/routes/{float,viewer,preview}/+layout@.svelte
updated to filter on lumotia_settings.

User-facing toast strings still say "Magnotia" — deferred to Phase 8
(frontend strings).

npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:10:50 +01:00
14313cfa84 agent: lumotia-rebrand — tauri productName, identifier, window title
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
Phase 6 of the rebrand cascade per locked decision D2.

src-tauri/tauri.conf.json:
- productName: "Magnotia" -> "Lumotia"
- identifier:  "uk.co.corbel.magnotia" -> "consulting.corbel.lumotia"
- window title: "Magnotia" -> "Lumotia"

D2 picks the reverse-DNS of corbel.consulting (the actual domain CORBEL
trades under) over the prior uk.co.corbel.* convention. This is the
identity the OS uses for installed-app keying, so the first launch under
the new identifier will look fresh to Tauri's plugin state (window-state,
autostart). Per D1 the user-data dir is migrated by lumotia_core::paths
on first boot, so transcripts and settings survive.

cargo build --workspace passes. cargo test --workspace: 339 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:47:28 +01:00
9336286e3c agent: lumotia-rebrand — fix QC blockers for phase 5
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
Phase 5 QC found two blockers + four advisories. All addressed:

B1 (FATAL) — Migration error now aborts startup instead of silently
  continuing past it. Without this fix a transient EACCES / EXDEV / ENOSPC
  would log a warning, init_db would create a fresh empty lumotia dir,
  and the user would appear to lose their transcripts.

B2 (FATAL) — Linux dot-home vs XDG mismatch. The old probe returned
  ~/.magnotia as legacy but the caller passed app_data_dir() as the new
  path — which could be $XDG_DATA_HOME/lumotia. fs::rename across
  filesystems would EXDEV-fail; even when it succeeded the user's
  storage convention silently changed.

  Refactored: legacy_and_target_paths() returns the (legacy, target)
  pair together. Dot-home legacy lands in ~/.lumotia; XDG-set legacy
  lands in $XDG_DATA_HOME/lumotia; XDG-default legacy lands in
  ~/.local/share/lumotia. macOS / Windows / non-tier-1 unchanged.

  migrate_legacy_data_dir() now takes no argument; src-tauri caller
  updated.

A1 — Removed dead new_db.exists() check inside rename_db_file_if_present
  (unreachable: rename_db is called only AFTER the legacy dir was just
  renamed to the new path, so a stray lumotia.db there is impossible).

A2 — Added 4 unit tests for migrate_legacy_setting_keys: lone-magnotia,
  both-present (orphan delete), no-magnotia, idempotent.

A3 — migrate_legacy_setting_keys now returns (renamed, orphans_deleted).
  When both keys exist, the legacy magnotia row is DELETED in the same
  transaction (lumotia row is authoritative).

A4 — Added dot-home convention regression test in paths::tests.

cargo build --workspace passes. cargo test --workspace: 339 pass, 0 fail
(up from 334 in the original Phase 5 commit; +5 new tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:43:45 +01:00
86f83b7a45 agent: lumotia-rebrand — data dir migration shim + paths.rs rename
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
Phase 5 of the rebrand cascade per locked decision D1 (migrate in place).

crates/core/src/paths.rs:
- Hardcoded subdir strings renamed: magnotia/Magnotia -> lumotia/Lumotia
  across all four OS branches (Linux XDG + dot-legacy, macOS Application
  Support, Windows LOCALAPPDATA, fallback dot-dir).
- Database filename: magnotia.db -> lumotia.db.
- Test path fixtures renamed: /tmp/magnotia-test -> /tmp/lumotia-test.
- New MigrationStatus enum (Migrated / TargetAlreadyExists / NoLegacyFound).
- New migrate_legacy_data_dir() that probes the platform-correct legacy
  magnotia path, renames it to the lumotia equivalent via fs::rename, and
  also renames magnotia.db -> lumotia.db inside if found. Idempotent: safe
  to call on every boot. Refuses to overwrite an existing lumotia dir to
  protect user data.
- Four new unit tests covering all branches via the test-friendly
  migrate_legacy_data_dir_inner that takes an explicit legacy path.

crates/storage/src/database.rs:
- New migrate_legacy_setting_keys(pool) that renames any settings rows
  with key matching magnotia_* to lumotia_*. Single SQL UPDATE with
  NOT EXISTS guard so it leaves rows alone if a lumotia_ row already
  exists. Re-exported from lib.rs.

src-tauri/src/lib.rs:
- Calls migrate_legacy_data_dir(&app_data_dir()) at the start of setup()
  BEFORE database_path() resolves the now-renamed dir. Logs migration
  outcome to lumotia_startup tracing target.
- Calls migrate_legacy_setting_keys(&db) immediately after init_db
  returns. Logs only when rows are actually renamed.

src-tauri/src/{lib,commands/{diagnostics,rituals,transcripts}}.rs +
crates/storage/src/database.rs:
- All in-code references to magnotia_preferences, magnotia_history (in
  comments), and magnotia_morning_triage_last_shown renamed to lumotia_*.

cargo build --workspace passes. cargo test --workspace: 334 pass, 0 fail
(up from 330; +4 paths::tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:21:45 +01:00
e2a5feb718 agent: lumotia-rebrand — tracing filter targets
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
Phase 4 of the rebrand cascade. Updates the default RUST_LOG filter
in src-tauri/src/lib.rs and all 'target: "magnotia_startup"' string
literals across src-tauri/src/lib.rs and src-tauri/src/commands/models.rs.

Default filter (before):
  warn,magnotia=info,lumotia_lib=info,lumotia_audio=info,
  magnotia_startup=info,lumotia_hotkey=info,lumotia_ai_formatting=info

Default filter (after):
  warn,lumotia=info,lumotia_lib=info,lumotia_core=info,
  lumotia_audio=info,lumotia_hotkey=info,lumotia_ai_formatting=info,
  lumotia_llm=info,lumotia_storage=info,lumotia_transcription=info,
  lumotia_startup=info

magnotia_startup was a logical tracing target (not a crate); renamed
to lumotia_startup for consistency. magnotia_lib was already renamed
to lumotia_lib in Phase 2. Added per-crate filter entries for parity
across the workspace.

cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:00:33 +01:00
ce6dc1e728 agent: lumotia-rebrand — fix QC blockers for phase 2
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
Phase 2 QC found three explicit blockers + a broader sweep needed:

Explicit (QC-named):
- crates/mcp/src/lib.rs:15 — SERVER_NAME public MCP wire identity
- crates/transcription/build.rs:59 — panic message prefix
- crates/llm/tests/content_tags_smoke.rs:7 — docstring -p flag

Swept (string literals + dev env vars + doc comments + test fixtures):
- crates/mcp/src/main.rs — eprintln log prefixes
- src-tauri/src/commands/diagnostics.rs — diagnostic filename + MAGNOTIA_VERSION
- src-tauri/src/commands/audio.rs — recording filename pattern lumotia-<secs>-...wav
- src-tauri/src/commands/fs.rs — test placeholder path
- crates/transcription/src/model_manager.rs — .lumotia-verified marker
- crates/storage/src/database.rs — lumotia-storage-ro-<pid> temp dirs + doc comments
- crates/cloud-providers/src/keystore.rs — LUMOTIA_API_KEY_<PROVIDER> env var
- crates/audio/src/{wav,decode}.rs — lumotia_test_* / lumotia_decode_* test fixtures
- crates/core/src/tuning.rs — LUMOTIA_INFERENCE_THREADS env var
- All MAGNOTIA_LLM_TEST_MODEL / MAGNOTIA_WHISPER_TEST_* env vars
- Doc comments referencing crate names

Excluded (intentional Phase 4/5 scope):
- magnotia_preferences, magnotia_history, magnotia_morning_triage_last_shown
  DB setting keys (Phase 5 paths.rs migration)
- magnotia_startup tracing target (Phase 4)
- crates/core/src/paths.rs (Phase 5 wholesale rewrite + migration shim)

cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:52:47 +01:00
089349d966 agent: lumotia-rebrand — rust workspace crates magnotia-* -> lumotia-*
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
Phase 2 of the rebrand cascade. Renames all 9 workspace crates from
magnotia-* to lumotia-* plus the src-tauri binary crate name:

- magnotia-ai-formatting   -> lumotia-ai-formatting
- magnotia-audio           -> lumotia-audio
- magnotia-cloud-providers -> lumotia-cloud-providers
- magnotia-core            -> lumotia-core
- magnotia-hotkey          -> lumotia-hotkey
- magnotia-llm             -> lumotia-llm
- magnotia-mcp             -> lumotia-mcp
- magnotia-storage         -> lumotia-storage
- magnotia-transcription   -> lumotia-transcription
- magnotia                 -> lumotia (src-tauri binary)
- magnotia_lib             -> lumotia_lib (src-tauri lib target)

Crate directories (crates/audio/ etc.) stay as-is; only the Cargo.toml
[package] name field changes plus all consumer module imports
(magnotia_core -> lumotia_core, etc.).

Remaining magnotia_* references at this point are intentional and
scoped to later phases: tracing targets (Phase 4), DB setting keys
magnotia_preferences/magnotia_history (Phase 5).

cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:48:09 +01:00
1d71e8e361 refactor(llm): remove GBNF grammar, switch to JSON-envelope extractor
extract_content_tags now generates with grammar=None and parses the
response via a manual brace-counting JSON envelope extractor that
handles Qwen <think>...</think> prefixes and trailing stop tokens.
Five new unit tests. Bumps llama-cpp-2 to 0.1.146. Explicit
features=[] on tauri dependency (no-op).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:25:55 +01:00
184214b60a agent: engine slop residuals B — eprintln → tracing sweep
Replaces 22 production eprintln! sites with structured tracing events
across 8 files. Closes Area B of the post-prognosis residuals plan
(docs/superpowers/plans/2026-05-12-engine-slop-residuals.md).

Files touched (22 sites):
- crates/hotkey/src/linux.rs (2) — hotplug watcher degraded-mode warnings
- crates/ai-formatting/src/pipeline.rs (1) — LLM cleanup fallback warning
- src-tauri/src/commands/transcription.rs (1) — chunking dispatch info
- src-tauri/src/commands/diagnostics.rs (1) — crashes-dir setup warning
- src-tauri/src/commands/tasks.rs (1) — malformed feedback row warning
- src-tauri/src/commands/power.rs (3) — App Nap acquire/release/fail
- src-tauri/src/commands/models.rs (5) — Whisper warmup lifecycle
- src-tauri/src/commands/live.rs (8) — session start, chunk dispatch,
  per-chunk delivery, inference errors, worker disconnects, listener
  loss, status-channel cascade

Levels: error for unrecoverable failures (inference disconnect, panic,
status cascade), warn for recoverable degradation (LLM fallback,
malformed rows, App Nap fail, hotplug watcher fail), info for lifecycle
(session start, chunk processed, App Nap acquire/release, warmup
complete, chunking dispatch), debug for per-chunk noise (speech-gate
skip, chunk dispatch).

Two new dependencies and two new filter targets:
- tracing = "0.1" added to crates/hotkey and crates/ai-formatting
- Default EnvFilter in src-tauri/src/lib.rs::init_tracing extended with
  magnotia_hotkey=info,magnotia_ai_formatting=info so the new targets
  emit at the default level

Out of scope (intentional, left as-is):
- crates/mcp/src/main.rs — CLI binary, stderr is the log contract
  (module docstring) so the JSON-RPC stdout stream stays clean
- crates/*/tests/*.rs and crates/core/examples/tuning_log_demo.rs —
  test/example diagnostic output relies on --nocapture stdio semantics

Discovery during sweep (not fixed — separate follow-up): hotkey crate
has 6 existing log:: calls (log::error/warn/info/debug) but the
workspace builds tracing-subscriber without the tracing-log feature, so
those events are currently silent. Worth a follow-up to either add the
tracing-log bridge or migrate hotkey's existing log:: calls to
tracing::.

Verification:
- cargo fmt --all
- cargo check --workspace --all-targets — clean
- cargo test --workspace — 330+ tests, zero failures
- rg eprintln! src-tauri/src/commands/ crates/hotkey/src/ crates/ai-formatting/src/ — zero hits

Pre-existing working-tree churn in crates/llm/, src/lib/pages/,
src/lib/utils/saveMarkdown.ts and the untracked phase10a dogfood notes
deliberately left unstaged per Jake's instruction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:27:06 +01:00
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
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
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
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
0f105f0e15 chore(llm): update callers for renamed model variants
Picks up the registry rename in the front-end and Tauri command layer:

- src/lib/types/app.ts: LlmModelIdStr now lists the four new ids
  (qwen3_5_2b / qwen3_5_4b / qwen3_5_9b / qwen3_6_27b).
- src/lib/pages/SettingsPage.svelte: LLM_MODELS table rebuilt with
  four tiers (Minimal / Standard / High / Maximum), matching subtitles
  and download-size copy. selectedLlmModelId fallback, hardware-warning
  thresholds, tier-availability check, and ensureRecommendedLlmTier
  fallback all retargeted at the new ids. The Maximum tier surfaces a
  64 GB / 24 GB warning so users with mid-range hardware see honest
  expectations.
- src-tauri/src/commands/llm.rs and commands/tasks.rs: doc-comment
  examples refreshed (Qwen3 4B → Qwen3.5 4B, Qwen3's tokenizer →
  Qwen's tokenizer — the BPE family is shared).
- src/lib/stores/llmStatus.svelte.ts: chip-detail example updated.

cargo build --workspace clean. cargo test --workspace clean.
npx svelte-check reports one pre-existing error in vite.config.js
(unused @ts-expect-error directive, dates back to the original
scaffold commit 9926a42); not introduced here, out of scope to fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:59:03 +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
Claude
17f4dff791 feat(android): bundle.android config + frontend isAndroid/isMobile helpers
Two small Phase 1 follow-ups for the Android target:

1. tauri.conf.json: add `bundle.android.minSdkVersion: 24`. Android 7.0
   is the floor — gives us Vulkan availability (for the eventual GPU
   feature flag), AAudio for cpal, and is what Pixel-class hardware
   tests against. Keeps the global `identifier` on `uk.co.corbel.kon`
   for now; the Corbie rebrand sweep will land `corbel.technology.corbie`
   as a single coherent commit.

2. src/lib/utils/runtime.ts: add `isAndroid()` and `isMobile()` helpers
   alongside the existing `hasTauriRuntime()`. Both use UA sniffing —
   sufficient for feature-gating UI, never for security decisions.
   These are how the Svelte side will hide:
   - hotkey config (no global hotkey API on Android)
   - paste-mode picker (auto-paste maps to a copy-only flow)
   - meeting auto-capture toggle (process list unavailable)
   - multi-window buttons (open-viewer, open-float, etc.)
   - system-tray-related affordances

Tauri 2 doesn't expose a synchronous platform-detection helper that
works during initial render, so UA sniffing is the pragmatic choice.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 12:50:46 +00:00
Claude
4abc2356c2 build(android): cfg-gate desktop-only Tauri surfaces for android target
Phase 1 of the Android same-repo target plan: make the workspace
compilable for `aarch64-linux-android` (and the other NDK ABIs) by
removing the desktop-only crate dependencies and command bodies from the
Android build. After this commit, `tauri android init` followed by
`cargo tauri android build` is structurally unblocked — the remaining
work is the SDK/NDK toolchain (off-sandbox), the Svelte single-window
refactor, and the Phase 3 MVP feature surface.

What's gated under `cfg(not(target_os = "android"))`:

- src-tauri/Cargo.toml: `tauri = { features = ["tray-icon"] }` is now
  declared in the desktop-only target block. The `global-shortcut`,
  `window-state`, and `autostart` plugins join it — none of the three
  support Android natively. The base `tauri = "2"` plus `dialog`,
  `opener`, and `notification` plugins remain unconditional because they
  do support Android.
- src-tauri/src/lib.rs: `mod tray` declaration, the matching
  `tray::setup(app)` call, the close-to-tray `WindowEvent::CloseRequested`
  handler, and the `.plugin(tauri_plugin_global_shortcut::*)` /
  `_autostart` / `_window_state` chain are all desktop-only. The
  builder is split with a single `#[cfg(not(target_os = "android"))]`
  branch that adds the desktop plugins on top of the universal base.
- src-tauri/src/commands/tts.rs: `tts_speak` previously had three
  `#[cfg(target_os = ...)]` branches but no fallback, so on Android the
  `spawned` binding was unbound and the function failed to compile.
  Mirrored the existing `paste.rs` not-implemented fallback. Same fix
  for `list_voices_impl`. Frontend will hide the Read Page Aloud button
  on Android via `isAndroid()`.
- src-tauri/src/commands/windows.rs: all four multi-window commands
  (`open_task_window`, `open_preview_window`, `close_preview_window`,
  `open_viewer_window`) get an Android stub that returns a clear
  "Multi-window is not supported on Android" error. Tauri on Android
  is single-Activity; the previously-secondary content (preview overlay,
  transcript viewer, task float) will live as routes inside the main
  window, gated by `isAndroid()` on the frontend.

What's *not* changed:
- Top-level `identifier` in tauri.conf.json stays `uk.co.corbel.kon`.
  The Phase 10b Kon → Corbie rename sweep will land
  `corbel.technology.corbie` as part of a coherent rebrand commit
  rather than fragmenting the rename across this branch.
- `bundle.android.minSdkVersion: 24` added so a future
  `tauri android init` knows to target Android 7.0+ (Vulkan available,
  scoped storage starts at 29 — we'll surface scoped-storage paths
  via Tauri's dialog plugin on Phase 3).
- `kon-hotkey` already exports a non-Linux stub; no changes needed.
- `commands/meeting.rs` still calls `process_watch::list_running_process_names()`
  which compiles on Android but returns an empty list (SELinux blocks
  /proc walk on API 24+). Frontend will hide the toggle on Android.

Verification: 91/91 tests still pass on the buildable-in-sandbox crates
(kon-storage 60, kon-core 16, kon-mcp 9, kon-hotkey 4, kon-cloud-providers
2). svelte-check 0/0 across 3957 files. The src-tauri crate itself can't
be compiled in this sandbox (no webkit2gtk); CI's desktop builders will
exercise the desktop branch, and Jake's Android-equipped dev box will
exercise the Android branch via `tauri android init`.

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 12:50:33 +00:00
Claude
73f8c45f86 fix(live): self-stop worker when both result and status channels are dead
`emit_live_result` already detected a lost result_channel listener: it
sent a one-shot status warning and from then on short-circuited future
result sends. But if the status_channel listener was also gone — which
is what happens when the user closes the main window without calling
stop_live_transcription_session — the worker kept polling inflight
inference every 10 ms forever, holding a model loaded on the GPU and
keeping the WAV writer file handle open until the process exited.

When the warning send to status_channel also returns Err, the entire
frontend channel pair is dead. Self-assert stop_flag from inside
emit_live_result so the worker drains and exits cleanly. Existing user-
initiated stop semantics are unchanged.

- Threaded `stop_flag: &Arc<AtomicBool>` through `emit_live_result` and
  the free `poll_inference` (instance method already had access via
  `self.stop_flag`).
- Existing `result_listener_loss_is_warned_once_*` test updated to pass
  a stop_flag and assert it stays false when only result_channel fails.
- New test `dead_result_and_status_channels_self_assert_stop_flag` proves
  the self-stop fires when both channels Err.

(src-tauri doesn't build in the audit sandbox — needs webkit2gtk; CI
cross-platform compiles it.)

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:38:16 +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
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
ef42c95000 feat(phase9): extract_content_tags_cmd Tauri wrapper
Bridges LlmEngine::extract_content_tags to the frontend with the same
spawn_blocking + PowerAssertion guard the cleanup_text command uses.
Returns a ContentTags object serialised to camelCase JSON. Errors
surface as readable strings so the frontend toast shows actionable
text on the rare grammar-bypass path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:03:38 +01:00
bfec88ccc9 feat(phase9): register write_text_file_cmd in invoke_handler
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:48:20 +01:00