Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3ce01e1
docs: spec for removing Ollama and local embeddings
emp3thy Sep 1, 2026
58ae9e8
docs: implementation plan for ollama/embeddings removal
emp3thy Sep 1, 2026
c4dad55
test: pin FTS5 substrate as unconditional local evidence leg
emp3thy Sep 1, 2026
886cdd9
refactor: stop constructing embedders in server, hook, UI; delete bac…
emp3thy Sep 1, 2026
118ff97
refactor: remove ollama/embeddings/vec-floor config knobs and EMBED_L…
emp3thy Sep 1, 2026
27752b2
refactor: remove embedding writes from SemanticMemoryService
emp3thy Sep 1, 2026
ad3d22b
refactor: remove embedding writes, heal, and vec ranks from reflectio…
emp3thy Sep 1, 2026
e419b05
fix: drop dead reflection_embeddings DELETE from merge
emp3thy Sep 1, 2026
1cd7bc2
refactor: remove vector legs from retrieval and observation write path
emp3thy Sep 1, 2026
8504d49
refactor: collapse hybrid search to FTS5-only, drop dead embedder plu…
emp3thy Sep 1, 2026
12d00a6
refactor: delete ollama embeddings package
emp3thy Sep 1, 2026
d32188a
feat(db): migration 0018 drops vec0 embedding tables
emp3thy Sep 1, 2026
ffafd87
docs: remove ollama/embeddings from website, README, living agentcore…
emp3thy Sep 1, 2026
a011177
docs: fix internal contradictions in agentcore gap docs' embedding co…
emp3thy Sep 1, 2026
cacb744
docs: fix remaining stale ollama/sqlite-vec prose from final remnant …
emp3thy Sep 1, 2026
75794ce
docs: fix remaining stale embed/embedder prose from broader bare-word…
emp3thy Sep 1, 2026
eaabe17
docs: fix stale ollama/vector-search prose from final whole-branch re…
emp3thy Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 6 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ It's a memory layer for Claude Code that (with the default sqlite backend) runs
- **[uv](https://docs.astral.sh/uv/getting-started/installation/)** for environment management — the bootstrap scripts below install it for you if it's missing.
- **Claude Code** installed

SQLite ships with Python; `sqlite-vec` is installed as a pip dependency — nothing else to set up.

better-memory's default embeddings backend (`BETTER_MEMORY_EMBEDDINGS_BACKEND=ollama`) needs [Ollama](https://ollama.com/) running locally with the `nomic-embed-text` model pulled (`ollama pull nomic-embed-text`) — the bootstrap scripts no longer install or pull this for you. Set `BETTER_MEMORY_EMBEDDINGS_BACKEND=sqlite` instead to skip Ollama entirely (pure-SQL trigram-FTS5 backend, no model downloads).
SQLite ships with Python — nothing else to set up. All local retrieval is pure in-SQL FTS5 (word + trigram); there is no embedding model and no external service to install or run.

## Storage backends

Expand Down Expand Up @@ -116,18 +114,14 @@ One env var roots the runtime filesystem layout:
|---|---|---|
| `BETTER_MEMORY_HOME` | `~/.better-memory` | Root dir for `memory.db`, `knowledge.db`, `spool/`, `knowledge-base/` |
| `BETTER_MEMORY_WIRING_AUTOCHECK` | (unset = on) | Session-start wiring drift autocheck: self-repairs `~/.claude.json`, `~/.claude/settings.json` hooks/env, the CLAUDE.md managed block, skill links, and the per-repo post-commit hook. Set to `off` (case-insensitive) to disable; any other value leaves it enabled. See [Doctor](#doctor). |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama endpoint |
| `EMBED_MODEL` | `nomic-embed-text` | Embedding model (must produce 768-dim vectors) |
| `AUDIT_LOG_RETRIEVED` | `true` | Whether `memory.retrieve` writes per-result audit rows |
| `BETTER_MEMORY_EMBEDDINGS_BACKEND` | `ollama` | `ollama` (default) — local Ollama at `OLLAMA_HOST`; `sqlite` — pure-SQL trigram-FTS5 fusion, no model downloads and no in-memory state. |
| `BETTER_MEMORY_STORAGE_BACKEND` | unset | `sqlite` or `agentcore`. Explicit override of the storage backend; when unset, `$BETTER_MEMORY_HOME/settings.json` (written by `better-memory agentcore init`) decides, falling back to `sqlite`. The env var always wins over settings.json. |
| `BETTER_MEMORY_AUTO_PRUNE` | (unset = `false`) | When set to `1`, the auto-retention runner (which fires on `memory.retrieve`, throttled to once per 24h) ALSO hard-deletes archived observations older than 365 days. **Irreversible.** Default is archive-only (status flip, reversible). Opt in only if you actively want disk space reclaimed. |
| `BETTER_MEMORY_PROJECT` | unset | Force the project name for all calls in this process. Highest-priority project-resolution signal — overrides both the `.better-memory` file and the git-derived name. Designed for subprocess scoping (e.g. ralph's executor sets it per-iteration so subagent observations land in the PBI's target_repo regardless of the worktree's cwd). Empty/whitespace-only values are treated as unset. |
| `BETTER_MEMORY_INJECT_MODE` | `legacy` (config default; `deferred` is the recommended and currently-deployed setting) | `deferred`: SessionStart injects only general-scope standing rules + a one-line index; everything else surfaces via the contextual channel as it becomes relevant. `legacy`: the original full bootstrap dump. Unknown values coerce to `legacy`. |
| `BETTER_MEMORY_CONTEXT_INJECT_MODE` | `both` | Contextual memory-injection hook trigger: `userprompt`, `pretool`, `both` (default), or `off`. PreToolUse fires once per session (any tool), latched. |
| `BETTER_MEMORY_CONTEXT_VEC_FLOOR` | `0.55` | Cosine floor for the contextual channel's vector-evidence leg. A memory injects only with a text match or a cosine ≥ floor; calibrated precision-first (see the deferred-injection spec). |
| `BETTER_MEMORY_BOOTSTRAP_TOP_N` | `5` | Legacy-mode only: project-scoped items the SessionStart bootstrap renders in full; the rest collapse into a one-line index. `0` = full dump. |
| `BETTER_MEMORY_CONTEXT_MIN_HITS` | `2` | **Deprecated, unused** — superseded by the evidence gate (BM25 / vector floor). Kept for back-compat only. |
| `BETTER_MEMORY_CONTEXT_MIN_HITS` | `2` | **Deprecated, unused** — superseded by the evidence gate (BM25 match, or a keyword-hit fallback when unavailable). Kept for back-compat only. |
| `BETTER_MEMORY_CONTEXT_MAX_ITEMS` | `3` | Max memories the contextual-injection hook injects per firing. |
| `BETTER_MEMORY_CONTEXT_REINJECT_TURNS` | `0` | Turns before a contextually-injected memory can be re-injected. `0` = never re-inject. A turn is one firing of the `contextual_inject` hook (each user prompt, plus each matched tool call in mode `both`), not one user prompt-response cycle. |

Expand Down Expand Up @@ -174,8 +168,8 @@ The server registers 22 tools, grouped below. Full schemas are in [`website/mcp-
| Tool | Purpose |
|---|---|
| `memory.observe(content, component?, theme?, trigger_type?, outcome?, tech?, scope?)` | Record an observation. Returns `{"id": ...}`. |
| `memory.retrieve(query?, project?, tech?, phase?, polarity?, limit_per_bucket?)` | Distilled reflections in `do` / `dont` / `neutral` buckets, ranked by a Wilson-score hit-rate prior; pass `query` (a plain-language task description) to fuse in BM25 + vector relevance. One shortlist slot per bucket is reserved for an under-rated memory so new lessons earn a score. Drains spool first. |
| `memory.retrieve_observations(query?, component?, theme?, outcome?, episode_id?, project?, limit?)` | Raw-observation drill-down with hybrid FTS5 + vector search. |
| `memory.retrieve(query?, project?, tech?, phase?, polarity?, limit_per_bucket?)` | Distilled reflections in `do` / `dont` / `neutral` buckets, ranked by a Wilson-score hit-rate prior; pass `query` (a plain-language task description) to fuse in BM25 relevance. One shortlist slot per bucket is reserved for an under-rated memory so new lessons earn a score. Drains spool first. |
| `memory.retrieve_observations(query?, component?, theme?, outcome?, episode_id?, project?, limit?)` | Raw-observation drill-down with hybrid FTS5 search (word + trigram BM25 legs, RRF-fused). |
| `memory.record_use(id, outcome?)` | Stamp reinforcement outcome on a memory after validation. |

**Semantic memory** — user-stated facts and preferences (current truth, not history).
Expand Down Expand Up @@ -265,7 +259,7 @@ New-Item -ItemType Junction -Path "$env:USERPROFILE\.claude\skills\start-better-

```bash
uv sync # install deps
uv run pytest # full suite (requires Ollama running for integration marker)
uv run pytest # full suite (integration marker needs real AWS / a browser)
uv run pytest -m "not integration" # unit tests only
uv run ruff check . # lint
```
Expand All @@ -280,9 +274,6 @@ It speaks JSON-RPC over stdio — pipe `initialize` / `tools/list` / `tools/call

## Troubleshooting

**"Ollama unreachable" on startup.**
Make sure Ollama is running (`ollama serve` on macOS/Linux; the tray app on Windows) and that `nomic-embed-text` is pulled (`ollama pull nomic-embed-text`). The MCP server continues booting and serves `knowledge.*` tools, but `memory.observe` / `memory.retrieve` will error until Ollama is up.

**MCP server not appearing in Claude Code after editing `~/.claude.json`.**
MCP servers don't hot-reload. Restart Claude Code.

Expand All @@ -307,7 +298,7 @@ See `docs/superpowers/specs/2026-04-06-better-memory-design.md` for the original
Three shipped upgrades sit on top of that foundation (specs: `2026-07-23-retrieval-quality-design.md`, `2026-07-23-deferred-injection-design.md`):

- **Wilson-score ranking.** Reflections and semantic memories rank by the lower bound of their observed hit rate — `(useful + overlooked) / (useful + overlooked + ignored)` — so a memory that helps 3 times out of 4 serves outranks one that helped 67 times out of 192. No raw-count rich-get-richer. One shortlist slot per bucket is reserved for memories with fewer than 3 ratings (the **exploration slot**), so new lessons get served, rated, and scored instead of starving; those serves are tagged (`via_exploration`) and excluded from headline usefulness metrics.
- **Deferred, evidence-gated injection.** SessionStart injects only your standing rules; each prompt (and the session's first tool call) is then scored against the store with three legs — BM25 text match, vector cosine (reflections and semantic memories are embedded at write time, self-healed on retrieve, backfillable via `python -m better_memory.cli.backfill_embeddings`), and the Wilson prior for ranking only. A memory injects solely on positive relevance evidence; popularity can never force an irrelevant injection. Ollama outages cost at most one bounded stall per minute (file-persisted circuit breaker shared across hook processes).
- **Deferred, evidence-gated injection.** SessionStart injects only your standing rules; each prompt (and the session's first tool call) is then scored against the store against an evidence gate — a BM25 text match for reflections, a keyword-hit fallback for semantic memories (which have no FTS substrate of their own) — with the Wilson prior fused in for ranking only. A memory injects solely on positive relevance evidence; popularity can never force an irrelevant injection.
- **Evidence-anchored ratings.** Non-`ignored` ratings require a one-line receipt, enforced server-side and stored on the exposure row; the management UI shows each memory's rating-evidence history. Ratings are the fuel for everything above, so their variance is the system's noise floor — the receipts anchor them to observable events.

## Observation lifecycle
Expand Down
28 changes: 6 additions & 22 deletions better_memory/_diag.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@
spawned server's stderr, so without the file mirror these lines are
discarded when running under Claude Code.

The gate resolves from the ``diag_logging`` config field, with the
``BETTER_MEMORY_EMBED_LOG`` environment variable kept as an emergency
override (env wins) so diagnostics can still be flipped without editing
config. The env var is named ``BETTER_MEMORY_EMBED_LOG`` for historical
reasons (originally added to localize Ollama embed hangs).
The gate resolves solely from the ``diag_logging`` config field (backed by
``BETTER_MEMORY_DIAG_LOGGING``).
"""

from __future__ import annotations

import contextlib
import os
import sys
import threading
import time
Expand All @@ -33,26 +29,14 @@
# call fires, ``config`` has finished importing and ``get_config()`` is safe.
_ENABLED: bool | None = None

# Values of ``BETTER_MEMORY_EMBED_LOG`` that mean "off"; any other non-empty
# value means "on", preserving the historic env-var semantics.
_ENV_OFF_VALUES = frozenset({"0", "false", "no", "off"})

_LOCK = threading.Lock()
_FILE_HANDLE: object | None = None # actually a text file, typed loosely to avoid import
_FILE_INIT_ATTEMPTED = False


def _resolve_enabled() -> bool:
"""Resolve whether diagnostics are on.

``BETTER_MEMORY_EMBED_LOG`` is an emergency override: when set to a
non-empty value it wins outright (env wins), keeping the run-time-flip
workflow intact. Otherwise the gate falls back to the ``diag_logging``
field on :class:`~better_memory.config.Config`.
"""
raw = os.environ.get("BETTER_MEMORY_EMBED_LOG", "").strip()
if raw:
return raw.lower() not in _ENV_OFF_VALUES
"""Resolve whether diagnostics are on from the ``diag_logging`` field on
:class:`~better_memory.config.Config`."""
try:
from better_memory.config import get_config

Expand Down Expand Up @@ -126,8 +110,8 @@ def step(fn: str, msg: str, **fields: object) -> None:
"""Emit an intermediate ``[bm-trace step ...]`` line.

Use inside a method already wrapped in :func:`trace` to mark a
sub-step (e.g. "about to call self._embedder.embed", "SAVEPOINT
open", "commit"). Cheap when diagnostics are off.
sub-step (e.g. "obs_id_minted", "SAVEPOINT open", "commit"). Cheap
when diagnostics are off.
"""
if not enabled():
return
Expand Down
66 changes: 0 additions & 66 deletions better_memory/async_bridge.py

This file was deleted.

144 changes: 0 additions & 144 deletions better_memory/cli/backfill_embeddings.py

This file was deleted.

Loading
Loading