diff --git a/README.md b/README.md index 2a58cb51..5efc8087 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/src/cli/cli.c b/src/cli/cli.c index a9850c77..b44ddee2 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -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) { diff --git a/src/cli/cli.h b/src/cli/cli.h index 783a24bb..40268611 100644 --- a/src/cli/cli.h +++ b/src/cli/cli.h @@ -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) ─────────────────────────── */ diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 39065260..fce62b64 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -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; } } diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 81613a6d..0b7ea2cf 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -8,10 +8,12 @@ #include "../src/foundation/constants.h" #include "../src/foundation/log.h" #include "test_framework.h" +#include "test_helpers.h" #include #include #include #include +#include #include #include #include @@ -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 /.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); }