Documents / Issues / issue-c5c089bcc1b2
The embedding model is downloaded into a directory the system may delete
fastembed's default cache is under the temp directory, so the 64 MB download is re-paid whenever the OS sweeps — and where there is no temp directory, every read crashes before docir runs.
What is wrong¶
docir left fastembed to pick where the embedding model is kept, and its default is
tempfile.gettempdir()/fastembed_cache. A downloaded model is durable state — 64 MB
on this machine — and a temp directory is where the system puts state it is entitled
to delete. So the download is re-paid whenever the OS sweeps, silently, as a read
that takes 16 seconds instead of two.
The same default is why a sandbox with no usable temporary directory cannot read at
all. define_cache_dir computes that path before it reads FASTEMBED_CACHE_PATH:
default_cache_dir = os.path.join(tempfile.gettempdir(), "fastembed_cache")
cache_path = Path(os.getenv("FASTEMBED_CACHE_PATH", default_cache_dir))
so gettempdir() raises before the override is consulted, and setting the variable
does not help. Only passing cache_dir does.
Measured¶
Left open by doctor and daemon status still crash where the daemon has nowhere to listen, which closed docir's half of the same sandbox.
With tempfile.gettempdir() made to raise, against this repository's own corpus:
| command | before | after |
|---|---|---|
docir query |
crashed in fastembed | answers |
docir get |
crashed in fastembed | answers |
docir search |
crashed in fastembed | answers |
docir context |
crashed in fastembed | answers |
docir check, doctor, daemon status |
answered | answers |
The reported environment was milder — writes to TMPDIR were denied but a read
answered — so the everyday half is the sweep, not the sandbox.
What would fix it¶
Pass cache_dir explicitly, pointing at a user-level directory. Not the resolved
store: a project store is one per repository and a committed artifact, so a copy
inside each would be downloaded per repository and would need gitignoring to stay out
of every teammate's working tree. The model is identical for every store on the
machine.
FASTEMBED_CACHE_PATH has to keep working where somebody set it. This repository's
own CI pins it and caches that directory, and two sides naming different directories
is the defect its workflow comment already records.
The reasoning is Where a downloaded embedding model lives.
Resolution¶
FIXED 2026-09-18. model_cache_home() answers ~/.docir/models, honouring
FASTEMBED_CACHE_PATH, and the composition root passes it to the adapter as fastembed's
cache_dir. The reasoning, the alternatives refused and the one-time cost are
Where a downloaded embedding model lives.
Against this repository's own 233-document corpus with tempfile.gettempdir() made to
raise, every read now answers — query, get, search and context, the last of which
loads the model. Before, all four crashed inside define_cache_dir. The two commands
doctor and daemon status still crash where the daemon has nowhere to listen fixed already worked, so the class is closed rather than narrowed.
Nothing re-embedded: model_id identifies the model and not its location, so
embeddings_pending stayed 0 and the released 0.27.0 reads the same store and ranks the
same first hit.
models/ is in the generated store .gitignore too. It only matters where the store is
the global ~/.docir — somebody keeping personal notes there under git would otherwise
commit 64 MB — and it arrived through the refresh The store's generated .gitignore misses release-check.json and is never refreshed built, which is
the first entry that machinery has carried in anger. Doing so exposed one flaw in it and
closed that as well: a second entry added by the same version repeated the "Added by
docir" header, which is noise in a committed file and happens whenever a store is upgraded
again before the next release.
docir doctor reports the path under embedding.cache, because "why did that command take
sixteen seconds" and "where did the disk go" are the same question and nothing answered
either.
Six injections, each proven to fail its guard: deriving the path from the temp directory
again, ignoring an explicit FASTEMBED_CACHE_PATH, treating an empty one as a choice, the
adapter dropping the path, the composition root not wiring it, and the header repeating
per run.