Drawer 01 · Ingest pipeline
Accessioning a repository
One POST /repos starts a background task that clones a
repository, walks it, cuts it into syntax-aware chunks, optionally writes a
one-paragraph description of each chunk, embeds them in batches of 32, and
appends the vectors to a FAISS index. The request returns before any of that
begins.
01.1The accession slip
@router.post("", response_model=AddRepoResponse)
async def add_repo(
request: AddRepoRequest,
req: Request,
x_github_token: Optional[str] = Header(None, alias="X-GitHub-Token"),
x_embedding_provider: Optional[str] = Header(None, alias="X-Embedding-Provider"),
x_embedding_key: Optional[str] = Header(None, alias="X-Embedding-Key"),
) -> AddRepoResponse:
# ... optional cloud-embedding reconfiguration, then:
record = await repo_manager.add_repo(
url=request.url, branch=request.branch,
name=request.name, github_token=x_github_token,
)
X-Embedding-Provider and X-Embedding-Key on this
request does more than authenticate: if the new provider's vector width
differs from the current one, repos.py:126 calls
vector_store.reinitialize(new_dims), which deletes
faiss.index and metadata.pkl and starts an empty
index. Every previously accessioned repository becomes unsearchable at that
moment.
01.2The pipeline
CodeIndexer.index(), called from an async task without an
executor (repo_manager.py:293). While a large repository is being
chunked and embedded, the event loop is blocked and the service will not
answer other requests.
01.3What a chunk is
ASTChunker declares node types per language and walks the whole
tree, recording every node whose type matches a function,
class or import entry for that language. Because the
walk is recursive and unconditional, a method inside a class produces a chunk
and its enclosing class produces a chunk containing it — the same lines
are embedded twice at two granularities. Ranges are de-duplicated by
(start_line, end_line) only (ast_chunker.py:364), which
does not catch nesting.
| Field | Source | Notes |
|---|---|---|
| id | f"{path}:{line}:{chunk_type}" | Also the FAISS metadata key; stable across re-index of unchanged code. |
| file | path relative to repo root | Carried into search results verbatim. |
| line_start / line_end | tree-sitter start_point / end_point, 1-based | Only line_start reaches the vector store. |
| content | joined source lines | Raw code. No natural-language rewrite is performed. |
| chunk_type | function · class · import · module · block | module = residue not covered by a node; block = line-window fallback. |
| name | first identifier-like child | Positional, not field-based; returns None for many node shapes. |
| signature | first ≤10 lines until parens balance | Prepended to the embedding text when both name and signature exist. |
- MAX_CHUNK_LINES = 150
- Nodes longer than this are split into overlapping windows by
_split_large_chunk(); parts are renamedname (part n)and lose their signature. - MIN_CHUNK_LINES = 5
- Uncovered regions shorter than five lines are discarded rather than emitted as
modulechunks. - OVERLAP_LINES = 10
- Applied when splitting oversized nodes and in the no-parser fallback, where the window is 80 lines with 10 lines of overlap.
- Language coverage
-
Fourteen languages have node maps. Five ship as hard dependencies —
Python, JavaScript, TypeScript, Go, Rust. The other nine (Java, C, C++, C#,
Ruby, PHP, Swift, Kotlin, Scala) sit behind the
languagesextra inpyproject.toml:62. Each grammar import is individually guarded, so a missing grammar silently downgrades that language to line-window chunking. - Files indexed but never parsed
-
The indexer's extension allow-list includes
.md,.txt,.rst,.json,.yaml,.toml,.sqland shell scripts. None of these appear inASTChunker.LANGUAGE_MAP, so documentation and configuration are always chunked as 80-line blocks.
01.4Contextual enrichment
ContextualRetrievalService implements the published contextual
retrieval pattern: before embedding, ask a small model to situate the chunk
inside its file, then prefix that sentence to the text being embedded. The
prompt is built at contextual_retrieval.py:222 and the result is
formatted as
[From {file}: {context}]\n\n{content}.
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
CONTEXT_MODEL = os.environ.get("CONTEXT_MODEL", "llama3.2:3b")
MAX_FILE_CONTEXT = 6000 # chars of the file sent as the document
MAX_CONTEXT_LENGTH = 200 # num_predict for the generated context
TIMEOUT = 30.0
AVAILABILITY_CACHE_TTL = 60.0
use_contextual_retrieval=True, indexer.py:87) and
runs one HTTP request per chunk, in a plain for loop
(contextual_retrieval.py:196). There is no batching, no
concurrency and no cache of generated contexts. If Ollama is unreachable the
availability probe fails, the result is cached for sixty seconds, and every
chunk is embedded as raw code — silently, with no change in the indexing
result object.
01.5Re-accession and incremental work
-
Change detection is per file, not per chunk. A file whose
SHA-256 prefix matches the manifest is skipped entirely
(
indexer.py:173). A file that changed is fully re-chunked and fully re-embedded. - Superseded vectors are not removed. Re-indexing a changed file appends new vectors; the old ones stay in the FAISS index. Only the metadata mapping is overwritten, and only for identical chunk ids.
-
Deletion is metadata-only.
delete_by_repo()anddelete_by_file()pop entries out of_id_to_idxand_idx_to_doc(vector_store_faiss.py:362-416). The comment is candid: “FAISS doesn't support true deletion.” The index file therefore grows monotonically until someone callsclear(). -
The manifest is global.
CodeIndexerreceives nodata_pathfrommain.py:175, so it uses its default ofPath("./data")(indexer.py:106) — onefile_hashes.jsonfor the whole service, keyed by repo-relative path. Two repositories that both containsrc/main.pyshare a key. -
There is no watcher. Re-indexing happens when someone calls
POST /repos/{id}/reindex. The push-webhook path that would automate it exists inroutes/github.pyandservices/github_sync.pybut its router is not included inmain.py.
01.6Accession in the log
$ docker compose --profile gpu up --build
autopilot-compute-gpu | INFO:compute_service.main:Starting Compute Service...
autopilot-compute-gpu | INFO:compute_service.main:GPU available: True (device: cuda)
autopilot-compute-gpu | INFO:...main:Initializing embedding service with model: BAAI/bge-m3
autopilot-compute-gpu | INFO:...embeddings:EmbeddingService initialized with device: cuda
autopilot-compute-gpu | INFO:...embeddings:Loading embedding model: BAAI/bge-m3
autopilot-compute-gpu | INFO:...embeddings:Model loaded successfully: BAAI/bge-m3, Dimensions: 1024, Device: cuda
autopilot-compute-gpu | INFO:...main:Embedding model loaded with 1024 dimensions
autopilot-compute-gpu | INFO:...main:Unified embedding service initialized (supports local + cloud)
autopilot-compute-gpu | INFO:...main:Initializing FAISS vector store with 1024 dimensions...
autopilot-compute-gpu | INFO:...vector_store_faiss:Loaded FAISS index with 0 vectors
autopilot-compute-gpu | INFO:...main:Initializing Zoekt search service...
autopilot-compute-gpu | INFO:...main:✓ Zoekt available for fast keyword search
autopilot-compute-gpu | INFO:...ast_chunker:Available tree-sitter languages: ['python', 'javascript', 'typescript', 'go', 'rust']
autopilot-compute-gpu | INFO:...ast_chunker:Initialized tree-sitter parsers: ['python', 'javascript', 'typescript', 'go', 'rust']
autopilot-compute-gpu | INFO:...main:AST chunker initialized for semantic code extraction
autopilot-compute-gpu | INFO:...reranker:Using CrossEncoderReranker (local model)
autopilot-compute-gpu | INFO:...main:Reranker initialized: CrossEncoderReranker
autopilot-compute-gpu | INFO:...main:Initializing repo manager at /app/data/repos...
autopilot-compute-gpu | INFO:...main:All services initialized successfully
autopilot-compute-gpu | INFO:...living_summary:Living summary database initialized at /app/data/cache/summaries.db
autopilot-compute-gpu | INFO:...main:Living summary service initialized
autopilot-compute-gpu | INFO:...main:Server running on 0.0.0.0:8080
autopilot-compute-gpu | INFO:...main:Max concurrent tasks: 10
$ curl -sS -X POST http://localhost:8080/repos \
-H 'Content-Type: application/json' \
-H "X-GitHub-Token: $GITHUB_TOKEN" \
-d '{"url":"https://github.com/OWNER/REPO","branch":"main"}'
{"id":"4f2a91c7","name":"REPO","status":"cloning","message":"Repository REPO added, cloning started"}
# service log, background task
INFO:...repos:Adding repository: https://github.com/OWNER/REPO
INFO:...indexer:Indexing project: /app/data/repos/global/OWNER/REPO (repo_id: 4f2a91c7)
INFO:...zoekt:Successfully indexed /app/data/repos/global/OWNER/REPO to /app/indexes/zoekt/REPO
INFO:...indexer:Zoekt indexing complete
INFO:...indexer:Found 412 files to index
INFO:...indexer:Generating embeddings for 3184 chunks...
INFO:...indexer:Processed 500/3184 chunks
INFO:...indexer:Processed 1000/3184 chunks
INFO:...indexer:Indexing complete: 412 files, 3184 chunks, skipped 0, errors: 0
INFO:...repo_manager:Repository 4f2a91c7 ready, indexed 412 files
$ curl -sS http://localhost:8080/repos | python3 -m json.tool
{
"repos": [
{
"id": "4f2a91c7",
"name": "REPO",
"branch": "main",
"status": "ready",
"files_indexed": 412,
"last_indexed": "2025-11-18T09:41:07.512884",
"last_commit": "9c1e0b4a…",
"error_message": null
}
],
"total": 1
}
The status values cloning → indexing → ready | error are the only
progress signal available. There is no chunk-level progress endpoint, and the
files_indexed figure is written once, at the end. A repository
that fails mid-embed remains indexing until the process is
restarted. See Drawer 06.