Troubleshooting¶
Common failure modes and how to root-cause them. Issues are grouped by the layer they originate in: infrastructure → ingest → MCP → agent.
Infrastructure¶
Weaviate pod is CrashLoopBackOff¶
kubectl -n ai-workloads logs deploy/weaviate --tail=80
Look for:
-
failed to load schema: PVC state is incompatible with a new image version. If you just upgraded from 1.27 to 1.35 and don't care about old data, delete the PVC and reapply:kubectl -n ai-workloads delete pvc weaviate-pvc kubectl apply -f k3s-deployment/base/weaviate-pvc.yaml kubectl -n ai-workloads rollout restart deploy/weaviate -
module text2vec-transformers not enabled: the env var didn't land. Double check the deployment manifest has bothENABLE_MODULES=text2vec-transformersandDEFAULT_VECTORIZER_MODULE.
Weaviate pod stays 0/1 Running and exits every ten minutes¶
kubectl -n ai-workloads logs deploy/weaviate --previous ends with could not
join raft join list: context deadline exceeded. Weaviate detected this node to
have state stored after RAFT_BOOTSTRAP_TIMEOUT (600 s), and the readiness
probe returns 503 the whole time. Weaviate names its Raft node after the pod
unless CLUSTER_HOSTNAME is set, so a re-created pod (image change, node
reboot, rollout restart) cannot rejoin the single-voter cluster its
predecessor left on the volume. The hint in the message about raising the
timeout does not apply: the database is not loading, the node identity changed.
The base manifest pins CLUSTER_HOSTNAME=node1 and sets
RAFT_ENABLE_ONE_NODE_RECOVERY=true, which lets a node whose name changed
rewrite the stored configuration to itself. Apply it and watch for the recovery
line:
kubectl apply -f k3s-deployment/base/weaviate-deployment.yaml
kubectl -n ai-workloads logs deploy/weaviate | grep -m1 'perform cluster recovery'
# ... "old_single_cluster_node_name":"weaviate-<old pod>","new_single_cluster_node_name":"node1" ...
On 2026-10-25 the pod was ready eleven seconds after the apply; the data on the volume is untouched by the recovery.
Keyword (BM25) scores are 0 and hybrid ranking looks arbitrary¶
Exact-title pages fall out of the top ten, every search_artifacts hit carries
the same score, and a direct probe returns matches whose score is exactly 0 on
the word-tokenized properties while the explanation still shows a term
frequency:
python -c "
from fractal_rag.client import client_session
from weaviate.classes.query import MetadataQuery
with client_session() as c:
res = c.collections.get('FractalArtifact').query.bm25(
query='lacunarity', query_properties=['content'], limit=3,
return_metadata=MetadataQuery(score=True, explain_score=True))
for o in res.objects: print(o.metadata.score, o.metadata.explain_score)
"
Weaviate before 1.35.3 writes a NaN average property length for a segment flush
that contains only deletions (weaviate/weaviate#10102); compaction spreads it
through the collection, and on arm64 the NaN becomes an average of 0, so the
BM25 term weight is 0 for every match in title, summary, content and
authors (tags, source_type and the other FIELD-tokenized properties keep
scoring). A delete_many of 97,762 objects on 2026-10-25 triggered it: the
keyword leg of every hybrid query became a flat bonus and the Hurst Exponent
concept page ranked 26th for its own title. The base manifest pins 1.35.23:
applying it restarted Weaviate on the patched release, whose loader recomputes
the averages from the stored per-document lengths, and the scores were back
without a rebuild (the concept page ranks first). On a version that cannot be
upgraded, schema bootstrap --drop plus a full re-ingest also clears the NaN
segments.
t2v-transformers pod is Pending forever¶
kubectl -n ai-workloads describe pod -l app=t2v-transformers | tail -40
If you see 0/1 nodes are available: 1 Insufficient nvidia.com/gpu:
- Confirm the GPU plugin is running:
kubectl -n kube-system get pods | grep nvidia-device-plugin - Confirm GPUs are visible:
kubectl describe node | grep nvidia.com/gpushould show capacity ≥1. - Check that the manifest's
resources.requests.nvidia.com/gpuisn't asking for more GPUs than exist.
t2v-transformers runs but inference is slow¶
kubectl -n ai-workloads exec deploy/t2v-transformers -- nvidia-smi — if no
GPU is listed, the container fell back to CPU. Usual cause: the NVIDIA
runtime isn't the default. Either set
spec.runtimeClassName: nvidia in the deployment or fix the K3s NVIDIA
runtime config (see nvidia-device-plugin-daemonset.yaml).
Weaviate is up but curl localhost:30080/v1/.well-known/ready fails¶
If you used the docker-compose stack instead of K3s, the port is 8080, not
30080. Either fix the URL or set the env override:
export FRACTAL_RAG_WEAVIATE_HTTP_PORT=8080
export FRACTAL_RAG_WEAVIATE_GRPC_PORT=50051
Schema¶
schema bootstrap returns Unsupported vectorizer module¶
config.py defaults to text2vec-transformers. If your Weaviate doesn't
have that module enabled, the bootstrap fails fast. Re-check the
ENABLE_MODULES env var on the Weaviate pod / container.
Second schema bootstrap is not idempotent¶
That means the prior run partially failed and left only one of the two collections. Inspect:
fractal-rag status # one of the collections will show null
Either delete the orphan and rerun, or use the destructive flag:
fractal-rag schema bootstrap --drop
Ingest¶
fractal-rag pull fails with gocmd: not found¶
gocmd isn't on PATH for the user running the CLI. Install per
GoCommands docs and make sure
~/.irods/ is initialized (gocmd ls /iplant/home/<you>).
pull succeeds but ingest --source json reports parsed_docs: 0¶
The corpus path doesn't match what ingest.discover_jsonl is looking for —
specifically, any *.jsonl whose path contains embeddings/. Either move
your file under embeddings/ or override the default corpus_root to point
at the JSONL's parent:
export FRACTAL_RAG_CORPUS_ROOT=/where/you/actually/put/it
ingest --source docs (or notebooks) reports parsed_docs: 0¶
The markdown source walks <repo_root>/docs/**/*.md, and repo_root
defaults to the K3s host path /opt/tswetnam/github/fractal-notebooks
(config.py). On a laptop, a CI runner or any other checkout location that
directory does not exist, and rglob on a missing path is not an error —
it silently yields zero documents. Point the setting at your checkout:
export FRACTAL_RAG_REPO_ROOT=/path/to/your/fractal-notebooks # or $PWD from the repo root
fractal-rag ingest --source docs
The notebooks source scans <repo_root>/docs/**/*.ipynb as well (skipping the
generated per-site 3DEP notebooks), so the same FRACTAL_RAG_REPO_ROOT covers it;
FRACTAL_RAG_CORPUS_ROOT (the iRODS mirror) only adds the mirrored copies and may
be absent. Do not point CORPUS_ROOT at the checkout: that tree is scanned without
the site's exclusion rules. To make the setting stick, put it in a gitignored .env
at the repo root together with any port overrides; Settings reads .env from the
directory you run fractal-rag in, so run the CLI from the repo root.
Re-index after a reorganisation¶
Object UUIDs are derived from file paths and ingest upserts without ever
pruning, so moving, renaming or deleting pages (as the docs reorganisation
into Foundations, Research, Notebooks, Apps and Knowledge tabs did) leaves
stale objects under the old paths. Rebuild from scratch:
export FRACTAL_RAG_REPO_ROOT=$PWD # the checkout whose docs/ you want indexed
fractal-rag schema bootstrap --drop # destructive: drops and recreates both collections
fractal-rag ingest --source docs
fractal-rag ingest --source notebooks
fractal-rag status
Expect the Concept count to be unchanged and the FractalArtifact count to
drop to the current page set. Searching for text that only existed on a
deleted page should then return no hits.
Ingest is dropping a lot of objects¶
Watch failed_chunks per source in the JSON report that fractal-rag ingest
prints (the last run is also saved to ~/.cache/fractal-rag/state.json;
fractal-rag status only prints collection counts) and tail the failures file:
tail -f ~/.cache/fractal-rag/failed_objects.jsonl
Common causes:
- Embedding sidecar saturated: GPU OOM or 429 backoff in the sidecar log.
The ingest sends one chunk at a time, so this points at an undersized pod
memory limit rather than at load (
FRACTAL_RAG_BATCH_CONCURRENCYis read but not used by the current loop). - Property too large: a single chunk above ~64 KiB will fail with a
Weaviate validation error. Reduce
chunk_max_tokensor fix the upstream parser that's emitting a giant blob. - Concept reference unknown: an artifact references a Concept UUID that
wasn't created in pass 1. This shouldn't happen with the deterministic
uuid5(...)scheme; if it does, rerun pass 1 (fractal-rag schema bootstrap+ reseed).
status shows fewer json objects than inserted_chunks¶
Object UUIDs derive from source_uri plus the chunk index, so two records with
the same source_uri overwrite each other while the loop counts both as
inserted. The json parser keys records on the Constellate id (a Portico
ark:// identifier or a JSTOR stable URL), which is unique across the export;
before 2026-10-25 it keyed them on the DOI, and DOIs are shared by book
chapters, reprints and corrigenda: 1,125 of the 50,000
records repeated an earlier DOI (48,875 distinct), so that run reported 100,012
inserted chunks and status showed 97,762 json objects. An index built with the
old parser must be rebuilt, because every json UUID changes:
fractal-rag schema bootstrap --drop # destructive: drops and recreates both collections
fractal-rag ingest --source all
Deleting only the json objects (delete_many filtered on source_type ==
json_corpus, repeated until matches is 0 because the server deletes at most
10,000 objects per call) and re-running ingest --source json saves the ten
minutes the docs and notebooks take, but only on Weaviate 1.35.3 or newer: on
1.35.0 the bulk delete zeroed every keyword score (see Keyword scores are
0). On the DGX
Spark the json re-ingest took two hours and left exactly 100,012 json objects
for 100,012 inserted chunks: an anchor chunk and one content chunk for each of
the 50,000 records (12 long records get a second content chunk).
Ingest takes forever on the JSONL corpus¶
The 50K-article JSONL is ~17 GB and dominates wall clock. Options:
- Ingest a slice first:
fractal-rag ingest --source json --limit 5000. - Run two sidecar replicas:
kubectl -n ai-workloads scale deploy/t2v-transformers --replicas=2took a DGX Spark from 8 to 14 chunks per second (the full corpus then takes about two hours).FRACTAL_RAG_BATCH_SIZEis not consulted by the current one-chunk-at-a-time loop, so raising it changes nothing. - Run the ingest in
screen/tmux; it's resumable because the deterministic UUID scheme upserts rather than appends.
MCP / Claude Code¶
/mcp doesn't list fractal-rag¶
- You're not in the repo root, or the repo's
.mcp.jsonisn't being picked up. Runcat .mcp.jsonto confirm. - Or
fractal-ragisn't on PATH for the Claude Code process. In a fresh terminal:which fractal-rag. If empty, re-runpip install -e .and reopen Claude Code so it picks up your shell's PATH.
MCP server starts but returns weaviate_error: connection refused¶
- Weaviate isn't running on the host/port the server expects. Check
FRACTAL_RAG_WEAVIATE_HTTP_PORT— defaults to30080(K3s NodePort). Local docker-compose users need8080. - The gRPC port is configured separately (
FRACTAL_RAG_WEAVIATE_GRPC_PORT). Weaviate v4 SDK uses both REST and gRPC.
MCP tool returns not_found for a concept you "know" is there¶
Concept names are case-sensitive in artifacts_by_concept and
get_concept. Use list_concepts() or search_concepts(query) first to
find the canonical name. The agent already does this — if you're hitting
this manually from the CLI, the same rule applies.
SSH-tunneled MCP server (remote VM) drops connection¶
- Use keys-only auth; a password prompt corrupts the stdio stream.
- Add
-o ServerAliveInterval=30to the SSH args in.mcp.jsonto keep long retrievals alive. - Pre-warm the venv: SSH connection setup + Python import can take ~3 s on cold start, which Claude Code may flag as a startup timeout. Bumping the MCP server start timeout (in Claude Code settings) helps.
Agent quality¶
Agent says "I couldn't find anything" but the topic obviously exists¶
-
Check the index actually has content for that source type:
fractal-rag status -
Run the same query directly against the index:
fractal-rag query "<the question>" --limit 10 -
If the CLI returns hits but the agent doesn't, the agent's tool selection is wrong. Re-prompt with an explicit nudge: "Use
search_artifactswithtarget_vector='content_vec'andalpha=0.3for that question." If that fixes it, codify the rubric in.claude/agents/fractal-explorer.md.
Agent invents citations¶
The agent shouldn't do this — the citation discipline section of the subagent file forbids it. If it slips, two fixes:
- Tighten the prompt: add an explicit "If a tool returned no objects, say so plainly and STOP — do not synthesize." line.
- Verify the returned
source_uriactually resolves (Readit from disk). The agent has theReadtool for exactly this.
Re-embedding after a model swap takes too long¶
After swapping the embedding model image, you need to re-vectorize every object. The reusable trick is to scope the schema drop to artifacts only, keep concepts (they're tiny), and rerun ingest:
python -c "
from fractal_rag.client import client_session
with client_session() as c: c.collections.delete('FractalArtifact')"
fractal-rag schema bootstrap
fractal-rag ingest --source all
Still stuck?¶
- Check
kubectl -n ai-workloads get events --sort-by=.lastTimestamp | tailfor cluster-level signals. - Read the design spec — the "Risks / mitigations" section calls out known weak spots.
- File an issue with the failing CLI command, the relevant log snippet, and
the output of
fractal-rag status.