31e3f5a09950449433f9a82ca76f9f0f604ebe82
456 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 31e3f5a099 |
agent: lumotia — Phase B.3 unlink .part on ResumeUnsupported so retry can recover
Phase B.3 audit of commit
|
|||
| 643985d2a8 |
agent: lumotia — Phase B.2 fix misleading "force-abort" doc + test name on supervisor shutdown
Phase B.2 audit of commit
|
|||
| e993786700 |
agent: lumotia — Phase B dogfood plan + B.1 audit trail
Captures the 15-item Phase B plan (code-atomiser-fix wave verification) at
docs/superpowers/plans/2026-05-14-phase-b-dogfood-plan.md so the per-item
methodology, status, and findings survive across sessions.
B.1 already done (commit
|
|||
| 6c212a0d2c |
agent: lumotia — Phase B.1 fix misleading comment on start_live lifecycle ordering
Phase B.1 survey finding (commit
|
|||
| ff8dda06d0 |
agent: lumotia — Phase A.7 fix startup-order race that silently orphaned legacy data
Critical bug surfaced by the dogfood drill: every upgrading Magnotia user
would silently keep a fresh empty Lumotia install while their Magnotia
data sat orphaned next to it. Drill caught it on the first real run
under sandboxed HOME.
ROOT CAUSE
src-tauri/src/lib.rs::run() previously called the migrations from inside
the Tauri setup hook (post `tauri::Builder::default()`). But three
sequential actions BEFORE the setup hook had already created the
destination directories:
1. init_tracing() -> logs_dir() -> create_dir_all(app_data_dir/logs)
creates the lumotia/ root.
2. install_panic_hook() -> crashes_dir() -> create_dir_all() ditto.
3. Tauri's WebKitGTK runtime / plugin chain creates the bundle-id-keyed
consulting.corbel.lumotia/ dir eagerly when the WebContext spins up
(mediakeys, storage, WebKitCache subdirs appeared even without our
hook explicitly creating them).
By the time the setup-hook migrations fired, every legacy candidate
returned `TargetAlreadyExists` (paths.rs) or `BothExistLegacyPreserved`
(tauri_app_data_migration.rs) — both silent no-op codepaths. Legacy
data was left untouched, fresh Lumotia install gained no transcripts,
settings, or window state.
FIX
Migrate BEFORE any other code touches app_data_dir().
src-tauri/src/tauri_app_data_migration.rs:
- NEW_BUNDLE_ID const ("consulting.corbel.lumotia"). MUST agree with
tauri.conf.json#identifier; reviewer-enforced invariant.
- Renamed private `legacy_tauri_app_data_dir_for` -> public
`tauri_app_data_dir_for(identifier)`. Function is parameterised by
bundle id; the "legacy" name was misleading after this change.
- New `current_tauri_app_data_dir()` resolves the NEW bundle path
from platform env vars (same convention Tauri 2 uses), so the
pre-runtime migration can address its destination without
needing an AppHandle.
src-tauri/src/lib.rs:
- New `migrate_user_data_pre_runtime()` orchestrates the two
migrations + ambiguity guard. Uses `eprintln!` for surface events
(tracing not yet initialised at this stage; stderr lands in
journald / foreground terminal which is the right transport for
boot-phase output). FATAL errors call process::exit(1) — the
setup-hook version returned Err from the closure, equivalent
effect.
- run() now calls migrate_user_data_pre_runtime() as its first line,
BEFORE init_tracing(), install_panic_hook(), and the Tauri
builder.
- Setup-hook migration blocks deleted (~90 lines). Setup hook now
starts with a one-line comment pointing at the pre-runtime fn.
VERIFICATION
Re-ran the dogfood drill (scripts/dogfood-rebrand-drill.sh) — 8/8 probes
pass after the fix (was 4/8). Both stderr lines fire:
[lumotia-startup] migrated legacy magnotia data dir to lumotia:
.../magnotia -> .../lumotia (renamed_db=true, elapsed_ms=0)
[lumotia-startup] migrated Tauri app_data_dir from legacy bundle
identifier: .../uk.co.corbel.magnotia ->
.../consulting.corbel.lumotia (elapsed_ms=0)
On-disk post-state confirms: magnotia/ gone, lumotia/ has migrated db
+ recordings, uk.co.corbel.magnotia/ preserved as backup,
consulting.corbel.lumotia/localStorage/leveldb/ has migrated data.
- cargo fmt --check: clean
- cargo clippy --workspace --all-targets -- -D warnings: clean
- cargo test --workspace: 409/0 (no regression)
|
|||
| 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.
|
|||
| 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 |
|||
| 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.
|
|||
| 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)
|
|||
| 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) |
|||
| 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. |
|||
| 65abfa2ed9 |
agent: code-atomiser-fix — span propagation across live + model-load spawns (Obs-3)
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>
|
|||
| d1391b34ac |
agent: code-atomiser-fix — drop lumotia_live custom target in live.rs (Obs-1, Obs-2)
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> |
|||
| 12b413d645 |
agent: code-atomiser-fix — broaden clipboard/paste guards to documented secondary windows
The Trust-3/Trust-6 fix in commit
|
|||
| 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> |
|||
| 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>
|
|||
| 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>
|
|||
| 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
|
|||
| 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> |
|||
| b3da58cd6b |
agent: code-atomiser-fix — write_text_file_cmd path scope (Trust-1, redo)
The earlier Trust-1 commits |
|||
| a48653c93c |
agent: code-atomiser-fix — write_text_file_cmd path scope (Trust-1, corrective)
Corrective re-apply of Trust-1. The original commit |
|||
| 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
|
|||
| 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>
|
|||
| 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> |
|||
| 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> |
|||
| 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>
|
|||
| 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
|
|||
| 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>
|
|||
| 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> |
|||
| 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>
|
|||
| 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 |
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 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> |
|||
| 3f4e5cc9a4 |
agent: code-atomiser-fix — migrate Tauri app_data_dir on bundle-identifier change
Commit |
|||
| 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> |
|||
| ab5f6ab995 |
agent: code-atomiser-fix — repair sed-scar provenance from 26c7307
The Magnotia->Lumotia rebrand commit
|
|||
| 2491c7a7dd |
agent: lumotia-rebrand — fix Codex cross-model review blockers
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
|
|||
| f093d18a5e |
agent: lumotia-rebrand — final residuals (HANDOVER + arch-map readme + ui-kit React fn)
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> |
|||
| 26c7307607 |
agent: lumotia-rebrand — docs, scripts, root config, residuals
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>
|
|||
| 681a9b26dc |
agent: lumotia-rebrand — frontend strings (svelte + i18n + design-system)
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>
|
|||
| 16081095e0 |
agent: lumotia-rebrand — localStorage keys + event channels migration
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>
|
|||
| 14313cfa84 |
agent: lumotia-rebrand — tauri productName, identifier, window title
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> |
|||
| 9336286e3c |
agent: lumotia-rebrand — fix QC blockers for phase 5
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> |
|||
| 86f83b7a45 |
agent: lumotia-rebrand — data dir migration shim + paths.rs rename
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>
|
|||
| e2a5feb718 |
agent: lumotia-rebrand — tracing filter targets
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> |
|||
| 42f4d07e48 |
agent: lumotia-rebrand — drop MagnotiaError prefix, now lumotia_core::Error
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> |