Skip to content

Commit b0a3ca2

Browse files
authored
Merge pull request DeusData#1105 from nguiaSoren/feat/335-watcher-enabled-config
feat(config): add watcher_enabled config key to disable the background watcher (DeusData#335)
2 parents feb7208 + d61811d commit b0a3ca2

8 files changed

Lines changed: 404 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ When enabled, new projects are indexed automatically on first connection. Previo
153153

154154
Watcher registration is controlled separately by `auto_watch` (default `true`). Set `config set auto_watch false` to keep a session from registering its project with the background watcher — useful when working across many projects and you want each session contained to explicit indexing.
155155

156+
To turn the watcher off entirely, set `config set watcher_enabled false` (default `true`): the background poll thread never starts and no project is registered, while `auto_index` and manual `index_repository` keep working. Unlike `auto_watch` — which is consulted per session — `watcher_enabled` is read once when the background daemon starts, so run `codebase-memory-mcp daemon stop` after changing it; reconnecting your MCP client alone will not restart the daemon. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md#2-cli-managed-runtime-settings).
157+
156158
### Keeping Up to Date
157159

158160
**Updates run from the install script on every platform, not from inside the running binary.** `codebase-memory-mcp update` validates your flags and then prints the exact command to run:
@@ -673,6 +675,7 @@ codebase-memory-mcp config list # show all settings
673675
codebase-memory-mcp config set auto_index true # auto-index on session start
674676
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
675677
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
678+
codebase-memory-mcp config set watcher_enabled false # stop the watcher thread entirely (default: true)
676679
codebase-memory-mcp config reset auto_index # reset to default
677680
```
678681

‎docs/CONFIGURATION.md‎

Lines changed: 29 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,34 @@ 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` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. |
92+
93+
> **`watcher_enabled` vs `auto_watch`.** `watcher_enabled` controls whether the
94+
> watcher *subsystem* starts at all (the background poll thread). `auto_watch` is
95+
> narrower: it only controls whether a connecting session registers *its own*
96+
> project with an already-running watcher. When `watcher_enabled=false`,
97+
> `auto_watch` has no effect — there is no watcher to register with.
98+
>
99+
> They also differ in **when they are read**, which matters because the watcher
100+
> lives in the background daemon, not in your MCP client:
101+
>
102+
> - `auto_watch` is consulted each time a session would register its project, so
103+
> a change applies to sessions that connect afterwards.
104+
> - `watcher_enabled` is read **once, when the daemon starts**, because it decides
105+
> whether the watcher is built at all. The daemon is long-lived and outlives
106+
> individual MCP sessions, so **reconnecting your client is not enough** — retire
107+
> the daemon so the next one picks the new value up:
108+
>
109+
> ```bash
110+
> codebase-memory-mcp config set watcher_enabled false
111+
> codebase-memory-mcp daemon stop # next session starts a daemon without the watcher
112+
> codebase-memory-mcp daemon status # confirm
113+
> ```
114+
>
115+
> Disabling the watcher does not disable anything else: the daemon still starts,
116+
> `auto_index` still runs, and `index_repository` stays available for manual
117+
> reindexing.
89118
90119
## 3. UI Settings
91120

‎scripts/test.sh‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,15 @@ CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_worker_error_response.
331331
# skipping it silently would hide the gap. Run it by hand:
332332
# make -f Makefile.cbm cbm TEST_SEAMS=1 && bash tests/test_hook_conflict_notice.sh
333333

334+
# Step 5e: watcher_enabled kill-switch process regression (#335). Reuses the
335+
# prod binary built in Step 5; drives a real daemon against an isolated cache
336+
# and proves watcher_enabled=false stops the watcher from being built, started
337+
# or registered, while auto_index and manual index_repository keep working.
338+
# Every wait is a bounded poll on an asserted state (a closed daemon lifecycle),
339+
# never a fixed sleep — see the header of the test for why.
340+
echo "=== Step 5e: watcher_enabled kill-switch regression (#335) ==="
341+
CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_watcher_disabled.sh"
342+
334343
# Step 6: security-strings URL allow-list regression. The MSYS2 CLANG64 toolchain
335344
# bakes its package-tracker URL into the static Windows .exe; the binary string
336345
# audit must allow-list it (Windows-only — Linux smoke never saw it).

‎src/cli/cli.c‎

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

6798+
/* Whether the background watcher subsystem should run (default true). The
6799+
* daemon host gates watcher construction and thread startup on this; see
6800+
* cbm_config_watcher_enabled in cli.h. NULL-safe via cbm_config_get_bool (a
6801+
* NULL cfg returns the default). */
6802+
bool cbm_config_watcher_enabled(cbm_config_t *cfg) {
6803+
return cbm_config_get_bool(cfg, CBM_CONFIG_WATCHER_ENABLED, true);
6804+
}
6805+
67986806
/* ── Config CLI subcommand ────────────────────────────────────── */
67996807

68006808
/* THE config-key table. list, get, help, and key validation all read this one
@@ -6814,6 +6822,8 @@ static const config_key_def_t CONFIG_KEYS[] = {
68146822
{CBM_CONFIG_AUTO_INDEX, "false", "Enable auto-indexing on MCP session start"},
68156823
{CBM_CONFIG_AUTO_INDEX_LIMIT, "50000", "Max files for auto-indexing new projects"},
68166824
{CBM_CONFIG_AUTO_WATCH, "true", "Register background git watcher on session connect"},
6825+
{CBM_CONFIG_WATCHER_ENABLED, "true",
6826+
"Run the background watcher thread (auto-reindex); false to disable"},
68176827
{CBM_CONFIG_UI_LANG, "auto", "Pin graph UI language: en, zh, or auto"},
68186828
{CBM_CONFIG_UI_ENABLED, "false", "Serve the graph UI on a loopback HTTP port"},
68196829
{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
@@ -423,11 +423,21 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key);
423423
#define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit"
424424
#define CBM_CONFIG_AUTO_WATCH "auto_watch"
425425
#define CBM_CONFIG_UI_LANG "ui-lang"
426+
#define CBM_CONFIG_WATCHER_ENABLED "watcher_enabled"
426427
/* #1558: the graph UI's loopback listener. Stored in the UI config file rather
427428
* than the key-value store, but surfaced through `config` so it is findable. */
428429
#define CBM_CONFIG_UI_ENABLED "ui_enabled"
429430
#define CBM_CONFIG_UI_PORT "ui_port"
430431

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

433443
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
@@ -12541,6 +12541,55 @@ TEST(cli_config_persists) {
1254112541
PASS();
1254212542
}
1254312543

12544+
TEST(cli_config_watcher_enabled_default_and_persist) {
12545+
/* #335: the background watcher is on by default and can be disabled via the
12546+
* persisted `watcher_enabled` key. This pins the config layer only — default
12547+
* preservation, the accepted spellings, and persistence across reopen. That
12548+
* the daemon host actually honours the key (watcher never built, thread
12549+
* never started, no registration) is proven separately by the process-level
12550+
* regression in tests/test_watcher_disabled.sh, which fails if the gate in
12551+
* host_state_prepare() is removed. */
12552+
char tmpdir[256];
12553+
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-cfg-XXXXXX");
12554+
if (!cbm_mkdtemp(tmpdir))
12555+
FAIL("cbm_mkdtemp failed");
12556+
12557+
/* A NULL config (store failed to open) defaults to ON — a config failure
12558+
* must never silently disable the watcher. */
12559+
ASSERT_TRUE(cbm_config_watcher_enabled(NULL));
12560+
12561+
cbm_config_t *cfg = cbm_config_open(tmpdir);
12562+
ASSERT_NOT_NULL(cfg);
12563+
12564+
/* Default: absent key → watcher runs. */
12565+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12566+
12567+
/* Disable: false / 0 / off all turn the watcher off. */
12568+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "false");
12569+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12570+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "0");
12571+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12572+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "off");
12573+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12574+
12575+
/* Re-enable: true / 1 / on all turn it back on. */
12576+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "on");
12577+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12578+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "1");
12579+
ASSERT_TRUE(cbm_config_watcher_enabled(cfg));
12580+
12581+
/* Persistence: the disabled state survives close + reopen. */
12582+
cbm_config_set(cfg, CBM_CONFIG_WATCHER_ENABLED, "false");
12583+
cbm_config_close(cfg);
12584+
cfg = cbm_config_open(tmpdir);
12585+
ASSERT_NOT_NULL(cfg);
12586+
ASSERT_FALSE(cbm_config_watcher_enabled(cfg));
12587+
12588+
cbm_config_close(cfg);
12589+
test_rmdir_r(tmpdir);
12590+
PASS();
12591+
}
12592+
1254412593
/* ═══════════════════════════════════════════════════════════════════
1254512594
* Group H: cbm_replace_binary (update command helper)
1254612595
* ═══════════════════════════════════════════════════════════════════ */
@@ -13555,6 +13604,7 @@ SUITE(cli) {
1355513604
RUN_TEST(cli_config_get_int);
1355613605
RUN_TEST(cli_config_delete);
1355713606
RUN_TEST(cli_config_persists);
13607+
RUN_TEST(cli_config_watcher_enabled_default_and_persist);
1355813608

1355913609
/* Replace binary (update command helper — group H) */
1356013610
#ifndef _WIN32

0 commit comments

Comments
 (0)