Skip to content

Commit 680bc72

Browse files
committed
feat(config): add watcher_enabled master switch for the background watcher (DeusData#335)
Add a persistent `watcher_enabled` config key (default true). When false, the daemon host skips building the background watcher entirely: the poll thread never starts and no projects are registered. Manual index_repository is unaffected, and the config wiring stays unconditional so auto_index / auto_watch keep working even when the watcher is off. This is distinct from auto_watch, which only gates per-session registration while the watcher IS running. watcher_enabled is the master switch that stops the subsystem from starting at all — the request in DeusData#335 (doc-heavy repos where every save churns the graph baseline). The key is read once, when the daemon builds its host state. Changing it takes effect when the daemon next starts; docs/CONFIGURATION.md says so explicitly. A disabled watcher is not a daemon-startup failure — the daemon still comes up and still owns IPC, HTTP/UI, project locks, and manual indexing; only the watcher and the in-memory store backing it go unbuilt. - cli.h/cli.c: CBM_CONFIG_WATCHER_ENABLED key + cbm_config_watcher_enabled() predicate (NULL-safe, default true), registered as a CONFIG_KEYS row next to auto_watch. The table is no longer only a print list: since DeusData#1522/DeusData#1558 it also backs key validation, so `config set/get/reset watcher_enabled` would be rejected as an unknown key without that row, and the default recorded there (true) is the one the table comment asks to keep in sync with the runtime reader's fallback. - daemon/host.c: gate watcher construction in host_state_prepare() and the thread create in host_background_start(); log watcher.disabled when skipped. Teardown needed no change — stop NULL-checks, join is gated on watcher_started, and cbm_watcher_free is NULL-safe. - test_cli.c: cli_config_watcher_enabled_default_and_persist pins the config layer — default on, disable/re-enable spellings, persistence across reopen, and a NULL-config default (a config-store failure must never silently disable the watcher). - docs: document watcher_enabled, and add the previously-undocumented auto_watch row to CONFIGURATION.md so the watcher knobs are described together; README example. Closes DeusData#335. Signed-off-by: soren <nguiasoren@gmail.com>
1 parent 010569f commit 680bc72

6 files changed

Lines changed: 102 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,7 @@ codebase-memory-mcp config list # show all settings
667667
codebase-memory-mcp config set auto_index true # auto-index on session start
668668
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
669669
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
670+
codebase-memory-mcp config set watcher_enabled false # stop the watcher thread entirely (default: true)
670671
codebase-memory-mcp config reset auto_index # reset to default
671672
```
672673

‎docs/CONFIGURATION.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ codebase-memory-mcp config list
7777
codebase-memory-mcp config get auto_index
7878
codebase-memory-mcp config set auto_index true
7979
codebase-memory-mcp config set auto_index_limit 50000
80+
codebase-memory-mcp config set watcher_enabled false
8081
codebase-memory-mcp config reset auto_index
8182
```
8283

@@ -86,6 +87,8 @@ Current keys:
8687
|---|---|---|
8788
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
8889
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
90+
| `auto_watch` | `true` | Register the session's project with the background git watcher on connect. Set `false` to keep a session from registering its project (the watcher still runs for other projects). |
91+
| `watcher_enabled` | `true` | Run the background watcher thread that auto-reindexes projects when they change. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. |
8992

9093
## 3. UI Settings
9194

‎src/cli/cli.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6734,6 +6734,14 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key) {
67346734
return rc;
67356735
}
67366736

6737+
/* Whether the background watcher subsystem should run (default true). The
6738+
* daemon host gates watcher construction and thread startup on this; see
6739+
* cbm_config_watcher_enabled in cli.h. NULL-safe via cbm_config_get_bool (a
6740+
* NULL cfg returns the default). */
6741+
bool cbm_config_watcher_enabled(cbm_config_t *cfg) {
6742+
return cbm_config_get_bool(cfg, CBM_CONFIG_WATCHER_ENABLED, true);
6743+
}
6744+
67376745
/* ── Config CLI subcommand ────────────────────────────────────── */
67386746

