Skip to content

Commit 3ffcbc9

Browse files
Merge branch 'main' into fix/quarantine-exit-nonzero
2 parents f599076 + b0a3ca2 commit 3ffcbc9

17 files changed

Lines changed: 809 additions & 7 deletions

File tree

‎Makefile.cbm‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ TEST_DISCOVER_SRCS = \
560560

561561
TEST_GRAPH_BUFFER_SRCS = tests/test_graph_buffer.c
562562

563-
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
563+
TEST_PIPELINE_SRCS = tests/test_registry.c tests/test_pipeline.c tests/test_cross_repo.c tests/test_fqn.c tests/test_route_canon.c tests/test_path_alias.c tests/test_configlink.c tests/test_infrascan.c tests/test_worker_pool.c tests/test_parallel.c tests/test_index_resilience.c tests/test_index_format.c tests/test_call_reference_contract.c tests/repro/repro_call_scope_usages.c tests/repro/repro_call_argument_usages.c tests/repro/repro_reference_precision.c tests/repro/repro_lexical_binding_precision.c tests/repro/repro_call_argument_matrix_a.c tests/repro/repro_call_argument_matrix_b.c tests/repro/repro_call_node_behaviors.c tests/repro/repro_language_registry.c tests/repro/repro_call_node_manifest.c tests/repro/repro_lsp_ordered_signatures.c tests/repro/repro_lsp_ordered_local.c tests/repro/repro_ts_overload_return_chains.c tests/repro/repro_harness_cleanup.c tests/repro/repro_runner_filter.c
564564

565565
TEST_WATCHER_SRCS = tests/test_watcher.c
566566

‎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;

‎src/mcp/mcp.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8364,6 +8364,9 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
83648364
srv, doc, root, project_name, repo_path, persistence, p, excluded_dirs, excluded_count,
83658365
file_errors, file_error_count, has_logfile ? logfile_path : NULL);
83668366
yyjson_mut_obj_add_str(doc, root, "status", degraded ? "degraded" : "indexed");
8367+
if (cbm_pipeline_had_format_migration(p)) {
8368+
yyjson_mut_obj_add_bool(doc, root, "format_migration", true);
8369+
}
83678370
} else {
83688371
yyjson_mut_obj_add_str(doc, root, "status", "error");
83698372
yyjson_mut_obj_add_str(doc, root, "hint",

‎src/pipeline/pipeline.c‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ struct cbm_pipeline {
191191
int committed_nodes;
192192
int committed_edges;
193193

194+
/* #769: set when a stale-format index was routed through the one-time
195+
* full rebuild, so the MCP response can surface the migration. */
196+
bool format_migration;
197+
194198
/* ADR (project_summaries) captured before a full-reindex DB delete, so it
195199
* can be restored after the rebuild. NULL when no ADR existed. Issue #516. */
196200
char *saved_adr;
@@ -225,6 +229,10 @@ void cbm_pipeline_set_pkgmap(CBMHashTable *map) {
225229
g_pkgmap = map;
226230
}
227231

232+
bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p) {
233+
return p && p->format_migration;
234+
}
235+
228236
/* ── Timing helper ──────────────────────────────────────────────── */
229237

230238
static double elapsed_ms(struct timespec start) {
@@ -1399,6 +1407,24 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
13991407
free(db_path);
14001408
return CBM_PIPELINE_FORCE_FULL_REINDEX;
14011409
}
1410+
1411+
cbm_store_t *fmt_store = cbm_store_open_path_query(db_path);
1412+
int fmt = 0;
1413+
if (fmt_store) {
1414+
cbm_store_get_format_version(fmt_store, &fmt);
1415+
cbm_store_close(fmt_store);
1416+
}
1417+
if (fmt != CBM_INDEX_FORMAT_VERSION) {
1418+
cbm_log_info("pipeline.route", "path", "format_change_reindex", "stored_format",
1419+
itoa_buf(fmt));
1420+
p->format_migration = true;
1421+
int adr_rc = capture_existing_adr(p, db_path);
1422+
(void)cbm_unlink(db_path);
1423+
(void)cbm_remove_db_sidecars(db_path);
1424+
free(db_path);
1425+
return adr_rc != 0 ? adr_rc : CBM_PIPELINE_FORCE_FULL_REINDEX;
1426+
}
1427+
14021428
cbm_log_info("pipeline.route", "path", "incremental_manifest");
14031429
int rc = cbm_pipeline_run_incremental(p, db_path, files, file_count, baseline_manifest,
14041430
baseline_count, force_full_on_mismatch);
@@ -1753,6 +1779,11 @@ int cbm_pipeline_publish_staged(char *stage_path, const cbm_pipeline_generation_
17531779
ok = cbm_store_adr_store(store, generation->project, generation->adr_content) ==
17541780
CBM_STORE_OK;
17551781
}
1782+
1783+
if (ok) {
1784+
ok = cbm_store_set_format_version(store, CBM_INDEX_FORMAT_VERSION) == CBM_STORE_OK;
1785+
}
1786+
17561787
cbm_log_info("publish.timing", "block", "writes", "elapsed_ms",
17571788
itoa_buf((int)elapsed_ms(t_pub)));
17581789
cbm_clock_gettime(CLOCK_MONOTONIC, &t_pub);

‎src/pipeline/pipeline.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ int cbm_pipeline_get_mode(const cbm_pipeline_t *p);
9393
* to NULL/0 when p is NULL or nothing was excluded. Do not free. */
9494
void cbm_pipeline_get_excluded(const cbm_pipeline_t *p, char ***out, int *count);
9595

96+
bool cbm_pipeline_had_format_migration(const cbm_pipeline_t *p);
97+
9698
/* Committed node/edge counts captured at dump time (-1 when dump did not run).
9799
* Nodes are the #334 plausibility-gate axis; edges are informational only. */
98100
void cbm_pipeline_get_committed_counts(const cbm_pipeline_t *p, int *nodes, int *edges);

0 commit comments

Comments
 (0)