Skip to content

fix: pin the model cache to a persistent dir so capture stops dying silently - #157

Merged
ccf merged 1 commit into
mainfrom
fix/persistent-model-cache
Aug 21, 2026
Merged

fix: pin the model cache to a persistent dir so capture stops dying silently#157
ccf merged 1 commit into
mainfrom
fix/persistent-model-cache

Conversation

@ccf

@ccf ccf commented Aug 20, 2026

Copy link
Copy Markdown
Owner

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:

ValueError: Could not find config.json in /private/var/folders/.../T/fastembed_cache/...

Why it was invisible — two independent silences

  1. Capture crashes, output discarded. cairn sweep fails hard, and the plugin hook ran it as nohup … >/dev/null 2>&1 &. No error anywhere.
  2. Recall pretends to work. recall_hook catches 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 doctor still printing status: 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

  • Pin cache_dir to ~/.cache/agentcairn/models (beside the index) for both the embedder and the cross-encoder reranker. An explicit FASTEMBED_CACHE_PATH still wins, so anyone who deliberately relocated their models keeps them.
  • cairn doctor now loads the embedder and fails with a PROBLEM if 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.)
  • The capture hook keeps its output in ~/.cache/agentcairn/logs/capture.log (truncated per run) instead of /dev/null.

Tests

773 passed + 25 plugin static tests (run separately — plugin/tests/ sits outside the default testpaths). The new guards assert both fastembed constructors receive an explicit cache_dir, so the temp default can't silently return. Doctor verified both ways: embedder: fastembed OK on 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 new paths.models_root()), for both TextEmbedding and 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_PATH still overrides the default.

cairn doctor now probes embedder load and exits with PROBLEM when 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 explicit cache_dir is always passed to fastembed.

Reviewed by Cursor Bugbot for commit 74fa35c. Bugbot is set up for automated code reviews on this repo. Configure here.

…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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 74fa35c. Configure here.

@ccf
ccf merged commit f9b334f into main Aug 21, 2026
8 checks passed
@ccf
ccf deleted the fix/persistent-model-cache branch August 21, 2026 01:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant