Skip to content

Commit 849b97b

Browse files
authored
Break-glass merge PR #2628
2 parents 8350db5 + 8592945 commit 849b97b

15 files changed

Lines changed: 606 additions & 73 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ codebase-memory-mcp config list # show all settings
798798
codebase-memory-mcp config set auto_index true # auto-index on session start
799799
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
800800
codebase-memory-mcp config set auto_watch false # don't register background git watcher (default: true)
801-
codebase-memory-mcp config set doc_candidates false # skip doc<->code candidate scoring on huge repos (default: true)
801+
codebase-memory-mcp config set doc_candidates false # skip doc<->code candidate scoring on huge repos (default: auto = on for full, off for moderate/fast)
802802
codebase-memory-mcp config set watcher_enabled false # stop the watcher thread entirely (default: true)
803803
codebase-memory-mcp config set watch_non_git true # also poll non-git project roots (default: false)
804804
codebase-memory-mcp config set index_max_files 250000 # optional per-index source-file limit

‎docs/CONFIGURATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ Current keys:
8888
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
8989
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
9090
| `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-
| `doc_candidates` | `true` | Score documentation sections against code to store doc↔code candidates (the possible targets of a section, and the sections that may describe a function or file, each with a judged probability). The scoring grows with sections × functions: under a second up to ~100k functions, ~15 s at ~400k, ~3 minutes on the Linux kernel. Set `false` to skip it; no candidates are stored and the rest of the graph is unchanged. `index_repository` also takes `doc_candidates` for a single index, which wins over this setting. Applies from the next index. |
91+
| `doc_candidates` | `auto` | Score documentation sections against code to store doc↔code candidates (the possible targets of a section, and the sections that may describe a function or file, each with a judged probability). Unset (`auto`), the index mode decides: on for `full` indexes (the default mode), off for `moderate` and `fast`. Set `true` or `false` to choose for every mode; `config reset doc_candidates` returns to `auto`. The scoring grows with sections × functions: under a second up to ~100k functions, ~15 s at ~400k, ~3 minutes on the Linux kernel. Off stores no candidates and leaves the rest of the graph unchanged; on in `fast` mode stores candidates without adding `fast`'s skipped `SIMILAR_TO` / `SEMANTICALLY_RELATED` edges. `index_repository` also takes `doc_candidates` for a single index, which wins over this setting. Applies from the next index. |
9292
| `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. Only an open MCP session's own project is ever watched; `index_status` reports the current state in its `watch` object. |
9393
| `watch_non_git` | `false` | Also poll project roots that are **not git repositories**. By default the watcher only follows git projects, so a project indexed from a plain directory is never refreshed after its first index — reindex it manually with `index_repository`. Set `true` to poll such roots on the same adaptive cadence with a file-tree scan: the indexer's own discovery walk (same skip lists, `.gitignore` and `.cbmignore` rules) hashed over each file's path, size and mtime. Any change reindexes once; paths the indexer skips (including cbm's own `.codebase-memory/` output) never trigger. The first poll after the daemon starts reindexes each such project once, since nothing records which tree state the index holds. The scan walks the whole tree every poll, so it costs more than git polling on very large trees. Read once when the daemon starts, like `watcher_enabled`. |
9494
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |

‎src/cli/cli.c‎

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7461,18 +7461,29 @@ const char *cbm_config_get(cbm_config_t *cfg, const char *key, const char *defau
74617461
return result;
74627462
}
74637463

7464-
bool cbm_config_get_bool(cbm_config_t *cfg, const char *key, bool default_val) {
7464+
bool cbm_config_get_bool_set(cbm_config_t *cfg, const char *key, bool *value) {
74657465
const char *val = cbm_config_get(cfg, key, NULL);
74667466
if (!val) {
7467-
return default_val;
7467+
return false;
74687468
}
7469+
bool parsed;
74697470
if (strcmp(val, "true") == 0 || strcmp(val, "1") == 0 || strcmp(val, "on") == 0) {
7470-
return true;
7471-
}
7472-
if (strcmp(val, "false") == 0 || strcmp(val, "0") == 0 || strcmp(val, "off") == 0) {
7471+
parsed = true;
7472+
} else if (strcmp(val, "false") == 0 || strcmp(val, "0") == 0 || strcmp(val, "off") == 0) {
7473+
parsed = false;
7474+
} else {
74737475
return false;
74747476
}
7475-
return default_val;
7477+
if (value) {
7478+
*value = parsed;
7479+
}
7480+
return true;
7481+
}
7482+
7483+
bool cbm_config_get_bool(cbm_config_t *cfg, const char *key, bool default_val) {
7484+
bool value = default_val;
7485+
(void)cbm_config_get_bool_set(cfg, key, &value);
7486+
return value;
74767487
}
74777488

74787489
int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val) {
@@ -7574,6 +7585,9 @@ static const config_key_def_t CONFIG_KEYS[] = {
75747585
{CBM_CONFIG_AUTO_INDEX, "false", "Enable auto-indexing on MCP session start"},
75757586
{CBM_CONFIG_AUTO_INDEX_LIMIT, "50000", "Max files for auto-indexing new projects"},
75767587
{CBM_CONFIG_AUTO_WATCH, "true", "Register background git watcher on session connect"},
7588+
/* Unset: the index mode decides (cbm_pipeline_doc_candidates_enabled). */
7589+
{CBM_CONFIG_DOC_CANDIDATES, "auto",
7590+
"Doc-code candidates: auto = on for full, off for moderate/fast; true/false forces"},
75777591
{CBM_CONFIG_WATCHER_ENABLED, "true",
75787592
"Run the background watcher thread (auto-reindex); false to disable"},
75797593
{CBM_CONFIG_WATCH_NON_GIT, "false",

‎src/cli/cli.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ const char *cbm_config_get(cbm_config_t *cfg, const char *key, const char *defau
434434
/* Get a config value as bool. "true"/"1"/"on" → true. */
435435
bool cbm_config_get_bool(cbm_config_t *cfg, const char *key, bool default_val);
436436

437+
/* A key the user set to a boolean ("true"/"1"/"on", "false"/"0"/"off"):
438+
* true, with *value. Unset or unparseable: false, *value untouched -- the
439+
* caller's own default applies (cbm_config_get_bool's reading). */
440+
bool cbm_config_get_bool_set(cbm_config_t *cfg, const char *key, bool *value);
441+
437442
/* Get a config value as int. Returns default_val if not found or invalid. */
438443
int cbm_config_get_int(cbm_config_t *cfg, const char *key, int default_val);
439444

‎src/cli/runtime_settings.c‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ static const setting_t settings[] = {
4646
CFG("auto_watch", "Automatic watching",
4747
"Register a background Git watcher when an MCP session connects.", "boolean", "true", 0, 0,
4848
NULL, "src/mcp/mcp.c", "next-session"),
49+
/* No fixed default: unset, the index mode decides (shown as automatic). */
4950
CFG("doc_candidates", "Doc-code candidates",
50-
"Score documentation sections against code for doc-code candidates. Costly on very large "
51-
"repositories; off stores none.",
52-
"boolean", "true", 0, 0, NULL, "src/mcp/mcp.c", "next-session"),
51+
"Score documentation sections against code for doc-code candidates. Automatic: on for "
52+
"full indexes, off for moderate and fast; on or off applies to every mode. Costly on very "
53+
"large repositories; off stores none.",
54+
"boolean", NULL, 0, 0, NULL, "src/mcp/mcp.c", "next-session"),
5355
CFG("watcher_enabled", "Background watcher", "Enable the daemon's background watcher thread.",
5456
"boolean", "true", 0, 0, NULL, "src/daemon/application.c", "restart"),
5557
{"ui-lang", "Interface language", "Language preference used by subsequent HTTP requests.",

‎src/daemon/application.c‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2319,12 +2319,16 @@ static bool application_index_args_add_policy(cbm_daemon_application_t *applicat
23192319
cbm_index_resource_policy_t policy;
23202320
char error[CBM_SZ_256] = {0};
23212321
bool loaded = cbm_config_load_index_policy(config, &policy, error, sizeof(error));
2322+
/* The worker reads no config: a doc_candidates setting the user set
2323+
* travels with the request; unset, the worker's index mode decides. */
2324+
bool doc_candidates_encoded =
2325+
!loaded || cbm_mcp_index_doc_candidates_add_to_args(document, root, config);
23222326
cbm_config_close(owned_config);
23232327
if (!loaded) {
23242328
cbm_log_error("daemon.index.policy", "error", error);
23252329
return false;
23262330
}
2327-
return cbm_mcp_index_policy_add_to_args(document, root, &policy);
2331+
return doc_candidates_encoded && cbm_mcp_index_policy_add_to_args(document, root, &policy);
23282332
}
23292333

23302334
static char *application_auto_index_args(cbm_daemon_application_t *application,

‎src/mcp/mcp.c‎

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,8 @@ static const tool_def_t TOOLS[] = {
549549
"\"persistence\":{\"type\":\"boolean\",\"default\":false,\"description\":"
550550
"\"Write .codebase-memory/graph.db.zst.\"},"
551551
"\"doc_candidates\":{\"type\":\"boolean\",\"description\":"
552-
"\"Doc-code candidates; default: the doc_candidates setting (on).\"},"
552+
"\"Doc-code candidates; default: the doc_candidates setting, else on for full, off for "
553+
"moderate/fast.\"},"
553554
"\"async\":{\"type\":\"boolean\",\"default\":false,\"description\":"
554555
"\"Start the index in the background and return immediately; it keeps running if this "
555556
"call is cancelled or times out. Not with status or cross-repo-intelligence. Refused for a "
@@ -11457,6 +11458,13 @@ bool cbm_mcp_index_policy_add_to_args(yyjson_mut_doc *doc, yyjson_mut_val *root,
1145711458
return valid && yyjson_mut_obj_add_val(doc, root, "_cbm_index_policy", encoded);
1145811459
}
1145911460

11461+
bool cbm_mcp_index_doc_candidates_add_to_args(yyjson_mut_doc *doc, yyjson_mut_val *root,
11462+
cbm_config_t *config) {
11463+
bool enabled = false;
11464+
return !cbm_config_get_bool_set(config, CBM_CONFIG_DOC_CANDIDATES, &enabled) ||
11465+
yyjson_mut_obj_add_bool(doc, root, "doc_candidates", enabled);
11466+
}
11467+
1146011468
/* Run index_repository in a supervised worker subprocess with skip-and-continue
1146111469
* (Stage 3c). Returns the response string (caller frees):
1146211470
* - the worker's own response on a clean first run (the common path);
@@ -11786,8 +11794,11 @@ static char *index_run_supervised_path(cbm_mcp_server_t *srv, const char *root_p
1178611794
yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL);
1178711795
yyjson_mut_val *root = yyjson_mut_obj(doc);
1178811796
yyjson_mut_doc_set_root(doc, root);
11797+
/* A doc_candidates setting the user set travels with the request (the
11798+
* worker reads no config); unset, the worker's index mode decides. */
1178911799
if (!yyjson_mut_obj_add_strcpy(doc, root, "repo_path", root_path) ||
11790-
!cbm_mcp_index_policy_add_to_args(doc, root, &policy)) {
11800+
!cbm_mcp_index_policy_add_to_args(doc, root, &policy) ||
11801+
!cbm_mcp_index_doc_candidates_add_to_args(doc, root, srv ? srv->config : NULL)) {
1179111802
yyjson_mut_doc_free(doc);
1179211803
return NULL;
1179311804
}
@@ -11837,32 +11848,52 @@ static bool resolve_session_repo_path(cbm_mcp_server_t *srv, char **repo_path) {
1183711848
return true;
1183811849
}
1183911850

11840-
/* doc_candidates for one index: an explicit boolean argument wins, else the
11841-
* doc_candidates setting (default on). The parent resolves it once and passes
11842-
* the decided value on, so a daemon job or supervised worker gets it as an
11843-
* argument instead of reading a config of its own. */
11844-
static bool index_doc_candidates_enabled(cbm_mcp_server_t *srv, const char *args) {
11851+
/* doc_candidates for one index, explicit or unset: an explicit boolean
11852+
* argument wins, else a doc_candidates setting the user set to a boolean.
11853+
* Neither: unset, and the pipeline's index mode decides (on for full, off
11854+
* for moderate and fast). The parent resolves an explicit value once and
11855+
* passes it on, so a daemon job or supervised worker gets it as an argument
11856+
* instead of reading a config of its own; an unset one is not passed. */
11857+
typedef enum {
11858+
INDEX_DOC_CANDIDATES_BY_MODE = 0,
11859+
INDEX_DOC_CANDIDATES_ON,
11860+
INDEX_DOC_CANDIDATES_OFF,
11861+
} index_doc_candidates_t;
11862+
11863+
static index_doc_candidates_t index_doc_candidates_choice(cbm_mcp_server_t *srv, const char *args) {
1184511864
yyjson_doc *doc = args ? yyjson_read(args, strlen(args), 0) : NULL;
1184611865
yyjson_val *val = doc ? yyjson_obj_get(yyjson_doc_get_root(doc), "doc_candidates") : NULL;
1184711866
bool explicit_value = val && yyjson_is_bool(val);
11848-
bool enabled = explicit_value ? yyjson_get_bool(val) : true;
11867+
bool enabled = explicit_value && yyjson_get_bool(val);
1184911868
yyjson_doc_free(doc);
11850-
if (!explicit_value && srv && srv->config) {
11851-
enabled = cbm_config_get_bool(srv->config, CBM_CONFIG_DOC_CANDIDATES, true);
11869+
if (!explicit_value) {
11870+
explicit_value =
11871+
cbm_config_get_bool_set(srv ? srv->config : NULL, CBM_CONFIG_DOC_CANDIDATES, &enabled);
11872+
}
11873+
if (!explicit_value) {
11874+
return INDEX_DOC_CANDIDATES_BY_MODE;
11875+
}
11876+
return enabled ? INDEX_DOC_CANDIDATES_ON : INDEX_DOC_CANDIDATES_OFF;
11877+
}
11878+
11879+
/* An explicit choice is set on the pipeline; unset leaves the mode default. */
11880+
static void index_doc_candidates_apply(cbm_pipeline_t *p, index_doc_candidates_t choice) {
11881+
if (choice != INDEX_DOC_CANDIDATES_BY_MODE) {
11882+
cbm_pipeline_set_doc_candidates_enabled(p, choice == INDEX_DOC_CANDIDATES_ON);
1185211883
}
11853-
return enabled;
1185411884
}
1185511885

1185611886
/* Preserve every index option while replacing all caller-supplied repo_path
1185711887
* keys with the one canonical path that was actually authorized. A non-empty
1185811888
* project_name likewise replaces any caller "name" (#2134: the project key the
1185911889
* parent resolved for this root travels to the daemon job / worker verbatim,
11860-
* so lease key and written index cannot diverge). doc_candidates is replaced
11861-
* by the value the parent resolved. */
11890+
* so lease key and written index cannot diverge). Every caller doc_candidates
11891+
* is replaced by the explicit value the parent resolved, or dropped when it
11892+
* is unset, so the worker's index mode decides. */
1186211893
static char *index_args_with_repo_path(const char *args, const char *canonical_repo_path,
1186311894
const char *project_name,
1186411895
const cbm_index_resource_policy_t *policy,
11865-
bool doc_candidates) {
11896+
index_doc_candidates_t doc_candidates) {
1186611897
if (!args || !canonical_repo_path || !policy) {
1186711898
return NULL;
1186811899
}
@@ -11906,7 +11937,9 @@ static char *index_args_with_repo_path(const char *args, const char *canonical_r
1190611937
if ((has_name && !yyjson_mut_obj_add_strcpy(copy, copy_root, "name", project_name)) ||
1190711938
!yyjson_mut_obj_add_strcpy(copy, copy_root, "repo_path", canonical_repo_path) ||
1190811939
!cbm_mcp_index_policy_add_to_args(copy, copy_root, policy) ||
11909-
!yyjson_mut_obj_add_bool(copy, copy_root, "doc_candidates", doc_candidates)) {
11940+
(doc_candidates != INDEX_DOC_CANDIDATES_BY_MODE &&
11941+
!yyjson_mut_obj_add_bool(copy, copy_root, "doc_candidates",
11942+
doc_candidates == INDEX_DOC_CANDIDATES_ON))) {
1191011943
yyjson_mut_doc_free(copy);
1191111944
return NULL;
1191211945
}
@@ -12272,7 +12305,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
1227212305
index_args_free(repo_path, mode_str, name_override);
1227312306
return cbm_mcp_text_result(policy_error, true);
1227412307
}
12275-
bool doc_candidates = index_doc_candidates_enabled(srv, args);
12308+
index_doc_candidates_t doc_candidates = index_doc_candidates_choice(srv, args);
1227612309

1227712310
/* A daemon session delegates the one physical write to its shared job
1227812311
* registry only after path canonicalization and workspace authorization. */
@@ -12371,7 +12404,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
1237112404
free(name_override);
1237212405
cbm_pipeline_set_persistence(p, persistence);
1237312406
cbm_pipeline_set_resource_policy(p, &resource_policy);
12374-
cbm_pipeline_set_doc_candidates_enabled(p, doc_candidates);
12407+
index_doc_candidates_apply(p, doc_candidates);
1237512408

1237612409
char *project_name = heap_strdup(cbm_pipeline_project_name(p));
1237712410

@@ -20080,7 +20113,7 @@ static void *autoindex_thread(void *arg) {
2008020113
cbm_log_warn("autoindex.err", "msg", "pipeline_create_failed");
2008120114
return NULL;
2008220115
}
20083-
cbm_pipeline_set_doc_candidates_enabled(p, index_doc_candidates_enabled(srv, NULL));
20116+
index_doc_candidates_apply(p, index_doc_candidates_choice(srv, NULL));
2008420117

2008520118
/* Block until any concurrent pipeline finishes */
2008620119
cbm_pipeline_lock();

‎src/mcp/mcp_internal.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ bool cbm_mcp_jsonrpc_response_prepend_notice(char **response_io, const char *not
5454
bool cbm_mcp_index_policy_add_to_args(yyjson_mut_doc *doc, yyjson_mut_val *root,
5555
const cbm_index_resource_policy_t *policy);
5656

57+
/* Encode a doc_candidates setting the user set to a boolean on an internal
58+
* worker request (the worker reads no config). Unset or unparseable adds no
59+
* key, so the worker's index mode decides. False only when encoding fails. */
60+
bool cbm_mcp_index_doc_candidates_add_to_args(yyjson_mut_doc *doc, yyjson_mut_val *root,
61+
struct cbm_config *config);
62+
5763
enum { CBM_MCP_DEFAULT_AUTO_INDEX_LIMIT = 50000 };
5864

5965
/* Count indexable files with the pipeline's native full-mode discovery policy,

‎src/pipeline/pass_semantic_edges.c‎

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3184,8 +3184,16 @@ static void phase6c_doc_sections(cbm_pipeline_ctx_t *ctx, const cbm_sem_func_t *
31843184
}
31853185

31863186
int cbm_pipeline_pass_semantic_edges(cbm_pipeline_ctx_t *ctx) {
3187-
/* Controlled by pipeline mode (moderate/full), not env var */
3188-
cbm_log_info("pass.start", "pass", "semantic_edges");
3187+
/* Controlled by pipeline mode, not env var. Full and moderate run every
3188+
* phase. Fast mode has no SEMANTICALLY_RELATED edges: it runs the pass
3189+
* only for doc -> code candidates, doc-only -- phases 1-4 (the corpus and
3190+
* vectors phase 6c scores against) and 6c, never the LSH + scoring of
3191+
* 5-6 -- and not at all with doc candidates off. */
3192+
bool doc_only = ctx->mode == CBM_MODE_FAST;
3193+
if (doc_only && !cbm_pipeline_doc_candidates_enabled(ctx->pipeline)) {
3194+
return 0;
3195+
}
3196+
cbm_log_info("pass.start", "pass", "semantic_edges", "doc_only", doc_only ? "true" : "false");
31893197

31903198
cbm_gbuf_t *gbuf = ctx->gbuf;
31913199
cbm_sem_config_t cfg = cbm_sem_get_config();
@@ -3254,22 +3262,25 @@ int cbm_pipeline_pass_semantic_edges(cbm_pipeline_ctx_t *ctx) {
32543262

32553263
cbm_log_info("pass.semantic.vectors_stored", "count", itoa_log(func_count));
32563264

3257-
/* Phase 5: LSH hyperplanes → signatures → buckets. */
3258-
CBM_PROF_START(t_phase5);
32593265
uint64_t *signatures = NULL;
32603266
sem_bucket_t **band_buckets = NULL;
3261-
phase5_lsh_build(funcs, func_count, worker_count, &signatures, &band_buckets);
3262-
CBM_PROF_END_N("semantic_edges", "5_lsh_build", t_phase5, func_count);
3267+
int total_edges = 0;
3268+
if (!doc_only) {
3269+
/* Phase 5: LSH hyperplanes → signatures → buckets. */
3270+
CBM_PROF_START(t_phase5);
3271+
phase5_lsh_build(funcs, func_count, worker_count, &signatures, &band_buckets);
3272+
CBM_PROF_END_N("semantic_edges", "5_lsh_build", t_phase5, func_count);
32633273

3264-
cbm_log_info("pass.semantic.lsh_built", "functions", itoa_log(func_count), "bands",
3265-
itoa_log(SEM_LSH_BANDS));
3266-
sem_mem_mark("5_lsh");
3274+
cbm_log_info("pass.semantic.lsh_built", "functions", itoa_log(func_count), "bands",
3275+
itoa_log(SEM_LSH_BANDS));
3276+
sem_mem_mark("5_lsh");
32673277

3268-
/* Phase 6: Parallel scoring + sequential edge merge. */
3269-
int total_edges =
3270-
run_scoring_phase(gbuf, funcs, signatures, band_buckets, cfg, func_count, worker_count);
3278+
/* Phase 6: Parallel scoring + sequential edge merge. */
3279+
total_edges =
3280+
run_scoring_phase(gbuf, funcs, signatures, band_buckets, cfg, func_count, worker_count);
32713281

3272-
sem_mem_mark("6_score");
3282+
sem_mem_mark("6_score");
3283+
}
32733284

32743285
/* Phase 6c: doc sections -> function candidates (doc_link_candidates). */
32753286
phase6c_doc_sections(ctx, funcs, func_count, corpus, worker_count);

0 commit comments

Comments
 (0)