GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

24. Probe the embedding endpoint before a forced rebuild empties the index

Context and problem

A forced rebuild — groove index --force, and the MCP tool rebuild_index with force: true — starts by emptying the index (reset_for_model in grooveseek/src/db/meta.rs, reached through reset_and_resolve_context_mode in grooveseek/src/indexer.rs). The reset runs in a transaction of its own and commits. Documents are embedded afterwards, one file at a time.

With FastEmbed that order costs nothing: the model is loaded before the reset and runs in the process. With provider = "openai-compatible" (ADR-0022) the provider is built without contacting the endpoint, and the first request goes out only after the reset has committed. A wrong or revoked api_key (401), an endpoint that is down, a rate limit (429), or a server now answering with vectors of another length all surface on that first request — after the index is already empty. The command fails, and what it leaves behind is an index with no documents in it.

The MCP path makes this worse. rebuild_index is reachable by anything that can reach the HTTP port, and GrooveSeek has no authentication by design, so a caller can empty the served index again and again while the endpoint refuses.

ADR-0022 decided that GrooveSeek never probes the endpoint. Its reasons were about opening an index: startup and Config::validate would stop being deterministic, a service that is down would look like a configuration error, and a request would leave the machine before the index had been opened. The question here is narrower: may a command whose purpose is to send the knowledge base to the endpoint first check that the endpoint will answer, before it destroys the index it is about to replace?

Decision drivers

Options considered

  1. Before the reset, embed one fixed text through the document side, and only in a forced rebuild. Taken.

  2. Build the new index beside the old one and swap it in on success. Rejected for now. A rebuild commits file by file, so a run that stops partway keeps what it finished; a whole-run transaction would hold the write lock for the length of the rebuild, blocking the watcher, and a shadow database doubles the disk while a vec_chunks of a new dimension is built. It is the stronger guarantee — it also covers an endpoint that fails halfway through — and a much larger change than this problem needs.

    The condition that would reopen it: a rebuild that is moved to build-then-swap for another reason. The probe then has nothing left to protect and can be removed.

  3. Delay the reset until the first real batch has been embedded. Rejected. The reset precedes several writes that assume an empty index (the context mode, the code chunk budget and policy, the declared-field set), and the first embed sits inside the per-file loop, after files have been read and chunked. Moving the destructive step into that loop spreads it across the code the reset exists to keep simple, to save one small request.

  4. Probe everywhere the provider is built — serve startup, validate, every index. Rejected, for the reasons ADR-0022 gave; nothing here changes them. A non-forced index destroys nothing, so a failure there costs no more than the run itself.

  5. Leave it, and document that --force needs a working endpoint. Rejected. The loss is silent until the next search returns nothing, and over MCP it can be repeated by any caller.

Decision

A forced rebuild sends one probe to the embedding provider before it resets the index. Nothing else probes.

Consequences

References