From 826385394da0c8fab1364978fe27dbdd22854c9d Mon Sep 17 00:00:00 2001 From: gethin Date: Fri, 24 Jul 2026 12:35:21 +0100 Subject: [PATCH 1/3] =?UTF-8?q?docs(readme):=20retrieval-quality=20layer?= =?UTF-8?q?=20=E2=80=94=20deferred=20injection,=20Wilson=20ranking,=20evid?= =?UTF-8?q?ence=20receipts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- README.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9655be2..3ada70a 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,14 @@ It's a memory layer for Claude Code that (with the default sqlite backend) runs 1. **As it works**, the assistant records what it tried and how it turned out — a fix that worked, an approach that failed, a preference you stated. 2. **Between sessions**, those raw notes are distilled into short, durable lessons — signal kept, noise dropped. -3. **Next session**, the relevant lessons are handed back to the assistant automatically, before it touches your code, so it starts already informed about your project. +3. **Next session**, lessons surface exactly when they become relevant: your standing rules inject at session start, and everything else arrives as you work — each prompt and the session's first tool call are scored against the memory store (text match + semantic similarity + track record), and only memories with real evidence of relevance inject. No firehose, no guessing. +4. **Every serve gets scored.** At session end the assistant rates each surfaced memory — and any non-trivial rating requires a one-line receipt (what the memory changed, or a quote). Hit rates drive the ranking: memories that keep helping rise, memories that keep getting ignored sink, and brand-new lessons get a reserved trial slot to earn their score. ## What you get - **Even the failures pay off** — a botched approach is recorded just as carefully as a working fix, so the assistant turns a dead end into a *don't* the next session won't repeat. -- **The right lesson at the right time** — past lessons come back sorted into *do this*, *avoid this*, and *context*, weighted by how reliable each has proven. +- **The right lesson at the right time** — past lessons come back sorted into *do this*, *avoid this*, and *context*, ranked by a Wilson-score hit rate (how often each memory actually helped, out of how often it was shown) fused with text and semantic relevance to what you're doing right now. +- **Ratings with receipts** — when the assistant claims a memory helped, it must say how, in one line; those evidence lines are stored and browsable per-memory in the management UI. - **Your standards, on hand** — drop your own conventions, language guides, and project docs into a knowledge base the assistant can search. - **Zero ceremony** — once it's installed, capture is automatic: hooks inside Claude Code snapshot your session for you, with nothing to call or manage mid-task. - **Nothing happens silently** — every change to memory is an append-only record you can audit. @@ -170,9 +172,11 @@ One env var roots the runtime filesystem layout: | `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_CONTEXT_INJECT_MODE` | `both` | Contextual memory-injection hook trigger: `userprompt`, `pretool`, `both` (default), or `off`. | -| `BETTER_MEMORY_BOOTSTRAP_TOP_N` | `5` | Number of project-scoped semantic memories/reflections the SessionStart bootstrap renders in full; the rest collapse into a one-line index. `0` = legacy full dump. | -| `BETTER_MEMORY_CONTEXT_MIN_HITS` | `2` | Minimum distinct keyword hits a memory needs to clear the contextual-injection floor. | +| `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_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. | @@ -219,7 +223,7 @@ 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(project?, tech?, phase?, polarity?, limit_per_bucket?)` | Distilled reflections in `do` / `dont` / `neutral` buckets. Drains spool first. | +| `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.record_use(id, outcome?)` | Stamp reinforcement outcome on a memory after validation. | @@ -252,9 +256,9 @@ The server registers 22 tools, grouped below. Full schemas are in [`website/mcp- | Tool | Purpose | |---|---| -| `memory.credit(kind, id, class)` | Opportunistic per-tool-use credit. `class` ∈ `cited` / `shaped` / `misled` / `overlooked` (not `ignored`). Call immediately when a retrieved memory is actually used. | +| `memory.credit(kind, id, class, evidence)` | Opportunistic per-tool-use credit. `class` ∈ `cited` / `shaped` / `misled` / `overlooked` (not `ignored`). `evidence` is required: one line — what the memory changed, or a quote. If you can't write one, the memory was ignored; don't call credit. | | `memory.list_session_exposures()` | Unrated exposure rows for the current session. Read-only; used by the `rate-session-memories` skill. | -| `memory.apply_session_ratings(ratings)` | Atomic end-of-session batch rating. Each entry: `{kind, id, class}` with `class` ∈ `cited` / `shaped` / `ignored` / `misled` / `overlooked`. | +| `memory.apply_session_ratings(ratings)` | Atomic end-of-session batch rating. Each entry: `{kind, id, class, evidence?}` — evidence is required for every non-`ignored` class (write the evidence line first; nothing to point at means the class is `ignored`). Violating batches are rejected whole. | **Knowledge** — human-authored markdown corpus. @@ -267,7 +271,7 @@ The server registers 22 tools, grouped below. Full schemas are in [`website/mcp- | Tool | Purpose | |---|---| -| `memory.session_bootstrap(source?, session_id?, cwd?)` | Open or reuse a session episode and inject project + general semantic memories and reflections as `additionalContext` markdown. Reflections retrieved up to 20 per polarity bucket, ranked by usefulness then confidence; only the top `BETTER_MEMORY_BOOTSTRAP_TOP_N` project-scoped items render in full, the rest collapse into a one-line index. Mirrors the SessionStart hook; callable manually for recovery, testing, or post-`/clear` re-injection. | +| `memory.session_bootstrap(source?, session_id?, cwd?)` | Open or reuse a session episode and inject startup context as `additionalContext` markdown. In `deferred` mode (recommended): general-scope standing rules in full plus a one-line index — everything else arrives contextually. In `legacy` mode: the original render (up to 20 reflections/bucket, Wilson-ranked, top `BETTER_MEMORY_BOOTSTRAP_TOP_N` in full). Mirrors the SessionStart hook; callable manually for recovery, testing, or post-`/clear` re-injection. The hook also runs a CLAUDE.md drift sentinel: if your CLAUDE.md documents tool parameters that don't exist in the live schemas, one warning line is appended. | | `memory.run_retention(retention_days?, prune?, prune_age_days?, dry_run?)` | Apply spec §9 retention rules; archive or hard-delete. | | `memory.start_ui()` | Spawn or reuse the management UI; returns `{url, reused}`. | @@ -339,7 +343,15 @@ Your hook command is using `python.exe`; switch to `.venv\Scripts\pythonw.exe`. ## Architecture -See `docs/superpowers/specs/2026-04-06-better-memory-design.md` for the full design spec — four-layer epistemic hierarchy, hybrid search via FTS5 + sqlite-vec + RRF, reinforcement-weighted ranking, and the consolidation pipeline that lives in Plan 2. +See `docs/superpowers/specs/2026-04-06-better-memory-design.md` for the original design spec — four-layer epistemic hierarchy, hybrid search via FTS5 + sqlite-vec + RRF, reinforcement-weighted ranking, and the consolidation pipeline. + +### The retrieval-quality layer (July 2026) + +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). +- **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 From 3033141e39f8e5111960eec5e79a533f57043fec Mon Sep 17 00:00:00 2001 From: gethin Date: Fri, 24 Jul 2026 12:45:20 +0100 Subject: [PATCH 2/3] docs: sweep non-superpowers docs for retrieval-quality staleness Co-Authored-By: Claude Fable 5 --- better_memory/skills/memory-feedback.md | 36 +++++++++------ better_memory/skills/memory-retrieve.md | 58 ++++++++++++++++--------- better_memory/skills/session-close.md | 4 +- docs/hooks-setup.md | 41 +++++++++++------ website/architecture.md | 37 +++++++++++----- website/index.md | 2 +- website/mcp-tools.md | 12 ++--- 7 files changed, 123 insertions(+), 67 deletions(-) diff --git a/better_memory/skills/memory-feedback.md b/better_memory/skills/memory-feedback.md index 48276f4..c62bdbf 100644 --- a/better_memory/skills/memory-feedback.md +++ b/better_memory/skills/memory-feedback.md @@ -4,11 +4,15 @@ When to use: immediately after evidence arrives that confirms or disproves a pri ## Two call sites -`record_use` is the canonical way to stamp an outcome once evidence is in hand. It's called in two situations: +`record_use` is the canonical way to stamp an outcome on a raw +**observation** (an id returned by `memory.observe`) once evidence is +in hand: **1. Closing the loop on a memory YOU wrote as neutral.** Every `memory.observe(outcome='neutral')` with a decision baked in should, eventually, get a matching `record_use(id, outcome=...)` once validation arrives. -**2. Validating a memory you RETRIEVED and applied.** If a retrieved memory influenced your work, close the loop with `record_use(retrieved_id, outcome=...)` once you know whether it held up. +**2. Validating an observation you RETRIEVED via `memory.retrieve_observations` and applied.** If a retrieved observation influenced your work, close the loop with `record_use(retrieved_id, outcome=...)` once you know whether it held up. + +For a **reflection** or **semantic memory** (returned by `memory.retrieve` / `memory.semantic_retrieve`), `record_use` is a no-op — use `memory.credit(kind, id, class, evidence)` instead (see Pattern 2 below). ```python memory.record_use(id, outcome='success' | 'failure' | None) @@ -32,7 +36,7 @@ Recording a `failure` against a stale memory is how you retire bad advice over t `memory.close_episode(outcome='success' | 'partial' | 'abandoned')` is a stronger reinforcement signal than per-observation `record_use`. Every observation made during the episode inherits the outcome at synthesis time. -If the work was opened with `memory.start_episode(...)` and the goal is now resolved, prefer hardening — call `close_episode` with a real outcome. Per-observation `record_use` is still right when you're closing the loop on a single decision (e.g. validating one retrieved memory you applied), but for goal-driven work, hardening is the higher-leverage move. +If the work was opened with `memory.start_episode(...)` and the goal is now resolved, prefer hardening — call `close_episode` with a real outcome. Per-observation `record_use` is still right when you're closing the loop on a single decision (e.g. validating one retrieved observation you applied), but for goal-driven work, hardening is the higher-leverage move. ## Pattern 1 — closing your own neutral observe @@ -53,22 +57,26 @@ memory.record_use(obs_id, outcome="success") # if tests passed memory.record_use(obs_id, outcome="failure") # if it broke something ``` -## Pattern 2 — validating a retrieved memory +## Pattern 2 — validating a retrieved reflection or semantic memory -```python -hits = memory.retrieve(query="add FK index", component="db") +`record_use` only operates on raw **observations** (ids from +`memory.observe`) — calling it with a reflection or semantic-memory id +is a silent no-op. For memories returned by `memory.retrieve` / +`memory.semantic_retrieve`, close the loop with +`memory.credit(kind, id, class, evidence)` instead: -for item in hits["do"]: - # About to apply — mark the read - memory.record_use(item["id"]) +```python +hits = memory.retrieve(query="add FK index to observations table", project="db") -# ... finish the work, observe the result ... +# ... apply the first approach, finish the work ... -# The first approach worked -memory.record_use(hits["do"][0]["id"], outcome="success") +# It worked — credit it as having shaped the change, with evidence +memory.credit("reflection", hits["do"][0]["id"], "shaped", + "reused the covering-index pattern for the new FK column") -# The second approach is stale — schema changed underneath -memory.record_use(hits["do"][1]["id"], outcome="failure") +# The second approach turned out stale — schema changed underneath +memory.credit("reflection", hits["do"][1]["id"], "misled", + "followed this but the referenced column no longer exists") ``` ## Rule of thumb diff --git a/better_memory/skills/memory-retrieve.md b/better_memory/skills/memory-retrieve.md index 8df0359..b82dbd7 100644 --- a/better_memory/skills/memory-retrieve.md +++ b/better_memory/skills/memory-retrieve.md @@ -7,8 +7,13 @@ When to use: before starting any meaningful coding task, or when entering a new `better-memory` exposes two distinct retrieve tools. Pick by purpose: - `memory.retrieve` — distilled **reflections** bucketed by polarity - (`do` / `dont` / `neutral`). Filter-only; no free-text query. Use this - as the default at the start of work to find generalised lessons. + (`do` / `dont` / `neutral`). ALWAYS pass `query` — a plain-language + description of the task at hand — or you get the same generic + top-ranked lessons every session regardless of what you're working on. + `query` fuses BM25 + vector search via reciprocal rank fusion against + a Wilson-score usefulness prior. Also filter by `project`, `tech`, + `phase`, `polarity`, `limit_per_bucket` (default 5 per bucket). Use + this as the default at the start of work to find generalised lessons. - `memory.retrieve_observations` — raw observations. Supports a free-text `query` (hybrid FTS5 + sqlite-vec) plus filters like `component`. Use this to drill into a specific incident or hunt for an exact prior @@ -16,13 +21,14 @@ When to use: before starting any meaningful coding task, or when entering a new ## Steps -1. Identify the project and tech you'll be working in. -2. Call `memory.retrieve` with whichever filters narrow the buckets - usefully — typically `project` and/or `tech`, optionally `phase` - (`planning` / `implementation` / `general`): +1. Identify the task you're about to do, plus the project/tech it's in. +2. Call `memory.retrieve` with a `query` describing the task, narrowed + by whichever filters apply — typically `project` and/or `tech`, + optionally `phase` (`planning` / `implementation` / `general`): ```python result = memory.retrieve( + query="refactoring the auth middleware to use dependency injection", project="better-memory", tech="python", phase="implementation", @@ -36,10 +42,12 @@ When to use: before starting any meaningful coding task, or when entering a new - `neutral` — general context, no strong signal either way. 4. Also read `insights` (confirmed patterns) and `knowledge` (standards, language conventions, project docs). -5. After reading, call `memory.record_use(id)` for any item you're about - to apply — even before you know the outcome. If the approach later - succeeds, re-call with `outcome='success'`; if it fails, - `outcome='failure'`. +5. When a returned reflection or semantic memory actually shapes your + work, call `memory.credit(kind, id, class, evidence)` — see + "Crediting memories you use" below. `memory.record_use` is a + different tool: it operates on raw **observations** (ids returned by + `memory.observe`), not on reflections/semantic memories, and is a + no-op if you pass it a reflection id. ## Golden rule @@ -48,9 +56,9 @@ that as a hard stop. Look for an alternative or ask the user. ## Drilling into raw observations -`memory.retrieve` returns *distilled reflections* — the -reinforcement-weighted lessons synthesis has built from observations. -That's almost always what you want. +`memory.retrieve` returns *distilled reflections* — the lessons +synthesis has built from observations, ranked by a Wilson-score +usefulness prior. That's almost always what you want. When reflections aren't specific enough — typically when investigating a specific incident or hunting for an exact prior decision — drop down to @@ -70,9 +78,12 @@ filters are ignored in query mode. ## Parameter reference -`memory.retrieve(project?, tech?, phase?, polarity?, limit_per_bucket?)` -— reflections, bucketed by polarity. No free-text query and no -component/scope/window filters; use `retrieve_observations` for those. +`memory.retrieve(query?, project?, tech?, phase?, polarity?, +limit_per_bucket?)` — reflections, bucketed by polarity. `query` ranks +by BM25 + vector RRF fused with the Wilson-score usefulness prior; +omitting it returns the same generic top-ranked lessons every time. +`limit_per_bucket` defaults to 5. No component/scope/window filters; +use `retrieve_observations` for those. `memory.retrieve_observations(project?, episode_id?, component?, theme?, outcome?, query?, limit?)` — raw observations. `outcome` is one of @@ -84,6 +95,7 @@ You're about to refactor the auth middleware. ```python result = memory.retrieve( + query="refactoring the auth middleware to use dependency injection", project="my-app", tech="python", phase="implementation", @@ -94,8 +106,8 @@ for item in result["dont"]: print(item["content"]) for item in result["do"]: - # Prior art. Reuse the pattern. - memory.record_use(item["id"]) # mark it as applied; outcome comes later + # Prior art worth reusing — credit it once it actually shapes the work, + # e.g. memory.credit("reflection", item["id"], "shaped", "reused its pattern for X") # Drill into prior auth-middleware incidents. incidents = memory.retrieve_observations( @@ -113,9 +125,13 @@ Find another way or ask. When you actively use one of the retrieved memories — quote a hint, follow its do/dont guidance, or it caused a wrong direction — call -`memory.credit(kind, id, class)` **immediately**. Class is `cited` -if quoted, `shaped` if it guided a decision, `misled` if it led you -astray. +`memory.credit(kind, id, class, evidence)` **immediately**. `kind` is +`reflection` or `semantic`. Class is `cited` if quoted, `shaped` if it +guided a decision, `misled` if it led you astray, `overlooked` if the +user pointed you back to a memory you already had but hadn't applied. +`evidence` is a required one-line string (max 500 chars) — what the +memory changed, or a quote. If you can't write one, the memory was +`ignored`; do not call credit. This is the fresh-context signal. Memories you don't credit will default to `ignored` at session end (caught by the diff --git a/better_memory/skills/session-close.md b/better_memory/skills/session-close.md index 6c492f2..3a3f37b 100644 --- a/better_memory/skills/session-close.md +++ b/better_memory/skills/session-close.md @@ -6,7 +6,7 @@ When to use: at the end of a coding session, before wrapping up. 1. Every decision point since the last `memory.observe()` — captured? 2. Every `observe(outcome='neutral')` whose outcome is now known — closed via `record_use(id, outcome=...)`? -3. Every retrieved memory you actually applied — stamped via `record_use(id, outcome=...)`? +3. Every retrieved reflection or semantic memory you actually applied — credited via `memory.credit(kind, id, class, evidence)`? 4. Any unexpected behaviour worth the next session knowing — recorded as `neutral`? If any are missing, write them now. Short. Specific. Past tense. @@ -18,7 +18,7 @@ Starting from the last `memory.observe()` you made, scan forward in the transcri - **Every decision point** → expect a matching `observe()` with an `outcome`. - **Every `observe(outcome='neutral')` you wrote as a decision** → expect a corresponding `record_use(id, outcome=...)` once validated. If the outcome is now known but the record_use never happened, call it now. - **Every failed attempt you reverted** → expect an `observe(outcome='failure')` (evidence was in hand at the time). -- **Every retrieved memory you applied** → expect a `record_use(id, outcome=...)`. +- **Every retrieved reflection or semantic memory you applied** → expect a `memory.credit(kind, id, class, evidence)` call. Anything left uncredited is caught by the `rate-session-memories` skill and defaults to `ignored`. The goal: no in-flight decisions with unknown outcomes, and no applied retrievals without feedback. diff --git a/docs/hooks-setup.md b/docs/hooks-setup.md index cf4d7c1..0f5621e 100644 --- a/docs/hooks-setup.md +++ b/docs/hooks-setup.md @@ -11,10 +11,15 @@ better-memory ships these hooks that wire into Claude Code's hook framework: The `contextual_inject` hook is gated by `BETTER_MEMORY_CONTEXT_INJECT_MODE` (`userprompt` \| `pretool` \| `both` (default) \| `off`). It runs in-process -against `memory.db`, whole-word-matches the curated memory set against the -prompt / tool-input, and injects the top matches; it never blocks a turn. -Note: whether `PreToolUse` fires for the built-in `Skill`/`Task` tools is -environment-dependent — `UserPromptSubmit` is the reliable trigger. +against `memory.db` and gates each candidate through a three-leg evidence +check (BM25 match against `reflection_fts`, or vector cosine similarity +>= `BETTER_MEMORY_CONTEXT_VEC_FLOOR` (default 0.55), or — only when both +those legs are structurally unavailable — a keyword-hit fallback), then +ranks qualifiers by reciprocal rank fusion over a Wilson-score usefulness +prior; it injects the top matches (capped at `BETTER_MEMORY_CONTEXT_MAX_ITEMS`) +and never blocks a turn. `PreToolUse` is registered unscoped (matches every +tool) but latches to one real firing per session — later tool calls +short-circuit on a state file before touching the DB. ## Registering the hooks @@ -52,8 +57,7 @@ project-scoped): "hooks": [ { "type": "command", - "command": "uv run python -m better_memory.hooks.session_close", - "async": true + "command": "uv run python -m better_memory.hooks.session_close" } ] } @@ -70,7 +74,6 @@ project-scoped): ], "PreToolUse": [ { - "matcher": "Skill|Task|Write", "hooks": [ { "type": "command", @@ -99,16 +102,28 @@ Adjust the `command` to match your environment — for example: directly (no MCP RPC on the hook critical path), calls `SessionBootstrapService.bootstrap`, which: - resolves the project name from `cwd` via the git-aware - `project_name(cwd)` helper (uses `git rev-parse --git-common-dir` so - worktrees share scope with their main repo); + `project_name(cwd)` helper (walks up looking for `.git`, handling + worktrees, so worktrees share scope with their main repo); - opens a fresh background episode for this session, or reuses an existing open background episode if `source=resume`; - - retrieves all project-scoped + general-scope semantic memories and - all distilled reflections (`do` / `dont` / `neutral` buckets) — no - per-bucket cap; + - retrieves project-scoped + general-scope semantic memories and + distilled reflections (`do` / `dont` / `neutral` buckets, capped at + 20 per bucket), then renders them per `BETTER_MEMORY_INJECT_MODE`: + - `deferred` (the mode actually deployed live) — renders only + general-scope semantic memories in full, plus a one-line index + ("better-memory knows N reflections + M semantic memories...") + telling Claude to pull specifics via `memory_retrieve` with a + task query. No reflections are rendered in full at bootstrap. + - `legacy` (the config default when the env var is unset) — renders + up to `BETTER_MEMORY_BOOTSTRAP_TOP_N` (default 5) semantic + memories and reflections in full, with the remainder listed in a + compact "Index (not expanded)" section. - renders a markdown block with a `## better-memory: session bootstrap` header summarising project / source / episode action, - followed by the memories and reflections. + followed by the memories and reflections; + - appends a CLAUDE.md drift-sentinel warning (at most one) if the + user's `~/.claude/CLAUDE.md` references a schema field/enum that no + longer matches the codebase. The hook prints a `hookSpecificOutput` JSON envelope with the rendered markdown as `additionalContext`. Claude Code injects this into the first turn's context. If anything fails, a fallback directive is diff --git a/website/architecture.md b/website/architecture.md index cf0c553..c51810a 100644 --- a/website/architecture.md +++ b/website/architecture.md @@ -7,7 +7,7 @@ better-memory is a four-layer epistemic hierarchy backed by a pluggable storage | Layer | Purpose | Lifecycle | |---|---|---| | **Observation** | A factual snapshot the AI writes at a decision point. Tagged with an outcome, component, theme, and trigger type. | Created by `memory.observe`. Eventually consumed into a reflection or archived. | -| **Reflection** | A distilled lesson synthesised from one or more observations. Has a polarity (`do` / `dont` / `neutral`), a confidence, and a use-cases description. | Created by the synthesis pipeline (LLM-driven). Reinforced by `memory.record_use`. | +| **Reflection** | A distilled lesson synthesised from one or more observations. Has a polarity (`do` / `dont` / `neutral`), a confidence, and a use-cases description. | Created by the synthesis pipeline (LLM-driven). Rated via `memory.credit`, ranked by the Wilson-score hit-rate prior described in [Self-rating loop](#self-rating-loop). | | **Episode** | A bounded session of work — opened on session start, closed when the goal is met or abandoned. Observations and reflections are scoped to an episode. | Background episodes open implicitly on first observe; foreground episodes are explicit (`memory.start_episode`). | | **Knowledge** | Human-authored markdown — standards, language conventions, per-project docs. Indexed via SQLite FTS5. Read-only for the AI. | Edited by humans. Reindexed on MCP server startup (mtime-only). | @@ -49,12 +49,19 @@ See [Configuration](configuration.md) for env vars and [AgentCore setup](agentco ## Retrieval -`memory.retrieve` returns three buckets — `do`, `dont`, `neutral` — built from a hybrid search: +Two distinct retrieve tools, two distinct ranking mechanisms: -1. **FTS5 lexical** match against observation content + reflection use-cases. -2. **sqlite-vec** dense vector match against observation embeddings (768-dim from `nomic-embed-text`). -3. **Reciprocal Rank Fusion (RRF)** combines the two ranked lists. -4. Results are filtered by the bucket's polarity and weighted by `reinforcement_score` (each `memory.record_use` shifts a memory's score up on success or down on failure). +- **`memory.retrieve`** returns three reflection buckets — `do`, `dont`, + `neutral` — ranked by the Wilson-score hit-rate prior on rated + exposures (see [Self-rating loop](#self-rating-loop) for the formula + and the query-driven BM25/vector re-fusion). +- **`memory.retrieve_observations`** returns raw observations via a + hybrid search: FTS5 lexical match against observation content, plus + a sqlite-vec dense vector match against observation embeddings + (768-dim from `nomic-embed-text`), combined by Reciprocal Rank Fusion + (RRF). Results are filtered by outcome bucket and weighted by + `reinforcement_score` (each `memory.record_use` shifts a memory's + score up on success or down on failure — see [Reinforcement](#reinforcement)). ## Injection strategies @@ -143,18 +150,26 @@ predating this feature, or rows written during a breaker outage: ## Reinforcement -Each observation and reflection has a `reinforcement_score` that decays slowly over time and is updated by validated use: +Each **observation** (not reflections or semantic memories — see below) +has a `reinforcement_score` that decays slowly over time and is updated +by validated use: - `memory.record_use(id, outcome="success")` → score goes up. - `memory.record_use(id, outcome="failure")` → score goes down. -This is the lever that keeps recall faithful: a well-validated `dont` will keep surfacing for the same query class; a once-true-now-misleading observation gets demoted by repeated failure stamps. +This is the lever that keeps observation recall faithful: a +well-validated failed approach will keep surfacing for the same query +class; a once-true-now-misleading observation gets demoted by repeated +failure stamps. ## Self-rating loop -`memory.record_use` is the in-band reinforcement primitive. Layered on -top of it is a closed-loop self-rating cycle that runs per session and -captures whether memories actually shaped Claude's work: +Reflections and semantic memories have no `reinforcement_score` column +and are never touched by `memory.record_use` — calling it with a +reflection/semantic id is a silent no-op. Instead, a closed-loop +self-rating cycle runs per session and captures whether memories +actually shaped Claude's work, feeding the Wilson-score hit-rate prior +described below: 1. **Exposure** — every reflection or semantic memory surfaced by `memory.retrieve` / `memory.semantic_retrieve` / the SessionStart diff --git a/website/index.md b/website/index.md index 5bcfda9..6011b41 100644 --- a/website/index.md +++ b/website/index.md @@ -75,7 +75,7 @@ memory.retrieve(query="growatt") → do bucket
"Python ZoneInfo unavailable on Windows without tzdata package."
-

