Troubleshooting
The errors Tennis raises, what each one is protecting you from, and how to clear it.
Tennis prefers a loud refusal to a quiet wrong answer. Most of what follows is a deliberate stop, not a bug.
"was indexed with … but the loaded embedder is …"
namespace "notes" was indexed with builtin:potion-retrieval-32M (512 dims) but the
loaded embedder is openai:text-embedding-3-small (1536 dims); reindex the
namespace or restore the original modelThe namespace is bound to one embedder for life. You are asking it to compare vectors from two different models, and a cosine between those is not a worse score — it is a meaningless one.
Either restore the original model (drop --openai, or set OPENAI_API_KEY again if the namespace really is an OpenAI one), or build a new namespace:
tennis ns create notes-v2 --openai text-embedding-3-small
tennis add ~/Documents/notes --ns notes-v2See Embedders.
"openai embedder requested but OPENAI_API_KEY is not set"
You passed --openai, or opened a namespace bound to an OpenAI model, without a key in the environment:
export OPENAI_API_KEY=sk-...Tennis never reads that variable to decide whether to use OpenAI — only to use it once you have asked. That is why a namespace bound to OpenAI fails here instead of quietly falling back to the built-in model.
"no matches"
$ tennis search "keep me signed in" --mode keyword
no matchesIn --mode keyword this is literal: no indexed document contains those words. Drop --mode to let the vectors answer.
In hybrid mode, no matches means the namespace is empty or your --where filter excluded everything. Check what is there:
tennis ns list"no indexable files in … (looking for .md,.txt, N skipped)"
add indexes .md and .txt by default. Name the extensions you want:
tennis add ./src --files --ext .go,.ts,.rsThe N skipped count includes files refused for content rather than extension: files containing binary content, and files over 10MB. Both are refused with a note on stderr, because a PDF's raw bytes would index without erroring and quietly pollute every future ranking.
"does not look like a chat export" / "nothing to import from …"
Detection reads the source rather than the filename, and it can be wrong about an unusual archive. Name the format:
tennis add --chatgpt ~/Downloads/export.zip
tennis add --claude ~/Downloads/export.zip
tennis add --claude-code ~/.claude
tennis add --codex ~/.codex
tennis add --files ./whatevernothing to import from X (read as Y) means detection picked Y and found no documents in it — usually the wrong Y. See Importing sessions.
"--format X contradicts --Y" / "--X and --Y name different sources"
You passed two source flags that disagree, e.g. --claude --codex, or --format files --chatgpt. Pick one.
"checksum mismatch for …"
checksum mismatch for potion-retrieval-32M/model.safetensors:
got a1b2…
want c3d4…
the upstream file changed or the download was corrupted; refusing to use itModel downloads are verified against SHA-256 checksums pinned in the source. A corrupted download is the common cause — clear the cache and retry:
rm -rf ~/.cache/tennis && tennis search "anything"If it persists, the upstream file genuinely changed, which is a bug to report rather than work around: a silently different model would change every vector the install ever produces.
"cannot parse filter … (want key=value, key>value, …)"
--where takes comma-separated comparisons, no spaces around the operator:
tennis search "auth" --where 'status=merged'
tennis search "auth" --where 'cost>5,status!=failed'
tennis search "flaky test" --where project=tennis,branch=mainSupported operators are =, !=, >, >=, <, <=.
"invalid namespace name" / "invalid attribute name"
Namespaces take letters, digits, dash, and underscore, up to 64 characters, and must start with a letter or digit. Attribute names additionally allow dots.
"N line(s) failed to parse" from add --ndjson
$ tennis add --ndjson --ns agents --json < events.ndjson
tennis: ndjson: line 3: unexpected end of JSON input
{
"chunks": 2,
"failed": 1,
"skipped": 0,
"written": 2
}
error: 1 line(s) failed to parseBad lines are reported with their line number and skipped; the good ones are still written. The command exits nonzero so a script notices. Each line needs id and text.
Results are there, but bad
Tennis has no relevance threshold — semantic search ranks everything, so a query always returns up to -n results even when nothing is a good match. Read the evidence rather than the position:
- A hit tagged with only one ranker (
[sem#3]) at a low score is usually noise. - A hit both rankers put first scores about
0.0328; one ranker alone tops out near0.0164. See why fuse by rank.
If keyword search finds nothing for Chinese, Japanese, or Korean text, that is a known limitation: the FTS5 tokenizer does not segment CJK. Semantic search still works.
Starting over
The database is one ordinary SQLite file, and the model cache is a directory:
tennis ns rm notes # one namespace
rm ~/.tennis/db.sqlite # everything indexed
rm -rf ~/.cache/tennis # the downloaded modelNothing else on your machine is state.