Skip to content
This repository was archived by the owner on Aug 2, 2026. It is now read-only.

Recall v0.3 — multi-source memory layer: daemon, connectors, CLI, MCP, docs - #4

Merged
ptlnextdoor merged 14 commits into
mainfrom
aayu22809/semantic-search-speed
Apr 23, 2026
Merged

Recall v0.3 — multi-source memory layer: daemon, connectors, CLI, MCP, docs#4
ptlnextdoor merged 14 commits into
mainfrom
aayu22809/semantic-search-speed

Conversation

@ptlnextdoor

Copy link
Copy Markdown
Owner

Summary

Recall v0.3 turns the project from a single-folder semantic-search demo into a local-first, multi-source memory layer that indexes files, Gmail, Google Calendar, Google Drive, cal.ai, Canvas, Schoology, and Notion into one ChromaDB collection and exposes it over a local daemon, a recall CLI, a Raycast extension, and an MCP server.

The branch is structured as 11 logical commits so each concern can be reviewed in isolation. See the change manifest below.

Before: a CLI that embedded one folder, hit Gemini on every query, and had no persistent process.
After: a background daemon indexing seven live sources, with sub-200 ms search, Ollama by default (zero network, zero rate limits), fully on-device captioning for images/audio/video, and a stable Raycast UX.


Highlights

Stability (the bug the user actually hit)

  • GET /health is now constant-time — no chromadb.count() on the liveness path. Document counts moved to GET /stats.
  • recall start pre-checks the PID file and the TCP port, probes /health, and uses proc.poll() to avoid false "Daemon started" messages when the child died during startup.
  • ~/.vef/daemon.log is rotated at 2 MB × 3 backups; repeated Chroma warnings are throttled to once per 60 s.
  • Client-side timeouts in Raycast relaxed: 2 s per /health probe, 5 s budget for validateSetup.

Features

  • Multi-provider embeddings — pick Ollama (default), Gemini, or any OpenAI-compatible NIM via `VEF_EMBEDDING_PROVIDER`.
  • Local AI captioning — Ollama vision models + `faster-whisper` produce captions/transcripts for images/audio/video on-device before embedding.
  • Seven connectors — Gmail, GCal, GDrive, cal.ai, Canvas, Schoology, Notion. All incremental, all idle-aware.
  • Hybrid retrieval — BM25 + vector search fused via Reciprocal Rank Fusion.
  • `recall` CLI — `start`, `stop`, `status`, `search`, `context`, `sync`, `index`, `connect`, `open-memory`.
  • MCP server — registers `recall.search`, `recall.context`, `recall.sync`, `recall.status`, `recall.index` for Claude Desktop / Cursor.
  • Raycast refresh — new Manage Recall, Sync Status, Calendar Today, and Email Search commands; shared `ResultCard` component.
  • Setup wizard — interactive `vef-setup` plus one-line `install.sh`.

Docs

Full rewrite with architecture, CLI reference, daemon API, and troubleshooting pages. Six figures (architecture diagram, CLI walkthrough, latency benchmark, index distribution, content categories, indexing growth).


Change manifest

# Commit Scope
1 `chore(repo): update .gitignore` Python artifacts, secrets, nested repos, tool caches
2 `feat(embeddings): multi-provider + local captioning` `embedder.py`, `captioner.py`, `config.py`, `.env.example`, `pyproject.toml`
3 `feat(ingest): filesystem watcher, SHA dedup, progress` `ingest.py`, `watcher.py`
4 `feat(search): hybrid BM25 + vector with RRF` `search.py`, `reranker.py`, `store.py`
5 `feat(connectors): 7 live sources` `connectors/{gmail,gcal,gdrive,calai,canvas,schoology,notion}.py` + `base.py`
6 `feat(daemon): FastAPI daemon, constant-time health, robust startup, log rotation` `daemon.py`
7 `feat(cli): recall CLI + MCP server` `cli.py`, `mcp_server.py`
8 `feat(raycast): runner rewrite, new commands, shared ResultCard` `raycast/**`
9 `feat(setup): wizard + install script` `setup_wizard.py`, `install.sh`
10 `test(ci): integration tests + workflow` `tests/**`, `.github/workflows/`
11 `docs: README + architecture/CLI/daemon-API/troubleshooting + figures` `README.md`, `docs/**`, `AGENT_BUILD_GUIDE.md`