reinforcement-weighted — memory.record_use promotes signal, demotes noise.

+

ranked by a Wilson-score hit-rate prior — memory.credit promotes signal, demotes noise.

diff --git a/website/mcp-tools.md b/website/mcp-tools.md index d0da2c2..d1c6ce2 100644 --- a/website/mcp-tools.md +++ b/website/mcp-tools.md @@ -27,11 +27,12 @@ Get reflections bucketed by polarity, plus drained spool observations. | Parameter | Type | Required | Notes | |---|---|---|---| +| `query` | string | optional (strongly recommended) | Plain-language description of the task at hand. Fuses BM25 + vector search via RRF with the Wilson-score usefulness prior. Omitting it degrades gracefully to the Wilson-prior-only order — the same generic top-ranked lessons every call. | | `project` | string | optional | Defaults to current project (cwd-derived). | | `tech` | string | optional | Filter by tech tag. | | `phase` | `planning` / `implementation` / `general` | optional | Filter by reflection phase. | | `polarity` | `do` / `dont` / `neutral` | optional | Restrict to one bucket. | -| `limit_per_bucket` | int | optional | Defaults to 20. | +| `limit_per_bucket` | int | optional | Defaults to 5. | Returns `{"do": [...], "dont": [...], "neutral": [...]}`. Drains the spool first. @@ -201,10 +202,11 @@ Returns a step summary: `{episode_id, counts, queue, failure}`. ## Rating tools -Memories prove their worth by being used. The rating loop closes the -feedback cycle on top of [`memory.record_use`](#memoryrecord_use): -mid-session credit via `memory.credit`, and an end-of-session sweep -driven by the +Memories prove their worth by being used. Reflections and semantic +memories are rated through a separate loop from +[`memory.record_use`](#memoryrecord_use) (which only ever touches raw +observations): mid-session credit via `memory.credit`, and an +end-of-session sweep driven by the [`rate-session-memories`](https://github.com/emp3thy/better-memory/blob/main/.claude/skills/rate-session-memories/SKILL.md) skill that classifies every exposed reflection / semantic memory as `cited`, `shaped`, `ignored`, `misled`, or `overlooked`. The session_close hook emits From aea6ddb548926bf742c6898977d0608b43d9c4d2 Mon Sep 17 00:00:00 2001 From: gethin Date: Fri, 24 Jul 2026 12:51:56 +0100 Subject: [PATCH 3/3] fix(bootstrap): footer credited the wrong tool; align memory-write skill The SessionStart bootstrap footer (services/session_bootstrap.py _FOOTER, rendered into both the legacy and deferred render paths) told Claude to call memory_record_use on injected memories -- but bootstrap only ever injects reflections/semantic memories, and record_use only writes to the observations table, so the instruction silently no-op'd every session. Same wrong-tool class as the docs sweep in the prior commit, except this one is live prompt text, not documentation. Replaced with the credit form (memory_credit(kind, id, class, evidence)) and updated the one test that pinned the old footer substring. Also reworded the matching leftover row in memory-write.md that instructed record_use for a retrieved memory. Co-Authored-By: Claude Fable 5 --- better_memory/services/session_bootstrap.py | 6 +++--- better_memory/skills/memory-write.md | 2 +- tests/services/test_session_bootstrap.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/better_memory/services/session_bootstrap.py b/better_memory/services/session_bootstrap.py index 4bbce6b..98a000d 100644 --- a/better_memory/services/session_bootstrap.py +++ b/better_memory/services/session_bootstrap.py @@ -34,9 +34,9 @@ _HINT_MAX_CHARS = 600 _FOOTER = ( - "Use mcp__better-memory__memory_record_use(id, success|failure) when a " - "memory materially helps or misleads. Use mcp__better-memory__memory_observe " - "to write new ones." + "When an injected memory materially helps or misleads, credit it: " + "memory_credit(kind, id, class, evidence) - one-line evidence statement. " + "Use mcp__better-memory__memory_observe to write new ones." ) diff --git a/better_memory/skills/memory-write.md b/better_memory/skills/memory-write.md index 5ceef63..943ac99 100644 --- a/better_memory/skills/memory-write.md +++ b/better_memory/skills/memory-write.md @@ -53,7 +53,7 @@ memory.close_episode( | Pure fact / observed system behaviour | `neutral` (no outcome inherent) | — | | Bug identified AND fixed same session | `success` (fix verified) | — | | Bug identified, not yet fixed | `neutral` | `success` when fix lands | -| Applied a memory someone else wrote | **don't observe** — call `record_use(retrieved_id, outcome=...)` instead | — | +| Applied a retrieved reflection or semantic memory | **don't observe** — credit it via `memory_credit(kind, id, class, evidence)` with a one-line evidence statement | — | ## Examples diff --git a/tests/services/test_session_bootstrap.py b/tests/services/test_session_bootstrap.py index c4752fc..db97ab1 100644 --- a/tests/services/test_session_bootstrap.py +++ b/tests/services/test_session_bootstrap.py @@ -212,7 +212,7 @@ def test_render_omits_empty_sections(conn, git_repo: Path) -> None: assert "Reflections" not in text # but the header and footer should still render assert "## better-memory: session bootstrap" in text - assert "memory_record_use" in text # footer + assert "memory_credit" in text # footer def test_render_truncates_long_hints(conn, git_repo: Path) -> None: