Releases: jarmstrong158/context-keeper
Release list
v0.10.0: Abstention + supersession-as-ranking
Two ideas adapted from studying Curion, a competitor MCP memory tool — kept dependency-free. Curion's LLM-controller architecture (an API call on every store and recall) was deliberately not adopted; context-keeper stays zero-dependency and offline by default.
Abstention: get_context can now say "I don't have anything relevant"
Previously it always returned its top-scored entries. But the composite score banks ~55 points from recency/status/origin regardless of query relevance, so a query with no relevant memory silently got a confident-looking top result. Measured confabulation was 100% on no-answer queries (evals/abstention.py).
Now the response carries top_relevance, and when the top entry's tag/text relevance falls below min_relevance (config, default 0.20), no_confident_match: true with guidance telling the agent not to present the entries as established fact. It annotates, never suppresses — weak matches are still returned, so the vocabulary-mismatch recall that retrieval_hints and the semantic blend preserve survives. 0.20 is the highest floor with 0% false-abstention on the eval set (TNR 38%). Honest limit documented: hard-negatives sharing real topic vocabulary still slip through — a field-wide hard problem for lexical and dense retrieval.
Supersession as a ranking signal, not just a filter
record_decision accepts supersedes: [ids]: the prior decisions become superseded — demoted in ranking but still recallable ("why did we change from X?"), distinct from deprecate_entry which removes an entry from retrieval entirely. Superseded entries are skipped by prune_stale/verify_quality (intentional history, not stale work) and marked **SUPERSEDED** by dec-NNN in the DECISIONS.md projection. Reuses infrastructure already present — score_entry already demoted superseded, get_context already only filtered deprecated; the gap was that nothing ever set the status.
146 tests passing (15 new). tools/list payload 2493/2500. Retrieval eval unchanged.
v0.9.0: Clustering, more embedding backends, critical summary fix
Critical fix: empty session-start injection for large stores
The get_project_summary truncation loop evaluated the original text in its condition, so any store whose summary exceeded the token budget (~30+ entries) silently popped every line and injected an empty summary at session start. Large stores were getting ~0 tokens of memory briefing. Found by measurement (see below); fixed with a regression test.
Topic clustering
Above 8 decisions, get_project_summary groups decisions by their most-frequent shared tag instead of one flat list -- a 59-decision store reads as a dozen topics.
OpenAI-compatible embedding backends
semantic.api: "openai" points the semantic blend at any /v1/embeddings endpoint -- LM Studio, llama.cpp server, or OpenAI itself (api_key_env names the env var holding the key). Ollama stays the default; same fail-safe lexical fallback. nomic task prefixes now apply only to nomic models.
Trust-aware conflict guidance
similar_entries matches now carry each entry's origin, and the guidance states the precedence: user-stated overrides agent-inferred overrides imported.
Token-reduction measurement published
evals/token_reduction.py, run against four real stores: 94-97% reduction on large stores (e.g. 78 entries: ~75k tokens dumped vs ~2k injected), with the by-construction caveat stated in-script. Running this measurement is what caught the empty-injection bug.
Six more MCP clients documented
OpenCode, GitHub Copilot CLI, Antigravity, OpenClaw, Hermes, and pi/oh-my-pi config snippets in the README. The server is standard stdio MCP; only the hooks are Claude Code specific.
136 tests passing (5 new). Tool schemas unchanged (2481/2500 token budget). Held-out retrieval eval unchanged.
v0.8.0: DECISIONS.md projection
DECISIONS.md projection (opt-in, render-on-write)
Mirror the canonical decisions store into a human-readable DECISIONS.md:
{ "markdown_export": { "enabled": true, "path": "DECISIONS.md" } }- Render-on-write. Every decision mutation (
record_decision,update_entry,deprecate_entry) regenerates the entire file fromdecisions.jsonafter the JSON write and before the tool returns -- so a subsequentgit commitcaptures JSON and markdown in the same commit. Deliberately on the write path, not a git/PostToolUse hook: rendering after the commit snapshot is taken would reintroduce drift. - JSON stays canonical; markdown is derived and read-only. Regenerated whole every time, never appended to or parsed back. Hand edits are not preserved -- a regenerated projection has no drift surface.
- New
export_markdowntool regenerates on demand (optional custompath), so existing repos can backfill without enabling the flag. - Entry layout mirrors the field-tested convention:
### <summary> -- MM-DD (dec-NNN)headings with Why / Tried / Tradeoff / Rejected bullets; pre-v0.4 legacy entries render their freeform rationale as Why. - Pure stdlib. Default behavior with the flag off is byte-for-byte unchanged.
131 tests passing (10 new). Smoke-tested against a real 59-decision store.
v0.7.1: Tool-schema token budget
Tool-schema token budget
Every connected MCP client loads the full tools/list payload into model context at every session start. This release trims the schema descriptions from ~2800 to ~2370 estimated tokens (record_decision alone was 738) and adds a regression test capping the payload at 2500 -- future fields must fit the budget or consciously raise it.
Nothing behavioral changed: rich field guidance still reaches the model where it is lazy-loaded (validation rejection messages via _FIELD_GUIDANCE, and CLAUDE.md), so capture quality enforcement is unchanged. 121 tests passing; held-out retrieval eval unchanged (hit@5 80%, MRR 0.700).
v0.7.0: Anticipated queries, origin trust, timeline filters
retrieval_hints (anticipated queries)
All record_* tools accept retrieval_hints: 2-4 alternate phrasings a future session might search for (synonyms, symptom descriptions, error messages). Indexed for both lexical and semantic retrieval -- the zero-dependency fix for vocabulary-mismatch queries. Field-proven on release day: two eval queries that store growth had crowded out of the top-5 recovered to rank 1 after their gold entries got hints (held-out hit@5 66.7% -> 80%, MRR 0.567 -> 0.700).
origin + trust weighting
Entries record who authored them: user (explicitly stated), agent (inferred, default), or import (backfilled). Retrieval scoring weights them 10/5/2, so a rule the user actually stated outranks one the agent guessed. Pre-v0.7 entries score as agent -- a uniform shift that preserves their relative order.
since / before temporal filters
get_context accepts ISO since/before bounds against each entry's verified/created timestamp. "What did we decide this month" is now a query.
Other MCP clients documented
The server is standard stdio MCP; README now has config snippets for Cursor, Codex CLI, Gemini CLI, and Windsurf. Only the hooks are Claude Code specific.
120 tests passing (8 new).
v0.6.0: Capture-time guardrails
Scoped constraint injection
New hooks/scope_guard.py (PostToolUse on Edit|Write|NotebookEdit): the moment the agent edits a file covered by a constraint's scope, that constraint is injected into context via additionalContext. Session-start injection briefs the rules once at turn one; this enforces them at the exact moment they are about to matter. Each constraint fires at most once per session (state in .context/scope_guard_state.json). Constraints scoped global never fire here.
Similar-entry surfacing at record time
record_* now compares the new entry against all active entries (word-set Jaccard, threshold configurable via similar_threshold, default 0.30) and returns similar_entries + resolution guidance when existing entries overlap heavily -- catching restatements and contradictions at capture instead of relying on MMR to mitigate duplicates at retrieval. Advisory only: the write always proceeds, and related_to-linked entries are exempt. CLAUDE.md documents the resolution protocol (deprecate restatements, supersede contradictions, link the genuinely distinct).
112 tests passing (9 new, including subprocess tests that exercise the hook end to end).
v0.5.0: Data integrity, semantic retrieval, hook fixes
Data integrity
- Atomic writes: entry files are written to a temp file and swapped in with
os.replace, so a crash mid-write can no longer corrupt the store. - Corrupt-store protection:
record_*/update_entry/deprecate_entryrefuse to write when an entry file exists but cannot be parsed. Previously a corrupt file read as empty and the next record silently replaced the entire history with one entry. update_entryenforces the v0.4 schema: structured fields are min-length validated on update, so entries cannot be hollowed out after recording.
Retrieval
- Opt-in semantic blend (
semantic.enabledin config, local Ollama + nomic-embed-text): hit@5 80% -> 93%, MRR 0.63 -> 0.88 on the held-out eval split. Fail-safe: falls back to lexical ranking if Ollama is unreachable. - Opt-in MMR diversity reordering so near-duplicate entries do not crowd the token budget.
evals/: retrieval eval harness, labeled datasets, and results.- Budget packing skips oversized entries instead of stopping at the first one that does not fit; recency scoring clamped against future timestamps.
Hooks
- SessionStart now runs the post-compaction snapshot comparison itself (SessionStart fires with source
compactbefore any Stop, so the injected report was previously one compaction stale) and injects a one-line quality-scan nudge -- the model-visible surface forverify_quality, since PreCompact stdout never reaches the model. pre_compact.py/post_compact.pyimport resolution and the quality scan fromserver.py, removing duplicated keep-in-sync copies.
Packaging & CI
semantic_index.pyandhooks/commit_capture_reminder.pynow ship in the wheel/sdist (both were missing from 0.4.0 -- semantic retrieval silently fell back to lexical on pip installs).- CI matrix extended to Windows (atomic-write path is OS-sensitive).
- 104 tests passing (14 new).