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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
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>
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>
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>
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>
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>
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>
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.
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
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
`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
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
`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
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>
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>
Thin UTF-8 writer used by the new save-dialog path. Caller owns path
safety; the source path is always OS-dialog-provided. Two unit tests:
roundtrips a small UTF-8 string with non-ASCII chars and asserts a
nonexistent parent path returns an actionable error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin wrapper over kon_storage::list_recent_completions, parameterised
by day count. Serialises to camelCase JSON (day, count).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small if-then automation layer. Rules persist in SQLite; the runner
lives on the frontend and binds to the Phase 6 event bus so the rule
pipeline reuses the same delivery primitives (timer events, TTS,
Tasks navigation).
Storage:
- Migration v12 adds implementation_rules (id, enabled, trigger_kind,
trigger_value, actions_json, last_fired_key, created_at,
updated_at) with enabled+trigger_kind index for the runner's hot
path.
- CRUD helpers: insert / list / get / set-enabled / mark-fired /
delete, plus a round-trip test.
Commands (all main-window-guarded via ensure_main_window):
- list_implementation_rules
- create_implementation_rule — validates HH:MM, checks the target
task exists at save time for surface-task actions, caps
speak-line at 240 chars, pins v1 timers to 5 minutes.
- set_implementation_rule_enabled
- mark_implementation_rule_fired — main-thread idempotency shim so
the runner can atomically claim a fire.
- delete_implementation_rule
Runner (implementationIntentions.svelte.ts):
- Subscribes to kon:task-completed and kon:morning-triage-finished
(MorningTriageModal now emits on all three exit paths — empty,
skipped, picked — so skip counts as finishing).
- 30 s poll for time-of-day rules, plus an immediate check on startup
so a rule whose time has already passed today catches up once.
- Idempotency via last_fired_key composed as YYYY-MM-DD@HH:MM for
time rules; new time rules whose HH:MM has already passed today
are pre-seeded so they don't fire retroactively on save.
- Rules are paused when Nudges "Mute for now" is on — a hard mute
stops all rule delivery in addition to OS notifications.
- Stale-task safety: if a surface-task action's target has been
deleted, the runner opens Tasks and warns clearly rather than
pretending to surface something that's gone.
Editor (ImplementationRulesEditor.svelte):
- Lives in Settings under a new "If-then rules" accordion section.
- `If` picker: time of day (with time input), a task completes,
morning triage finishes.
- `Then` composer: optional surface (inbox / today / all tasks /
specific task), optional 5-min timer, optional speak-aloud line.
- Saved rules list with enable toggle + delete.
Rules table integration for Phase 10b rename sweep: add
implementation_rules to the kon.db → corbie.db migration shim when
that phase lands.
Gates: fmt, clippy -D warnings, cargo test 265/0, svelte-check
0/0, npm build green. Pre-existing Vite chunk warning on sounds.ts
is unrelated to Phase 7.
Frontend-owned nudge bus that consumes in-app signals Corbie already
produces, applies suppression, and fans out to OS notification + an
optional TTS read-aloud. OS-wide keyboard/window activity detection
stays deferred per the revised roadmap — the plan before rewrite
would have been brittle on Wayland, permission-heavy on macOS, and
low-quality everywhere.
Triggers (v1, all in-app signals):
- inactivity_with_active_timer — timer running, window blurred ≥ 90 s,
at least 60 s into the timer.
- pending_morning_triage — ritual enabled, past 10:00 local, last
shown ≠ today. Polls every 5 min while focused.
- micro_step_idle — micro-step decomposition created, no child step
or parent task completed within 15 min.
Suppression:
- Respects nudgesEnabled + nudgesMuted.
- No nudge while the app has focus (document.hasFocus).
- Hard cap 3 per rolling hour.
- Permission requested via @tauri-apps/plugin-notification on first
delivery; denial is silently respected.
Rust side:
- tauri-plugin-notification registered + ACL entries on the main-
window capability only (secondary windows can't fire nudges).
- commands::nudges::deliver_nudge — thin wrapper, security-guarded
via ensure_main_window, delegates to the plugin. No DB writes —
the roadmap's nudges-audit table is deferred until a concrete need
emerges.
Frontend glue:
- nudgeBus.svelte.ts — subscribes to window events, applies
suppression, calls deliver_nudge (+ tts_speak when speakAloud is
on).
- kon:task-completed now dispatched on complete_task_cmd success.
- kon:microstep-generated + kon:step-completed dispatched from
MicroSteps so the idle trigger can clear itself on any engagement.
- kon:focus-timer-cancelled added to focusTimer so the bus can reset
its inactivity state on cancel, not only on natural completion.
- nudgeBus started from +layout.svelte onMount, stopped on destroy.
Settings:
- New "Nudges" section with three toggles: Enable nudges, Mute for
now (separate so a hard mute doesn't lose preferences),
Speak nudges aloud (reuses Phase 4 TTS).
- All default OFF. No first-run prompt — nudges are a Settings-found
feature rather than a walkthrough step.
Out of scope per the revised Phase 6 spec: OS-wide keyboard/window
hooks, biometric signals, custom trigger editor (Phase 7), notification
sound (platform variance too high for Layer-1 — revisit in Phase 9
polish with a bundled .wav).
Gates: fmt clean, clippy -D warnings clean, cargo test 262/0 (4
existing + 262 current), npm run check 0/0, npm run build green.
Three opt-in rituals, all default OFF. Research-anchored (Barkley's
point-of-performance, Sweller cognitive-load theory, Newport shutdown
ritual, Gollwitzer implementation intentions, Thaler/Sunstein nudge
with informed consent for the ADHD audience).
Morning triage: modal gated on ritualsMorning toggle, configurable
trigger time (default 08:00 to respect ADHD sleep inertia rather than
the spec's 06:00), "pick up to three for today" with a gentle swap
message on the fourth attempt. Skip sets last-shown-today so it never
re-prompts the same calendar day. last-shown persists via kon_storage.
Evening wind-down: dedicated page, user-triggered only (tray menu +
Settings button). Mechanical closure + physical reset + intentional
cue — the whole Newport template. Open loops are read-only reflection;
Tasks page owns transactions. Copy is additive throughout: "you
finished X today", never "you didn't finish Y".
Autostart: tauri-plugin-autostart registered (LaunchAgent on macOS,
.desktop on Linux, registry Run on Windows). No bespoke Rust commands
— frontend calls the plugin's invoke-handlers directly. Toggle in
Settings is one-way (click → OS call → state update) to avoid the UI
lying during the round-trip. First-run presents a forced-choice prompt
for all three options, with "skip all" escape hatches per step.
Copy audit against RSD literature: no "overdue", "failed", or
day-to-day comparison framing anywhere in ritual surfaces.
Post-v0.1 ideas captured in the roadmap: calendar integration
(read-only ICS as interim, cloud sync parked) and right-click-to-task
(in-app simple, system-wide a separate phase).
Platform-dispatched TTS (spd-say + espeak-ng fallback on Linux, say on
macOS, PowerShell System.Speech on Windows) with a shared SpeakerButton
component. Tap to speak, tap again to stop; only one button speaks at
a time so two surfaces don't talk over each other. Text always travels
via argv (or a PowerShell here-string delivered through -EncodedCommand
on Windows) so user content never enters a shell string.
Mount points: DictationPage transcript footer, transcript viewer header,
per-step in MicroSteps. Settings gains a "Read aloud" accordion with
voice picker (lazy-loaded from the OS synth), rate slider 0.5-2.0x,
and a British-English test utterance.
Rust tests cover rate mapping, NaN handling, and Windows here-string
terminator safety. No pause/resume, no SSML, no cloud voices — that
stays out of scope per the Layer-1 roadmap.
Closes Phase 3 of the 2026-04-23 feature-complete roadmap. Incorporates
the Codex plan-review fixes from this session: profile-free index, tri-
state update command, and de-prioritise-not-hide semantics.
Storage (kon-storage):
- Migration v11 adds `energy TEXT` to `tasks` with a CHECK constraint on
`high | medium | brain_dead | NULL`. Index `(energy, created_at DESC)`
— deliberately not per-profile because the tasks table carries no
profile_id column yet (tracked as a separate gap in HANDOVER).
- `TaskRow.energy: Option<String>` plus `task_row_from` read.
- `insert_task` signature grows by one optional arg (`energy`). Allowed
`too_many_arguments` with a rationale comment — the positional shape
matches the column order and flipping to a params struct would have
rippled through every caller for cosmetic benefit only.
- New `set_task_energy(pool, id, Option<&str>) -> TaskRow`. Lives as its
own function because `update_task` uses COALESCE to let `None` mean
"preserve" — which would make clearing the tag impossible.
- Two new tests: round-trip including explicit NULL clear, and CHECK
constraint rejection of unknown values.
- Tests updated for the v10 → v11 version bump.
Tauri (src-tauri):
- `TaskDto.energy`. `CreateTaskRequest.energy` (optional). Inline
validation against the allowed set before hitting the DB, so frontend
bugs surface as friendly errors instead of CHECK-constraint failures.
- New `set_task_energy_cmd` command mirroring the storage tri-state API.
Frontend (svelte):
- `EnergyLevel` type added to `types/app.ts`. `TaskDto`, `TaskEntry`, and
`TaskDraft` grow an `energy` field.
- `SettingsState.currentEnergy` (persisted) + `matchMyEnergy` (persisted
toggle). Defaults: null + false — no surface change until user opts in.
- `setTaskEnergy(id, EnergyLevel | null)` action on the task store.
Calls the dedicated Tauri command, updates local state, broadcasts to
sibling windows.
- `EnergyChip.svelte` — new component. Cycles unset → High → Medium →
Brain-Dead → unset on click. Colour tokens: accent / warning /
text-tertiary (deliberately not danger-red for Brain-Dead — the brief
is explicit that this state must not feel pathologised).
- Chip rendered on every task row in TasksPage and every row in
WipTaskList. Hidden-until-hover when energy is unset so untagged rows
stay calm; always visible once tagged because the colour is the signal.
- Tasks page header gains a "I feel" segmented control and a
"Match my energy" toggle. When both are active, matching tasks sort
to the top — unset tasks are treated as Medium-equivalent. Nothing is
ever hidden; this is a de-prioritisation, not a filter.
Deferred / out of scope:
- LLM-driven surfacing (brief says "The AI surfaces...") — deterministic
client-side sort is v1; LLM layer is a later phase.
- tasks.profile_id + per-profile energy sort — separate migration.
All green: cargo build + 251 tests + clippy -D warnings (0 warnings)
+ fmt + svelte-check (0/0) + npm run build.
Independent review surfaced three majors and one nit. All actioned.
MAJOR 1 — profile scoping:
`decompose_and_store` and `extract_tasks_from_transcript_cmd` now
accept an optional `profile_id` (wired from `profilesStore.activeProfileId`
in MicroSteps.svelte and DictationPage.svelte), and thread it into the
feedback-retrieval query so per-profile decomposition styles do not
leak into each other. `record_feedback` gets the same treatment.
MAJOR 2 — prompt-budget regression on long inputs:
New `trim_to_budget` helper + `FEW_SHOT_CHAR_BUDGET = 2000` char cap
in `src-tauri/src/commands/tasks.rs`. Retrieval still pulls up to 5
rows but they are char-counted and truncated against the budget
before being sent to the LLM. Char cost matches the `Input: ...\n
Good output: ...` render path so the budget maps cleanly to ~570
Qwen3 tokens, well inside the 8192-context reserve after the 512-
or 768-token response allocation. Oldest-first drop order (iteration
stops at cost exceeded) preserves the most recent correction which
is the one carrying the user's live preference.
MAJOR 3 — inline edit stale-rollback race:
`saveEdit` in MicroSteps.svelte now stamps a monotonic per-step
`saveToken`. Each edit bumps the token; on failure the rollback
only fires if `saveToken[step.id] === myToken`, so a slow-failing
first save can no longer overwrite a faster successful second save.
NIT — retrieval ordering stability:
`list_feedback_examples` ORDER BY now `created_at DESC, id DESC`.
SQLite timestamp precision is one second; without the secondary
key, bursty feedback within the same second would select
non-deterministically.
Also: malformed `context_json` now warns via eprintln! rather than
disappearing silently — Codex minor.
All green: cargo build + 249 tests + clippy -D warnings + fmt
+ svelte-check (0/0) + npm run build.
Closes the human-in-the-loop gap from docs/brief/feature-set.md and
Phase 2 of the 2026-04-23 feature-complete roadmap.
Storage (kon-storage):
- Migration v10 adds the `feedback` table: (target_type, target_id,
rating, original_text, corrected_text, context_json, profile_id,
created_at) with CHECK constraints on target_type and rating, plus
indexes on (target_type, rating, created_at DESC) for prompt-time
retrieval and (profile_id, target_type, created_at DESC) for
per-profile scoping.
- New public API: `FeedbackTargetType`, `RecordFeedbackParams`,
`FeedbackRow`, `record_feedback`, `list_feedback_examples`.
- Tests updated — the RB-02 rollback regression now discovers the
real max version at runtime instead of hard-coding v10 for its
poison migration.
LLM (kon-llm):
- `prompts::FeedbackExample` — local shape for few-shot exemplars so
kon-llm stays independent of kon-storage.
- `prompts::build_conditioned_system_prompt` — appends a "here is
the style this user prefers" block to the base system prompt
when examples are available; returns the base prompt unchanged
when empty, so new users and early sessions see generic output.
- `LlmEngine::decompose_task_with_feedback` and
`LlmEngine::extract_tasks_with_feedback` thread examples through
to the builder. The old one-arg variants are preserved and now
call through with an empty slice.
- 4 unit tests covering empty, empty-input-skip, correction-wins,
and thumbs-up-only fallback.
Tauri (src-tauri):
- New commands::feedback module: `record_feedback`,
`list_feedback_examples_cmd`.
- `decompose_and_store` and `extract_tasks_from_transcript_cmd`
now fetch the last 5 positive/neutral feedback rows for their
target type and pass them through to the LLM, wiring the
learning loop end-to-end.
- Shared `to_llm_examples` helper parses the `context_json.input`
field (where the recorder stashes the parent task text / transcript
chunk) back into the exemplar shape.
Frontend (MicroSteps.svelte):
- Thumbs-up and thumbs-down buttons on every micro-step row.
Hover-revealed; the vote recolours the icon; clicking again
clears the local highlight (the row itself stays in the audit
trail).
- Pencil icon + double-click to edit step text. Save flows through
update_task_cmd for persistence and records a correction feedback
row with (original_text, corrected_text) — the highest-value
training signal.
- Parent task text is captured in context_json.input at record time
so the prompt builder can reconstruct the (input, preferred-output)
pair on subsequent decompositions.
- Feedback capture is best-effort — a record_feedback failure never
interrupts the primary action.
What's deferred to a later phase:
- Thumbs + corrections on extracted tasks (same pipeline, different
surface — probably TasksPage after the AI-extraction path)
- Thumbs on transcript cleanup output
- Semantic retrieval over the feedback corpus (once there is enough
data to justify embedding infrastructure; the storage shape is
already ready for it)
Auto-applied cargo clippy --fix across 11 files — needless return,
unnecessary cast, map_or simplification, repeat().take() → repeat_n(),
iter().any() → contains(), manual char comparison, lifetime elision,
push_str single-char, reference immediately dereferenced.
Also fixed three lints on file_storage.rs manually: two doc-list-item
overindentations, plus the same needless-return. Baseline main was
not clippy-clean with -D warnings before; after this pass one
needless_range_loop warning remains (live.rs:1089) that clippy's
suggested rewrite would make less readable — left for a dedicated
refactor session.
Build + workspace tests remain green (245 passing, 0 failing, 1
ignored).
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>
get_runtime_capabilities was returning `accelerators = ["cpu",
"vulkan"]` and `whisper.supports_gpu = true` regardless of build
config or runtime state. On a macOS build it falsely advertised
"vulkan" (the backend actually resolves via MoltenVK as Metal); on a
whisper-disabled build it claimed GPU support for an engine that
hadn't been linked.
Added `compose_accelerators(whisper_enabled, loader_available, target)`
— a pure helper that always emits "cpu" first and appends the
platform-appropriate GPU name only when whisper is compiled in AND the
Vulkan loader resolves. `supported_accelerators()` wraps it with the
live `cfg!(feature = "whisper")`, loader probe, and target OS.
`get_runtime_capabilities` now calls `supported_accelerators()` and
sets `whisper.supports_gpu = cfg!(feature = "whisper")`. Parakeet
stays CPU-only.
Five tests in `commands::models::tests` cover the permutation matrix:
whisper on/off, loader present/missing, macOS vs other. Both feature
configurations (`--features whisper` and `--no-default-features`)
build and pass tests.
Macos Metal-loader resolution on real hardware stays on the
ship-gate checklist — the detection logic is verifiable from Linux
but runtime behaviour is not.
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).
2026-04-22 review MINORs and NITs:
- crates/core/src/providers.rs: delete entire module. SpeechToText /
TextProcessor / ProviderRegistry were forward-looking traits that
never got wired — the Transcriber trait in kon-transcription
(A.2 #13) has since superseded SpeechToText, and the Registry
pattern was redundant against LocalEngine. Keeping them as dead
public surface signalled future direction that is no longer
accurate.
- crates/core/src/types.rs: delete TranscriptMetadata. Forward-
looking struct with an unfulfilled TODO; storage has evolved
independently through v7/v8 migrations without adopting it.
- crates/ai-formatting/src/llm_client.rs: remove #[allow(dead_code)]
from CLEANUP_PROMPT and format_dictionary_suffix. Both are
actively called; the suppressions would hide future genuine
dead-code warnings in this regression-sensitive prompt file.
- src-tauri/src/commands/live.rs: remove #[allow(dead_code)] from
LiveStatusMessage. Every variant (Warning, Overload, Error,
Finished) is constructed in the module today.
- README.md: update kon-transcription row from "SpeechToText
trait" to "Transcriber trait" and mention the new streaming/
module.
Workspace test gate green (225 lib tests across all crates).