Skip to content

Commit f14844b

Browse files
committed
feat(index): enforce opt-in storage resource limits
An index that fits in memory and finishes in time can still fill the disk. Publication needs room for the staging artifacts and the final database at the same time, and running out of space during publication is the one failure that can cost a working index. Add index_cache_max_mb and index_min_free_disk_mb, measured before staging and again before publication, together with internal ceilings on the final database, the staging artifacts and the task temporary directory. Those three have no public keys because they are only meaningful as part of one composed decision. Crossing any of them fails the attempt before the old database is touched, so atomic publication is unchanged and the previous index keeps serving. Staging cleanup is scoped by a private per-task token, so a worker removes only the artifacts it created and never a concurrent run's. An old database is treated as replaceable only after an integrity verdict distinguishes real corruption from a transient busy error. A probe that cannot complete fails closed. Both public keys default to off. The shell fixture that stands in for the supervisor names them too, so the worker still recognises the policy it is handed. Signed-off-by: 刘冲 <mail@liuchong.dev>
1 parent eaab28c commit f14844b

26 files changed

Lines changed: 1770 additions & 99 deletions

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -671,12 +671,16 @@ codebase-memory-mcp config set index_max_files 250000 # optional per-index so
671671
codebase-memory-mcp config set index_max_source_mb 16384 # optional per-index source-size limit
672672
codebase-memory-mcp config set index_max_rss_mb 8192 # optional worker-tree current RSS limit
673673
codebase-memory-mcp config set index_max_duration_seconds 3600 # optional total worker duration
674+
codebase-memory-mcp config set index_cache_max_mb 32768 # optional projected cache-size limit
675+
codebase-memory-mcp config set index_min_free_disk_mb 4096 # optional free-space reserve
674676
codebase-memory-mcp config reset auto_index # reset to default
675677
```
676678

677-
The four `index_max_*` settings default to `off`. Exceeding one fails the complete
678-
index attempt rather than publishing a partial graph; an existing serving index
679-
is preserved. See [Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
679+
The six index resource settings default to `off`. Exceeding one fails the
680+
complete index attempt rather than publishing a partial graph; an existing
681+
serving index is preserved. Storage probes also fail closed when an enabled
682+
measurement cannot be completed. See
683+
[Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
680684

681685
### Environment Variables
682686

‎docs/CONFIGURATION.md‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,17 @@ Current keys:
9090
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
9191
| `index_max_rss_mb` | `off` | Optional maximum current RSS in MiB for the complete contained index-worker process tree (`64..1048576`). |
9292
| `index_max_duration_seconds` | `off` | Optional maximum total worker duration in seconds (`1..86400`). |
93-
94-
The four `index_max_*` settings are independent and disabled by default. They
95-
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
96-
to `cross-repo-intelligence`, which does not scan repository source files.
97-
Equality is allowed; exceeding any setting fails the complete index request and
98-
preserves any previously serving database. Worker RSS covers descendants and is
99-
not the same as the internal `CBM_MEM_BUDGET_MB` allocation budget. Total
100-
duration is independent of the existing 15-minute no-log-progress timeout. See
93+
| `index_cache_max_mb` | `off` | Optional maximum projected cache size in MiB after publication. |
94+
| `index_min_free_disk_mb` | `off` | Optional minimum free MiB reserved on the cache filesystem while indexing. |
95+
96+
The six index resource settings are independent and disabled by default.
97+
They apply to explicit indexing, automatic indexing, and watcher re-indexing,
98+
but not to `cross-repo-intelligence`, which does not scan or publish repository
99+
source indexes. Equality is allowed; exceeding a setting fails the complete
100+
index request and preserves any previously serving database. Worker RSS covers
101+
descendants and is not the same as the internal `CBM_MEM_BUDGET_MB` allocation
102+
budget. Total duration is independent of the existing 15-minute no-log-progress
103+
timeout. See
101104
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
102105
error-response details.
103106

‎docs/INDEX_RESOURCE_LIMITS.md‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,31 @@ Worker limit failures use the same shape with `stage=worker`,
9494
and omit `observed`, `limit`, and `unit` because no trustworthy observation was
9595
available.
9696

97+
## Storage settings
98+
99+
| Key | Default | Accepted value | Protects |
100+
|---|---:|---:|---|
101+
| `index_cache_max_mb` | `off` | `off` or `1..1048576` | Projected cache bytes after replacement |
102+
| `index_min_free_disk_mb` | `off` | `off` or `1..1048576` | Free bytes reserved on the cache filesystem |
103+
104+
Set or reset these keys through the same `config set` and `config reset`
105+
commands. With both keys `off`, indexing does not scan the cache tree or probe
106+
filesystem capacity.
107+
108+
Projected cache usage is the current cache size, minus the old project
109+
database and SQLite sidecars only when that generation is confirmed valid and
110+
replaceable, plus the current operation's staging artifacts. Other projects
111+
and unrelated files always count toward the limit and are never evicted.
112+
113+
Free space is checked before staging, after the staged build completes, and
114+
immediately before atomic publication. Equality is allowed. An enabled
115+
measurement that cannot be completed fails closed with
116+
`code: "resource_probe_failed"` and
117+
`stage: "storage"`. A limit breach uses `code: "resource_limit_exceeded"`.
118+
Both cases preserve the old serving database. Staging files created by a
119+
terminated supervised worker are tagged with a private task token and removed
120+
after its process tree is quiescent; cleanup cannot match another attempt.
121+
97122
## Trust and compatibility
98123

99124
Limits are read from the CLI-managed `_config.db`; they are not MCP request
@@ -104,5 +129,5 @@ parent policy.
104129
These settings do not replace or increase `auto_index_limit`, change the 512 MiB
105130
single-file cap, alter workspace-root authorization, or affect
106131
`cross-repo-intelligence`. With all settings `off`, discovery follows the
107-
existing path and the supervisor performs no periodic RSS probe or total-duration
108-
termination.
132+
existing path, the supervisor performs no periodic RSS probe or total-duration
133+
termination, and the pipeline performs no cache-tree or free-space probe.

‎scripts/test-runtime.sh‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ _cbm_test_runtime_daemon() {
113113
# nothing else.
114114
cbm_test_index_worker_policy_json() {
115115
printf '%s' '"_cbm_index_policy":{"index_max_files":"off","index_max_source_mb":"off"'
116-
printf '%s' ',"index_max_rss_mb":"off","index_max_duration_seconds":"off"}'
116+
printf '%s' ',"index_max_rss_mb":"off","index_max_duration_seconds":"off"'
117+
printf '%s' ',"index_cache_max_mb":"off","index_min_free_disk_mb":"off"}'
117118
}
118119

119120
cbm_test_runtime_cleanup() {

‎src/cli/cli.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6779,6 +6779,9 @@ static const config_key_def_t CONFIG_KEYS[] = {
67796779
{CBM_INDEX_CONFIG_MAX_SOURCE_MB, "off", "Max accepted source MiB per index, or off"},
67806780
{CBM_INDEX_CONFIG_MAX_RSS_MB, "off", "Max worker process-tree RSS MiB, or off"},
67816781
{CBM_INDEX_CONFIG_MAX_DURATION_SECONDS, "off", "Max worker duration in seconds, or off"},
6782+
{CBM_INDEX_CONFIG_CACHE_MAX_MB, "off", "Max projected cache MiB after publish, or off"},
6783+
{CBM_INDEX_CONFIG_MIN_FREE_DISK_MB, "off",
6784+
"Minimum free cache-filesystem MiB during indexing, or off"},
67826785
};
67836786

67846787
/* #1558: ui_enabled and ui_port were reachable ONLY by hand-editing

‎src/daemon/application.c‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,16 +306,19 @@ static int application_worker_start_default(void *context, const char *args_json
306306
(void)context;
307307
cbm_index_resource_policy_t resource_policy;
308308
char error[CBM_SZ_256] = {0};
309+
char task_db_path[CBM_SZ_1K];
309310
if (!cbm_mcp_index_policy_from_internal_args(args_json, &resource_policy, error,
310-
sizeof(error))) {
311-
cbm_log_error("daemon.index.policy", "error", error);
311+
sizeof(error)) ||
312+
!cbm_mcp_index_task_db_path(args_json, task_db_path, sizeof(task_db_path))) {
313+
cbm_log_error("daemon.index.worker_policy", "error",
314+
error[0] ? error : "could not resolve worker index path");
312315
*worker_out = NULL;
313316
return -1;
314317
}
315318
cbm_index_worker_handle_t *worker = NULL;
316-
int result =
317-
cbm_index_worker_start_with_policy(args_json, memory_budget_bytes, &resource_policy, false,
318-
marker_file, quarantine_file, &worker);
319+
int result = cbm_index_worker_start_with_storage_policy(
320+
args_json, memory_budget_bytes, &resource_policy, task_db_path, false, marker_file,
321+
quarantine_file, NULL, NULL, &worker);
319322
*worker_out = worker;
320323
return result;
321324
}

‎src/daemon/bootstrap.c‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ static bool bootstrap_worker_budget_valid(const char *text) {
9595
return value > 0;
9696
}
9797

98+
static bool bootstrap_worker_stage_token_valid(const char *token) {
99+
size_t length = token ? strlen(token) : 0;
100+
if (length < 6U || length >= 64U) {
101+
return false;
102+
}
103+
for (size_t index = 0; index < length; index++) {
104+
unsigned char ch = (unsigned char)token[index];
105+
if (!((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
106+
ch == '-' || ch == '_')) {
107+
return false;
108+
}
109+
}
110+
return true;
111+
}
112+
98113
/* Keep the bootstrap role boundary exact and fail closed before any client or
99114
* worker state is initialized. index_supervisor owns the matching builder and
100115
* performs the captured-build comparison after this syntax-only classification. */
@@ -129,6 +144,12 @@ static bool bootstrap_worker_argv_exact(int argc, char *const argv[]) {
129144
}
130145
next += 2;
131146
}
147+
if (next < argc && bootstrap_arg_is(argv[next], "--index-worker-stage-token")) {
148+
if (next + 1 >= argc || !bootstrap_worker_stage_token_valid(argv[next + 1])) {
149+
return false;
150+
}
151+
next += 2;
152+
}
132153
return next == argc;
133154
}
134155

0 commit comments

Comments
 (0)