Every issue under docs/issues/ links to this file as its Source. It was
created for 592b894 but not staged, leaving dangling links in the
release-blocker tracker.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
name, description, type, tags, date
| name | description | type | tags | date | |||||
|---|---|---|---|---|---|---|---|---|---|
| Code Review — 2026/04/22 | Full-sweep audit findings across all Kon crates + src-tauri, with triage buckets for quick wins vs release-blockers | reference |
|
2026/04/22 |
Kon Code Review — 2026/04/22
Full-sweep read-only audit of every .rs file across the Kon workspace. Four parallel Codex agents scanned:
- Agent A —
crates/transcription/,crates/audio/ - Agent B —
crates/ai-formatting/,crates/llm/,crates/storage/ - Agent C —
src-tauri/src/(commands layer + lib.rs + main.rs + types.rs) - Agent D —
crates/core/,crates/cloud-providers/,crates/hotkey/,crates/mcp/,src-tauri/build.rs
Summary
| Severity | Count |
|---|---|
| CRITICAL | 4 |
| MAJOR | 16 |
| MINOR | 15 |
| NIT | 3 |
CRITICAL items are all real bugs — not speculative. Three were introduced or touched during the whisper-ecosystem sprint; one is a latent data-integrity issue in the storage layer.
Recommended path:
- Fix the four CRITICALs this session.
- Log all MAJORs as release-blockers (must land before v0.1).
- MINORs become a boy-scout backlog — picked up opportunistically when adjacent code is touched.
- NITs resolve inline when the surrounding file is next edited.
CRITICAL
C1 — Racy single-session guard in live.rs
- Path:
src-tauri/src/commands/live.rs:193-338 - Issue:
start_live_transcription_sessionchecksrunningis None before multipleawaits and only stores the handle at the end;stop_live_transcription_sessionremovesrunningbefore awaiting the worker join. Two overlapping IPC calls can admit a second live session OR expose an empty slot while the first session is still shutting down. - Fix scope: large — requires holding the mutex across the async boundary or restructuring the state machine.
- Bucket: RELEASE-BLOCKER (this is the file's core invariant).
C2 — RmsVadChunker::flush() drops chunks
- Path:
crates/transcription/src/streaming/rms_vad.rs:294-311 - Issue:
flush()zero-pads the final partial frame and callsconsume_frame()vialet _ = ..., discarding the returnedVadChunk. If the padded frame triggers end-of-utterance ormax_chunk_samples, the emitted chunk is lost and the outer state check either returnsNoneor an empty chunk. - Fix scope: small — change
flushtrait signature to returnVec<VadChunk>, collect chunks from both theconsume_framecall and the finalemit_active_chunk_and_close. - Bucket: QUICK WIN. Regression test in the same commit.
- Attribution: Introduced in
05eea41yesterday.
C3 — Multi-statement migrations can half-apply
- Path:
crates/storage/src/migrations.rs:263-299 - Issue:
run_migrationsexecutes statements individually and only records schema version after the full migration succeeds. A crash mid-migration leaves the schema half-mutated while still appearing unapplied; the next startup replays it against the partially-mutated DB. - Fix scope: medium — wrap each migration in
BEGIN/COMMITtransaction, update version row within the same transaction. - Bucket: RELEASE-BLOCKER. A user with a mid-migration crash today gets a bricked DB.
C4 — Transcript provenance can reference deleted profiles
- Path:
crates/storage/src/migrations.rs:208-216,crates/storage/src/database.rs:61-89,:697-708 - Issue: v8 migration adds
transcripts.profile_idwithout a foreign-key constraint.insert_transcriptaccepts anyprofile_id;delete_profiledoesn't guard against existing transcript references. Transcripts can keep orphaned profile IDs, breaking provenance integrity. - Fix scope: large — v9 migration to add FK constraint + reconcile existing orphans; update delete_profile to either cascade or block.
- Bucket: RELEASE-BLOCKER. Silent data-integrity hole.
MAJOR (16)
src-tauri — Commands layer
[MAJOR] poll_inference treats IPC listener loss as session-fatal
src-tauri/src/commands/live.rs:721-813- Closing the frontend or reloading it kills the whole live session via
?onresult_channel.send(...). Non-fatal Tauri channel lifecycle should not terminate capture. - Fix scope: medium. Bucket: RELEASE-BLOCKER.
[MAJOR] run_live_session is a 200+ line multi-responsibility monolith
src-tauri/src/commands/live.rs:349-579- Owns mic startup, runtime error draining, resampling, progressive WAV persistence, overload dropping, inference scheduling, and shutdown finalisation in one function. Known lifecycle bugs trace to this.
- Fix scope: large. Bucket: RELEASE-BLOCKER (refactor enables C1 fix).
[MAJOR] Native capture worker is detached and can outlive stop/start
src-tauri/src/commands/audio.rs:46-228start_native_capturespawns a worker but never retains a join handle. A previous capture can flush intoall_samplesafterstop_native_captureclears it — truncation and cross-session contamination possible.- Fix scope: medium. Bucket: RELEASE-BLOCKER.
[MAJOR] resolve_recording_path collides within the same second
src-tauri/src/commands/audio.rs:236-257- Filename derived from
SystemTime::now().as_secs(). Two recordings started in the same second get the same path → overwrite or merge. - Fix scope: small. Bucket: QUICK WIN (append milliseconds + session_id).
[MAJOR] get_runtime_capabilities advertises wrong accelerators
src-tauri/src/commands/models.rs:435-489- Hard-codes
accelerators = ["cpu", "vulkan"]even whendetect_active_compute_devicewould reportmetalon macOS or the binary was compiled without thewhisperfeature. - Fix scope: medium. Bucket: RELEASE-BLOCKER (frontend shows wrong settings otherwise).
[MAJOR] paste_text_replacing doesn't snapshot the clipboard
src-tauri/src/commands/paste.rs:181-217- Inconsistent with
paste_text. Replacing leaves the raw transcript on the clipboard and destroys whatever the user had copied before. - Fix scope: small. Bucket: QUICK WIN.
[MAJOR] PowerAssertion::begin is a non-functional macOS stub
src-tauri/src/commands/power.rs:41-121begin_activityalways returnsErr→ guard never acquires an App Nap assertion. The plan for A.1 #9 explicitly deferred this; still flagging so it's not forgotten.- Fix scope: medium. Bucket: RELEASE-BLOCKER (before macOS ship).
Transcription + audio
[MAJOR] Decoder returns partial audio on errors
crates/audio/src/decode.rs:58-79- Packet-read errors break the loop; decoder errors are skipped; function still returns
Okif any samples were produced. Truncated files silently accepted. - Fix scope: medium. Bucket: RELEASE-BLOCKER.
[MAJOR] read_wav() silently drops sample decode errors
crates/audio/src/wav.rs:135-145filter_map(|s| s.ok())for both integer and float iterators. Corrupt samples silently discarded.- Fix scope: small. Bucket: QUICK WIN.
[MAJOR] Model downloads don't validate non-resume HTTP status
crates/transcription/src/model_manager.rs:161-262- Resume branch checks 206/200. Normal downloads never call
error_for_status()→ a 4xx/5xx response body gets written to.partand renamed. - Fix scope: small. Bucket: QUICK WIN.
LLM + storage
[MAJOR] LLM prompts not preflighted against context window
crates/llm/src/lib.rs:143-166,:317-321generatetokenises the full prompt;context_window_sizehard-caps at 8192. Long transcripts reach inference with prompts bigger than context → late runtime failure.- Fix scope: medium. Bucket: RELEASE-BLOCKER.
[MAJOR] uncomplete_task doesn't reopen auto-completed parents
crates/storage/src/database.rs:389-449complete_subtask_and_check_parentauto-completes a parent when the last child completes.uncomplete_taskonly flips the requested row → reopening a child leaves the parent wrongly marked done.- Fix scope: small. Bucket: QUICK WIN.
Core + small crates
[MAJOR] keystore::store_api_key is a thread-unsafe safe API
crates/cloud-providers/src/keystore.rs:6-18std::env::set_varis UB outside single-threaded init per documented precondition. The safepub fndoesn't enforce this.- Fix scope: medium. Bucket: RELEASE-BLOCKER.
[MAJOR] Hotkey device filtering hard-codes KEY_A / KEY_R
crates/hotkey/src/linux.rs:236-241try_attach_deviceclaims to check for the configured hotkey's key but tests for hard-codedKEY_AorKEY_R. Hotkeys on other keys get silently dropped.- Fix scope: small. Bucket: RELEASE-BLOCKER (correctness bug in a feature users rely on).
[MAJOR] Malformed JSON-RPC silently dropped
crates/mcp/src/main.rs:26-30- stdio entry point logs malformed lines and moves on without sending a JSON-RPC parse-error response.
handle_messagehas parse-error handling that never runs. - Fix scope: small. Bucket: QUICK WIN.
[MAJOR] list_transcripts accepts invalid params as defaults
crates/mcp/src/lib.rs:188-195serde_json::from_value(args).unwrap_or_default()converts malformed args into defaults. Every other handler in the file returns-32602instead. Inconsistent behaviour.- Fix scope: small. Bucket: QUICK WIN.
[MAJOR] CSP guard matches connect-src by prefix
src-tauri/build.rs:47-64strip_prefix("connect-src")would also matchconnect-src-elem(if ever added to CSP3). Defensive: exact directive name match.- Fix scope: small. Bucket: QUICK WIN.
- Attribution: Introduced in
6fd3893yesterday.
MINOR (15)
Grouped here for brevity — full details in agent outputs. Bucket: BOY SCOUT (fix when adjacent code touched).
commands/live.rs:341-347—pick_engineduplicates dispatch logic fromcommands/models.rsandcommands/transcription.rscommands/live.rs:123-145— stale#[allow(dead_code)]onLiveStatusMessage(all variants are constructed)crates/audio/src/capture.rs:355-499—open_and_validate()is 145 lines; only one unit test in the filecrates/audio/src/lib.rs:14+vad.rs:14-34—SpeechDetectorre-exported but no in-repo uses (stub awaiting Silero)crates/audio/src/resample.rs:25-39+streaming_resample.rs:63-80— rubato tuning duplicated between offline and streamingcrates/transcription/src/local_engine.rs:83-157—load/unload/capabilities/transcribe_synchave no direct testscrates/transcription/src/whisper_rs_backend.rs:54-107— multi-responsibility function, behaviour-testing limited toDisplaycrates/ai-formatting/src/pipeline.rs:38-100—post_process_segmentsdoes filtering + formatting + LLM invocation + failure handling in one functioncrates/storage/src/database.rs(×4 sites) — repeatedSELECTcolumn lists invite schema driftcrates/storage/src/database.rs(×3 sites) —list_transcripts_paged,count_transcripts,update_transcript,uncomplete_task,log_error,list_recent_errorsall untestedcrates/storage/src/database.rs:774-775— TODO flagging that Tauri command failures aren't wired intoerror_logcrates/core/src/providers.rs:35-40— deadProviderRegistrysuppressed with#[allow(dead_code)]crates/core/src/types.rs:169-184— deadTranscriptMetadatasuppressed with#[allow(dead_code)]crates/hotkey/src/lib.rs:44-77— parser silently discards extra triggers (Ctrl+A+Bparses asB); no malformed-combo testscrates/hotkey/src/linux.rs:46-142—EvdevHotkeyListener::startis ~100 lines mixing channel setup + device scanning + watcher + retry + task orchestrationcrates/mcp/src/lib.rs:168-303—list_transcripts,get_transcript,search_transcripts,list_taskshandlers untested
NIT (3)
crates/ai-formatting/src/llm_client.rs:26-27,:59-60—#[allow(dead_code)]on actively-usedCLEANUP_PROMPTandformat_dictionary_suffixcrates/storage/src/file_storage.rs:12-14— open TODO for consolidating OS-path helperssrc-tauri/src/commands/live.rs:123-145— covered above (re-flagged by Agent C as NIT)
Triage buckets
Quick wins (this session or next)
One concern per commit. TDD where testable — failing regression test, then fix.
- C2 flush() drops chunks → change return type to
Vec<VadChunk> - paste_text_replacing clipboard snapshot
- resolve_recording_path collision → append millis + session_id
- read_wav propagate sample errors
- model_manager check HTTP status on non-resume path
- uncomplete_task reopen auto-completed parents
- CSP guard exact-name directive match (Rule: my own commit, Boy Scout)
- MCP parse-error reply on malformed JSON-RPC
- list_transcripts return -32602 on invalid params
- Dead-code cleanups:
ProviderRegistry,TranscriptMetadata,CLEANUP_PROMPT/format_dictionary_suffixallows,LiveStatusMessageallow
That's 10 items, ~1 commit each. Maybe 2–3 hours.
Release-blockers (before v0.1 ship)
Tracked items that must land before first public release:
- C1 racy single-session guard — needs
run_live_sessionrefactor first - C3 migrations atomicity — BEGIN/COMMIT wrap + version in same tx
- C4 transcript-profile FK + delete_profile guard (v9 migration)
run_live_sessionmonolith refactor (unblocks C1)poll_inferenceIPC channel loss resilience- Native capture worker join handle
get_runtime_capabilitiesaccelerator correctnessPowerAssertionmacOS objc2 bridge (known deferred)- Decoder error propagation (
audio/src/decode.rs) - LLM prompt preflight against context window
- Keystore thread-safety
- Hotkey linux device filtering KEY_A/KEY_R bug
Boy Scout backlog
All MINORs + NITs. Pick up opportunistically when adjacent code is touched.
Deferred (quality improvements, not release-blocking)
- SQL SELECT list refactoring (needs macro or typed query builder)
- Test coverage improvements across
local_engine,whisper_rs_backend,pipeline, storage APIs, MCP handlers - Resampler tuning consolidation
- File-storage path helpers consolidation
Notes
- No
TODO/FIXME/HACK/XXXmarkers in the transcription + audio crates (Agent A confirmed). - Clean files:
transcription/src/lib.rs,transcriber.rs,concurrency.rs,streaming/buffer_trim.rs,streaming/commit_policy.rs,streaming/mod.rs,audio/src/concurrency.rs,ai-formatting/src/{correction_learning,lib,rule_based,to_plain_text}.rs,llm/src/{grammars,prompts}.rs,storage/src/lib.rs. - Most-touched files in the sprint (
streaming/*,wav.rs,commit_policy,buffer_trim) came back clean from A and B — the sprint code itself is in reasonable shape; the bugs cluster inlive.rsand older storage surfaces.