Tools
This page documents Orleans.Lattice.Api.Mcp.RepoContext, which is unreleased, in the documentation for Orleans.Lattice 9.9.0 (release line 9.9), built 2026-10-04. It is also published as markdown, with every table and list, at tools.md, and llms.txt lists every page.The module contributes the repocontext_* MCP tools in two host-selected shapes. In the default single-repository mode it offers the read-only tools, offered to any caller whose data read-or-write permission unlocks the repository-context group, plus the mutating tools, contributed only when the host calls AddRepoContextTools(enableWrites: true). In workspace mode - what the bundled container runs - the read-only repocontext_list_repos is added and the mutating repocontext_bootstrap is replaced by repocontext_add_repo, repocontext_remove_repo, and repocontext_reset_index, so the client manages many repositories under one mounted root and can repair a wedged code index without destroying the memory attached to that repository. Every tool, in either mode, clears the same fail-closed authorization gate at both advertisement and invocation.
In either mode the onboarding tool - repocontext_bootstrap in single-repository mode, repocontext_add_repo in workspace mode - takes the working tree to walk from the wire, so it additionally requires a configured workspace root. Pass workspaceRoot to AddRepoContextTools: the onboarding tools (and the read-only repocontext_changed, which also takes a caller-supplied path) are advertised only when a root is supplied there, and each re-checks at invocation that the effective path guard is enforcing, refusing otherwise, because an unbounded guard would let any caller with a write grant have the server index an arbitrary directory on the host.
Read-only tools
Always contributed to a caller whose effective permissions include a data-plane operation that unlocks the repository-context group, regardless of the enableWrites flag. The one exception is repocontext_changed, which takes a caller-supplied path and is therefore contributed only when a workspace root is configured (see its row).
| Tool | What it does |
|---|---|
repocontext_health |
With repoId, reports the passive per-repository verdict and component evidence described under Repository readiness. Without repoId, preserves the following host-only behavior: reports whether the repository-context surface is registered and reachable for the authenticated caller (available), and whether retrieval can actually serve (retrievalReady, retrievalPhase), alongside the facade group (always repocontext) and a human-readable status line that names any degradation. Returns success only when the caller cleared the authorization gate, so an agent can confirm the surface is wired end to end before using the other tools. available reports reachability only and stays true on a degraded host, because capture, recall, and scan keep working there; read retrievalReady for whether searches are trustworthy. A retrievalPhase of building means the vector plane is not serving and searches are answered by degraded keyword recall, so treat results as incomplete; keyword_only is an intended deployment with no embedding provider bound and IS ready. serving (semantic retrieval is serving) and nothing_registered (no repository is registered yet, so there is nothing to serve) are ready too. With repoId, the same tags describe the requested repository, and a nothing_registered verdict there - a measured empty vector corpus together with a completed zero-file ingest - reports retrievalReady: false instead (see Repository readiness). A retrievalPhase of saturated_unavailable means the vector plane is not serving and is not expected to start at the present capacity, because an admission gate refused its open past a declared bound (issue #3286); it is distinct from building precisely so a plane that will never arm is distinguishable from one still arming, and it clears by itself once admission recovers. |
repocontext_recall |
Fetches a single record by its full key - a structural node, a symbol, or a memory entry - and returns its flattened fields, tags, links, and remaining life. For a memory entry it also evaluates link staleness: every live structural link is checked against its target's present state, and one whose digest has drifted, whose target has no live record at all, or for which no digest was ever captured is reported through stale and staleLinks. The subset pointing at nothing is also named in danglingLinks - always a subset, never a partition - because the two states have opposite remedies: drift asks the caller to re-read the file, a dangling link asks it to wait for the target to be indexed. A key with no live entry returns exists=false, so an absent or expired entry is distinguishable from an empty one. |
repocontext_scan |
Walks an ordered range under a scope (all files, packages, or symbols; all memory; or the memory under one topic) and returns one page at a time with an opaque continuation token. Expired and tombstoned entries are never returned. As a bulk read it does not evaluate staleness, so stale/staleLinks/danglingLinks come back null ("not evaluated"), mirroring the expiry convention; repocontext_recall a key for its authoritative staleness. |
repocontext_list_topics |
Enumerates the distinct memory topics for a repository, each with its live entry count, so an agent can discover what notes and decisions exist before recalling them. |
repocontext_search |
Finds the records most relevant to a natural-language query, ranked best-first and hydrated from the store of record. Runs a semantic (nearest-neighbour) search when an embedding provider and vectors are available - answered from the approximate index by default, or by the complete-recall exact scan when the host sets LATTICE_REPOCONTEXT_SEMANTIC_RETRIEVAL=exact; otherwise degrades to a deterministic keyword/structural scan that ranks file content as well as names with Okapi BM25. The result's mode reports which path answered (semantic, keyword, or empty), and retrievalPath names the guarantee or keyword cause that served it (see Semantic search). Every hit also carries a machine-readable, deterministic reasons list explaining why it ranked - a semantic hit lists semantic, the matched chunk kind (chunk:file, chunk:symbol, or chunk:memory), and symbol:<fqName> for a symbol or topic:<topic> for a memory entry; a keyword hit lists whichever projected fields the query terms hit (path-name-match, symbol:<fqName>, tag:<tag>, topic-match, content-match, key-match). |
repocontext_outline |
Returns the structural skeleton of one indexed file without reading its body: each declared symbol (kind, signature, and 1-based start/end line span) ordered by position, plus the token cost of reading the whole file under the configured tokenizer profile. The cheapest way to grasp a file's shape and decide whether a full read is worth the tokens. The token count is null only when the file was never content-processed; a path with no stored file node returns exists=false. A pure read over stored records that never touches the workspace on disk. |
repocontext_changed |
Reports how the current workspace has drifted from the stored index - the files added, updated, and removed - by comparing each file's content digest against the indexed digest, without invoking git, so it works in any checkout. It also lists the indexed files that depend on the changed ones (the reverse-reference impact set), so an agent sees the blast radius of a set of edits before re-indexing. The walk is rooted at the repository's indexed root and reuses the filters it was ingested with, so the report always compares the same path space the index was built in; path is a scope - pass the repository root for the whole tree, or a directory inside it to restrict the report to that subtree - and a path outside the indexed root is refused rather than compared. Unchanged files are settled by a stat against the stored size and ingest anchor instead of being re-read, so a whole-repository report stays cheap. The workspace is walked only through the fail-closed workspace boundary; a path resolving outside the mounted workspace is refused, and so is a repository with no persisted onboarding request, since there is then no indexed root to contain the caller's path against. Because the path is caller-supplied, the tool is contributed only when a workspace root is configured - under an inert guard it is withheld exactly as the onboarding tools are. |
repocontext_related |
Resolves the structural neighbourhood of one file so an agent can navigate the code graph without full-file reads: the type-names the file references (outbound imports), the indexed symbols that reference the file's declarations (inbound dependents, resolved to their declaring files), and the test types that cover it (from the {Name}Tests/{Name}Test convention). Dependents and tests are maintained incrementally from a reverse cross-reference projection, so the lookup is a bounded read. Edges are keyed by simple (unqualified) type-name, a syntactic approximation. A path with no stored file node returns exists=false. |
repocontext_context |
Returns a ranked, explained bundle of source for a natural-language task, packed under a hard token ceiling in a single call - collapsing the search -> recall -> read loop into one round trip that can never overrun the context budget. It packs each resolved file at a detail level (paths, outline, or slices; auto picks the richest level that yields a non-empty bundle). Every entry carries its match reasons, its exact BPE tokenCount, and the whole-file fullReadTokenCount; responseTokens (the estimated cost of the response as delivered - content plus JSON envelope, multiplied by a conservative dual-emission factor of 3.5 because the SDK serializes every result twice and the second copy is escaped JSON text) never exceeds budgetTokens (an empty bundle reports 0), while totalTokens reports the narrower BPE sum of the packed source alone. A unit is a descriptor, not a second copy of the text: an entry's content is the newline-join of its units, so source ships exactly once. When even the cheapest entry does not fit it fails closed (entries empty, retryBudgetTokens reports a budget guaranteed to admit one entry). Reuse economics: each delivered unit carries a stable opaque receipt and each entry a per-version contentHash; hand receipts back in seen, assert whole-file possession as known (path@hash), or pass a session to persist that bookkeeping across calls. A whole-file claim is honoured only for a version actually delivered as a complete body, so partial evidence can never be promoted to whole-file possession. Suppressed content is acknowledged in reused and is never charged against top or the budget. |
repocontext_stats |
Reports an aggregate summary of the surface's own usage over a bounded recent window, so a team can see whether it actually reduces context cost. Returns only summed token figures: calls answered (calls), the response tokens spent (responseTokens - each bundle's own conservative estimate of what its response cost, not a BPE count), the whole-file read tokens conservatively replaced (readsReplacedTokens - credited only for delivered whole-file-equivalent content, never for discovery, partial detail, or content the caller already held), the net tokens saved (netSavedTokens - readsReplacedTokens minus responseTokens, so it is negative when the responses cost more than the reads they replaced), and the window length (windowSeconds - the last hour, so it reads 3600). Only repocontext_context calls are recorded, and the window is kept in memory on the host that answers. It carries no body, query, path, or repository identity. |
repocontext_index_status |
Reports the progress of a repository's indexing job: its lifecycle status (None, Running, Completed, or Failed), the current phase (Pending, Walking, Reconciling, Applying, Vectorising, or Done for a build, and Resetting while a repocontext_reset_index sweep runs), the running file and chunk counters, the symbolsEmbedded and filesContentProjected counters, the treesSwept and entriesDeleted counters a reset advances, the attempt count, timing, any failure reason, and - while the run is Running - a live pacing reading (see Adaptive pacing). A repository that was never onboarded reports status=None without erroring. Because onboarding runs asynchronously, a caller polls this tool to follow a repocontext_bootstrap or repocontext_add_repo pass to completion. |
repocontext_neighbors |
Walks the typed knowledge-linking edges out of a memory entry and returns the adjacent entries, hydrated from the store, as a bounded breadth-first traversal - optionally restricted to one relation, up to a depth of hops, and stopping once maxNodes neighbours are collected (truncated reports that the cap stopped the walk with at least one further linked record unvisited). A seed key with no live entry returns exists=false; a dangling edge whose target has no live value is still returned with its own exists=false, so a broken link is observable. Each walked memory entry has its link staleness evaluated (stale/staleLinks/danglingLinks), as repocontext_recall does. Use it to explore the curated concept graph an agent has captured across sessions. |
repocontext_claim_status |
Reports the claim recorded on a memory record - whether a claim is live (claimed), its fencing token (fencingToken), the highest released token (releasedFencingToken), and the owner and region that took it - alongside the live state of the distributed lock that grants it (lockName, isHeld, lockFencingToken, leaseExpiresAtUtc, and the queueDepth of agents waiting behind it). A key with no live record reports exists=false, so an absent record is distinguishable from an unclaimed one. The two halves can disagree, and the disagreement is informative: after a lease lapses, claimed stays true with the last fencingToken while isHeld reads false and lockFencingToken reads 0, until the next claim or a release; a record that was never claimed reports claimed=false and no fencingToken. Use it to see who holds an item and how deep the queue is before deciding to wait. The answer is a point-in-time observation and never an entitlement: authoritative is always false, because a claim can lapse or be superseded between this read and the caller's next write. Only repocontext_claim grants the right to write, and only the fencing check on the write path enforces it - never gate a write on this tool's answer. |
Mutating tools
Contributed only under enableWrites: true. Each is annotated destructive and offered only to a caller who cleared the gate, whose grant includes a write-shaped data-plane operation (write, delete, range delete, CRDT apply, atomic write, or bulk load), and for whom the host opted writes in.
| Tool | What it does |
|---|---|
repocontext_bootstrap |
Onboards a codebase: walks the repository at repoRoot, records a structural node and content digest for every file under the repoId keyspace, and reconciles the scan against the stored records. Starts asynchronously off the request thread and returns the running job's acceptance snapshot at once (poll repocontext_index_status for the outcome), so a dropped client stream never aborts an index. Idempotent and resumable - re-running on an unchanged repository is a no-op, a changed repository updates only changed files and prunes deleted ones, and an interrupted run resumes without duplication. A repository configured with a git source is refused, because the mounted and git index sources are mutually exclusive. |
repocontext_remember |
Creates or updates a memory or decision entry under a topic, with an optional time-to-live. Omit id to create a new entry with a generated id; supply an existing id to merge into it in place with CRDT semantics. When no explicit ttlSeconds is given, a new entry inherits the repository's default memory TTL if one is configured, otherwise it is durable. |
repocontext_update |
Patches scalar fields and tags on an existing structural or memory record using CRDT-merge semantics: each field is a last-writer-wins register applied at a fresh logical tick, so concurrent updates converge instead of clobbering each other. Any remaining time-to-live is preserved. Fails if no record exists at the key. |
repocontext_forget |
Removes an entry. By default it hard-deletes immediately; set lapse to true to re-write it with a short time-to-live (lapseSeconds, default 60 seconds) so it lapses on its own, letting concurrent readers drain gracefully. On a memory entry whose value decodes, the lapse is written through the multi-value register and bound by its later-expiry rule: it lapses a durable entry, but it cannot shorten one whose existing expiry is later than the lapse window, and the result's expiresAtUtc reports the expiry actually in force (see Memory and TTL). A lapse succeeds even when the stored value cannot be decoded - retiring a record never requires reading it, so a corrupt entry is recoverable without the hard delete that would destroy it - and reports that case as undecodable. |
repocontext_claim |
Claims a memory record for exclusive authorship and returns the fencing token every later write under that claim must present, so two agents draining one backlog cannot both believe they own an item. It introduces no locking scheme of its own: mutual exclusion, first-in-first-out fairness, the bounded lease, expiry reclaim, and the strictly increasing token all come from the cluster's distributed lock, and this tool records the granted token on the record as a fencing high-water mark. Losing a race is reported, not thrown - granted=false with a reason of contended, timeout, or missing. Omit maxWaitSeconds to fail fast, which is what a work-stealing agent wants; supply it to queue. Omitting leaseSeconds requests the cluster's deliberately short default lease (LatticeOptions.DefaultLockLeaseDuration, 30 seconds unless the host overrides it), and every lease is clamped to LatticeOptions.MaxLockLeaseDuration (5 minutes by default; the bundled container raises it to 30 minutes through LATTICE_MAX_LOCK_LEASE_SECONDS), so honour the returned leaseSeconds and leaseExpiresAtUtc rather than the length asked for, and renew before it lapses. Memory records only, because the fencing check is enforced on the memory record's own write path. |
repocontext_renew_claim |
Extends the lease on a claim the caller already holds, without changing its fencing token, so a long-running agent keeps its claim alive rather than having it reclaimed mid-task. A renew presenting a token that no longer holds the lock returns granted=false with reason superseded rather than throwing: that is the authoritative signal that the holder's lease is gone. The lock reclaims a lapsed lease at the start of every renew, waiter or not, so every renew after a lapse reports superseded whether or not anyone has claimed the item since - and the next claim, by anyone, is granted a strictly higher token that fences this one out. The holder must stop writing and re-claim rather than continue. Always pass leaseSeconds explicitly: omitting it defers to the same deliberately short cluster default a claim uses, so a renew that omits it shortens a claim currently held for longer while still reporting granted=true, with the loss surfacing only on the next renew. A renew that shortens its lease sets leaseShortened, alongside previousLeaseExpiresAtUtc; a null there means the prior lease could not be read, never that nothing shrank. |
repocontext_release_claim |
Releases a claim, handing the record to the next waiter in the lock's queue and marking the record's claim as no longer live so unfenced writes are admitted again. It never lowers the fencing high-water mark, so a released token stays refused: write every result owed to the record before releasing. Idempotent and safe to retry, and never an error. The outcome is decided by the record's fence, not by the lock: a token that is still the record's high-water mark is released (released=true) even if its lease has lapsed, and repeating that release reports the same; a token below or above the high-water mark is a no-op reported as released=false with reason stale; and released=false carries reason missing for a record that does not exist, or unclaimed for one that was never claimed. |
How a claim is enforced
The claim tools would be advisory - a flag one agent could ignore - if nothing checked them. They are not. Once a memory record carries a claim, repocontext_remember, repocontext_update, and repocontext_forget all evaluate a fencing check on that record before applying anything, and the write is refused with RepoContextClaimConflictException when it fails. None of the three has a bypass or a per-tool opt-out, so a claim constrains every write those tools make to the record, including its body. The fence is per record, so it does not stand in the way of repocontext_remove_repo, which drops the repository's memory wholesale - claimed records included - without consulting any claim.
The check is deterministic, and a caller can reason about which outcome it will get. A refusal names its cause in RepoContextClaimConflictException.Reason, as the value shown in parentheses:
| Record state | Token presented | Outcome |
|---|---|---|
| Never claimed | any, or none | Accepted - an unclaimed record behaves exactly as it did before the claim surface existed |
| Claim live | none | Refused (ClaimRequired) |
| Claim released | none | Accepted - a released record readmits ordinary unfenced writes |
| Claimed (live or released) | below the record's high-water mark | Refused (StaleToken) - a superseded holder can never write, even after the claim is released |
| Claimed (live or released) | above the high-water mark, but not the token the lock currently holds | Refused (UnissuedToken) - a token ahead of the record's stamp is honoured only when the lock confirms it issued it, so a caller cannot write under an integer of its own choosing |
| Claim released | at or above the high-water mark | Refused (ClaimReleased) - re-claim first |
| Claim live | at or above the high-water mark, claim's region | Accepted |
| Claim live | at or above the high-water mark, another region | Refused (ForeignRegion) |
A claim is live from the moment it is stamped until it is released; the write path never reads the lease's expiry. A holder whose lease has lapsed therefore still passes the check under its token until another claim is granted: that claim stamps a strictly higher token, and from then on the lapsed holder's token is below the high-water mark and refused. Expiry reaches the write path through that higher token, not through a clock.
One path cannot read the record's stamp. A repocontext_forget of a memory record whose stored value cannot be decoded resolves the claim against the lock instead: it is admitted when the lock is not held or the presented token is the one the lock holds, and is otherwise refused as ClaimRequired (no token presented) or StaleToken, so a record whose stamp nobody can read can still be retired.
The high-water mark is stored as a register keyed by the token itself, so it is a maximum under CRDT merge: a concurrent replica carrying an older token cannot lower it, and a fenced-out holder stays fenced out after the record converges. That is what makes a claim safe across regions rather than only within one silo.
The claim surface exists to make an agent-operated backlog safe for several workers to drain at once. For the item schema, the worker protocol, and the reasoning behind them, see The agent-operated backlog and the walkthrough in samples/AgentBacklog.
Workspace-mode tools
Contributed only when the host runs in workspace mode: a broad parent directory is mounted read-only and the client manages repositories under it. repocontext_list_repos needs only the data read-or-write permission that unlocks the group; repocontext_add_repo, repocontext_remove_repo, and repocontext_reset_index are mutating and, like the other mutating tools, require the host to have opted writes in. In this mode they stand in for the single-repository repocontext_bootstrap.
| Tool | What it does |
|---|---|
repocontext_list_repos |
Lists every repository currently registered in the store, each with its last-ingested marker (re-stamped by every completed indexing pass, including one that found nothing to change, so it reads as when the index was last verified current), recorded file count, and embeddedVectorCount (the number of sources - files, captured symbols, and embedded memory entries - that currently carry a live embedding), in ascending id order, so an agent can discover which repositories under the workspace are queryable before recalling, scanning, or searching. It enumerates committed, materialised structural records - a different source from the live counters repocontext_index_status reports - so a repository still in its first ingest can read Running there while it is absent here, and not yet answering scan or search, until its structural records materialise; treat repocontext_index_status as the authority for an onboarding still in progress. embeddedVectorCount is served from the last completed membership walk rather than measured on demand: the field is absent when no walk has completed yet (which is not the same answer as 0), and embeddedVectorCountPending is true whenever a refresh is outstanding - expect that throughout an active ingest, and poll again once it settles for an exact figure. Each row also carries indexedRoot, the absolute path the repository was actually indexed from, so a caller can see that a repository id does NOT imply a root: an id defaults to the final segment of the indexed path but can be supplied explicitly to repocontext_add_repo, and in a git worktree the base repository is what is indexed, so the id will not match the directory the caller is sitting in. It is read from the durable index request, so it is absent (null) for a repository whose index was reset and not yet re-onboarded, joining the three nulls a reset leaves on the row (lastIngested, fileCount, and indexedCommit). indexedCommit is the commit SHA a git-sourced repository's index generation was built from; it is absent for a repository indexed from the mounted workspace, which has no commit anchor. The result also carries the row count. Read-only. |
repocontext_add_repo |
Registers a repository under the mounted workspace and starts indexing it. Takes path (which must resolve inside the workspace root - a .. or symlink escape is rejected) and an optional repoId, derived from the final path segment when omitted, plus the same walk filters as repocontext_bootstrap. Starts asynchronously and returns the running job's snapshot; poll repocontext_index_status. Idempotent and resumable. A repository configured with a git source is refused, because the mounted and git index sources are mutually exclusive. |
repocontext_remove_repo |
Removes every record for a repository - structural nodes, symbols, the content and cross-reference projections, session bookkeeping, memory, and every vector tree including the approximate index and the coverage digest - and drops it from repocontext_list_repos, cancelling any in-flight run and tearing down its indexing grains. The working tree on disk is never touched, and removing an unknown repository is a no-op. Prefer repocontext_reset_index when the goal is repairing a wedged or stale index - it drops the code index and its vectors but preserves the durable memory records. |
repocontext_reset_index |
Drops a repository's code index and its derived planes (structural, symbol, content, cross-reference, session bookkeeping, and every vector tree) and leaves the durable agent-memory records for that repository intact. Use it to repair a wedged, stale, or corrupt code index without discarding notes, decisions, or gotchas: the repository stays registered and stays listed by repocontext_list_repos, reporting no ingest, no file count, and no indexed commit - which is exactly the state it is in, and which keeps the preserved memory discoverable rather than reachable only by an id the caller already knew. A subsequent repocontext_add_repo rebuilds the code index and its vectors from the working files and repopulates those fields, and the surviving memory records are re-embedded on the next indexing pass. The vector-membership memkey- markers are dropped together with the payloads, so surviving memory is not left flagged-but-unreachable by semantic search. Resetting an unknown repository is a no-op that does not register it. The result reports repoId, entriesDeleted, elapsedMilliseconds, the treesSwept it dropped (named, in sweep order), memoryPreserved (always true), and censusCleared (whether the repository root marker's index-derived fields were cleared). A lighter-consent operation than repocontext_remove_repo, which remains the verb for destroying memory too - and the only verb that drops a repository from the listing. The reset reports its own lifecycle through repocontext_index_status (Running in phase Resetting with advancing tree and entry counters, then Completed or Failed), and its sweep runs on a background task bound to the host lifetime rather than to the call: a caller that times out or loses its connection abandons only its wait, the reset keeps running under that caller's credential, and polling repocontext_index_status reports how it ended - so do not re-run it just because the response never arrived. Only a host restart interrupts it, leaving it Running/Resetting and never complete; re-running the reset, which is idempotent, finishes it. |
Repository readiness
Call repocontext_health with repoId to answer the serving question in one call,
rather than correlating ingest completion, vector counts, logs and exposition.
Without it the existing host-only JSON payload is unchanged. With it,
retrievalReady, retrievalPhase and status describe the requested repository,
and repository carries RepoContextRepositoryReadiness:
repoIdnames the repository the snapshot describes.verdictusesRepoContextRetrievalReadinessPhase:Serving,Building,KeywordOnly,NothingRegistered, orSaturatedUnavailable. The existing top-levelretrievalPhasekeeps its canonical snake-case tags.reasonnames the actual blocking gate or last observed query fault, not a generic degradation. It is also present for an intended keyword-only or empty repository.authoritativeis alwaysfalse.ingestretains the job's own counters, including files embedded versus scanned; when the job's metadata cannot be read it is null andingestReasonnames why.vectorCoveragereports the existing embedded-source count andpendingflag. These are different populations: embedded sources include symbols and memory. A missing count is unknown, never zero, and this call schedules no count refresh.embeddingSpacecomes from provider configuration.anncontains this repository/space's local build phase, generation and counts; null means no local handle, not an empty index.annCanServereads the same gate the handle's search uses. An unpartitioned, non-empty index serves exhaustively and is ready.breakerOpenandbreakerProbeDueInare non-consuming reads. A health call cannot claim the half-open probe reserved for a real query.lastRetrievalPathandlastQueryAtdescribe the latest query observation for this repository only. A proven repository retains the 30-second fault hold-down; its pending fault remains visible inreasonduring that grace period.contentPhase,contentReasonandcontentObservedAtreport the latest bounded keyword content-tree scan:Servingwhen it was readable,NothingRegisteredwhen it completed empty, andBuildingwhen it faulted or none was observed. They do not execute a scan and do not certify all content or hydration paths.
This is a passive snapshot on the answering server. It does not call the embedder
(including its health endpoint), run search/hydration, open an ANN handle, or
advance any build. External embedder availability and hydration are known only
through the last real query. With no serving ANN or previous successful query,
Building with semantic_serving_not_yet_demonstrated is honest: a first successful
real query demonstrates the eligible exact fallback. Ingest Completed alone
never promotes it. A measured empty vector corpus together with a completed
zero-file ingest reports NothingRegistered, with retrievalReady: false at
repository scope; unknown coverage does not establish emptiness. A keyword-only
configuration remains ready without requiring a vector plane.
The verdict is advisory, not an authorization or correctness gate. Keep reading
retrievalPath on each result for what that particular query actually did, and
index_status for ingest progress. Neither tool is replaced. No part of this
verdict is read from Prometheus or process-wide readiness/arming.
Tool parameters
Every argument is passed by name. Required arguments are in bold; every other argument is optional, and the defaults and bounds below are the ones the handlers apply. An argument is a string unless noted.
| Tool | Parameters |
|---|---|
repocontext_health |
repoId - optional; omitted preserves the host-only payload, supplied returns passive repository readiness, and a blank value is refused. |
repocontext_stats, repocontext_list_repos |
None. |
repocontext_recall, repocontext_claim_status |
key - the full repository-context key (a memory-record key for repocontext_claim_status). |
repocontext_list_topics, repocontext_index_status, repocontext_remove_repo, repocontext_reset_index |
repoId. |
repocontext_scan |
repoId; scope - Files, Packages, Symbols, Memory, or MemoryTopic, matched case-insensitively; topic - required for MemoryTopic, otherwise ignored; pathPrefix - narrows a Files scan to a directory and is rejected for every other scope; continuationToken - the previous page's token; pageSize (integer) - at most 500, and 0 or less means the default of 100. |
repocontext_search |
repoId; query; k (integer) - the most hits to return, at most 100, and 0 or less means the default of 10. |
repocontext_outline, repocontext_related |
repoId; path - a repository-relative file path. |
repocontext_changed |
repoId; path - the repository root, or a directory inside it to scope the report. |
repocontext_context |
repoId; task; top (integer) - at most 50, and 0 or less means the default of 10; responseBudgetTokens (integer) - at most 200000, and 0 or less means the default of 8192; detail - paths, outline, slices, or auto (the default, and what any other value is treated as); seen (string array) - unit receipts already held; known (string array) - path@hash whole-file claims; session - a caller session id. |
repocontext_neighbors |
key; relation - restrict the walk to one relation; depth (integer) - at most 3, and 0 or less means the default of 1; maxNodes (integer) - at most 100, and 0 or less means the default of 50. |
repocontext_bootstrap |
repoRoot; repoId; includeGlobs and excludeGlobs (string arrays); respectGitignore (boolean) - default true; excludeBinary (boolean) - default true. See Filtering the walk. |
repocontext_add_repo |
path - a repository under the workspace root; repoId - defaults to the final path segment; and the same four walk filters as repocontext_bootstrap. |
repocontext_remember |
repoId; topic; id - omit to create with a generated id; kind - Decision, Note, or Memory, applied only when the entry is created (default Note; any other value, including the internal Unspecified, is rejected); title; body; author; provenance; tags (string array); addLinks and removeLinks - maps from a relation name to target keys; ttlSeconds (integer) - positive when supplied; fencingToken (integer). |
repocontext_update |
key; fields - scalar patches keyed by field name (see below); addTags and removeTags (string arrays); addLinks and removeLinks - memory records only; fencingToken (integer) - memory records only. |
repocontext_forget |
key; lapse (boolean) - default false, a hard delete; lapseSeconds (integer) - the lapse window, default 60, positive when supplied and ignored for a hard delete; fencingToken (integer) - memory records only, refused on any other record. |
repocontext_claim |
key - a memory-record key; owner - the claiming agent's identity; leaseSeconds and maxWaitSeconds (integers) - positive when supplied; see the repocontext_claim row above for what omitting each means. |
repocontext_renew_claim |
key; fencingToken (integer); leaseSeconds (integer) - positive when supplied, and pass it explicitly. |
repocontext_release_claim |
key; fencingToken (integer). |
repocontext_update matches fields names case-insensitively and refuses any name outside the record family's own set: displayName, defaultBranch, and lastIngested on a repository root; language, version, and lastIngested on a package; digest, language, sizeBytes, and lastIngested on a file; filePath, startLine, endLine, signature, and digest on a symbol; and title, body, author, and provenance on a memory entry. No claim field is in any set, so a claim cannot be patched.
Every tool also accepts the optional region argument the MCP host adds to each group tool (see Region targeting). No peer region serves the repository-context group, so a region naming a peer is refused; omit it to use the current region.
A refused call reaches the caller as an MCP error result naming the problem; for a classified refusal the MCP host first sanitizes the message (control and line-separator characters are replaced) and caps its length, both in the text it returns and in its own Debug line. These refusals are classified as the caller's mistake - a missing, blank, unbindable, or undeclared argument, a malformed key, an unrecognised kind or scope, a pathPrefix outside a Files scan, a non-positive ttlSeconds or lapseSeconds, free text refused for leaked tool-call framing or a credential-bearing URL, a path outside the workspace or the repository's indexed root, and a target that does not exist - so they are counted on orleans.lattice.api.mcp.tool.client_errors by tool and reason, and the MCP host logs them at Debug without a stack rather than as server faults (see Client errors are answered, not logged as faults). This package's own per-call log line, under the Orleans.Lattice.Api.Mcp.RepoContext.ToolInvocation category, still records every call that reaches a tool and fails at Warning with its exception. An undeclared argument, a refused region, and a refusal by the registered ILatticeApiMcpAuthorizer are turned away before the tool runs and leave no such line. An access-gate denial raised inside the tool - the LatticeAuthorizationDeniedException the tree access gate throws when it refuses the caller's credential on a tree operation - does reach it, so it leaves that Warning line with its stack. The remaining refusals are still raised unclassified, so the host also logs them at Error: a fencing conflict, a non-positive claim duration, a claim tool pointed at a non-memory key or a fencing token presented against a non-memory record, an update against a record family that cannot be patched or with a field, link, or integer value its family does not accept, a remember or update link whose relation name is empty or whose target is not a well-formed key, an onboarding call refused because the repository is git-sourced or the workspace boundary is not enforcing, a repocontext_reset_index refused because a damaged code-index tree it would have to drop whole is shared with another repository, a region naming a peer, an authorizer refusal, and an access-gate denial, which is never classified as a client error.
Asynchronous indexing lifecycle
Onboarding a repository (repocontext_bootstrap, or repocontext_add_repo in workspace mode) is a potentially long walk-digest-reconcile-vectorise pass, so it does not run on the client request. The tool records a durable job, hands the work to a background runner bound to the host lifetime (not to the client stream), and returns immediately with a Running snapshot. A client follows the pass by polling repocontext_index_status with the same repoId until status is Completed or Failed.
Because the run is decoupled from the request, a dropped MCP stream or client disconnect can no longer abort an index. Each job is anchored by an Orleans reminder: while a run is in flight the reminder beats as a single-flight heartbeat, and after a host restart it re-fires, reactivates the job, and re-enqueues the persisted request so the interrupted pass resumes from where it left off (the bootstrap pass is idempotent, so already-committed files are skipped by digest). The attempt counter on the status snapshot is a cumulative tally of index runs started for the repository - the initial onboarding plus every re-drive (each periodic reconcile that picks up edits and deletions, each gap back-fill, and each re-drive of a failed run; a reminder-driven resume after a restart continues the interrupted run and does not add one) - so it rises steadily on a healthy, actively-maintained repository and a high value is normal, not a sign of failure or interruption. A durable grain-storage provider and the Orleans reminder service must therefore be configured on the host; the bundled container image wires both.
Adaptive pacing
While a run is Running, the repocontext_index_status snapshot carries a pacing object read live from the silo's indexing pacer (it is absent once the run completes or fails, and on a host with pacing switched off by LATTICE_REPOCONTEXT_PACING=false it reports Disabled). It explains why an embedding pass is going slower than the hardware could, so a deliberately slowed job reads as slowed rather than as stalled:
| Field | Meaning |
|---|---|
state |
Idle (no batch recently), Pacing (full rate), Backoff (a congestion signal raised the inter-batch delay), Waiting (a vector tree is saturated, bounded at 30 s), Resting (the rest between work slices), Yielding (a search or context call is in flight, bounded at 2 s), or Disabled. |
reason |
A human-readable cause for the current state, for example an embedding batch failed. |
batchDelayMilliseconds |
The current congestion-driven delay before each batch; 0 at the full rate. |
since |
When the pacer entered its current state; absent while Idle. |
foregroundRequests |
How many search or context calls are in flight on this silo right now. |
A Backoff, Waiting, Resting, or Yielding state with filesEmbedded or symbolsEmbedded still advancing between polls is a healthy, paced job. The pacer never skips or fails a batch, so a paced pass lands exactly what an unpaced one would, only spread over more wall-clock time. A Pacing reason ending is treated as advisory names a vector tree that is still Throttled but did not clear while indexing held its delay at the ceiling for 60 seconds, so the drain runs at the full rate past it until it clears or worsens (issue #3456). The variables that tune it are listed under container configuration.
A run of failed vector store or membership writes does not by itself end an embedding pass. After three consecutive failures the pass consults the silo's WAL saturation signal, and defers its remaining batches to the next reconcile only when a vector tree reports Saturated; against a Throttled or Healthy tree it keeps attempting under the pacer's backoff, so each further batch tests whether the plane admits the write again (issue #2683). A host that registers no saturation signal falls back to deferring on the run alone.
Staying fully indexed: the self-index grain
Onboarding a repository does more than complete once: a per-repository self-index grain, keyed by repoId, owns that repository's "reach and stay fully indexed" guarantee from onboarding until the repository is removed or its index is reset. The same onboarding call that starts the first pass (repocontext_bootstrap, or repocontext_add_repo in workspace mode) arms this grain - a repository configured with a git source is armed from that configuration when the host starts - and removing the repository (repocontext_remove_repo) or resetting its index (repocontext_reset_index) tears it down. Onboarding and self-heal recovery therefore funnel through exactly one path and cannot drift.
The grain runs a continuous, low-cost background scan of its own repository's structural file range and re-drives the idempotent index whenever it finds work - all without a client call:
- It is cheap and bounded. A grain-local timer drives the scan, which reads stable source identifiers only, never the embeddings themselves. Once the repository's per-page vector-coverage digest is built, one tick classifies every file: a keys-only walk of the structural file range, checked in memory against the digest's fixed set of rows, reads nothing from the membership tree and names every uncovered file (up to 4,096 per scan) rather than stopping at the first. Before the digest is built, or after a membership reset drops it, the grain falls back to one bounded page of that walk per tick, point-probing the page's files against the add-wins membership and stopping at the first file with no live embedding. Between full scans the grain idles behind a jittered cooldown, and the first tick after each activation is itself jittered, so a fleet of repositories reactivated together never all scan at the same instant.
- It back-fills missing embeddings. When the scan finds an unembedded file, the grain re-drives the whole-repository index. The embedding pass is an idempotent back-fill: it re-embeds every file the membership says is missing and skips the rest, so a single trigger closes all of that repository's gaps. This is what makes embedding presence self-healing - a file whose content digest is unchanged is structurally skipped, but the independent presence flags still catch a vector that was never written (for example because the embedder was unavailable at first onboarding) and closes the gap once it is back. The same periodic reconcile also carries the symbol, content-projection, and cross-reference back-fills, so a repository indexed before symbol extraction, the content projection, or the reverse cross-reference index existed heals its symbol, content, and reverse-reference trees on its own, with no client call (see record-model.md).
- It rescues a failed run. At the start of each scan cycle the grain checks the job's status and re-drives a run that outright failed. A failure before any structural record was written leaves nothing for the file scan to detect, so this status check - not the gap scan - is what keeps a failed onboarding from being abandoned. Both re-drives are single-flight: they are a no-op while a run is already in flight.
- It picks up on-disk edits and deletions. On a longer cadence than the gap scan (a jittered interval of roughly fifteen minutes, or a git-sourced repository's own refresh interval), the grain re-drives the whole idempotent index so files edited or deleted on disk after onboarding are reconciled automatically, with no client call. The gap scan only finds missing embeddings; this periodic reconcile is what re-walks the tree to notice changed and removed content. Each reconcile is kept cheap by the walk's stat fast-path (below), so in steady state it reads almost nothing.
- It audits the coverage digest. On a long cadence (24 hours by default, plus jitter; see
LATTICE_COVERAGE_DIGEST_AUDIT_INTERVAL_SECONDSunder container configuration), the grain re-derives the digest from an authoritative scan of the whole membership, bounding how far the digest can drift from what it mirrors. It is the one whole-membership read on this path, so it is postponed - at most 12 times in a row - while the indexing pacer reports foreground search load or a congested vector plane.
The reconcile leans on a stat fast-path to stay cheap. Every file's record carries the wall component of the hybrid-logical clock stamped when it was last ingested (its ingest anchor), recovered directly from the digest register's order key, so no extra field is stored. During a reconcile walk a candidate whose size is unchanged and whose on-disk modification time is strictly older than that anchor is assumed unchanged: its stored digest and language are reused without reading or hashing the file. The strict comparison stays clear of the racy-clean window (a file touched in the same tick as its last ingest is re-read), and the content digest remains the sole source of truth - a real edit either changes the size or leaves the modification time at or after the anchor, both of which fall through to a full read. A file that is touched but byte-for-byte identical is re-hashed once, kept at its stored digest (so it is not re-embedded), and has its node rewritten to advance the anchor, so the fast-path skips it from then on.
The content digest itself is a non-cryptographic XxHash128 fingerprint - change detection only, never a security boundary - which is roughly ten times cheaper to compute than SHA-256 and dominates the cost of a cold walk. Digests are self-describing and the switch is non-breaking: a modern digest is written xx128:<hex>, while a legacy bare 64-character hex digest is read back as an implicit SHA-256 value. A reconcile always recomputes a file's fingerprint under its stored digest's own algorithm, so a store written before the switch keeps reconciling correctly with no forced bulk re-hash; a file migrates to the cheaper algorithm only when its content genuinely changes.
The grain stays durable through a one-minute keep-alive reminder that keeps it activated and re-fires it after a host restart, at which point it re-arms its scan timer and resumes from the persisted checkpoint. This keep-alive is the repository's standing backstop, and only removing the repository or resetting its index unregisters it; a reset leaves the repository registered but unindexed until a subsequent repocontext_add_repo - or, for a git-sourced repository, the next host start - re-arms the grain. A job's own resume reminder is cleared when a run fails (so a deterministic logic failure does not retry forever inside the job), but the self-index grain's keep-alive survives that failure and is what re-drives the run - a failed onboarding is never silently abandoned.
Removing a repository
Removing a repository (repocontext_remove_repo) tears down everything indexing owns for it: it cancels any in-flight run, unregisters the job's resume reminder and the self-index grain's keep-alive reminder, clears both grains' durable state, and disarms the repository's approximate-index build coordinator, so a removed repository never resumes, never scans, and never rebuilds an index again. repocontext_reset_index runs the same teardown before its sweep, except that a step that times out or meets an unavailable tree is recorded and skipped rather than aborting the reset (a removal stops at the first fault).
A reset's own sweep is tolerant in the same way, because it is the verb invoked when the index is already damaged. A code-index tree it cannot drain because a call timed out is skipped, and the sweep moves on to the next. A tree holding a leaf that cannot be activated is dropped whole when this repository is the only one registered - it then counts as swept but adds nothing to entriesDeleted - and the reset is refused, naming the tree, when another repository shares it. The structural tree is never dropped whole, so any fault there aborts the reset. A reset that skipped a tree or a teardown step ends Failed, with an error naming each one, rather than Completed; everything it did drain stays dropped.
Filtering the walk
The onboarding tools (repocontext_bootstrap and repocontext_add_repo) take optional includeGlobs and excludeGlobs to narrow which files are ingested, a respectGitignore flag that defaults to true, and an excludeBinary flag that also defaults to true.
When respectGitignore is on, the walk honours the repository's own .gitignore files with a dependency-free, hierarchical matcher: rules layer from the repository root down, a deeper .gitignore overriding a shallower one and the last matching pattern within a file winning (including a ! re-include). It covers the forms real repositories use - comments, blank lines, ! negation, a leading or interior / to anchor a pattern to its .gitignore directory, a trailing / for a directory-only match, [...] character classes (ranges and !/^ negation), the *, ?, and ** wildcards, and a \ escape that makes the next character literal (so \#*\#, \*, and an escaped trailing space match as git matches them) - and prunes an ignored directory during descent rather than walking it, so a build output tree never enters the index and never costs a hash. The matcher does not read .git/info/exclude or the user's global excludes, and the container needs no git binary. Set respectGitignore to false to index every file the include/exclude globs allow, tracked or not. When globs and .gitignore are combined, a file must satisfy both to be ingested.
When excludeBinary is on, a file whose leading bytes look non-text - a NUL byte anywhere in the first 8,000 bytes, the same cheap, language- and extension-agnostic heuristic Git uses - is dropped before it is hashed, embedded, or indexed, so compiled artefacts, images, archives, and other blobs never enter the index. Because the walk already reads each surviving file's bytes to hash it, the sniff is essentially free and never reads more than a bounded prefix. Set excludeBinary to false to ingest binary files too.
Discovery and gating
Tool advertisement and invocation both defer to the core permission-aware discovery filter and the fail-closed gate - the module registers exactly one tool group and adds no per-session state. A caller with no matching data-plane capability sees none of these tools; a caller whose permissions only allow reads sees the read-only tools (the always-on read-only tools, plus repocontext_changed when a workspace root is configured and repocontext_list_repos in workspace mode); the mutating tools appear only when the host enabled writes. See the MCP server docs for the credential bridge and grant model.
A host that also passes registerAsApp: true to AddRepoContextTools additionally offers the always-on read-only tools - every read-only tool except repocontext_changed and repocontext_list_repos - under app-local names such as repo-context_search, once the repo-context app is installed and enabled; they run the same handlers and never replace the group tools. See As an installable app.