67396747
/* THE config-key table. list, get, help, and key validation all read this one
@@ -6753,6 +6761,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
67536761
{CBM_CONFIG_AUTO_INDEX, "false", "Enable auto-indexing on MCP session start"},
67546762
{CBM_CONFIG_AUTO_INDEX_LIMIT, "50000", "Max files for auto-indexing new projects"},
67556763
{CBM_CONFIG_AUTO_WATCH, "true", "Register background git watcher on session connect"},
6764+
{CBM_CONFIG_WATCHER_ENABLED, "true",
6765+
"Run the background watcher thread (auto-reindex); false to disable"},
67566766
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
67576767
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
67586768
{CBM_CONFIG_UI_PORT, "9749", "Port for the graph UI listener when enabled"},

‎src/cli/cli.h‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,11 +422,21 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key);
422422
#define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit"
423423
#define CBM_CONFIG_AUTO_WATCH "auto_watch"
424424
#define CBM_CONFIG_UI_LANG "ui-lang"
425+
#define CBM_CONFIG_WATCHER_ENABLED "watcher_enabled"
425426
/* #1558: the graph UI's loopback listener. Stored in the UI config file rather
426427
* than the key-value store, but surfaced through `config` so it is findable. */
427428
#define CBM_CONFIG_UI_ENABLED "ui_enabled"
428429
#define CBM_CONFIG_UI_PORT "ui_port"
429430

431+
/* Whether the background watcher subsystem should run at all (default true).
432+
* When false, the daemon host skips building and starting the watcher entirely:
433+
* the poll thread never starts and no projects are registered (#335). Read once
434+
* at daemon startup. Distinct from auto_watch, which only gates per-session
435+
* registration while the watcher IS running. NULL-safe — a NULL cfg returns the
436+
* default (true), so a failure to open the config store never silently disables
437+
* the watcher. */
438+
bool cbm_config_watcher_enabled(cbm_config_t *cfg);
439+
430440
/* ── Binary activation safety ─────────────────────────────────── */
431441

432442
typedef void *cbm_cli_activation_lock_t;

