Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,13 @@ jobs:
- name: Run core tests
run: pytest tests/ -v --tb=short --timeout=900 --timeout-method=thread

- name: Check published minimum Core compatibility (online)
env:
MNEMOSYNE_TEST_PUBLISHED_CORE: "1"
run: >-
pytest tests/test_hermes_core_release_contract.py::test_published_minimum_core_completes_the_provider_lifecycle
-v --tb=short --timeout=900 --timeout-method=thread

- name: Run Hermes integration tests
run: pytest integrations/hermes/tests/ -v --tb=short --timeout=900 --timeout-method=thread

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [SemVer](https://semver.org/) starting from v3.1.2.
- **Advisory offline linear BEAM throughput benchmark (#901).** A fixed synthetic workload on a temporary database reports warm per-call latency, sequential throughput, raw samples and environment/revision metadata as JSON, with compatible same-machine baseline comparisons and a documented retention/interpretation policy. No runtime recall changes or CI timing threshold.

### Fixed
- **Russian fallback matches optional ё/е spelling (#1295, normalization portion).** Candidate prefixes and trigram scoring now fold case and ё to е, so an inflected query such as `свеклы` can retrieve stored `Свёкла`. The bounded candidate pool gives original-prefix and folded-only matches separate slots, then fills any leftover space with original-prefix matches. Stored text, FTS indexes, query-word mean aggregation and lexical gate thresholds are unchanged. Enhanced-recall cache generation advances to 19, invalidating results from Main generation 18. Multi-word score dilution remains separate work.
- **Hermes CLI selects the provider's profile database (#1346).** With `HERMES_HOME` set, packaged `stats`, `inspect`, `doctor`, `export`, `import`, `sleep`, and `clear` use the provider's explicit `<home>/mnemosyne/data` root, including its selected profile bank. Doctor's constructed memory and its initial beam now target the same database. A divergent `MNEMOSYNE_DATA_DIR` still controls standalone Core callers, and the CLI keeps the Core resolver when `HERMES_HOME` is absent. No database migration or merge is performed.
- **Fleet census bounds SQLite lock waits for read-only candidate banks (#1336).** Each locked file previously consumed SQLite's five-second default busy timeout. Census reads now wait up to 50 milliseconds per candidate; unreadable files remain in the report and keep the cross-bank bound unmeasured. Discovery and bank contents are unchanged.
- **Opt-in input caps now cover local embeddings as well as the API (Refs #1153).** Local document and query calls apply the existing character and UTF-8 byte caps to their prefixed text before fastembed runs. Both caps remain disabled by default, and query-cache entries remain keyed by effective cap and prefix. Enhanced recall snapshots both effective input caps for its cache key and embedding calls, with generation 18 invalidating results from the current Main predecessor, generation 17. Changing caps does not rebuild stored vectors; reindex when applying the new policy to existing documents. This limits input size only; it does not establish an ONNX memory ceiling or close the reported arena issue.
Expand Down
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ MNEMOSYNE_NO_EMBEDDINGS=1 python -m pytest integrations/hermes/tests/ -v
The Hermes provider is packaged separately from `mnemosyne-memory`, so its
editable install is required before running `integrations/hermes/tests/`.

The published minimum-Core compatibility test is an explicit online check:
it downloads the pinned Core wheel from PyPI, verifies its SHA256, builds the
current Hermes wheel, and exercises the provider lifecycle. It is skipped in
normal test runs and runs in a separate CI step on every supported Python version.
To run it locally:

```bash
MNEMOSYNE_NO_EMBEDDINGS=1 MNEMOSYNE_TEST_PUBLISHED_CORE=1 python -m pytest \
tests/test_hermes_core_release_contract.py::test_published_minimum_core_completes_the_provider_lifecycle -v
```

## What You Can Do

**No contribution is too small.**
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,12 @@ Full reference: [docs/configuration.md](docs/configuration.md)

### Language Support

The Russian lexical fallback folds case and `ё`/`е` spelling when selecting
candidates and scoring inflections (for example, `свеклы` can match `Свёкла`).
Stored text is preserved. The fallback still averages scores over query words
and uses the normal lexical admission gate; this does not guarantee admission
for multi-word paraphrases or cross-language queries.

Default embeddings are English-optimized (`bge-small-en-v1.5`). For **non-English or multilingual** recall, swap the model:

```bash
Expand Down
2 changes: 1 addition & 1 deletion docs/api/configuration.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Configuration"
version: 4.0.0
version: 4.1.0
config_key_count: 117
env_only_count: 52
generated_at: "auto"
Expand Down
4 changes: 2 additions & 2 deletions docs/api/tool-schema.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
---
title: "MCP Tool Schema"
version: 4.0.0
version: 4.1.0
tool_count: 38
mcp_tool_count: 30
plugin_only_tool_count: 8
generated_at: "auto"
---

# MCP Tool Schema (v4.0.0)
# MCP Tool Schema (v4.1.0)

> **Generated file. Do not edit by hand.**
> Source: `mnemosyne/tool_schemas.py` and `mnemosyne/mcp_tools.py`.
Expand Down
2 changes: 1 addition & 1 deletion hermes_memory_provider/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: mnemosyne
version: 4.0.0
version: 4.1.0
description: "Native local memory for Hermes — SQLite with vector search, FTS5 hybrid ranking, episodic consolidation, temporal triples, and bidirectional sync with optional client-side encryption. Zero cloud. Zero latency."
author: Abdias J
provides_tools:
Expand Down
2 changes: 1 addition & 1 deletion mnemosyne/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
>>> results = recall("user preferences")
"""

__version__ = "4.0.0"
__version__ = "4.1.0"
__author__ = "Abdias J"
__license__ = "MIT"

Expand Down
69 changes: 52 additions & 17 deletions mnemosyne/core/beam.py
Original file line number Diff line number Diff line change
Expand Up @@ -5598,23 +5598,29 @@ def _ngrams(s: str, n: int = 3) -> set:
return {s[i:i + n] for i in range(len(s) - n + 1)}


def _normalize_cyrillic_text(text: str) -> str:
"""Fold case and optional Russian ё spelling for fallback matching only."""
return text.lower().replace("ё", "е")


def _cyrillic_score(query: str, content: str, n: int = 3) -> float:
"""Trigram Jaccard score in [0, 1] for Russian/Cyrillic recall.

For each query word, find the best-matching content word by Jaccard
similarity of their character n-gram sets. Return the mean across
query words. Words shorter than 3 characters are ignored to avoid
noisy matches on stop words and punctuation.
noisy matches on stop words and punctuation. Case and ё/е spelling
are folded before extracting n-grams; stored content is unchanged.

N-gram sets are computed once per unique word and cached in a dict
to avoid redundant recomputation when the same content word is
compared against multiple query words.
"""
q_words = [
w for w in re.findall(r"[а-яёa-z0-9]+", query.lower()) if len(w) >= 3
w for w in re.findall(r"[а-яёa-z0-9]+", _normalize_cyrillic_text(query)) if len(w) >= 3
]
c_words = [
w for w in re.findall(r"[а-яёa-z0-9]+", content.lower()) if len(w) >= 3
w for w in re.findall(r"[а-яёa-z0-9]+", _normalize_cyrillic_text(content)) if len(w) >= 3
]
if not q_words or not c_words:
return 0.0
Expand Down Expand Up @@ -5652,8 +5658,10 @@ def _cyrillic_like_search(
receive comparable scores regardless of which surface form appears
in the stored text.

The candidate-generation LIMIT is ``k * 5`` to keep the Python-side
scoring bounded; this matches the CJK fallback's behaviour.
Python scoring is bounded to ``k * 5`` candidates. Original-prefix
matches get half the pool first, and folded-only matches get the
remaining slots. Extra original-prefix matches fill any space left.
Both groups use a stable identifier order.
"""
if not _has_cyrillic(query):
return []
Expand All @@ -5664,34 +5672,60 @@ def _cyrillic_like_search(

# SQLite's default LOWER() and LIKE are ASCII-only, so they cannot
# case-fold Cyrillic letters (Т ≠ т, Ё ≠ ё). Register a Python
# UDF on the connection that performs a real Unicode lower(). It
# is safe to register the same name multiple times — SQLite will
# overwrite the previous function with the same name and arity.
conn.create_function("_py_lower", 1, lambda s: s.lower() if isinstance(s, str) else s)
# UDF for Unicode case-folding and ё/е normalization, matching the
# scorer. Without the spelling fold here, candidates are lost before
# re-ranking. Re-registering the same name and arity is safe.
conn.create_function(
"_py_cyrillic_normalize", 1,
lambda s: _normalize_cyrillic_text(s) if isinstance(s, str) else s,
)
conn.create_function(
"_py_cyrillic_lower", 1,
lambda s: s.lower() if isinstance(s, str) else s,
)

# Use a 4-character prefix for LIKE matching. Query words and
# stored content are inflected forms of the same lemma (тёмная
# vs тёмную), so the 4-char prefix is the most reliable cheap
# over-selector that still prunes the candidate set down to a
# trigram-scoring-sized slice.
q_words = [
w for w in re.findall(r"[а-яёa-z0-9]+", query.lower()) if len(w) >= 4
w for w in re.findall(r"[а-яёa-z0-9]+", _normalize_cyrillic_text(query)) if len(w) >= 4
]
if not q_words:
return []

conditions = " OR ".join(
["_py_lower(content) LIKE ? ESCAPE '\\'"] * len(q_words)
original_words = [
w for w in re.findall(r"[а-яёa-z0-9]+", query.lower()) if len(w) >= 4
]
budget = k * 5
if budget <= 0:
return []
original_conditions = " OR ".join(
["_py_cyrillic_lower(content) LIKE ? ESCAPE '\\'"] * len(original_words)
)
folded_conditions = " OR ".join(
["_py_cyrillic_normalize(content) LIKE ? ESCAPE '\\'"] * len(q_words)
)
params = [f"%{w[:4].lower()}%" for w in q_words]
original_params = [f"%{w[:4]}%" for w in original_words]
folded_params = [f"%{w[:4]}%" for w in q_words]
try:
all_rows = conn.execute(
f"SELECT {id_col}, content FROM {table} WHERE {conditions} LIMIT ?",
params + [k * 5],
original_rows = conn.execute(
f"SELECT {id_col}, content FROM {table} WHERE {original_conditions}"
f" ORDER BY {id_col} LIMIT ?",
original_params + [budget],
).fetchall()
original_count = min(len(original_rows), budget // 2)
variant_rows = conn.execute(
f"SELECT {id_col}, content FROM {table} WHERE ({folded_conditions})"
f" AND NOT ({original_conditions}) ORDER BY {id_col} LIMIT ?",
folded_params + original_params + [budget - original_count],
).fetchall()
except Exception:
return []

remaining = budget - len(variant_rows)
all_rows = original_rows[:original_count] + variant_rows + original_rows[original_count:remaining]
if not all_rows:
return []

Expand Down Expand Up @@ -11240,8 +11274,9 @@ def _get_query_embedding():
# 16: clamp future-dated recency ages before exponentiation.
# 17: strongest eligible graph traversal and scoped active-seed selection.
# 18: local embedding input caps and effective cap policy in cache keys.
# 19: Cyrillic ё/е folding in fallback candidates and scores.
# Recheck the actual predecessor before each semantic integration.
_ENHANCED_RECALL_CACHE_VERSION = 18
_ENHANCED_RECALL_CACHE_VERSION = 19
# NOTE: the key carries the env MNEMOSYNE_VEC_TYPE, not the table's
# live DDL type — a reindex under a different type without a version
# bump serves stale admission/ranking. Reindex flows must bump.
Expand Down
Loading
Loading