Merge pull request #849 from DeusData/distill/625-auto-watch

feat(watcher): auto_watch config gate for background watch registration (default on)
This commit is contained in:
Martin Vogel
2026-07-04 15:52:17 +02:00
committed by GitHub
5 changed files with 158 additions and 7 deletions
+3
View File
@@ -122,6 +122,8 @@ codebase-memory-mcp config set auto_index true
When enabled, new projects are indexed automatically on first connection. Previously-indexed projects are registered with the background watcher for ongoing git-based change detection. Configurable file limit: `config set auto_index_limit 50000`.
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.
### Keeping Up to Date
```bash
@@ -466,6 +468,7 @@ See [docs/cbmignore.md](docs/cbmignore.md) for the full `.cbmignore` how-to: syn
codebase-memory-mcp config list # show all settings
codebase-memory-mcp config set auto_index true # auto-index on session start
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
codebase-memory-mcp config reset auto_index # reset to default
```
+4
View File
@@ -2822,6 +2822,8 @@ int cbm_cmd_config(int argc, char **argv) {
"Enable auto-indexing on MCP session start");
printf(" %-25s default=%-10s %s\n", CBM_CONFIG_AUTO_INDEX_LIMIT, "50000",
"Max files for auto-indexing new projects");
printf(" %-25s default=%-10s %s\n", CBM_CONFIG_AUTO_WATCH, "true",
"Register background git watcher on session connect");
printf(" %-25s default=%-10s %s\n", CBM_CONFIG_UI_LANG, "auto",
"Pin graph UI language: en, zh, or auto");
return 0;
@@ -2849,6 +2851,8 @@ int cbm_cmd_config(int argc, char **argv) {
cbm_config_get(cfg, CBM_CONFIG_AUTO_INDEX, "false"));
printf(" %-25s = %-10s\n", CBM_CONFIG_AUTO_INDEX_LIMIT,
cbm_config_get(cfg, CBM_CONFIG_AUTO_INDEX_LIMIT, "50000"));
printf(" %-25s = %-10s\n", CBM_CONFIG_AUTO_WATCH,
cbm_config_get(cfg, CBM_CONFIG_AUTO_WATCH, "true"));
printf(" %-25s = %-10s\n", CBM_CONFIG_UI_LANG,
cbm_config_get(cfg, CBM_CONFIG_UI_LANG, "auto"));
} else if (strcmp(argv[0], "get") == 0) {
+1
View File
@@ -297,6 +297,7 @@ int cbm_config_delete(cbm_config_t *cfg, const char *key);
/* Well-known config keys */
#define CBM_CONFIG_AUTO_INDEX "auto_index"
#define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit"
#define CBM_CONFIG_AUTO_WATCH "auto_watch"
#define CBM_CONFIG_UI_LANG "ui-lang"
/* ── Subcommands (wired from main.c) ─────────────────────────── */
+26 -7
View File
@@ -5632,6 +5632,30 @@ static void detect_session(cbm_mcp_server_t *srv) {
}
}
/* auto_watch config: gates background watcher registration (default on).
* Multi-project users can contain a session to its own project with
* `config set auto_watch false`. */
static bool auto_watch_enabled(cbm_mcp_server_t *srv) {
if (!srv->config) {
return true; /* default on */
}
return cbm_config_get_bool(srv->config, CBM_CONFIG_AUTO_WATCH, true);
}
/* Register the session project with the background watcher for ongoing
* change detection — unless auto_watch is disabled. */
static void register_watcher_if_enabled(cbm_mcp_server_t *srv) {
if (!srv->watcher || srv->session_project[0] == '\0' || srv->session_root[0] == '\0') {
return;
}
if (!auto_watch_enabled(srv)) {
cbm_log_info("watcher.register.skipped", "reason", "auto_watch_off", "project",
srv->session_project);
return;
}
cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root);
}
/* Background auto-index thread function */
static void *autoindex_thread(void *arg) {
cbm_mcp_server_t *srv = (cbm_mcp_server_t *)arg;
@@ -5654,10 +5678,7 @@ static void *autoindex_thread(void *arg) {
if (rc == 0) {
cbm_log_info("autoindex.done", "project", srv->session_project);
/* Register with watcher for ongoing change detection */
if (srv->watcher) {
cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root);
}
register_watcher_if_enabled(srv);
} else {
cbm_log_warn("autoindex.err", "msg", "pipeline_run_failed");
}
@@ -5680,9 +5701,7 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
/* Already indexed → register watcher for change detection */
cbm_log_info("autoindex.skip", "reason", "already_indexed", "project",
srv->session_project);
if (srv->watcher) {
cbm_watcher_watch(srv->watcher, srv->session_project, srv->session_root);
}
register_watcher_if_enabled(srv);
return;
}
}
+124
View File
@@ -8,10 +8,12 @@
#include "../src/foundation/constants.h"
#include "../src/foundation/log.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <cli/cli.h>
#include <mcp/mcp.h>
#include <pipeline/pipeline.h>
#include <store/store.h>
#include <watcher/watcher.h>
#include <yyjson/yyjson.h>
#include <string.h>
#include <stdlib.h>
@@ -4032,6 +4034,124 @@ TEST(readonly_query_succeeds_on_readonly_fs) {
#undef ROQ_PROJECT
/* ══════════════════════════════════════════════════════════════════
* AUTO_WATCH GATE (distilled from PR #625)
*
* Background watcher registration on session connect is gated by the
* `auto_watch` config key (default TRUE = existing behavior).
* ══════════════════════════════════════════════════════════════════ */
/* Drive the already-indexed connect path (initialize → maybe_auto_index →
* watcher registration) and return the resulting watch count.
* auto_watch_value: NULL leaves the key unset (exercises the default),
* otherwise the key is set to that value before initialize.
* Returns a negative code on fixture setup failure. */
static int auto_watch_connect_watch_count(const char *auto_watch_value) {
char cache[256];
snprintf(cache, sizeof(cache), "/tmp/cbm-autowatch-cache-XXXXXX");
if (!cbm_mkdtemp(cache)) {
return -1;
}
char repodir[512];
snprintf(repodir, sizeof(repodir), "%s/repo", cache);
if (th_mkdir_p(repodir) != 0) {
th_rmtree(cache);
return -2;
}
/* Same derivation detect_session uses on the cwd — realpath-based, so
* the name matches even where /tmp is a symlink (macOS). */
char *project = cbm_project_name_from_path(repodir);
if (!project) {
th_rmtree(cache);
return -3;
}
/* Pre-create <cache>/<project>.db so maybe_auto_index takes the
* "already indexed" branch — the watcher-registration site under test. */
char db_path[1024];
snprintf(db_path, sizeof(db_path), "%s/%s.db", cache, project);
if (th_write_file(db_path, "") != 0) {
free(project);
th_rmtree(cache);
return -4;
}
free(project);
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_CACHE_DIR", cache, 1);
char old_cwd[1024];
if (!cbm_getcwd(old_cwd, sizeof(old_cwd)) || cbm_chdir(repodir) != 0) {
restore_cache_dir(saved_copy);
free(saved_copy);
th_rmtree(cache);
return -5;
}
int count = -6;
cbm_config_t *cfg = cbm_config_open(cache);
cbm_store_t *wstore = cbm_store_open_memory();
cbm_watcher_t *watcher = wstore ? cbm_watcher_new(wstore, NULL, NULL) : NULL;
if (cfg && watcher) {
if (auto_watch_value) {
cbm_config_set(cfg, CBM_CONFIG_AUTO_WATCH, auto_watch_value);
}
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
if (srv) {
cbm_mcp_server_set_watcher(srv, watcher);
cbm_mcp_server_set_config(srv, cfg);
char *resp = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}");
free(resp);
count = cbm_watcher_watch_count(watcher);
cbm_mcp_server_free(srv);
}
}
if (watcher) {
cbm_watcher_free(watcher);
}
if (wstore) {
cbm_store_close(wstore);
}
if (cfg) {
cbm_config_close(cfg);
}
(void)cbm_chdir(old_cwd);
restore_cache_dir(saved_copy);
free(saved_copy);
th_rmtree(cache);
return count;
}
/* Default (key unset) → watcher registered on connect. Guards the
* no-behavior-change promise of the auto_watch gate: existing users keep
* background auto-sync without touching config. */
TEST(mcp_auto_watch_default_registers_watcher_on_connect) {
int count = auto_watch_connect_watch_count(NULL);
if (count < 0) {
PASS(); /* fixture setup failed (tmpdir/cwd unavailable) — skip */
}
ASSERT_EQ(count, 1);
PASS();
}
/* auto_watch=false → NO watcher registered on connect. RED on pre-gate code
* (registration was unconditional and the key did not exist). */
TEST(mcp_auto_watch_false_skips_watcher_on_connect) {
int count = auto_watch_connect_watch_count("false");
if (count < 0) {
PASS(); /* fixture setup failed (tmpdir/cwd unavailable) — skip */
}
ASSERT_EQ(count, 0);
PASS();
}
/* ══════════════════════════════════════════════════════════════════
* SUITE
* ══════════════════════════════════════════════════════════════════ */
@@ -4206,4 +4326,8 @@ SUITE(mcp) {
RUN_TEST(tool_bad_project_name_no_overflow_issue235);
RUN_TEST(tool_bad_project_error_valid_json_issue235);
RUN_TEST(tool_resolve_store_by_internal_name_issue704);
/* auto_watch gate (distilled from PR #625) */
RUN_TEST(mcp_auto_watch_default_registers_watcher_on_connect);
RUN_TEST(mcp_auto_watch_false_skips_watcher_on_connect);
}