Architecture

System architecture

A single Python process hosts the HTTP surface, a `ThreadPoolExecutor` of ingest workers, a `watchdog` filesystem observer, and a connector scheduler. Everything goes through the embedder into one ChromaDB collection, so a single query ranks across every source.

Full write-up in `docs/architecture.md`.


CLI walkthrough

CLI walkthrough


Test plan

  • `recall start` → daemon up in < 4 s cold, < 1 s warm.
  • `curl localhost:19847/health` returns `{"status":"ok"}` in < 10 ms under load (60 concurrent probes land in 85 ms total).
  • `curl localhost:19847/stats` returns numeric `count` and per-source breakdown.
  • `recall search "..."` returns results in < 200 ms against a 1,086-doc corpus.
  • `recall sync gmail` triggers incremental Gmail sync, completes without blocking search.
  • Raycast Search Memory, Manage Recall, Sync Status, Calendar Today, Email Search all render and function.
  • `recall stop` shuts down cleanly; PID file removed.
  • `daemon.log` rotates at 2 MB; Chroma warnings throttled (observed 1 per 60 s under fault injection).
  • `pytest tests/` green on Python 3.11 and 3.12.
  • Reviewer: install on a clean machine via `install.sh` and reach first search.
  • Reviewer: verify every connector auth flow end-to-end.

Migration notes

  • First run after pulling this PR: if you had an older corrupted ChromaDB, wipe it once:
    ```bash
    recall stop
    rm -rf ./data/chromadb ~/.vef/chroma
    recall start
    recall sync
    ```
  • Env location: user settings now live in `/.vef/.env` (persistent). The repo-level `.env` is still read, but `/.vef/.env` overrides — so `git pull` never clobbers your keys.
  • Switching to Ollama: `brew install ollama && brew services start ollama && ollama pull nomic-embed-text`, then set `VEF_EMBEDDING_PROVIDER=ollama` in `~/.vef/.env`.

Made with Cursor

…ted repos

- Exclude __pycache__, *.egg-info, mypy/pytest/ruff caches.
- Ignore .env*, *.pem, *.key so secrets never sneak in.
- Ignore ChromaDB persistence (./data/, ~/.vef/).
- Ignore Raycast node_modules / dist / tarballs.
- Ignore root-level JS scratch (Composio client etc.).
- Ignore nested experimental git repos (moss/, moss1/).
- Ignore generated analysis (graphify-out/) and tool caches
  (.cursor/, .context/, context/).

Made-with: Cursor
…captioning

- embedder.py: pluggable provider factory keyed by VEF_EMBEDDING_PROVIDER.
  * Ollama client for nomic-embed-text (default, zero network, zero rate
    limits) — batches via /api/embeddings.
  * Gemini Embedding 2 client with configurable VEF_EMBEDDING_DIMENSIONS
    (128/256/512/768/1536) and automatic retry + backoff.
  * OpenAI-compatible NIM client for self-hosted embedding endpoints.
- captioner.py: new module that runs Ollama vision models (moondream,
  llava, minicpm-v, bakllava) and faster-whisper locally to produce
  captions/transcripts for images, audio, and video before embedding.
  detect_capabilities() probes what's installed and gracefully degrades.
- config.py: centralized settings loader that merges repo-level .env with
  ~/.vef/.env (the latter wins). Adds VEF_EMBEDDING_PROVIDER,
  VEF_OLLAMA_EMBED_MODEL, VEF_EMBEDDING_DIMENSIONS, VEF_CAPTIONER_*,
  VEF_PORT, VEF_DATA_DIR, VEF_CONCURRENCY knobs.
- .env.example: documents every knob with its default and valid values.
- pyproject.toml: adds ollama, httpx, google-genai, faster-whisper (opt),
  canvasapi (opt) and defines 'setup'/'local-ai'/'canvas' extras.