‎src/daemon/host.c‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,22 @@ static bool host_state_prepare(host_state_t *host, const cbm_daemon_ipc_endpoint
583583
cbm_log_error("daemon.runtime_config_open_failed", "reason", "config_db_unavailable");
584584
return false;
585585
}
586-
host->watch_store = cbm_store_open_memory();
587586
host->project_locks = cbm_project_lock_manager_new(endpoint);
588-
host->watcher = cbm_watcher_new(host->watch_store, host_watcher_index, host);
587+
/* #335: watcher_enabled (default true) is the master switch for the
588+
* background watcher. When false the daemon never builds the watcher or the
589+
* in-memory store backing it, so the poll thread never starts
590+
* (host_background_start has nothing to run) and no project ever registers
591+
* (register_watcher_if_enabled early-returns on a NULL watcher). The daemon
592+
* still starts and still owns everything else: IPC, HTTP/UI, project locks,
593+
* and manual index_repository. Read once here at daemon startup, so a change
594+
* takes effect when the daemon next starts — see docs/CONFIGURATION.md. */
595+
bool watcher_enabled = cbm_config_watcher_enabled(host->runtime_config);
596+
if (watcher_enabled) {
597+
host->watch_store = cbm_store_open_memory();
598+
host->watcher = cbm_watcher_new(host->watch_store, host_watcher_index, host);
599+
} else {
600+
cbm_log_info("watcher.disabled", "reason", "config");
601+
}
589602
cbm_daemon_application_config_t application_config = {
590603
.watcher = host->watcher,
591604
.config = host->runtime_config,
@@ -598,7 +611,12 @@ static bool host_state_prepare(host_state_t *host, const cbm_daemon_ipc_endpoint
598611
if (host->application && host->permanent) {
599612
cbm_daemon_application_set_permanent(host->application, true);
600613
}
601-
if (!host->watch_store || !host->watcher || !host->project_locks || !host->application) {
614+
if (!host->project_locks || !host->application) {
615+
return false;
616+
}
617+
/* A watcher deliberately left unbuilt by watcher_enabled=false is not a
618+
* startup failure; an allocation failure while it is enabled still is. */
619+
if (watcher_enabled && (!host->watch_store || !host->watcher)) {
602620
return false;
603621
}
604622
return true;
@@ -824,10 +842,14 @@ bool cbm_daemon_host_http_thread_create_failure_lifecycle_for_test(void) {
824842
}
825843

826844
static bool host_background_start(host_state_t *host) {
827-
if (cbm_thread_create(&host->watcher_thread, 0, host_watcher_thread, host->watcher) != 0) {
828-
return false;
845+
/* No watcher object when watcher_enabled=false (#335) — nothing to run, and
846+
* the daemon must still come up with its remaining subsystems. */
847+
if (host->watcher) {
848+
if (cbm_thread_create(&host->watcher_thread, 0, host_watcher_thread, host->watcher) != 0) {
849+
return false;
850+
}
851+
host->watcher_started = true;
829852
}
830-
host->watcher_started = true;
831853

832854
host_http_reconcile_at(host, cbm_now_ms(), true);
833855
return true;

‎tests/test_cli.c‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12291,6 +12291,55 @@ TEST(cli_config_persists) {
1229112291
PASS();
1229212292
}
1229312293

12294+
TEST(cli_config_watcher_enabled_default_and_persist) {
12295+
/* #335: the background watcher is on by default and can be disabled via the
12296+
* persisted `watcher_enabled` key. This pins the config layer only — default
12297+
* preservation, the accepted spellings, and persistence across reopen. That
12298+
* the daemon host actually honours the key (watcher never built, thread
12299+
* never started, no registration) is proven separately by the process-level
12300+
* regression in tests/test_watcher_disabled.sh, which fails if the gate in
12301+
* host_state_prepare() is removed. */
12302+
char tmpdir[256];
12303+
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
12304+
if (!cbm_mkdtemp(tmpdir))
12305+
FAIL("cbm_mkdtemp failed");
12306+
12307+
/* A NULL config (store failed to open) defaults to ON — a config failure
12308+
* must never silently disable the watcher. */
12309+
ASSERT_TRUE(cbm_config_watcher_enabled(NULL));
12310+
12311+
cbm_config_t *cfg = cbm_config_open(tmpdir);
12312+
ASSERT_NOT_NULL(cfg);
12313+
12314+
/* Default: absent key → watcher runs. */
12315+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12316+
12317+
/* Disable: false / 0 / off all turn the watcher off. */
12318+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "false");
12319+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12320+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "0");
12321+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12322+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "off");
12323+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12324+
12325+
/* Re-enable: true / 1 / on all turn it back on. */
12326+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "on");
12327+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12328+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "1");
12329+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12330+
12331+
/* Persistence: the disabled state survives close + reopen. */
12332+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "false");
12333+
cbm_config_close(cfg);
12334+
cfg = cbm_config_open(tmpdir);
12335+
ASSERT_NOT_NULL(cfg);
12336+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12337+
12338+
cbm_config_close(cfg);
12339+
test_rmdir_r(tmpdir);
12340+
PASS();
12341+
}
12342+
1229412343
/* ═══════════════════════════════════════════════════════════════════
1229512344
* Group H: cbm_replace_binary (update command helper)
1229612345
* ═══════════════════════════════════════════════════════════════════ */
@@ -13301,6 +13350,7 @@ SUITE(cli) {
1330113350
RUN_TEST(cli_config_get_int);
1330213351
RUN_TEST(cli_config_delete);
1330313352
RUN_TEST(cli_config_persists);
13353+
RUN_TEST(cli_config_watcher_enabled_default_and_persist);
1330413354

1330513355
/* Replace binary (update command helper — group H) */
1330613356
#ifndef _WIN32

0 commit comments

Comments
 (0)