Files
codebase-memory-mcp/src/cli/runtime_settings.h
T
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

39 lines
1.7 KiB
C

#ifndef CBM_RUNTIME_SETTINGS_H
#define CBM_RUNTIME_SETTINGS_H
#include <stdbool.h>
#include <stddef.h>
/* Call once before starting threads or initializing environment-dependent
* subsystems. Saved environment values are immutable for this process; they
* never enter the inherited process environment. */
bool cbm_runtime_settings_init(const char *cache_dir);
bool cbm_runtime_settings_initialized(void);
/* JSON API. Returned strings use CBM_MEM_CLASS_OTHER; the caller releases them
* with cbm_free(CBM_MEM_CLASS_OTHER, json). HTTP status is
* 200, 400 (invalid batch), 409 (stale revision), or 500 (storage failure).
* A batch either commits completely or changes nothing. */
char *cbm_runtime_settings_get_json(const char *cache_dir);
char *cbm_runtime_settings_apply_json(const char *cache_dir, const char *body, size_t length,
int *status);
/* UI JSON is the legacy fallback. Once initialized, CLI/daemon UI writes use
* the same transactional store as the Config dialog. Last explicit write wins. */
void cbm_runtime_settings_load_ui(bool *enabled, int *port);
bool cbm_runtime_settings_save_ui(bool enabled, int port);
/* The HTTP handler supplies its actual bound port for truthful listener state. */
void cbm_runtime_settings_note_http_port(int port);
/* Every connection to _config.db waits for a committing writer instead of
* failing on its lock. Logs config_db.busy_wait once per busy wait. */
struct sqlite3;
void cbm_runtime_settings_wait_for_writers(struct sqlite3 *db);
#ifdef CBM_ENABLE_TEST_SEAMS
/* Single-threaded test teardown only; never exposed in production builds. */
void cbm_runtime_settings_reset_for_tests(void);
#endif
#endif