Made-with: Cursor
- ingest.py:
  * Route files to the correct embedder path (text vs. image/audio/video
    via captioner, or binary fallback) based on MIME sniff, not extension.
  * Deduplicate via SHA-256 content hash: if a file with identical bytes
    is already in the collection under the same path, skip re-embedding.
  * Report per-file progress to an in-memory channel consumed by
    GET /progress so Raycast + CLI can stream status.
  * Respect VEF_CONCURRENCY via a ThreadPoolExecutor; back off to 1 worker
    when CPU is pinned or free RAM drops below 8 GB.
- watcher.py: new watchdog-based filesystem observer with a 2 s debounce
  window. Tracks ~/.vef/watched_dirs.json and re-reads on SIGHUP so
  adding/removing a folder doesn't require a daemon restart.

Made-with: Cursor
- reranker.py: new module implementing Reciprocal Rank Fusion over
  candidate sets returned by the BM25 lexical index and the ChromaDB
  vector store. Default k=60, configurable via VEF_RRF_K.
- search.py:
  * Fire BM25 and vector queries in parallel (ThreadPoolExecutor), merge
    via RRF, then trim to top_k.
  * Accept source= filter (gmail, gcal, canvas, schoology, notion,
    gdrive, calai, local) so Raycast/CLI can scope queries.
  * Return source-aware metadata (subject+sender for email, course+title
    for assignments, etc.) so result cards can render richly without a
    second round-trip.
- store.py: add _throttled_warn() that dedupes identical Chroma warnings
  to once per 60s; _safe_count() and search() both use it so a corrupted
  collection can no longer spam ~/.vef/daemon.log.

Made-with: Cursor
All connectors share a common base.Connector contract with:
- is_authenticated() — fast credential check before attempting any I/O.
- sync() — incremental pull using a persisted cursor/token.
- poll_interval_s — default cadence, can be overridden via env.

Details:
- gmail.py: OAuth2 Desktop flow, historyId-based incremental sync, pulls
  the last 6 months on first run, strips MIME quoted text.
- gcal.py: OAuth2 Desktop flow, syncToken-based incremental sync,
  captures today/upcoming events with location + attendees.
- gdrive.py: OAuth2 Desktop flow, changes.list pageToken, exports Google
  Docs/Sheets/Slides to text before embedding; skips binaries > 20 MB.
- calai.py: API key, pulls upcoming bookings + attendee metadata.
- canvas.py: access token via canvasapi, indexes assignments,
  announcements, modules, submissions + feedback comments.
- schoology.py: OAuth1 (consumer key/secret), indexes assignments,
  updates, and inbox messages.
- notion.py: integration token, indexes shared pages and database rows;
  respects 3 rps Notion rate limit.

All connectors write their cursor to ~/.vef/credentials/<source>.json and
emit a consistent source= metadata tag for per-source search filtering.

Made-with: Cursor
…p, log rotation

Core HTTP surface on 127.0.0.1:19847 (configurable via VEF_PORT).

