Skip to content

Commit d82f760

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 87da442 commit d82f760

26 files changed

Lines changed: 1736 additions & 89 deletions

‎README.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -737,12 +737,16 @@ codebase-memory-mcp config set index_max_files 250000 # optional per-index so
737737
codebase-memory-mcp config set index_max_source_mb 16384 # optional per-index source-size limit
738738
codebase-memory-mcp config set index_max_rss_mb 8192 # optional worker-tree current RSS limit
739739
codebase-memory-mcp config set index_max_duration_seconds 3600 # optional total worker duration
740+
codebase-memory-mcp config set index_cache_max_mb 32768 # optional projected cache-size limit
741+
codebase-memory-mcp config set index_min_free_disk_mb 4096 # optional free-space reserve
740742
codebase-memory-mcp config reset auto_index # reset to default
741743
```
742744

743-
The four `index_max_*` settings default to `off`. Exceeding one fails the complete
744-
index attempt rather than publishing a partial graph; an existing serving index
745-
is preserved. See [Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
745+
The six index resource settings default to `off`. Exceeding one fails the
746+
complete index attempt rather than publishing a partial graph; an existing
747+
serving index is preserved. Storage probes also fail closed when an enabled
748+
measurement cannot be completed. See
749+
[Index resource limits](docs/INDEX_RESOURCE_LIMITS.md).
746750

747751
### Environment Variables
748752

‎docs/CONFIGURATION.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ Current keys:
9393
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
9494
| `index_max_rss_mb` | `off` | Optional maximum current RSS in MiB for the complete contained index-worker process tree (`64..1048576`). |
9595
| `index_max_duration_seconds` | `off` | Optional maximum total worker duration in seconds (`1..86400`). |
96+
| `index_cache_max_mb` | `off` | Optional maximum projected cache size in MiB after publication. |
97+
| `index_min_free_disk_mb` | `off` | Optional minimum free MiB reserved on the cache filesystem while indexing. |
9698

9799
> **`watcher_enabled` vs `auto_watch`.** `watcher_enabled` controls whether the
98100
> watcher *subsystem* starts at all (the background poll thread). `auto_watch` is
@@ -120,13 +122,14 @@ Current keys:
120122
> `auto_index` still runs, and `index_repository` stays available for manual
121123
> reindexing.
122124
123-
The four `index_max_*` settings are independent and disabled by default. They
124-
apply to explicit indexing, automatic indexing, and watcher re-indexing, but not
125-
to `cross-repo-intelligence`, which does not scan repository source files.
126-
Equality is allowed; exceeding any setting fails the complete index request and
127-
preserves any previously serving database. Worker RSS covers descendants and is
128-
not the same as the internal `CBM_MEM_BUDGET_MB` allocation budget. Total
129-
duration is independent of the existing 15-minute no-log-progress timeout. See
125+
The six index resource settings are independent and disabled by default.
126+
They apply to explicit indexing, automatic indexing, and watcher re-indexing,
127+
but not to `cross-repo-intelligence`, which does not scan or publish repository
128+
source indexes. Equality is allowed; exceeding a setting fails the complete
129+
index request and preserves any previously serving database. Worker RSS covers
130+
descendants and is not the same as the internal `CBM_MEM_BUDGET_MB` allocation
131+
budget. Total duration is independent of the existing 15-minute no-log-progress
132+
timeout. See
130133
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
131134
error-response details.
132135

‎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
@@ -7164,6 +7164,9 @@ static const config_key_def_t CONFIG_KEYS[] = {
71647164
{CBM_INDEX_CONFIG_MAX_SOURCE_MB, "off", "Max accepted source MiB per index, or off"},
71657165
{CBM_INDEX_CONFIG_MAX_RSS_MB, "off", "Max worker process-tree RSS MiB, or off"},
71667166
{CBM_INDEX_CONFIG_MAX_DURATION_SECONDS, "off", "Max worker duration in seconds, or off"},
7167+
{CBM_INDEX_CONFIG_CACHE_MAX_MB, "off", "Max projected cache MiB after publish, or off"},
7168+
{CBM_INDEX_CONFIG_MIN_FREE_DISK_MB, "off",
7169+
"Minimum free cache-filesystem MiB during indexing, or off"},
71677170
};
71687171

71697172
/* #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
@@ -307,16 +307,19 @@ static int application_worker_start_default(void *context, const char *args_json
307307
(void)context;
308308
cbm_index_resource_policy_t resource_policy;
309309
char error[CBM_SZ_256] = {0};
310+
char task_db_path[CBM_SZ_1K];
310311
if (!cbm_mcp_index_policy_from_internal_args(args_json, &resource_policy, error,
311-
sizeof(error))) {
312-
cbm_log_error("daemon.index.policy", "error", error);
312+
sizeof(error)) ||
313+
!cbm_mcp_index_task_db_path(args_json, task_db_path, sizeof(task_db_path))) {
314+
cbm_log_error("daemon.index.worker_policy", "error",
315+
error[0] ? error : "could not resolve worker index path");
313316
*worker_out = NULL;
314317
return -1;
315318
}
316319
cbm_index_worker_handle_t *worker = NULL;
317-
int result =
318-
cbm_index_worker_start_with_policy(args_json, memory_budget_bytes, &resource_policy, false,
319-
marker_file, quarantine_file, &worker);
320+
int result = cbm_index_worker_start_with_storage_policy(
321+
args_json, memory_budget_bytes, &resource_policy, task_db_path, false, marker_file,
322+
quarantine_file, NULL, NULL, &worker);
320323
*worker_out = worker;
321324
return result;
322325
}

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