Files
Martin Vogel eb294298e0 fix(daemon): settings startup survives busy and interrupted writes
Nondeterministic Windows reds came from races on _config.db:
test-windows-guards section_start_status_port / daemon_lifecycle ("the
daemon did not accept the UI configuration"), section_churn_stability
("could not load saved runtime settings"), the windows-2025 release
smoke whose `--ui=true --port` server exited about 5 s after start
(Phase 15, read as "no embedded UI assets"), and section_crash_recovery
("the stale daemon state never cleared").

- Every _config.db connection now waits out a committing writer
  (cbm_runtime_settings_wait_for_writers: one busy handler, logs
  config_db.busy_wait once per busy wait, a 3 s bound for a wedged
  holder). Startup's 50 ms window let a daemon's routine commit end
  unrelated CLI starts; cbm_config_open and the UI load had no wait at
  all, so a concurrent commit failed the daemon's UI save and made the
  load keep defaults.
- A process killed mid-write (crash, taskkill /F, power loss) left a hot
  rollback journal that the read-only settings reads cannot roll back
  (SQLITE_READONLY_ROLLBACK). Every later command then failed with
  "could not load saved runtime settings", permanently. Settings startup
  and the UI load now recover once through a read-write open that rolls
  the interrupted write back (config_db.hot_journal_recovered); a failed
  recovery is reported with the journal path and stays fatal.
- write_value no longer rewrites an unchanged value. The upsert fired the
  revision trigger, so every `daemon start` on a UI build committed and
  locked readers out although nothing changed.
- `daemon start` and the stdio server wait for the set_context and
  set_ui_config replies within their startup budget instead of the 1 s
  probe budget (MAIN_CONNECT_TIMEOUT_MS) that decided the command
  whenever the daemon's save ran long.

Tests, red on 8df0639c and green here:
- runtime_settings suite: three single-threaded lock tests (a log sink
  commits the held lock when the waiter logs config_db.busy_wait, so no
  test depends on a duration) and two hot-journal tests (a portable
  fixture snapshots the database and its journal mid-transaction and
  checks the journal is hot before the code under test runs).
- tests/windows/test_daemon_stability.py: new section
  start_port_waits_for_settings_writer holds the lock against a real
  `daemon start --port` and releases it on events; the crash section
  now prints the status output it saw.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-15 18:07:25 +02:00
..