Commit Graph

451 Commits

Author SHA1 Message Date
2aac366f32 agent: lumotia — Phase A.6 dogfood drill for rebrand migration on real OS paths
scripts/dogfood-rebrand-drill.sh — end-to-end probe that launches the real
lumotia binary against synthetic legacy magnotia state on disk, then
verifies both migration paths produced the expected outcome:

  1. paths.rs: ~/.local/share/magnotia/ -> ~/.local/share/lumotia/, including
     magnotia.db -> lumotia.db rename + non-DB companion files carried along
     by the directory rename.
  2. tauri_app_data_migration.rs: ~/.local/share/uk.co.corbel.magnotia/
     copied via atomic staging to ~/.local/share/consulting.corbel.lumotia/,
     with legacy preserved as a backup and staging dir cleaned up.

Closes the last gap in Phase A: every other test (paths::tests + storage
integration test + localStorageMigration.test.ts) uses synthetic in-process
state. The drill is the only verification that the real binary's startup
hook calls migrate_legacy_data_dir + migrate_tauri_app_data_dir_with_paths
against real OS path resolution.

Two modes:
  (default)            Sandbox: HOME=<tempdir>, faithful on Linux. NOT
                       faithful on macOS — Tauri 2 uses
                       NSSearchPathForDirectoriesInDomains which ignores
                       HOME overrides. Drill refuses to start in sandbox
                       mode on macOS rather than silently writing to the
                       user's real Application Support tree.
  --against-real-home  Real $HOME. Refuses to start if any lumotia data
                       already exists at the real paths (no clobbering
                       real user data). Cleans up planted state on exit
                       unless --keep is passed.

Eight probes covering: data-dir rename outcome, db file rename, legacy
removal, companion file survival, Tauri app_data_dir copy, legacy-backup
preservation, staging-dir cleanup, and lumotia_startup log line presence.

README: documents the drill alongside cargo test + npm test in the
Testing section, with the macOS caveat clearly flagged.

Not run as part of this commit — the drill launches a Tauri WebView
window for a few seconds. Jake to invoke when ready to dogfood.
2026-05-14 07:40:33 +01:00
206ac6219d agent: lumotia — Phase A.5 vitest scaffold + localStorageMigration unit tests
First frontend unit test framework on Lumotia. Pinned exact versions for
supply-chain hygiene (matches the rust-toolchain.toml discipline from the
27661c8 hygiene pass and the npm audit signatures pre-flight from e4d56b8):

  - vitest 4.1.6 (compatible with vite 6, supports vite 6/7/8)
  - jsdom 29.1.1

Installed with `npm install --save-dev --save-exact --ignore-scripts` per
the install discipline documented in the README — the --ignore-scripts
flag blocks the postinstall vector that npm worms (Shai-Hulud,
mini-Shai-Hulud) rely on.

vite.config.js:
  - Switched defineConfig import to vitest/config (superset of vite/config;
    production builds ignore the `test` key).
  - test.environment = "jsdom" so storage-shim tests drive real browser APIs.
  - test.include scoped to src/**/*.{test,spec}.{ts,js} — colocated with
    source, mirrors the Rust #[cfg(test)] sibling pattern.
  - test.exclude blocks src-tauri/ (owned by cargo test).
  - restoreMocks + clearMocks + unstubAllGlobals on so module-level state
    can't leak between tests.

src/lib/utils/localStorageMigration.test.ts — 12 tests:
  migrateLocalStorageKey:
    - copies value + removes old when only old exists
    - removes old + keeps new when both exist (lumotia is authoritative)
    - no-op when only new exists
    - no-op when neither exists
    - idempotent (second call after first migrates nothing)
    - preserves the value's exact bytes (no JSON round-trip)
    - preserves empty-string values (distinct from null)
    - survives DOMException / quota errors without re-raising
    - no-op when localStorage is undefined (SSR-safe)
  migrateLocalStorageKeys:
    - processes pairs in order
    - per-pair failure does not strand remaining pairs (resilience)
    - empty pairs list is a clean no-op

package.json:
  - "test": "vitest run" (one-shot, CI-friendly)
  - "test:watch": "vitest" (dev loop)

