fix: pin the model cache to a persistent dir so capture stops dying silently - #157
Conversation
…ilently
fastembed defaults its ONNX cache to tempfile.gettempdir(); macOS purges that
directory on a timer. The purge takes the small config/tokenizer blobs first and
leaves the snapshot symlinks dangling, so every model load then fails with
`Could not find config.json`.
Two failure modes, both silent:
- `cairn sweep` crashes outright, and the plugin's capture hook redirected its
output to /dev/null — so capture can stop for weeks with no error anywhere.
- recall keeps "working" because it falls back to BM25 when the embedder fails,
quietly losing semantic retrieval.
Found on a real install: capture had been dead 3 weeks (vault frozen at Jul 30)
while recall fired hourly against a degraded keyword-only path, and
`cairn doctor` still reported status: OK.
Pin cache_dir to ~/.cache/agentcairn/models for both the embedder and the
cross-encoder reranker (an explicit FASTEMBED_CACHE_PATH still wins), make
doctor actually load the embedder and fail loudly when it can't, and keep the
capture hook's output in ~/.cache/agentcairn/logs/capture.log.
Plugin 0.4.2 / Codex plugin 0.1.4 ship the hook change. Release 0.25.3.
Tests: 773 passed + 25 plugin static tests; new guards assert both fastembed
constructors receive an explicit cache_dir, so the temp default can't return.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
agentcairn | 74fa35c | Commit Preview URL Branch Preview URL |
Aug 20 2026, 02:34 PM |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 74fa35c. Configure here.
| # Pin the cache to a persistent dir; fastembed's default is the OS temp | ||
| # dir, which macOS purges out from under us (see paths.models_root). | ||
| cache_dir = models_root() | ||
| cache_dir.mkdir(parents=True, exist_ok=True) |
There was a problem hiding this comment.
Cache dirs created without private modes
Medium Severity
models_root().mkdir(parents=True) (and the new logs mkdir -p) create ~/.cache/agentcairn with the process umask instead of ensure_private_dir. On first warm/sweep, that runs before index setup, so the cache root stays world-traversable and capture.log is left world-readable. Sweep output can include vault paths and distilled memory text.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 74fa35c. Configure here.


Found while diagnosing a real install where capture had been dead for three weeks and nobody noticed. Releases as 0.25.3.
What happens
fastembed defaults its ONNX cache to
tempfile.gettempdir(). macOS purges that directory on a timer — and it takes the small config/tokenizer blobs first, leaving the snapshot's symlinks dangling. Every model load then dies:Why it was invisible — two independent silences
cairn sweepfails hard, and the plugin hook ran it asnohup … >/dev/null 2>&1 &. No error anywhere.recall_hookcatches the embedder failure and falls back to BM25 (emb = None), so recall keeps returning results — just keyword-only, semantic retrieval quietly gone.On the affected machine: vault frozen at Jul 30, recall firing hourly against the degraded path, and
cairn doctorstill printingstatus: OK. After repair, one sweep recovered the backlog — "swept: 21 memory note(s) written; 9 deduped, 3 superseded" — so nothing was lost, it just never ran.The fix
cache_dirto~/.cache/agentcairn/models(beside the index) for both the embedder and the cross-encoder reranker. An explicitFASTEMBED_CACHE_PATHstill wins, so anyone who deliberately relocated their models keeps them.cairn doctornow loads the embedder and fails with aPROBLEMif it can't. A healthy index never implied a healthy model — the index is a static file, the model is a cache that rots later. (Note: on a machine whose model isn't cached yet, doctor will download it. That's deliberate — doctor's job is to answer "will recall and capture actually work?", which requires really loading it.)~/.cache/agentcairn/logs/capture.log(truncated per run) instead of/dev/null.Tests
773 passed+25plugin static tests (run separately —plugin/tests/sits outside the default testpaths). The new guards assert both fastembed constructors receive an explicitcache_dir, so the temp default can't silently return. Doctor verified both ways:embedder: fastembed OKon a healthy install,PROBLEM+ non-zero exit on a broken one.Ships as Claude Code plugin 0.4.2 / Codex plugin 0.1.4 (they share
session-end.sh).🤖 Generated with Claude Code
Note
Medium Risk
Changes default model cache location and capture/doctor behavior on the critical embedder path; low logic risk but affects all hybrid recall, sweep, and rerank on macOS and fresh installs.
Overview
Pins fastembed’s ONNX model cache to
~/.cache/agentcairn/models(via newpaths.models_root()), for bothTextEmbeddingand the cross-encoder reranker, instead of the OS temp dir that macOS can purge and leave with broken symlinks (Could not find config.json).FASTEMBED_CACHE_PATHstill overrides the default.cairn doctornow probes embedder load and exits withPROBLEMwhen the model cannot load, since a healthy DuckDB index does not guarantee capture/recall will work.Session capture hook writes sweep output to
~/.cache/agentcairn/logs/capture.log(truncated each run) instead of/dev/null. Release 0.25.3; Claude Code plugin 0.4.2, Codex 0.1.4. Tests assert explicitcache_diris always passed to fastembed.Reviewed by Cursor Bugbot for commit 74fa35c. Bugbot is set up for automated code reviews on this repo. Configure here.