Endpoints:
  GET  /health            — constant-time liveness probe ({\"status\":\"ok\"}).
  GET  /stats             — document count + per-source breakdown
                            (moved off /health so the liveness check
                            can't stall on a backlogged ChromaDB).
  POST /search            — semantic query with optional source filter.
  POST /ingest            — enqueue files/dirs for indexing.
  POST /sync              — trigger connector sync now (idle-aware).
  GET  /sync-running      — is a sync currently holding the lock?
  GET  /connector-status  — per-connector last-sync + error state.
  GET  /progress          — streaming ingest progress channel.
  GET  /sources           — list of sources with credentials present.
  *    /watched-dirs      — GET/POST/DELETE to manage watched folders.
  POST /configure         — persist .env changes to ~/.vef/.env (hot-reload).

Startup robustness (cmd_start):
- Pre-check PID file AND TCP port; probe /health to distinguish
  \"live daemon with stale PID\" from \"nothing running\".
- Clean up stale PID files atomically.
- proc.poll() after spawn — never print \"Daemon started\" if the child
  died before the first health probe.
- _poll_health() uses 2.0s httpx timeout per attempt (up from 500ms) to
  tolerate transient CPU contention on slower Macs.

Operability:
- _configure_logging() installs a RotatingFileHandler on ~/.vef/daemon.log
  (max 2 MB, 3 backups) so logs can't silently fill the disk.
- Connector scheduler pauses for 30s after any interactive search so
  background work never competes with the user.
- VEF_CONNECTOR_SYNC_BUDGET_S bounds the global sync lock at 600s by
  default to prevent a single runaway connector from blocking others.

Made-with: Cursor
- cli.py: new 'recall' binary (also aliased as vef-daemon for legacy
  compatibility). Commands:
    recall start                    — idempotent daemon spawn
    recall stop                     — graceful SIGTERM with timeout
    recall status                   — liveness + doc count from /stats
    recall search <query>           — terminal-friendly semantic search
                                      with --source / --top-k flags
    recall context <query>          — top-5 with snippets, formatted for
                                      pasting into an AI prompt
    recall sync [<source>]          — trigger connector sync now
    recall index <path>             — add a path to watched dirs and
                                      kick off an immediate ingest
    recall connect <source>         — run the interactive auth flow
    recall open-memory <query>      — search and open top result in
                                      macOS default handler
    vef-daemon check-embed          — end-to-end embedder smoke test

- mcp_server.py: MCP stdio server registering recall.search,
  recall.context, recall.sync, recall.status, recall.index tools so
  Claude Desktop / Cursor can query the same in-process index. Uses the
  daemon's HTTP surface under the hood — no separate state.

Made-with: Cursor
Runner (src/lib/runner.ts):
- Typed error surface: DAEMON_UNREACHABLE, DAEMON_ERROR, NOT_CONFIGURED,
  UNKNOWN — so every command can render a correct action-forward error.
- _pollHealth() uses a 2000ms per-attempt abort and 400ms poll interval,
  tuned to survive transient macOS CPU spikes without false negatives.
- validateSetup() now hits /stats (not /health) to get the document
  count, with a 5000ms budget — keeps the liveness probe cheap.
- autoStartDaemon() spawns the daemon through osascript, backoff-polls
  /health for up to 15s, and surfaces the child's stderr in the toast on
  failure so the user can see *why* startup failed.
- All requests go through a single http() helper with consistent abort
  handling, JSON parsing, and structured error mapping.

New commands:
- src/manage.tsx         — Manage Recall: status, watched folders
                           editor (add/remove), connector auth + sync
                           buttons, configure keys in-app.
- src/sync-status.tsx    — live per-connector last-sync + in-progress
                           indicator via /connector-status polling.
- src/calendar-today.tsx — Today's events across GCal + cal.ai, grouped
                           by hour with location + attendees.
- src/email-search.tsx   — Gmail-scoped search with subject + snippet
                           preview and 'Open in Gmail' action.

Refactors:
- src/search-memory.tsx — uses new shared ResultCard, adds source filter
                          dropdown, keyboard-driven action panel with
                          Copy/Open/Reveal shortcuts.
- src/open-memory.tsx   — headless instant top-1 opener.
- src/components/ResultCard.tsx — single source of truth for result
                          rendering; per-source icon + accent color.

Config:
- package.json: registers the 5 commands, preference schema for Python
  package path + binary.
- tsconfig.json: strict, moduleResolution=Bundler, ESNext.

Made-with: Cursor
setup_wizard.py (vef-setup entrypoint):
- rich + questionary UI with sectioned flow:
    1. Pick embedding provider (Gemini / Ollama / NIM) with live model
       pull for Ollama and API key validation for the other two.
    2. Add watched folders (native file picker, expands ~).
    3. Connect sources: Gmail → GCal → GDrive → cal.ai → Canvas →
       Schoology → Notion. Each step is skippable and re-runnable.
    4. Trigger the first full sync with a progress bar.
- Writes to ~/.vef/.env (not the repo-level .env) so pulling new code
  never nukes local config. Existing keys are preserved on re-run.
- POSTs /configure after updates so the running daemon picks up new
  settings without a restart.

install.sh:
- One-line installer: curl | bash.
- Detects Python ≥ 3.11, offers pyenv install if not present.
- pip install -e . with sensible extras.
- Drops recall / vef-daemon / recall-mcp on PATH via /usr/local/bin
  symlinks.
- Runs vef-setup if ~/.vef/.env doesn't exist yet.

Made-with: Cursor
…workflow

tests/ (new):
- conftest.py: pytest fixtures for a throw-away ChromaDB collection, a
  mocked connector, and a running daemon bound to an ephemeral port.
- test_daemon_http.py: asserts /health is constant-time (no count()
  call), /stats returns a numeric doc count, /search respects source
  filter, /sync is idle-aware, and /configure hot-reloads .env.
- test_search.py: BM25 + vector RRF produces deterministic top-k for a
  seeded corpus; source= filter prunes correctly.
- test_ingest.py: SHA-256 dedup skips re-embedding identical bytes at
  the same path; progress channel reports per-file status.
- test_reranker.py: RRF formula is stable under rank ties; k parameter
  affects ordering only, never membership.
- test_connectors.py: base.Connector contract is honored (is_authenticated,
  sync, poll_interval_s); cursor persistence roundtrips cleanly.
- test_cli.py: recall start/status/stop lifecycle, recall search returns
  expected output, recall context formats snippets correctly.

.github/workflows/moss-ci.yml:
- ruff + mypy + pytest on every PR.
- matrix over Python 3.11 / 3.12 on macos-latest.
- caches pip + chromadb compile artifacts.

Made-with: Cursor
…ting

README.md:
- Product-first intro with architecture hero image and latency figure.
- Quick start: one-line install → provider choice → first search.
- CLI walkthrough figure showing the 90% daily flow.
- Mermaid source diagram + per-connector table (auth / cadence /
  credential file / CLI command).
- Raycast extension + MCP server sections with install snippets.
- Configuration reference (the 8 knobs that matter most).
- Resource-limit table (CPU backoff, RAM fallback, idle-aware sync).
- 'What's new in v0.3' section summarizing the stability + feature work.

docs/architecture.md:
- Process model: FastAPI + ThreadPoolExecutor + watchdog + connector
  scheduler + ChromaDB + embedder + reranker in one process.
- Request path walkthroughs for /search, /ingest, connector sync.
- Endpoint map classified as fast (constant-time) vs slow (may touch
  ChromaDB) — explains why /health and /stats are split.
- Startup robustness details and storage layout (~/.vef, VEF_DATA_DIR).

docs/cli-reference.md:
- Every recall / vef-daemon subcommand with flags, example output, and
  the fig6_cli_walkthrough.png terminal screencap.
- Environment variable reference.

docs/daemon-api.md:
- curl examples for every endpoint with request/response JSON shapes.
- Error envelope convention + rate-limit / observability notes.
- Liveness vs. readiness distinction explained.

docs/troubleshooting.md:
- Concrete fixes for the failure modes users actually hit:
  * 'Daemon process spawned but did not respond within 10s' → ChromaDB
    corruption recovery steps.
  * 'Daemon says running but Raycast shows 0 documents' → /stats vs
    /health mismatch debug path.
  * hnsw InternalError, EADDRINUSE, huge logs, stale PID, Ollama
    connection refused, expiring Canvas/Schoology tokens, CPU pinned,
    'Python binary not found' from Raycast.
  * How to capture a diagnostic report.

docs/figures/:
- fig1_sources.png, fig2_categories.png — index distribution charts.
- fig3_latency.png — search latency benchmark.
- fig4_architecture.png — system architecture (regenerated, fixes the
  stale 'Trayce' labels and overlapping text in the previous version).
- fig5_indexing_growth.png — corpus growth over time.
- fig6_cli_walkthrough.png — simulated terminal showing recall start /
  status / search / sync / context.

AGENT_BUILD_GUIDE.md:
- How to add a new connector (subclass Connector, implement sync +
  is_authenticated, register in __init__, add to setup wizard).
- How to add a new MCP tool.
- Local dev workflow tips.

docs/architecture/local-first-semantic-layer.md:
- Design notes for the experimental Rust + WASM runtime scaffold.

Made-with: Cursor
Copilot AI review requested due to automatic review settings April 23, 2026 08:36

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@ptlnextdoor
ptlnextdoor merged commit dd24833 into main Apr 23, 2026
6 of 8 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants