fix(rag): share one index across agents referencing the same RAG by aheritier · Pull Request #4650 · docker/docker-agent · GitHub
Skip to content

fix(rag): share one index across agents referencing the same RAG - #4650

Merged
dgageot merged 5 commits into
docker:mainfrom
aheritier:fix/rag-shared-index
Oct 20, 2026
Merged

dgageot merged 5 commits into
docker:mainfrom
aheritier:fix/rag-shared-index

Conversation

@aheritier

Copy link
Copy Markdown
Collaborator

Fixes #4637

Problem

Every type: rag toolset built its own RAG manager. With N agents referencing one RAG definition there were N vector stores, N file watchers, N debounce queues and N max_embedding_concurrency caps over a single database. One file change was embedded N times at once, so the embedding endpoint saw N × concurrency simultaneous requests (~43 re-index jobs per write in the reported setup, enough for the gateway to drop connections).

A second defect made it worse: VectorStore.needsIndexing only read the store's in-memory hash map, so even after one store committed a file, every other store still saw it as stale and re-embedded it.

Changes

  • Skip files another store already indexed (pkg/rag/strategy). On an in-memory miss, needsIndexing consults the database metadata and skips the file when its hash matches and chunks exist. A metadata read error still re-indexes, so a transient database error cannot leave a file stale.
  • Share one started toolset across identical references (pkg/tools/builtin/rag/shared.go). CreateToolSet returns a per-reference handle. On Start, the first reference installs its toolset in a process-wide registry and later ones attach; queries and events go through the started toolset; the last Stop closes it and removes the entry. Toolsets that never start never enter the registry, and a failed start leaves nothing behind so a later reference can retry.
  • Don't attribute shared background indexing to a session. The shared index starts on a context with the session ID cleared, so gateway-bound embedding calls no longer carry X-Cagent-Session-Id of whichever session happened to start it.
  • End-to-end test and docs.

Security considerations

The registry is process-wide, so in serve api it can span sessions. The key is designed so sharing never crosses a trust boundary:

  • It fingerprints every input that builds the manager: RAG name, RAG config, canonical (symlink-resolved) parent directory, gateway URL, models and providers. Identical inputs resolve to the same database, including the default path used when database: is unset.
  • The env provider and provider registry are compared by identity, so an embedding client is never shared across different credentials. No environment.Provider varies its values by context, so one instance means one set of credentials. Per-load RuntimeConfig.Clone() keeps the instance; EnvProviderOverride and CloneWithFreshEnvironment (e.g. ACP re-auth) produce a new one, which falls back to an unshared index, i.e. today's behaviour.
  • An env provider whose type is not comparable disables sharing rather than risking a map panic.
  • Queries still run on each caller's own request context, so per-request session identity is unchanged.

Out of scope: references that override name: keep their own index (the name feeds the default database path), and two different configs pointing at the same database: file are not detected; both behave as before.

Testing

  • TestSharedDB_SecondStoreSkipsFileAlreadyReindexed, TestNeedsIndexing_DBFallbackIgnoresZeroChunkMetadata: the database fallback.
  • pkg/tools/builtin/rag/shared_test.go: identical keys index once, different or unshareable keys don't share, concurrent starts index once, failed starts are retried, events reach every reference, queries hit the initialized index, Tools doesn't block on indexing, background work carries no session, and key identity (credentials, name, directory, symlinks).
  • TestRAGReferencesShareOneIndex: three agents loaded through the real team loader, sqlite and file watcher; initial indexing and a file change each embed once. With sharing disabled it fails with 2 embeddings for the change.
  • Each commit builds and passes the RAG packages on its own; go test -race on the touched packages, golangci-lint v2.14.0, project cops and markdownlint are clean.

VectorStore.needsIndexing only consulted the store's in-memory hash map,
which tracks that store's own writes. When several stores share one
database (e.g. many agents referencing the same RAG definition), a file
re-indexed and committed by one store was still seen as stale by every
other store, so each of them embedded it again.

On an in-memory miss, fall back to the database metadata: if the stored
hash matches the file and at least one chunk exists, record the hash
locally and skip indexing. A metadata read error still re-indexes, so a
transient database error cannot leave a file stale.

Refs docker#4637
Every `type: rag` toolset built its own manager, so N agents referencing
the same RAG definition ran N indexes, N file watchers and N embedding
queues over one database. A single file change was embedded N times at
once, multiplying the load on the embedding endpoint by N.

References built from identical inputs now share one started toolset.
The key fingerprints everything that builds the manager (RAG name, RAG
config, canonical parent directory, gateway, models and providers) and
compares the env provider and provider registry by identity, so an
embedding client is never shared across different credentials.

Each reference still builds its own toolset at load time. On Start the
first reference installs it and later ones attach; queries and events go
through the started toolset. The last Stop closes it and removes it from
the registry, and a toolset that never starts never enters it. A failed
start leaves nothing behind, so a later reference retries.

Refs docker#4637
A shared RAG toolset's file watcher and indexing run on a context derived
from the Start call, which drops cancellation but keeps values. The
session ID of whichever session started the index therefore stayed on
every background embedding request, and gateway-bound calls forwarded it
as X-Cagent-Session-Id, even after that session had ended and while the
index served other sessions.

Clear the session ID before starting a shared index, keeping the
context's logging and tracing values. Queries still run on each caller's
own request context and carry its session.

Refs docker#4637
Load a config where three agents reference the same RAG definition
through the real team loader, toolset registry, sqlite database and file
watcher, with a fake embedding provider counting inputs. Initial indexing
and re-indexing after a file change must each embed the document once,
not once per agent.

Refs docker#4637
Document that references to one knowledge base share a single index,
file watcher and embedding budget, when they stop sharing (a name:
override or different credentials), and that shared background indexing
is not attributed to a session.

Refs docker#4637
Copilot AI balanced review requested due to automatic review settings October 19, 2026 20:46
@aheritier
aheritier requested a review from a team as a code owner October 19, 2026 20:46
@aheritier
aheritier enabled auto-merge October 19, 2026 20:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Path-key collisions, lost startup events and cached handlers targeting uninitialized managers remain unresolved.

3 open issues
What changed in this PR

Addresses #4637 by sharing RAG indexes across matching agent references, reducing duplicate indexing and embedding requests.

Changes:

  • Adds reference-counted sharing with session-neutral background indexing.
  • Checks database metadata before re-embedding files.
  • Adds regression tests and sharing documentation.
File Description
pkg/​tools/​builtin/​rag/​shared.go Implements shared lifecycle, queries and events.
pkg/​tools/​builtin/​rag/​shared_test.go Tests sharing, identity and lifecycle behavior.
pkg/​tools/​builtin/​rag/​rag.go Wraps created toolsets for sharing.
pkg/​teamloader/​toolsets/​rag_shared_test.go Tests indexing across multiple agents.
pkg/​rag/​strategy/​vector_store.go Adds database metadata fallback.
pkg/​rag/​strategy/​vector_store_test.go Updates checks to pass context.
pkg/​rag/​strategy/​vector_store_shared_db_test.go Tests metadata reuse and zero-chunk handling.
docs/​tools/​rag/​index.md Documents shared knowledge bases.

Review effort: Balanced

Comment on lines +42 to +48
dir := parentDir
if abs, err := filepath.Abs(dir); err == nil {
dir = abs
}
if resolved, err := filepath.EvalSymlinks(dir); err == nil {
dir = resolved
}
// Tools serves the toolset that was actually initialized: a reference that
// attached never indexed its own manager.
func (s *sharedToolSet) Tools(ctx context.Context) ([]tools.Tool, error) {
return s.target().Tools(ctx)
Comment on lines +213 to +228
@aheritier aheritier added area/docs Documentation changes area/rag For work/issues that have to do with the RAG features area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only. labels Oct 19, 2026
@dgageot
dgageot disabled auto-merge October 20, 2026 06:41
@dgageot
dgageot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into docker:main with commit 0d6cd9c Oct 20, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation changes area/rag For work/issues that have to do with the RAG features area/tools For features/issues/fixes related to the usage of built-in and MCP tools kind/fix PR fixes a bug (maps to fix:). Use on PRs only.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rag: every agent referencing a RAG gets its own manager/watcher, so one file change is re-embedded N times at once

3 participants