Skip to content

Architecture

Daniel Nevoigt edited this page May 29, 2026 · 1 revision

Architecture

🇬🇧 English

Components

bastra-recall is a TypeScript monorepo (packages/):

Package Role
@bastra-recall/core The engine. Vault loader (vault.ts), BM25 index + hybrid recall pipeline (search.ts), embeddings (embeddings.ts), save path + frontmatter schema (save.ts, schema.ts). No transport, no I/O beyond the vault.
@bastra-recall/daemon The process. Wraps core in two transports — MCP over stdio (index.ts) and HTTP REST (http.ts) — plus the tool handlers (tool-handlers.ts), the reflex hooks (hook.ts, session-hook.ts), the MCP forwarder (mcp-forwarder.ts), and telemetry.
skill The Claude Code Skill (SKILL.md) — the trigger discipline that teaches the assistant when to save and recall.
@bastra-recall/statusline A powerline statusline segment showing live recall stats + vault size, fed out-of-band (Claude Code does not render MCP progress in the TUI).

One daemon, many clients

The core idea: one local daemon holds the in-memory index and serves every AI tool at once, instead of each client spawning its own vault.

            Vault (Markdown + YAML frontmatter, Obsidian-compatible)
              │  chokidar file-watcher (auto-polls on cloud mounts)
              ▼
   ┌──────────────────────────────────────────────┐
   │  bastra-recall daemon  (single local process) │
   │   • core: BM25 + embeddings + RRF             │
   │   • MCP stdio server (index.ts)               │
   │   • HTTP server  :6723 (http.ts)              │
   │   • idle self-shutdown (respawns on demand)   │
   └──────────────────────────────────────────────┘
        ▲ stdio (per session)        ▲ HTTP /api/v1/*
        │                            │
  mcp-forwarder                ChatGPT Custom GPT,
  (Claude Code / Desktop /     web apps, scripts
   Cursor — stdio → HTTP)
  • MCP clients (Claude Code, Desktop, Cursor) connect to a thin mcp-forwarder: it speaks MCP over stdio to the client and forwards to the shared daemon over loopback HTTP. The forwarder is a per-session stdio child; the daemon is shared.
  • Non-MCP clients (ChatGPT, scripts) call the HTTP REST surface directly: POST /api/v1/recall, /api/v1/load_memory, etc.
  • Reflex hooks (PreToolUse, SessionStart) are separate short-lived CLI processes that POST to the daemon's loopback endpoints.

HTTP surface (daemon)

Route Auth Purpose
GET /health open liveness + version + vault size
POST /hook/recall open recall for the reflex hooks; supports SSE streaming of pipeline stages
POST /api/v1/recall token* the recall tool for REST clients (→ recallHandler)
POST /api/v1/load_memory token* load_memory (→ loadMemoryHandler)
POST /api/v1/save_memory token* save_memory
POST /api/v1/{find,read,save,...}_document token* document tools

* loopback requests skip auth by default (BASTRA_AUTH_LOOPBACK_SKIP=1).

⚠️ The non-obvious bit: how the MCP recall tool is routed

This trips people up. The mcp-forwarder does not send the recall tool call to /api/v1/recall. It routes it through /hook/recall (SSE) via callRecallStreaming, so it can stream pipeline stages for live progress UX. Consequences:

  • The forwarder reassembles the tool result from the SSE stream and historically attached a stages block itself (now removed).
  • The /hook/recall path defaults expand_hops differently than the explicit tool — the forwarder sets expand_hops=0 for the MCP path (exactly k hits), while the PreToolUse hook CLI uses expand_hops=1 (1-hop neighbors help proactive hints).

Takeaway: to change what Claude Code sees from recall, edit mcp-forwarder.ts (callRecallStreaming + reassembly), not only recallHandler. recallHandler / /api/v1/recall is the path for REST clients and the Mac-App. Forwarder code changes only take effect in a new session (the forwarder is loaded once at session start).

Process lifecycle & idle self-shutdown

  • The forwarder dies with its client session (stdin EOF / SIGTERM). If the shared daemon is down when a tool call arrives, the forwarder auto-spawns it.
  • The daemon self-terminates after BASTRA_DAEMON_IDLE_SHUTDOWN_MS (default 30 min) of no activity — /health pings don't count as activity. The next recall respawns it. This keeps the process table clean (no orphaned daemons after sessions end).
  • Restart rule: to pick up new daemon code, just pkill -f "packages/daemon/dist/index.js" and let the forwarder respawn it. Do not manually nohup node dist/index.js — that creates an orphan that fights the auto-spawn for the port.

🇩🇪 Deutsch

Komponenten

bastra-recall ist ein TypeScript-Monorepo (packages/):

Paket Rolle
@bastra-recall/core Die Engine. Vault-Loader (vault.ts), BM25-Index + Hybrid-Recall-Pipeline (search.ts), Embeddings (embeddings.ts), Save-Pfad + Frontmatter-Schema (save.ts, schema.ts). Kein Transport, keine I/O außer dem Vault.
@bastra-recall/daemon Der Prozess. Verpackt core in zwei Transports — MCP über stdio (index.ts) und HTTP REST (http.ts) — plus Tool-Handler (tool-handlers.ts), Reflex-Hooks (hook.ts, session-hook.ts), MCP-Forwarder (mcp-forwarder.ts) und Telemetry.
skill Der Claude-Code-Skill (SKILL.md) — die Trigger-Disziplin, die dem Assistenten beibringt, wann gespeichert und abgerufen wird.
@bastra-recall/statusline Ein Powerline-Statusline-Segment mit Live-Recall-Stats + Vault-Größe, out-of-band gefüttert (Claude Code rendert MCP-Progress nicht im TUI).

Ein Daemon, viele Clients

Die Kernidee: ein lokaler Daemon hält den In-Memory-Index und bedient alle AI-Tools gleichzeitig — statt dass jeder Client seinen eigenen Vault hochfährt.

            Vault (Markdown + YAML-Frontmatter, Obsidian-kompatibel)
              │  chokidar File-Watcher (auto-polling auf Cloud-Mounts)
              ▼
   ┌──────────────────────────────────────────────┐
   │  bastra-recall daemon  (ein lokaler Prozess)  │
   │   • core: BM25 + Embeddings + RRF             │
   │   • MCP-stdio-Server (index.ts)               │
   │   • HTTP-Server  :6723 (http.ts)              │
   │   • Idle-Self-Shutdown (respawnt on demand)   │
   └──────────────────────────────────────────────┘
        ▲ stdio (pro Session)        ▲ HTTP /api/v1/*
        │                            │
  mcp-forwarder                ChatGPT Custom GPT,
  (Claude Code / Desktop /     Web-Apps, Skripte
   Cursor — stdio → HTTP)
  • MCP-Clients (Claude Code, Desktop, Cursor) verbinden sich mit einem dünnen mcp-forwarder: er spricht MCP über stdio mit dem Client und leitet per Loopback-HTTP an den geteilten Daemon weiter. Der Forwarder ist ein stdio-Kind pro Session; der Daemon ist geteilt.
  • Nicht-MCP-Clients (ChatGPT, Skripte) rufen die HTTP-REST-Fläche direkt: POST /api/v1/recall, /api/v1/load_memory, usw.
  • Reflex-Hooks (PreToolUse, SessionStart) sind separate kurzlebige CLI-Prozesse, die an die Loopback-Endpunkte des Daemons POSTen.

HTTP-Fläche (Daemon)

Route Auth Zweck
GET /health offen Liveness + Version + Vault-Größe
POST /hook/recall offen Recall für die Reflex-Hooks; unterstützt SSE-Streaming der Pipeline-Stages
POST /api/v1/recall Token* das recall-Tool für REST-Clients (→ recallHandler)
POST /api/v1/load_memory Token* load_memory (→ loadMemoryHandler)
POST /api/v1/save_memory Token* save_memory
POST /api/v1/{find,read,save,...}_document Token* Dokument-Tools

* Loopback-Requests überspringen Auth standardmäßig (BASTRA_AUTH_LOOPBACK_SKIP=1).

⚠️ Das Nicht-Offensichtliche: wie das MCP-recall-Tool geroutet wird

Hierüber stolpert man. Der mcp-forwarder schickt den recall-Tool-Call nicht an /api/v1/recall. Er routet ihn über /hook/recall (SSE) via callRecallStreaming, um die Pipeline-Stages für die Live-Progress-UX zu streamen. Folgen:

  • Der Forwarder baut das Tool-Result aus dem SSE-Stream selbst zusammen und hängte historisch einen stages-Block an (jetzt entfernt).
  • Der /hook/recall-Pfad hat einen anderen expand_hops-Default als das explizite Tool — der Forwarder setzt für den MCP-Pfad expand_hops=0 (genau k Hits), während die PreToolUse-Hook-CLI expand_hops=1 nutzt (1-Hop-Nachbarn helfen den proaktiven Hints).

Merke: Um zu ändern, was Claude Code von recall sieht, editiere mcp-forwarder.ts (callRecallStreaming + Reassembly), nicht nur recallHandler. recallHandler / /api/v1/recall ist der Pfad für REST-Clients und die Mac-App. Forwarder-Code-Änderungen wirken erst in einer neuen Session (der Forwarder wird einmal beim Session-Start geladen).

Prozess-Lebenszyklus & Idle-Self-Shutdown

  • Der Forwarder stirbt mit seiner Client-Session (stdin-EOF / SIGTERM). Ist der geteilte Daemon beim Eintreffen eines Tool-Calls unten, auto-spawnt der Forwarder ihn.
  • Der Daemon beendet sich nach BASTRA_DAEMON_IDLE_SHUTDOWN_MS (Default 30 min) ohne Aktivität — /health-Pings zählen nicht als Aktivität. Der nächste Recall respawnt ihn. Das hält die Prozessliste sauber (keine verwaisten Daemons nach Session-Ende).
  • Restart-Regel: Um neuen Daemon-Code zu laden, einfach pkill -f "packages/daemon/dist/index.js" und den Forwarder respawnen lassen. Nicht manuell nohup node dist/index.js — das erzeugt einen Orphan, der mit dem Auto-Spawn um den Port kämpft.