Repository navigation
Description
Problem
After artifact bootstrap (and to a lesser degree on steady-state incremental runs), an incremental re-index ends up re-doing almost the work of a full index. Three independent causes, verified against main (74b27f3):
P1 — Artifact bootstrap doesn't actually avoid the full-index cost
try_artifact_bootstrap (src/mcp/mcp.c) byte-copies the shared artifact DB into the local cache; the pipeline then routes to cbm_pipeline_run_incremental because hash rows exist (src/pipeline/pipeline.c, the pipeline.route path=incremental branch).
classify_files (src/pipeline/pipeline_incremental.c) classifies changed/unchanged only by mtime_ns + size. The imported file_hashes rows carry the exporter's mtimes; a fresh clone stamps every file with checkout time → essentially every file mismatches → full extract + resolve + post-passes. Bootstrap saves almost nothing on run 1 — the exact run it exists to make cheap.
Two building blocks already exist but are dead/unused in production:
cbm_artifact_commit()(src/pipeline/artifact.c) reads the git commit fromartifact.json— never called in production.cbm_store_upsert_file_hash_batch()(src/store/store.c) — exists, never called in production.
P2 — persist_hashes is row-at-a-time and double-stats every file
persist_hashes (src/pipeline/pipeline_incremental.c) re-stat()s every discovered file (classify already statted them all) and calls cbm_store_upsert_file_hash per row → one implicit SQLite write transaction per file.
Correctness bonus: re-statting after the run records the post-run mtime, so a file edited mid-index is stamped "current" and the next run misses the edit. Using the classify-time stat is both faster and more correct.
P3 — Artifact re-export on every incremental run
dump_and_persist (src/pipeline/pipeline_incremental.c): if an artifact exists, every incremental run — even a 1-file edit — reads the whole DB, zstd-compresses it, and rewrites .codebase-memory/graph.db.zst (+ metadata), churning the user's git working tree and burning I/O/CPU proportional to the whole graph.
Proposed fix (3 focused changes)
I'd like to submit these as three small, stacked PRs referencing this issue, each under ~500 lines and independently green:
- P1 — Git-based hash re-stamp at artifact import ("bootstrap reconciliation"). Right after a successful artifact import, re-stamp
file_hashesrows for files git reports unchanged between the artifact's commit and the local working tree, using localstat()values. Zero changes to the incremental pipeline itself. Graceful degradation: any uncertainty (no git, unknown commit, dirty/stale export) → skip → today's slow-safe behavior. This is the headline win. - P2 — Batch + single-stat
persist_hashes.classify_filesoutputs classify-time stamps;persist_hashesbuilds onecbm_file_hash_tarray and calls the existingcbm_store_upsert_file_hash_batch(single transaction), with a row-at-a-time fallback on batch failure. Fixes the mid-run-edit blind spot too. - P3 — Debounce artifact auto re-export. Export only when
changed_total >= 10(default) OR the artifact'sindexed_atis older than 24 h.CBM_ARTIFACT_EXPORT_MIN_CHANGES=0restores per-run export. Skip is logged.
Two design points I'd like maintainer sign-off on before/as the PRs land
Per CONTRIBUTING these touch areas that need explicit approval:
- (a) Optional
artifact.jsontrust marker (P1). Reconciliation requires a guarantee that the imported DB matches a clean checked-out tree at a known commit (otherwise a stale/corrupt artifact could mark a genuinely-changed file unchanged → graph corruption). I propose adding an optional"reconcile_basis":"git-clean-head"field to the existingartifact.json— written only when export can prove: commit is a valid hex OID, the working tree is clean outside.codebase-memory/, and everyfile_hashesrow's on-diskmtime+sizematches. No new metadata file, noschema_versionbump (older binaries ignore the unknown field). Reconciliation skips unless the marker + commit + local checks all pass. I'd appreciate a thumbs-up on reusingartifact.jsonthis way vs. any other convention you prefer. - (b) Makefile test registration (all PRs). Each PR adds tests following the existing
tests/test_*.c+TEST()+ tmpdir fixtures conventions. P1/P2/P3 unit tests extendtests/test_artifact.c(no Makefile change). A new offline integration test (tests/test_incremental_bootstrap.c) needs a one-line addition toTEST_INTEGRATION_SRCSinMakefile.cbm— flagged since CONTRIBUTING calls out Makefile changes as approval-gated.
Before/after evidence (full matrix in the PRs)
Synthetic Rust-weighted repos (500 / 2000 / 8000 modules), 3 reps, median. K = 7 changed files (5 modified + 2 added). Full protocol + raw CSV are kept outside the repo.
| Scenario | N | baseline wall / changed | candidate wall / changed |
|---|---|---|---|
| S2 bootstrap + 1st incremental | 8000 | 53691 ms / 8003 | 2563 ms / 7 |
| S2 bootstrap + 1st incremental | 2000 | 4561 ms / 2003 | 735 ms / 7 |
| S3 steady-state incremental | 8000 | 2290 ms (export ran) | 1755 ms (export skipped) |
| S4 no-op | all | 77–119 ms | 77–119 ms |
| S1 full (control) | 8000 | 3700 ms | 3866 ms (within noise) |
Candidate S2 wall is 4.8 % of baseline at N=8000 (~21× faster). Graph equivalence: after S2, candidate node/edge counts exactly equal a from-scratch full index of the same tree at every size.
Explicitly deferred (follow-up issues, not in this changeset)
- In-place SQLite delta dump instead of load-all → purge → full dump → full FTS5 rebuild (biggest remaining win, biggest blast radius).
- Gating/incrementalizing
pass_similarity+pass_semantic_edgesfor small change sets. - Populating real content hashes in the
sha256column + a hash fallback inclassify_files(covers non-git repos / mtime noise; P1's git reconciliation covers the bootstrap case). - Cross-path artifact bootstrap (discovered while benchmarking):
cbm_project_name_from_pathderives the project name from the full normalized repo path, so two clones at different paths get different project names — the imported artifact DB is keyed by the exporter's project, so the importing teammate finds 0 hash rows and falls through to a full reindex (and reconcile restamps 0). This is pre-existing (independent of these three changes) but worth tracking since it limits P1's real-world team-sharing benefit until addressed. Happy to file it separately.
Happy to split, re-scope, or drop any of the three based on your feedback. Code is written and tested (ASan/UBSan suite green locally; full matrix above) — I'll open the PRs once there's rough agreement on (a)/(b).
Activity
moofone commentedon Jul 5, 2026
Opened three stacked PRs implementing the proposal above (all DCO-signed, ASan/UBSan suite green locally, benchmark matrix attached to each):
fix(artifact): reconcile imported hashes against git on bootstrap(the headline win; includes theartifact.jsontrust-marker design from point (a)).refactor(incremental): batch + single-stat persist_hashes(also fixes the mid-run-edit correctness bug).feat(incremental): debounce artifact auto re-export(closes this issue when merged).They're stacked (#869 on #868, #870 on #869) — each PR's description notes which single commit to review. Intended merge order: #868 → #869 → #870.
Happy to rework the trust-marker convention, split/merge PRs, or hold any of them pending your feedback on (a)/(b).
DeusData commentedon Jul 5, 2026
Thanks for the very concrete breakdown and for splitting the fixes into stacked PRs. Triage: performance/correctness bug under #593. Review should proceed in order (#868 -> #869 -> #870), with special attention to artifact trust boundaries, stale hash correctness, and keeping the incremental path cheaper without making change detection less conservative.
fix(incremental): export on cumulative drift; add max-age env override
moofone commentedon Jul 5, 2026
Following up on the triage notes: I re-audited all three PRs against the stated focus areas (artifact trust boundaries, stale hash correctness, change-detection conservatism) and pushed one hardening commit to each end of the stack, plus detailed concern→code maps as comments on each PR:
git ls-tree), closing a hole where a gitignored-yet-indexed file (.cbmignorenegation, Please make ALWAYS_SKIP_DIRS configurable by user #500) was invisible togit diff/ls-filesand could have been restamped as unchanged. Regression test added.indexed_at, computed from stamps already in hand — stateless, zero extra I/O), so many consecutive below-threshold runs can no longer leave the artifact stale short of the age cap. Also adds aCBM_ARTIFACT_EXPORT_MAX_AGE_Soverride for symmetry. Tests added.Also filed #885 for the cross-path bootstrap limitation from the deferred list. Full ASan/UBSan suite green on all three branches; stack still merges in order #868 → #869 → #870.
DeusData commentedon Sep 25, 2026
@moofone, thank you again. This report and the #868–#870 stack were among the most careful performance work anyone has sent us. Here's where the three parts ended up:
3de05cd6) with your co-author credit and shipped in v0.11.0.I'm closing this in favour of #1884 and #885 so nothing is tracked twice. If you get a chance, re-running your S2 bootstrap scenario on v0.11.0 would be a great confirmation. Thanks!
docs(fork): roadmap + community alignment sweep — phase-2 index econo…
feat(watcher): two-round confirmation + cooldown + index_status fresh…