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>
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>
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.
The accumulator task was fire-and-forget — `tokio::spawn` without
retaining the JoinHandle. `stop_native_capture` sent a stop signal,
slept 50ms, and returned; the worker could still be running its
final flush and appending to `all_samples` when the next
`start_native_capture` cleared it. Rapid start→stop→start could
leak tail samples from one session into another.
Replace `NativeCaptureState.stop_tx` with `worker:
AsyncMutex<Option<CaptureWorker>>`, where CaptureWorker owns both
the stop sender and the spawned task's JoinHandle. New helper
`stop_worker(worker)` sends stop, drops the sender, and `.await`s
the join. Both the prior-worker tear-down in `start_native_capture`
and `stop_native_capture` itself go through the helper, so the
worker is always fully terminated before any downstream read or
next-session cleanup.
AsyncMutex (not std::sync::Mutex) because the stop path awaits
while holding the lock. Also drops the 50ms sleep from
stop_native_capture — the join is an exact barrier.
Two regression tests:
- stop_worker_awaits_full_termination_no_writes_after_join:
synthetic worker with an atomic counter and a flush marker.
After stop_worker the flush must have run and no further
writes may appear.
- stop_worker_is_idempotent_on_a_worker_that_has_already_exited:
tasks that stop themselves must still join cleanly.
A full cpal-backed start→stop→start integration test is not
feasible in Linux CI without an audio device. The component tests
cover the invariant the real flow depends on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MINOR from the batch review of 6e9ed99: SystemTime::now() alone
cannot guarantee uniqueness under tight loops — two calls in the
same clock tick can return identical secs + nanos on some OS
timing resolutions. The filename reduction from "every second"
to "every nanosecond" addresses the flagged bug but leaves a
theoretical gap.
Adds a process-lifetime AtomicU64 counter, zero-padded to 4 digits,
as the third filename component. New shape:
kon-<secs>-<nanos_in_sec>-<counter>.wav
e.g. kon-1776828000-123456789-0000.wav
Across process restarts the counter resets to 0, but the wall-clock
secs/nanos have advanced — no cross-launch collisions possible.
Within a single process, the counter guarantees uniqueness regardless
of clock behaviour.
Test strengthened from ">=32 of 64 unique" (probabilistic) to
"1024 of 1024 unique" (absolute).
MAJOR from the 2026-04-22 review (audio.rs:236-257): filenames were
derived from SystemTime::now().as_secs(), so two recordings started
within the same second resolved to the same path — possible overwrite
or merge.
Extracts the filename generation into a private helper and appends
the sub-second nanosecond component. Format is now
`kon-<secs>-<nanos_in_sec>.wav`, e.g. `kon-1776828000-123456789.wav`,
which stays human-readable, sortable by timestamp, and effectively
uncollidable under any realistic live-capture pattern.
Two tests cover the regression:
- recording_filenames_are_unique_across_rapid_calls: 64 tight-loop
calls must produce at least 32 unique names.
- recording_filename_has_expected_shape: prefix/suffix plus the
zero-padded 9-digit nanos component.
The Vec<f32> in-memory accumulator on run_live_session had three
failure modes: (a) a crash during transcription took the recording
with it, (b) RAM grew linearly with session length, (c) OOM killed
the capture thread silently.
New kon_audio::WavWriter wraps hound::WavWriter<BufWriter<File>> with
an append-friendly API and a 500 ms-granularity header flush. On any
abort after a flush the on-disk file is a valid, playable WAV. Unit
test (brief item #19 acceptance) simulates the abort with
std::mem::forget and asserts the pre-flush samples are recoverable.
Live capture now:
- resolves the destination path at start_live_transcription_session
time via a new resolve_recording_path helper extracted from
persist_audio_samples,
- opens a WavWriter before any samples arrive, sample rate taken from
LocalEngine::capabilities() (#13 wiring) with 16 kHz fallback,
- feeds the resampler output through WavWriter::append inside
append_resampled_audio — drops the writer with a user-visible
warning if a write fails mid-session,
- calls flush() at stop (after resampler tail), finalise() on clean
exit, and drops-to-last-flushed state on abort.
LiveSessionSummary.audio_samples → audio_path: the path is already
written by the time stop_live_transcription_session runs; no
post-session write step remains for live capture.
persist_audio_samples is kept for the offline save_audio command.
Major quality pass on top of Phase 2. Five substantive changes plus
cross-cutting touches across audio, hotkey, transcription, and Tauri
command layers.
Transcription quality
- Long-audio chunking in commands/transcription.rs: Parakeet and large
file transcription now chunk-and-recompose with overlap trimming, so
the live-path chunking advantage extends to file-based workflows.
- Stateful live speech gate in commands/live.rs on top of the earlier
duplicate-boundary filtering — distinguishes start-of-speech from
mid-speech and holds state across chunks.
Auto-learning corrections
- New crates/ai-formatting/src/correction_learning.rs: extracts user
text corrections from viewer edits and proposes additions to the
active profile's vocabulary.
- src-tauri/src/commands/profiles.rs bridge for frontend-driven
confirmation of learned terms.
- src/routes/viewer/+page.svelte hooks the learning path into the
segment-edit flow so corrections feed profile_terms without a
separate 'train this profile' UX.
Transcript profile provenance
- Migration v8 (crates/storage/src/migrations.rs) adds profile_id to
transcripts, defaulting to DEFAULT_PROFILE_ID so existing rows stay
valid.
- crates/storage/src/database.rs: TranscriptRow + CRUD carry profile_id.
- src-tauri/src/commands/transcripts.rs: add_transcript accepts and
persists profile_id.
- DictationPage.svelte + FilesPage.svelte send activeProfileId on
capture so learned corrections are attributed to the right profile.
Cleanup prompt contract
- crates/ai-formatting/src/llm_client.rs hardened: the CLEANUP_PROMPT
now specifies concrete do/do-not rules, ready for a real model-backed
cleanup pass. The llm_client is still a stub — kon-llm remains unwired
— but the prompt shape is final.
Cross-cutting polish
- Minor touches in audio (capture/decode/resample), hotkey (lib/linux/stub),
core, transcription (concurrency/model_manager/local_engine/whisper_rs),
and the rest of src-tauri/src/commands/*: error-path tightening, log
clarity, TS-migration follow-ups (@ts-nocheck additions for incremental
typing).
Verified locally: npm run check, cargo test -p kon-ai-formatting,
cargo test -p kon-storage, cargo test -p kon --lib commands::live::tests,
cargo check — all green.
Scope boundary: kon-llm crate is still a stub; task extraction remains
rule-based. Bundled local-LLM runtime is the next clean step and is not
in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the 6 Codex review findings on the Day 1 mic-capture work
(commits 96980c7 + 41db162). Detail in
output/reports/kon-codex-mic-capture-followup-2026-04-17.md (CORBEL
workspace).
src-tauri/src/commands/audio.rs:
- D1: Sending an explicit stop signal before dropping stop_tx, so the
accumulator task wakes up immediately rather than waiting for the
Disconnected detection added below.
- D2: Wrapping MicrophoneCapture::start() in tokio::task::spawn_blocking.
start() can spend up to 350ms × N_devices × 2 passes; running it on
the async runtime froze other Tauri commands.
- M3: Match on rx.try_recv() error variants. Empty -> sleep + continue.
Disconnected -> exit accumulator task immediately. Previous behaviour
was an infinite loop after the capture stream died.
crates/audio/src/capture.rs:
- D3: Added DEAD_SILENCE_FLOOR (1e-7) gate that applies even in the
fallback pass. PulseAudio/PipeWire can stream zero-valued bytes from
a borked device; that is worse than failing fast.
- M1: Validation requeue (`for chunk in collected { try_send }`) now
counts drops in the same dropped_chunks counter.
- M2: New CaptureRuntimeError type. The cpal stream error callback now
forwards errors via a separate sync_channel (capacity 16) that the
live session can drain via take_error_rx() and surface as toasts.
Re-exported from kon_audio crate root.
cargo check -p kon-audio passes clean.
NOT YET DONE (M4): JACK-specific monitor name patterns. Defers until
testing on a JACK host. Current is_monitor_name() may miss JACK
conventions.
Wiring the new error_rx into the live session and surfacing as toasts
lands with the Day 3 error-toast system commit.
Day 1 follow-up to 96980c7. The device-picker UI in Settings now
actually takes effect: settings.microphoneDevice flows from the Svelte
store, through the Tauri invoke, into MicrophoneCapture::start_with_device
on the Rust side.
Touched paths (back-to-front):
- src-tauri/src/commands/audio.rs:start_native_capture — new optional
`device_name: Option<String>` parameter; routes to start_with_device
when set, falls back to auto-select start() when None or empty.
- src-tauri/src/commands/live.rs:StartLiveTranscriptionConfig — new
optional `microphone_device: Option<String>` field with same
semantics (rename_all = "camelCase" maps it to microphoneDevice on
the wire).
- src-tauri/src/commands/live.rs:run_live_session — picks
start_with_device when an explicit name is provided.
- src/lib/pages/DictationPage.svelte — passes
microphoneDevice: settings.microphoneDevice || null in the invoke.
Behaviour:
- "Auto" in the picker (empty string) -> backend auto-selects, skipping
monitor sources and validating by RMS energy.
- Specific device -> backend opens that device by exact name; if it has
been disconnected the user gets a clear error pointing them back at
Settings.
cargo check -p kon-audio passes clean. Tauri-crate cargo check requires
cmake (pre-existing infra dependency for whisper-rs-sys); install via
`sudo dnf install cmake clang-devel`.
Day 1 of the upgrade plan (output/reports/kon-upgrade-plan-2026-04-17.md
in the CORBEL workspace). Fixes the HANDOVER.md blocker: native live
transcription was capturing silence because PulseAudio/PipeWire monitor
sources (speaker loopback) were winning the device-selection race —
they deliver zero-valued bytes that satisfied the original
"any device that produces data within 350ms" check.
WHAT CHANGED:
crates/audio/src/capture.rs (rewrite):
- New `DeviceInfo` struct (serde-derived) for the Settings device picker
- New `MicrophoneCapture::list_devices()` enumerates inputs with metadata
- New `MicrophoneCapture::start_with_device(name)` for explicit selection
- Refactored `start()` with monitor-source filtering by name pattern
(.monitor suffix, "Monitor of " prefix, "loopback" substring) and
RMS-energy validation in a 350ms window
- Two-pass selection: real inputs first, monitor sources only as
last-resort fallback with explicit warning log
- Drop counter (Arc<AtomicU64>) tracks chunks dropped by `try_send`
failure under load — Codex review caught this as a silent-failure risk
- `dropped_chunks()` accessor for the live session
- Verbose tracing at every step for diagnosis
- Unit test for monitor-name detection
- `cargo check -p kon-audio` passes clean
crates/audio/src/lib.rs:
- Re-export `DeviceInfo`
crates/audio/Cargo.toml:
- Add serde dependency (for DeviceInfo derives)
src-tauri/src/commands/audio.rs:
- New `list_audio_devices` Tauri command (returns Vec<DeviceInfo>)
src-tauri/src/lib.rs:
- Register `list_audio_devices` in invoke_handler
src/lib/pages/SettingsPage.svelte:
- New "Audio" section with microphone picker dropdown
- Auto-populates on mount via `list_audio_devices`
- Refresh button + clear messaging about monitor sources
- Likely-monitor entries marked disabled in the dropdown
- Auto mode is the default (empty string in settings.microphoneDevice)
src/lib/stores/page.svelte.js:
- New `microphoneDevice` field in defaults (empty = auto-select)
NEXT STEPS (per the upgrade plan):
- Wire `microphoneDevice` from settings into `MicrophoneCapture::start_with_device`
in the live and standalone capture paths (currently both still call
the auto-selecting `start()`)
- Test on real hardware (Wayland + multiple input devices)
- Codex sanity-check of this diff is running in parallel; addendum to
follow if anything substantive comes back
Refs: /home/jake/Documents/CORBEL-Projects/kon/HANDOVER.md
output/reports/kon-upgrade-plan-2026-04-17.md (CORBEL workspace)