Commit Graph
2 Commits
Author SHA1 Message Date
d6f24dafd5 feat(hardening): six recurrence guards from the closed-issue-history audit (#1141)
* feat(hardening): six recurrence guards from the closed-issue-history audit

An agent audit swept every closed issue, clustered the error classes, and
checked each for fix + regression test + upgrade/reinstall survival. Six of
the "fixed but fragile" gaps are closed here; each guard has a regression
test in tests/test_recurrence_hardening.py (9 tests).

1. Evict-then-load (class 1, ~90 issues): a plain TTS load on a tight
   unified-memory box could still be OS-killed — the dub path frees memory
   before ASR loads (#1119) but nothing did before a TTS load.
   _make_room_before_tts_load() releases the idle capture-ASR model, clone
   prompts, and allocator caches when free RAM < the unified headroom.
   Deliberately NOT admission control: the #1111 decision (advisory-only,
   never refuse a load on an estimate) stands; this only does earlier what
   idle reclaim does later, and roomy machines skip it entirely.
2. Honest SIGKILL attribution (class 1): crashCauseHint() says "the OS ran
   out of memory (RAM)" for signal 9 instead of guessing VRAM on machines
   that have none. VRAM guidance kept for real GPU aborts (signal 6 etc.).
3. Clone-kind save sanitize (class 3, recurred 3x): the server-side instruct
   heal was gated to design-kind; a clone profile saved by any bypassing
   client could persist prose that 400s on every use. profiles.py now
   sanitizes both kinds at the single choke point.
4. Stale user_env validation (class 5): ~/.config/omnivoice/env is inherited
   verbatim by reinstalls; path-valued keys (OMNIVOICE_CACHE_DIR/DATA_DIR)
   that don't exist and can't be created are dropped for the run with a loud
   log line (file untouched — replugging the drive restores the setting).
   The two #480 precedence tests updated to use creatable paths (they test
   precedence, not path validity).
5. omni_ui schema guard (class 6): sanitizeOmniUi() whitelists + shape-checks
   every persisted field before restore — one malformed field used to throw
   mid-restore and silently discard everything after it, and every future
   field re-opened the #1067 class. Includes a lockstep test failing when
   useAppData reads a field missing from the schema.
6. safe_replace EXDEV helper (class 7): os.replace across devices raises
   EXDEV (the Windows D:-drive Errno 18/22 class); utils/fsops.safe_replace
   degrades to copy+fsync+replace. Adopted at the two cross-directory movers
   (log rotation, persona restore); temp-sibling writers stay on os.replace.
Plus: the generate timeout scales with text length (class 4's 503 wave —
   +1s per 40 chars past the first 1200, env floor respected), so long texts
   on slow hardware stop dying at exactly 300s with a "set an env var" remedy.

Deliberately NOT done, with reasons:
- ASR auto-promotion to the crash-isolated engine after a wedge: the code
  records an explicit owner rule against silent engine switching
  (asr_backend.py "we never switch engines automatically") — flagged to the
  owner instead of overridden.
- Rust items (webview cache-clear unit test, crash-marker versioning across
  updates): deferred to their own PR — the local cargo target was reclaimed
  for disk space, so they can't be verified locally right now.

Full suite: 2999 backend + 1243 frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): correct PR ref to #1141

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(hardening): review round — reclaim at the shared load boundary, write-probe path validation

Both Greptile P1s were real:

- "Startup preload skips reclaim": _make_room_before_tts_load() ran only in
  get_model(); preload_model() calls _load_model_with_timeout() directly, so
  a memory-tight machine was protected on demand loads but could still be
  OS-killed during the startup preload — the exact window the guard exists
  for. The reclaim now lives in _load_model_with_timeout(), the boundary both
  callers share.
- "Read-only paths pass validation": an existing directory on a read-only
  mount passes makedirs+isdir but fails on first real use, so the stale
  setting survived validation only to break downloads later. The check now
  probes actual write capability (create+delete a probe file). New test with
  a chmod-0o500 dir (skipped under root, where the probe cannot fail).
- CodeQL: the two intentional best-effort excepts in fsops.py now carry
  their explanatory comments.

Full suite: 3000 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 23:19:58 +05:30
a245d96684 fix(settings): make in-app models dir authoritative over launcher-injected env (#480) (#483)
Changing the model download location in Settings had no effect: after the
prompted restart, new downloads still went to the old folder and "Effective
location" stayed stuck on it.

Two stores hold the models dir. The in-app Settings panel writes the new path to
the durable per-user env file (`~/.config/omnivoice/env`, OMNIVOICE_CACHE_DIR),
but the desktop launcher injects the OLD value from its own Tauri config into the
backend's environment before startup — and main.py loaded the per-user file with
`override=False`, so the launcher's stale value always won. main.py then maps
OMNIVOICE_CACHE_DIR → HF_HOME/HF_HUB_CACHE/TORCH_HOME, pointing downloads at the
old dir; `_effective_models_dir()` reads that live env, so the UI faithfully
reported the old path as if the change had failed.

Fix: load the per-user env file with override so it beats launcher-injected
defaults — restoring this file's documented "values written here take effect on
the next backend launch" contract. Centralized as `user_env.load_into_environ()`
(the file is the in-app Settings source of truth) and called from main.py. Both
keys this file can hold (OMNIVOICE_CACHE_DIR, HF_ENDPOINT) are the user's explicit
Settings choice and should beat the launcher default, so the override is correct
for both (this also fixes the same latent bug for a Settings-set HF mirror).
HF_TOKEN isn't launcher-injected, so its behavior is unchanged.

Follow-up (separate PR): add a Tauri `set_models_dir` command so the launcher's
config.json stays in sync, covering the reset-to-default edge too.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:53:34 +05:30