README: documents `npm run test` alongside `cargo test --workspace` and
`npm run check` in the Testing section.

Verification:
- npm run test: 12/12 pass
- npm run check: 0 errors, 0 warnings (the new .ts test type-checks clean
  against jsconfig.json's strict typescript settings)
2026-05-14 07:29:57 +01:00
18a64f5c56 agent: lumotia — Phase A.3 remove dead migration_sentinel method + fix architecture-map claim
Phase A.3 finding: AppPaths::migration_sentinel was added with intent
during the rebrand-architecture phase but never wired to any caller.
Exhaustive grep across crates/ src-tauri/ src/ docs/ surfaces:

  - 1 definition (paths.rs)
  - 1 architecture-map description that ASSERTS the method is in use
  - 0 production callers
  - 0 test references

Both boot-time migrations (migrate_legacy_data_dir +
migrate_tauri_app_data_dir_with_paths) are idempotent by construction:
each re-probes the legacy path via Path::exists() on every boot and
short-circuits on the steady state. A sentinel file would optimise the
probe but is not required for correctness; one syscall per legacy
candidate at startup is negligible.

Per the atomiser principle of removing dead surface area rather than
keeping stale promises:
  - Delete AppPaths::migration_sentinel entirely
  - Update docs/architecture-map/.../core-paths.md to describe the actual
    idempotency model (re-probing) rather than the sentinel pattern that
    was never implemented
  - Steer future migrations toward storage/src/migrations.rs schema_version
    (transactional, survives backup/restore) rather than reintroducing
    filesystem sentinels

Verification:
- cargo test -p lumotia-core paths::: 17/17 (no test relied on the method)
- cargo clippy -p lumotia-core --all-targets -- -D warnings: clean

Phase A.4 (stray-magnotia string scan): clean. Every magnotia reference
in the tree is legitimate — migration source paths, documentation, or
test fixtures. The rebrand cascade was thorough.
2026-05-14 07:23:02 +01:00
43d319fd5a agent: lumotia — Phase A.1+A.2 rebrand migration tests + copy_dir_recursive hardening
Phase A of dogfood verification for the Magnotia -> Lumotia rebrand
cascade. The existing in-crate unit tests prove the migration copies
bytes correctly; this commit closes the gaps an atomiser-grade review
would flag.

Phase A.1 — end-to-end integration test (crates/storage/tests/legacy_db_migration.rs):

  Seeds a real on-disk magnotia.db via lumotia_storage::init (which runs
  every schema migration head-to-tail), inserts a transcript via the
  public API, drops the pool, runs migrate_legacy_data_dir_with_pairs,
  then re-opens the migrated lumotia.db and asserts the transcript is
  queryable. Three scenarios covered:
    1. Legacy-only -> migrate -> reopen -> row survives. Also verifies a
       non-DB companion file is carried along by the directory rename.
    2. Idempotency: first boot migrates, user writes new data, second
       boot is a no-op and BOTH rows survive.
    3. Both-paths-present: refuses to merge, target's empty DB is
       preserved, legacy retained on disk as a backup.

  Wires the test surface by renaming the previously-private
  migrate_legacy_data_dir_inner to pub migrate_legacy_data_dir_with_pairs
  (mirroring migrate_tauri_app_data_dir_with_paths in the sibling
  tauri_app_data_migration module).

Phase A.2a — copy_dir_recursive hardening (crates/core/src/paths.rs):

  Pre-existing footgun: the fall-through branch called std::fs::copy()
  on any DirEntry that was not a symlink or a directory. On Unix that
  includes FIFOs, sockets, and char/block device nodes. Opening a FIFO
  for read with no writer attached blocks forever — a stale debug FIFO
  in the user's ~/.magnotia tree would silently hang first launch.

  The branch now explicitly distinguishes is_file() (real regular file
  -> copy) from anything else (-> Err with ErrorKind::Unsupported,
  naming the offending path). Migration becomes re-runnable once the
  user cleans up the offending node. Same-filesystem rename via
  std::fs::rename is atomic and unaffected; only the EXDEV fallback path
  touches the new guard.

Phase A.2b — three adversarial probes (crates/core/src/paths.rs tests):

  - FIFO inside the legacy tree: copy_dir_recursive must return an
    Unsupported error WITHOUT hanging. Test bounded by a 5s wall clock
    + a worker thread so a regression to the old fall-through would
    surface as a panic, not a stalled CI job.
  - Unreadable file (mode 0000): copy_dir_recursive must surface
    PermissionDenied, not silently skip. Skips its core assertion under
    euid 0 (root bypasses DAC permissions, would mask the regression).
  - Dangling symlink (target nonexistent): symlink is recreated at
    destination with link target preserved verbatim; the migration
    does NOT try to dereference and does NOT abort the rest of the copy.

Verification:
- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test --workspace: 409 passed, 0 failed (up from 405 pre-commit;
  3 storage integration tests + 3 paths adversarial + 1 net carry-over)
2026-05-14 07:20:18 +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
e4d56b831f agent: lumotia — supply-chain pre-flight (npm audit signatures + install discipline)
Adds defence-in-depth against npm-worm attacks (Shai-Hulud / mini-Shai-Hulud).

- run.sh: gates dev launch on `npm audit signatures` whenever package-lock.json
  is newer than .lumotia-last-audit. Fails loud on signature mismatch. Skip
  with LUMOTIA_SKIP_AUDIT=1 for offline dev.
- README: documents `npm ci --ignore-scripts` as the install discipline
  (blocks the postinstall vector worms exploit) and explains the audit hook.
- .gitignore: excludes the per-clone audit stamp.

Lumotia's current tree (192 packages) cross-references clean against the
mini-Shai-Hulud affected-package list — this is preventive, not remedial.
2026-05-14 06:41:53 +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
87e6248774 agent: code-atomiser-fix — trash view + restore button in HistoryPage (Rev-2 UI)
Adds a Live | Trash toggle to the HistoryPage header. The Trash tab
lists soft-deleted transcripts via the new list_trashed_transcripts
Tauri command and offers per-row Restore via restore_transcript.

Permanently-delete-from-trash is intentionally deferred — the 30-day
startup purge (TRANSCRIPT_TRASH_RETENTION_DAYS in src-tauri/src/lib.rs)
handles hard-removal until that UI lands. The retention policy is
surfaced in the panel header: Trashed items are kept for 30 days,
then permanently deleted on next app start.

Design notes:
  - View toggle is a tablist of two pill buttons (Live | Trash); the
    aria-selected attribute reflects the active mode.
  - Switching to Trash triggers an effect that loads the list once.
    Switching back to Live discards the trash data so stale rows
    don't reappear on toggle.
  - Live-mode header controls (Starred, Tag all untagged, Clear All)
    are hidden in Trash mode and replaced with a Refresh button.
  - Restore drops the row from the in-memory trash list rather than
    splicing into history; the live store reloads from SQLite on its
    own initialisation path, so we avoid drifting the in-memory shape
    from the canonical source.
  - The created timestamp is shown rather than the deletion
    timestamp; deleted_at is not currently in the TranscriptDto and
    expanding the DTO is out of scope for this fix.
  - Audio files may already have been removed by delete_transcript's
    best-effort filesystem cleanup, so restored text + metadata may
    surface without playable audio (documented in the storage-layer
    restore_transcript contract).

TODO(test): no Svelte component test framework wired in the repo
(vitest not installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:04:39 +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
50d0715488 agent: code-atomiser-fix — type-the-word DELETE modal for clearAll (Rev-2 UI)
The prior clearAll UX was a 4-second inline arm-confirm: one click on
Clear All morphed into Confirm/Cancel pills; a second click within
the window wiped every transcript. With the soft-delete backend
(commit 15b74db) now under it, the data-loss class is partially
closed — items land in Trash, not /dev/null — but an absent-minded
double-tap still soft-deletes the entire history at once.

Fix: replace the inline arm-confirm with a modal that requires the
user to type the word DELETE (case-sensitive, exact) before the
Confirm button activates. The single-transcript and bulk-selection
delete flows keep their lighter arm-confirm pattern; only the
all-at-once nuke is gated by the modal.

Modal details:
  - autofocuses the input on open
  - Enter submits when the word matches
  - Escape closes; click-outside closes (when not in flight)
  - Confirm button disabled until input == DELETE
  - Clearing... spinner state while the SQLite soft-delete loop runs
  - retention policy (kept for 30 days) surfaced in the body copy
  - dialog role, aria-labelledby, aria-describedby, labelled input,
    tabindex=-1 on the card; backdrop carries role=presentation to
    keep the click-outside-to-close affordance out of the AT tree

clearAllArmed / armClearAll / disarmClearAll and their announcement
fragment are removed; bulkDeleteArmed (selection-subset) keeps the
arm-confirm pattern unchanged.

TODO(test): no Svelte component test framework wired in the repo
(vitest not installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:01:10 +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
a2b47db193 agent: code-atomiser-fix — restrict write_text_file_cmd to app data + download dirs (Trust-1)
The Tauri command `write_text_file_cmd` 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. The
in-file comment "the dialog already constrains the user's choice"
described an intended invariant the IPC surface never enforced.

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 by 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:53:17 +01:00
cde985d0c1 agent: code-atomiser-fix — narrow LlmEngine critical section + drop old model first (Race-3, Lifecycle-1)
Race-3 (conf 86): `LlmEngine::load_model` previously held the inner
`std::sync::Mutex` for the entire `LlamaBackend::init` +
`LlamaModel::load_from_file` call (5-15 s on a cold load). `is_loaded()`,
`loaded_model()`, and `loaded_model_id()` all take that same mutex and
are called from sync Tauri handlers (`get_llm_status`, `check_llm_model`,
`delete_llm_model`, `test_llm_model`) WITHOUT `spawn_blocking`. During a
first-run load, parallel `refreshLlmStatus()` polls from the frontend
parked tokio worker threads on the std-mutex; a handful of concurrent
status polls was enough to deadlock the default `num_cpus`-sized Tauri
runtime.

Lifecycle-1 (conf 80): same function held the OLD `Arc<LlamaModel>` in
`guard.model` during the new `load_from_file` call, so a model swap
peaked at ~2x VRAM. A 27B Q4 (~17 GB) swap OOMed a 24 GB card even
though either model fit alone.

Fix: redesign `load_model` so the slow llama-cpp work happens OUTSIDE
the mutex.

  1. Short crit section: compare against currently-loaded triple —
     return Ok on match (no-op fast path).
  2. CAS a new `loading: AtomicBool` from false → true. If a load is
     already in flight, refuse with the new `EngineError::AlreadyLoading`
     rather than starting a parallel one. A `LoadingGuard` RAII drop
     clears the flag on every exit path including panic.
  3. Short crit section: drop the OLD model Arc (releasing VRAM via
     `llama_free_model`) BEFORE the new load begins — fixes Lifecycle-1.
  4. Heavy `LlamaBackend::init` + `LlamaModel::load_from_file` run
     OUTSIDE the mutex.
  5. Short crit section: install the new state.

`is_loaded()`, `loaded_model()`, `loaded_model_id()` now only contend
on the brief state-mutation sections, not the multi-second file load.
A new `is_loading()` accessor exposes the in-flight state for callers
that need to distinguish "loading" from "not loaded".

Backend lifecycle: `LlamaBackend::init` is process-singleton —
llama-cpp-2 enforces this via `LLAMA_BACKEND_INITIALIZED: AtomicBool`
and returns `BackendAlreadyInitialized` on a second call. The Drop impl
DOES call `llama_backend_free` and flips the flag back, so re-init
would technically work, but we keep the backend Arc resident across
loads/unloads to avoid init/free churn. The Lifecycle-1 fix drops only
the model Arc, NOT the backend Arc — dropping the backend mid-swap
would still work (the new load would re-init), but the comment trail
documents the singleton contract for the next reader.

`is_loaded()` now reports false briefly during a swap window (model
dropped, new model not yet installed). Documented in the doc comment.
Callers wanting "model X is loaded" must check `loaded_model_id()`
against their target, not just `is_loaded()`.

Regression tests added in `crates/llm/src/lib.rs::tests`:

  - `is_loaded_does_not_block_on_slow_load`: holds the lock-discipline
    open via a Barrier and asserts `is_loaded()` / `loaded_model_id()`
    return in ≤50 ms while the slow section is mid-flight. Verified
    that a pre-fix structure (`std::sync::Mutex` held across a 500 ms
    sleep) makes the probe block ~450 ms; the new structure makes it
    return in microseconds.
  - `second_concurrent_load_is_refused`: two concurrent
    `__test_run_with_lock_discipline` calls; the second gets
    `EngineError::AlreadyLoading` and never reaches its op closure.
    Doubles as the TOCTOU guard for Race-4/5 at the engine layer.

A `pub(crate) #[cfg(test)] __test_run_with_lock_discipline` helper
exposes the locking skeleton (loading flag + drop-old-model + slow op
outside lock + install) without requiring a real GGUF on disk; this is
the harness the regression tests use.

Verification: cargo test --workspace + npm run check both green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:51:29 +01:00
e0e9a6e17a agent: code-atomiser-fix — require Transcriber::transcribe_sync_with_abort (Lifecycle-2)
The trait's default implementation of transcribe_sync_with_abort fell
through to plain transcribe_sync, silently dropping the abort flag for
any backend that did not explicitly override it. That re-introduced the
5725836 wedge for SpeechModelAdapter (Parakeet today, any future
cloud STT or transformer adapter tomorrow): the live session's
drain_inference timeout would set the flag, the backend would ignore
it, the orphan inference thread would keep the engine Mutex held, and
the next start/stop would deadlock on it.

Removing the default impl makes the method required, so any backend
that does not implement it fails to compile. Compile-time enforcement
of cancellation completeness.

SpeechModelAdapter now implements the method explicitly with a
pre-decode short-circuit: if the abort flag is already set before we
dispatch into transcribe-rs (which owns the Parakeet decoder behind an
opaque call that has no cancellation hook), return an error
immediately. The in-flight decode itself is still uncancellable, but
that is documented with a SAFETY comment and bounded by the live
session's drain timeout dropping the receiver — the orphan exits the
instant it tries to send.

Regression tests:
- transcriber::tests::transcribe_sync_with_abort_is_required_and_flag_is_observed —
  fake backend records the abort flag at dispatch time; proves the
  trait method is required (file would not compile without it) and the
  flag is actually piped through.
- local_engine::tests::speech_model_adapter_short_circuits_when_abort_set_pre_dispatch —
  SpeechModelAdapter must NOT call the underlying decoder when the
  abort flag was set before dispatch.
- local_engine::tests::speech_model_adapter_dispatches_decoder_when_abort_clear —
  companion: clear flag must still dispatch (we did not kill
  transcription entirely).

cargo test -p lumotia-transcription --lib: 64 passed, 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:50:58 +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
9f67ab2d86 agent: code-atomiser-fix — atomic model download + manifest (Rev-1, Rev-5)
Two reversibility kills in the model-download path both followed the
same pattern: SHA mismatch on an existing file triggered
`remove_file(&dest)` BEFORE the network round-trip. A network blip /
power loss between the unlink and the eventual `rename(.part, dest)`
left users with neither the old (corrupt-but-readable) model nor a
fresh one — 1.5-20 GB redownload from scratch with no fallback.

Rev-1 (crates/llm/src/model_manager.rs):
  - Extract the existing-file decision into `download_to`, drop the
    pre-emptive unlink. `download_impl` already writes via `.part`
    and atomically renames; the rename overwrites on success and
    leaves dest untouched on failure.
  - Regression test `download_failure_preserves_existing_file` plants
    a sentinel "OLD" file at dest, points at a 500-returning server,
    and asserts dest still exists with original contents after the
    failed download.

Rev-5 (crates/transcription/src/model_manager.rs):
  - Drop the pre-emptive unlink in the outer `download()` SHA-mismatch
    branch. Same atomic rename via `download_file`.
  - Make `write_verified_manifest` atomic: write to `.tmp`, fsync,
    rename. Previous direct `fs::write` truncates-then-writes, so a
    crash mid-write left an empty/torn manifest and triggered a full
    GB-sized redownload on next boot.
  - `download_file_failure_preserves_existing_dest_file` and
    `manifest_write_is_atomic` regression tests added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:21:01 +01:00
8becb1aec4 agent: code-atomiser-fix — restore lumotia.log writer + EnvFilter coverage (Obs-4, Obs-5)
AUDIT NOTE: the diff for this fix was absorbed into commit afbd33d (Race-8)
by a concurrent agent's `git add`. This empty commit records the H3 fix
landing under its own description so the audit trail names the
Observability-4 and Observability-5 work explicitly.

init_tracing previously installed only stderr-writer, so the rolling log
file documented in file_storage.rs and bundled into diagnostic reports
by diagnostics.rs was never actually written. Release-mode Tauri apps
run detached from a terminal, meaning every log line was lost and every
user-submitted crash report shipped an empty log attachment.

Fix: add tracing_appender daily rolling file layer with 7-day retention.
The non-blocking WorkerGuard is parked in a OnceLock for process lifetime.
File-side filter is more verbose than stderr-side. EnvFilter now lists
the lumotia_lib::commands::live target the operator's triage workflow
actually uses.

Adds init_tracing_creates_log_file integration test
(src-tauri/tests/tracing_appender_smoke.rs).

Files touched (in afbd33d):
- src-tauri/Cargo.toml (added tracing-appender 0.2.5)
- src-tauri/src/lib.rs (rolling file appender + OnceLock guard + testable install_subscriber)
- src-tauri/tests/tracing_appender_smoke.rs (new smoke test)
- crates/storage/src/file_storage.rs (doc comment clarification only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:13:08 +01:00
24c9bf8f0a agent: code-atomiser-fix — regenerate package-lock with lumotia name (Prov-1)
Lockfile drift from the Magnotia → Lumotia rebrand: root and packages[""]
name fields still said "magnotia" while package.json correctly said
"lumotia". Regenerated via npm install --package-lock-only --no-audit
--no-fund. Diff scope: only the two name fields changed; no version
pins, resolved URLs, or integrity hashes shifted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:13:03 +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
5ba761a4b8 agent: code-atomiser-fix — focusTimer rehydrate startTick invariant (Race-10)
Lock the invariant: the already-expired branch of rehydrate() must call
startTick() so the tick loop observes now >= completionFlashUntil and
auto-clears the flash. The call was already present; this commit adds
the inline comment so future edits cannot silently drop it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:12:10 +01:00
6aa6a434fb agent: code-atomiser-fix — FirstRunPage unlisten on all exits (Race-9)
Move both Tauri listen() calls inside the try block with let-declared
unlisten handles. A throw on the second listen() previously leaked the
first subscription and the finally block could itself throw on an
undefined unlistenParakeet, masking the original error. Finally now
guards each handle before invoking it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:12:06 +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
2491c7a7dd agent: lumotia-rebrand — fix Codex cross-model review blockers
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
Codex independent review found 11 blockers post-cascade. All addressed.

CRITICAL (data-loss / crash):

10. crates/core/src/paths.rs — migrate_legacy_data_dir_inner used
    fs::rename which fails with EXDEV when source + target are on
    different filesystems (encrypted-home, bind mounts, separate
    $XDG_DATA_HOME partition). Combined with the Phase 5 QC fix that
    made migration errors fatal, this would crash on first launch
    for any user whose data dir spans filesystems. Added
    rename_or_copy_tree() that falls back to copy_dir_recursive +
    remove_dir_all on CrossesDevices / errno 18 (EXDEV). Symlinks
    preserved verbatim. Same fallback applied to magnotia.db ->
    lumotia.db inside the dir.

11. Added 4 unit tests: copy_dir_recursive preserves nested
    structure, rename_or_copy_tree same-filesystem happy path,
    is_cross_device classifies CrossesDevices kind + raw errno 18.

Doc residuals (blockers 1-9):

1. crates/cloud-providers/Cargo.toml — "Wyrdnote pending rebrand"
   description.
2. crates/cloud-providers/src/provider.rs — module docs + test
   fixture Wyrdnote refs.
3. crates/transcription/src/orchestrator.rs — module docs + test
   fixture Wyrdnote refs.
4. docs/roadmap/2026-05-10-pkm-phase-tooling-shortlist.md — phase
   name + outputs/wyrdnote path refs.
5. docs/architecture-map/04-llm-formatting-mcp/llm-tests.md —
   MAGNOTIA_LLM_TEST_MODEL env var.
6. .../cloud-providers-stubs.md — MAGNOTIA_API_KEY_*.
7. docs/architecture-map/03-audio-transcription/tests-and-fixtures.md
   — MAGNOTIA_WHISPER_*.
8. docs/gpu-tuning/plan.md — MAGNOTIA_BENCH_RUN.
9. docs/superpowers/specs/2026-05-09-battery-gpu-aware-thread-tuning-
   design.md + corresponding plan — MAGNOTIA_POWER_STATE_OVERRIDE,
   MAGNOTIA_INFERENCE_THREADS.

cargo test --workspace: 343 pass / 0 fail (up from 339; +4 EXDEV
fallback tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lumotia-rebrand-cascade-complete
2026-05-13 13:39:21 +01:00
f093d18a5e agent: lumotia-rebrand — final residuals (HANDOVER + arch-map readme + ui-kit React fn)
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 15.1 final-grep residuals:
- HANDOVER.md "Rebrand note" + Phase 10b row updated to reflect that
  the cascade completed 2026/05/13 (15 phases, both repos, QC-gated).
- HANDOVER.md two surviving sed artefacts: "Lumotia -> Lumotia" line
  restored to "Magnotia -> Lumotia" historical context;
  MAGNOTIA_LLM_TEST_MODEL test gate -> LUMOTIA_LLM_TEST_MODEL.
- src/design-system/ui_kits/index.html: MagnotiaApp React function ->
  LumotiaApp (sed boundary missed the no-separator boundary).
- docs/architecture-map/README.md: MAGNOTIA_LLM_TEST_MODEL doc note.

Preserved (audit trail):
- docs/handovers/ — historical handover docs.
- docs/superpowers/plans/2026-05-12-engine-slop-residuals.md and
  -area-a-storage-errors-survey.md — describe the slop-pass work
  using the names current at the time.
- build/index.html, package-lock.json — regenerate on next build/install.

cargo test --workspace: 339 pass / 0 fail. npm run check: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:19:34 +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
42f4d07e48 agent: lumotia-rebrand — drop MagnotiaError prefix, now lumotia_core::Error
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 3 of the rebrand cascade per locked decision D4. MagnotiaError ->
Error in crates/core/src/error.rs (the crate name already qualifies it).
92 usages across 14 .rs files renamed via word-boundary sed.

One collision required disambiguation: lumotia_storage already had its
own local Error type (introduced by the slop-pass Area A residuals work).
crates/storage/src/error.rs aliases the imported core error as CoreError
on import; the From<Error> for CoreError boundary impl and the
CoreError::Storage construction site use the alias.

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:58:05 +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
bc2db91520 agent: lumotia-rebrand — fix QC blockers for phase 0
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 0 QC found two real blockers in the baseline commits:

1. crates/llm/src/lib.rs:215 — early-break guard was inverted. The
   guard fired when grammar.is_some() (where grammar already enforces
   shape, making the check redundant) and was disabled for
   grammar.is_none() (the content-tags path that actually needs it).
   Flipped to grammar.is_none() so the JSON-envelope short-circuit
   fires for free-form JSON generation as the commit message
   implied.

2. src/lib/pages/FilesPage.svelte — handleExport() success path had
   no toast, asymmetric with DictationPage which does. Added the
   toasts import and a toasts.success() call after the native save
   so both export surfaces give consistent feedback.

cargo test -p magnotia-llm --lib: 21 pass. npm run check: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:42:22 +01:00
2ca01e7c9d feat(export): prefer native save dialog over browser blob fallback
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
DictationPage + FilesPage handleExport() now use Tauri save() +
write_text_file_cmd when in the desktop runtime; browser blob path
remains as fallback when hasTauriRuntime() is false. saveMarkdown.ts
surfaces dialog errors via toasts. Adds txt to the extension map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pre-lumotia-rebrand
2026-05-13 08:26:19 +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