diff --git a/packages/core/src/agent_memory/core/recall.py b/packages/core/src/agent_memory/core/recall.py index c56eec46..bc368eb5 100644 --- a/packages/core/src/agent_memory/core/recall.py +++ b/packages/core/src/agent_memory/core/recall.py @@ -113,7 +113,8 @@ def _eligible( return eligible def _in_scope(self, path: str, scope: str) -> bool: - return path.startswith(scope.strip("/")) + scope = scope.strip("/") + return path == scope or path.startswith(scope + "/") def _current_at(self, row: sqlite3.Row, moment: dt.datetime) -> bool: if timestamp.parse(str(row["valid_from"])) > moment: diff --git a/tests/unit/test_storage.py b/tests/unit/test_storage.py index 5459efce..d7789946 100644 --- a/tests/unit/test_storage.py +++ b/tests/unit/test_storage.py @@ -163,3 +163,13 @@ def test_slugify_is_idempotent_and_produces_valid_slugs(config): if once: assert is_valid_slug(once) assert slugify(once, config.storage.slug_max_length) == once + + +def test_scope_filter_does_not_match_similar_prefixes(store): + """Regression: scope=user must not match username/, user-notes/, etc. (#17).""" + for name in ("user/preferences/a.md", "username/settings.md", "user-notes/x.md", "user2/p.md"): + seeded.record(abstract=f"Entry in {name}", type="fact", name=name.replace("/", "-")) + + results = seeded.recall(scope="user") + paths = [r["path"] for r in results] + assert all(p == "user" or p.startswith("user/") for p in paths), paths