Skip to content

fix(mcp): reuse the project that owns a root on unnamed re-index (#2134) - #2344

Merged
DeusData merged 5 commits into
mainfrom
fix/issue-2134
Oct 1, 2026
Merged

DeusData merged 5 commits into
mainfrom
fix/issue-2134

Conversation

@DeusData

@DeusData DeusData commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

index_repository without name derived the project key from the path only. So re-indexing a root first indexed under an explicit name forked a second index with the same root_path, and the original stayed stale and was still served. The key is now resolved from the indexes on disk:

  • the derived project exists → keep it;
  • exactly one other project owns the canonical root → update that one in place;
  • several own it → fail loudly, list them and ask for name;
  • none → derived name, as before.

The resolved name is written into the daemon/worker args, so the lease key and the written index match.

Test: mcp::tool_index_repository_reuses_existing_project_for_root_issue2134 (fails before the fix, passes after, fails again with the fix reverted; also covers the ambiguous case). Proven end-to-end with the CLI binary: one project and one .db after an unnamed re-index, where there used to be two.

Notes for review:

Follow-up for #2144 (rebased onto main after it merged): the unnamed index_repository(status: true) poll now resolves its key the same way as the unnamed start, owner first, then path-derived. For a root owned under another name, polling with the same repo_path now finds the async job that was just started. An ambiguous root gets the same "several indexed projects share root_path" error on both calls.

Fixes #2134

index_repository without `name` derived the project key from the path
alone. When the root had first been indexed under an explicit name, a
later unnamed re-index forked a second index (same root_path,
path-derived name) and left the original untouched, so the stale index
kept being served while the call reported success.

Resolve the key from the indexes on disk before any work starts:
- the path-derived project exists -> keep it (unchanged behavior);
- exactly one other project owns the canonical root -> adopt its name
  and update it in place, so existing indexes are never orphaned;
- several projects own the root -> fail loudly, name them and ask for
  `name` instead of guessing or forking again;
- none -> first index under the derived name, as before.

The resolved name is written into the args handed to the daemon job
coordinator and the supervised worker, so the project lease key and the
index actually written stay the same project.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The memory-core ratchet flagged src/mcp/mcp.c growing by 10 raw allocator
sites (784 -> 794) from index_root_owner_resolve and its caller. The owner
list buffer, which this code allocates and frees itself, now uses
cbm_realloc/cbm_free (class other). Strings handed over by other APIs
(cbm_project_name_from_path, heap_strdup, the index argument strings) are
released through safe_free, as elsewhere in the file, so no memory the core
never counted reaches cbm_free. Behaviour is unchanged; mcp.c is back at its
baseline of 784.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…e test path

Linux, MSan, TSan and diag CI failed in mcp_mutation_guard:
tool_cross_repo_missing_inputs_fail_without_creating_ghost_databases
ASSERT(source_failed). handle_index_repository resolved the root owner
before dispatching cross-repo-intelligence mode. That test registers its
named target with the source's own root, so the unindexed source adopted
existing-cross-target as its project (log: index.root_owner_reused
project=existing-cross-target), and the missing source was no longer
reported. Cross-repo mode reads an existing source index and never writes
one, so the #2134 reuse now runs only on the re-index path, after that
dispatch. macOS CI stayed green because cbm_tmpdir() is /tmp, a symlink
to /private/tmp there, so the canonical repo_path never equalled the
fixture's stored root; with the fixture path canonicalized, the failure
reproduced 3/3 on macOS and passes 3/3 with this change.

The #2134 test itself called realpath() into a 1 KiB buffer. glibc's
FORTIFY check aborts on that ("buffer overflow detected", a SIGABRT of the
mcp suite on the ubuntu 2/3 shards), and MinGW has no realpath at all
(Windows compile error). It now uses cbm_canonical_path with a 4 KiB
buffer, the same canonicalizer production uses.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
tool_index_repository_reuses_existing_project_for_root_issue2134 counts
how often the canonical repo root occurs in list_projects. On Windows,
cbm_canonical_path returns "C:\Users\...", while the stored root_path is
"C:/Users/...", so the count was 0 and the test failed on the CLANG64
leg (test_mcp.c:8448: owners == 0, expected 1).

Normalize the expected root with cbm_normalize_path_sep, a no-op on
POSIX, before counting.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
With #2134, an unnamed index_repository of a root that another indexed
project already owns adopts that owner's name, and the daemon keys the
job by it (application_index_project_key reads the forwarded "name").
The unnamed status poll that the #2144 async reply asks for ("the same
repo_path and status: true") still derived its key from the path alone
in index_status_project_key. For an owned root it never found the job
it had just started and answered "no index job or index is known for
project '<path-derived>'"; an ambiguous root got that same misleading
answer instead of the loud error the start returns.

index_status_project_key now runs the start path's
index_root_owner_resolve on the same canonical repo_path: an explicit
name still wins, a single other owner is the key, several owners fail
with the same error naming them, otherwise the path-derived key stays.
The status error buffer grows to hold that owner list.

The daemon's other readers of the key (job dedupe in
application_job_subscribe_locked, the synchronous cut-short marker set
and taken around application_index_run_sync) derive it from the
forwarded args, which already carry the owner name, so they agree with
the start; status was the only reader that diverged.

Refs #2134, #2144

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit 3f0f49c into main Oct 1, 2026
40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

index_repository without name silently creates a duplicate index

1 participant