Skip to content

feat(mcp): opt-in async index_repository with status polling (#2144) - #2357

Merged
DeusData merged 1 commit into
mainfrom
fix/issue-2144
Sep 30, 2026
Merged

DeusData merged 1 commit into
mainfrom
fix/issue-2144

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Adds an opt-in async mode and a status query to index_repository, so indexes that outlive an MCP client's per-call deadline (e.g. Copilot for IntelliJ) still complete.

  • async: true starts (or joins) the project's index job in the daemon and returns immediately. The job is decoupled from the request and its session: a client cancel, deadline or disconnect no longer cancels it. It never queues behind the job limit (busy is reported with a retry hint).
  • status: true (same repo_path / name) reports queued / running / cancelling / succeeded / failed / cancelled, started_at, finished_at and an error summary, from a per-project record in the daemon's job registry. index_status is untouched and no freshness key is added.
  • Async needs a host that outlives the call. It is refused when the request is a one-shot cli call that is the only client of a temporary daemon (the daemon would stop and cancel the job when the command exits); the error points to codebase-memory-mcp daemon start or an open MCP session. An IDE/MCP session on a temporary daemon is a valid host. status is never refused. The daemon lifecycle is unchanged.
  • Validation: async+status together, non-boolean values, and async with cross-repo-intelligence are refused; in-process servers refuse both modes.
  • A synchronous call that gets cut short (client cancel/timeout) offers the async alternative in the cancel reply (including the frontend's -32800 reply) and as a notice on the next index/status call for that project. No new server-side timeouts or budgets.
  • Tool description/schema, CLI --help and README document the polling pattern and the host requirement. Default synchronous behaviour and its cancel-on-last-subscriber semantics are unchanged.
  • New allocations go through mem_core; make lint-ci passes (memory-core: none grew).

Tests (deterministic, held fake worker, no sleeps or wall-time assertions): async returns before completion; status polls running → succeeded; an async job survives its session closing; a sync job of the same shape is still cancelled; validation errors; the cut-short hint in the cancel reply and on the next call; refusal for a lone one-shot client of a temporary daemon while an MCP session on the same daemon is accepted. Each is RED before the change and RED on revert.

Proof: a 57 s index of dotnet/runtime src/libraries (~20.9k .cs files) started by a call that returned in 1.44 s, polled to succeeded; 595,714 nodes / 3,570,617 edges persisted.

Related: #2031 (live progress in index_status), not touched here and not conflicting.

Fixes #2144

index_repository blocks until the whole index completes. MCP clients with
a per-call deadline (Copilot for IntelliJ) give up on a large repository;
their cancel drops the daemon job's last subscriber, the daemon cancels
the worker, and every retry starts over, so the index never finishes.

Add an opt-in async mode and a status query to the same tool; the default
synchronous behaviour is unchanged:

- async: true starts the project's index job in the daemon, or joins the
  one already running for it, and returns at once with its state. The
  application holds one subscriber reference for an async job until its
  terminal publish, so the request returning, the client cancelling or
  the starting session closing no longer cancels it. Only daemon shutdown
  (or the final session of a non-permanent generation) ends it. Async
  requests never queue behind the physical job limit; a full daemon
  answers busy. async/status are stripped from the worker args, so an
  async request coalesces with an identical synchronous one.
- status: true reports the running or last job for the project
  (queued/running/cancelling/succeeded/failed/cancelled, started_at,
  finished_at, error summary) from a small per-project record kept in the
  daemon's job registry after the job itself is reaped. An unknown
  project is an error; a project indexed before this daemon generation
  reports idle. No freshness key is added: index_status keeps the #1561
  freshness object.
- async + status together, non-boolean values, and async with
  cross-repo-intelligence are refused. In-process servers (index worker,
  embedders) refuse both modes: nothing there outlives the call.
- On a temporary (non-permanent) daemon, async is refused when the
  request arrives on the one-shot `cli` tool channel and that session is
  the daemon's only live session: the daemon would stop and cancel the
  job the moment the command exits. The error points at `daemon start`
  (or an open MCP session). A long-lived MCP session stays a valid host
  even when it is the only one, which is the IDE case this exists for;
  status stays allowed everywhere. The daemon lifecycle is unchanged.
- New allocations go through the memory core (cbm_alloc/cbm_calloc/
  cbm_mem_strdup/cbm_free); blocks returned by non-core APIs are released
  with safe_free, so no file's raw allocator count grows.
- A cut-short synchronous call now offers the async alternative: in the
  cancellation text the daemon or frontend delivers (including the
  -32800 reply to a cancelled index_repository), and, because a timed-out
  client never reads that reply, as a notice on the next index_repository
  or status call for the project.
- The tool description and schema, the CLI help and the README explain
  the flags, the deadline problem and the polling pattern.

No server-side timeout or budget is introduced.

Tests (deterministic, held fake worker, bounded observation waits):
async returns before completion and status observes running then
succeeded; an async job survives its session closing while a sync job in
the same shape is still cancelled; validation errors; the cancellation
reply and the next-call notice carry the advice; a lone one-shot client
of a temporary daemon is refused async while status and an MCP session
on the same daemon are allowed; in-process refusal; schema; notice
helper.

Refs #2031

Fixes #2144

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData mentioned this pull request Sep 25, 2026
2 tasks done
@DeusData
DeusData merged commit cb251ed into main Sep 30, 2026
41 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.

Failed to index rdue to timeout

1 participant