pre-release: combined patch - cli 0.3.10 (hermes install TypeError) + client 0.4.6 (negative-limit guard)
Browse files- sibyl-memory-cli/CHANGELOG.md +11 -0
- sibyl-memory-cli/pyproject.toml +1 -1
- sibyl-memory-cli/src/sibyl_memory_cli/setup.py +1 -1
- sibyl-memory-cli/tests/test_hermes_install_args.py +31 -0
- sibyl-memory-client/CHANGELOG.md +10 -0
- sibyl-memory-client/pyproject.toml +1 -1
- sibyl-memory-client/src/sibyl_memory_client/client.py +2 -0
- sibyl-memory-client/tests/test_negative_limit_guard.py +30 -0
sibyl-memory-cli/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to `sibyl-memory-cli` are recorded here. Format follows
|
|
| 4 |
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows
|
| 5 |
[SemVer](https://semver.org/).
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## [0.3.9] — 2026-05-31
|
| 8 |
|
| 9 |
Guided migration plus first-class Codex support and the real fix for Claude
|
|
|
|
| 4 |
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows
|
| 5 |
[SemVer](https://semver.org/).
|
| 6 |
|
| 7 |
+
## [0.3.10] — 2026-06-01
|
| 8 |
+
|
| 9 |
+
### Fixed
|
| 10 |
+
|
| 11 |
+
- **`sibyl setup hermes` raised a TypeError on every first-run wiring.**
|
| 12 |
+
`HermesWirer._install_plugin()` called `install()` with only `hermes_home`
|
| 13 |
+
(as a `str`), but `sibyl_memory_hermes.install_plugin.install()` requires
|
| 14 |
+
`(hermes_home: Path, force: bool, dry_run: bool)` with no defaults, so it
|
| 15 |
+
raised `TypeError` before the plugin could install. Now calls
|
| 16 |
+
`install(hermes_home=Path(self.hermes_home), force=False, dry_run=False)`.
|
| 17 |
+
|
| 18 |
## [0.3.9] — 2026-05-31
|
| 19 |
|
| 20 |
Guided migration plus first-class Codex support and the real fix for Claude
|
sibyl-memory-cli/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "sibyl-memory-cli"
|
| 7 |
-
version = "0.3.
|
| 8 |
description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
|
| 9 |
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
| 10 |
license = { text = "MIT" }
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "sibyl-memory-cli"
|
| 7 |
+
version = "0.3.10"
|
| 8 |
description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
|
| 9 |
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
| 10 |
license = { text = "MIT" }
|
sibyl-memory-cli/src/sibyl_memory_cli/setup.py
CHANGED
|
@@ -206,7 +206,7 @@ class HermesWirer:
|
|
| 206 |
|
| 207 |
def _install_plugin(self) -> None:
|
| 208 |
from sibyl_memory_hermes.install_plugin import install
|
| 209 |
-
install(hermes_home=
|
| 210 |
|
| 211 |
def _backup_config(self) -> Optional[Path]:
|
| 212 |
if not self.config_path.exists():
|
|
|
|
| 206 |
|
| 207 |
def _install_plugin(self) -> None:
|
| 208 |
from sibyl_memory_hermes.install_plugin import install
|
| 209 |
+
install(hermes_home=Path(self.hermes_home), force=False, dry_run=False)
|
| 210 |
|
| 211 |
def _backup_config(self) -> Optional[Path]:
|
| 212 |
if not self.config_path.exists():
|
sibyl-memory-cli/tests/test_hermes_install_args.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression: ``HermesWirer._install_plugin`` must call ``install()`` with its
|
| 2 |
+
full required signature ``(hermes_home: Path, force: bool, dry_run: bool)``.
|
| 3 |
+
|
| 4 |
+
cli 0.3.9 shipped a ``TypeError`` here (called with only ``hermes_home`` as a
|
| 5 |
+
``str``) because the wider setup suite stubs ``_install_plugin`` and masked it.
|
| 6 |
+
This test exercises the real method with ``install`` mocked, so the arg contract
|
| 7 |
+
is enforced without needing the hermes runtime. Source: beta reports
|
| 8 |
+
"sibyl setup hermes fails on fresh install" (2026-06-01).
|
| 9 |
+
"""
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
import sibyl_memory_hermes.install_plugin as ip
|
| 13 |
+
from sibyl_memory_cli import setup as cli_setup
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def test_install_plugin_passes_full_signature(monkeypatch):
|
| 17 |
+
calls = []
|
| 18 |
+
monkeypatch.setattr(
|
| 19 |
+
ip, "install",
|
| 20 |
+
lambda hermes_home, force, dry_run: (calls.append((hermes_home, force, dry_run)), 0)[1],
|
| 21 |
+
)
|
| 22 |
+
w = cli_setup.HermesWirer.__new__(cli_setup.HermesWirer)
|
| 23 |
+
w.hermes_home = Path("/tmp/fake-hermes-home")
|
| 24 |
+
|
| 25 |
+
w._install_plugin() # must not raise TypeError
|
| 26 |
+
|
| 27 |
+
assert len(calls) == 1
|
| 28 |
+
hermes_home, force, dry_run = calls[0]
|
| 29 |
+
assert isinstance(hermes_home, Path)
|
| 30 |
+
assert force is False
|
| 31 |
+
assert dry_run is False
|
sibyl-memory-client/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `sibyl-memory-client` are recorded here. Format
|
|
| 4 |
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning
|
| 5 |
follows [SemVer](https://semver.org/).
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
## [0.4.5] - 2026-05-30
|
| 8 |
|
| 9 |
Adversarial QA remediation (Acer stress-test suite): two findings + a review hardening.
|
|
|
|
| 4 |
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning
|
| 5 |
follows [SemVer](https://semver.org/).
|
| 6 |
|
| 7 |
+
## [0.4.6] - 2026-06-01
|
| 8 |
+
|
| 9 |
+
### Fixed
|
| 10 |
+
|
| 11 |
+
- **A negative `limit` could broaden search instead of narrowing it.**
|
| 12 |
+
`search()` and `search_entities()` passed `limit` straight into SQLite
|
| 13 |
+
`LIMIT ?`, where `LIMIT -1` means unbounded, so `limit=-1` returned more
|
| 14 |
+
rows rather than fewer. Both methods now clamp `limit` with `max(0, limit)`
|
| 15 |
+
so an invalid negative limit can never broaden results.
|
| 16 |
+
|
| 17 |
## [0.4.5] - 2026-05-30
|
| 18 |
|
| 19 |
Adversarial QA remediation (Acer stress-test suite): two findings + a review hardening.
|
sibyl-memory-client/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "sibyl-memory-client"
|
| 7 |
-
version = "0.4.
|
| 8 |
description = "Local-first agentic memory SDK. SQLite-backed five-tier hierarchical schema, FTS5 search, multi-tenant, with self-learning skill detection and local memory linter. Foundation of the Sibyl Memory Plugin family."
|
| 9 |
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
| 10 |
license = { text = "MIT" }
|
|
|
|
| 4 |
|
| 5 |
[project]
|
| 6 |
name = "sibyl-memory-client"
|
| 7 |
+
version = "0.4.6"
|
| 8 |
description = "Local-first agentic memory SDK. SQLite-backed five-tier hierarchical schema, FTS5 search, multi-tenant, with self-learning skill detection and local memory linter. Foundation of the Sibyl Memory Plugin family."
|
| 9 |
authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
|
| 10 |
license = { text = "MIT" }
|
sibyl-memory-client/src/sibyl_memory_client/client.py
CHANGED
|
@@ -889,6 +889,7 @@ class MemoryClient:
|
|
| 889 |
|
| 890 |
Raises: StorageError on backend failure; empty list on empty / invalid query.
|
| 891 |
"""
|
|
|
|
| 892 |
match_q = _sanitize_fts5_query(query, prefix=prefix)
|
| 893 |
if not match_q:
|
| 894 |
return []
|
|
@@ -934,6 +935,7 @@ class MemoryClient:
|
|
| 934 |
|
| 935 |
Raises: StorageError on backend failure.
|
| 936 |
"""
|
|
|
|
| 937 |
match_q = _sanitize_fts5_query(query, prefix=prefix)
|
| 938 |
if not match_q:
|
| 939 |
return []
|
|
|
|
| 889 |
|
| 890 |
Raises: StorageError on backend failure; empty list on empty / invalid query.
|
| 891 |
"""
|
| 892 |
+
limit = max(0, limit) # negative limit must not broaden: SQLite LIMIT -1 = unbounded
|
| 893 |
match_q = _sanitize_fts5_query(query, prefix=prefix)
|
| 894 |
if not match_q:
|
| 895 |
return []
|
|
|
|
| 935 |
|
| 936 |
Raises: StorageError on backend failure.
|
| 937 |
"""
|
| 938 |
+
limit = max(0, limit) # negative limit must not broaden: SQLite LIMIT -1 = unbounded
|
| 939 |
match_q = _sanitize_fts5_query(query, prefix=prefix)
|
| 940 |
if not match_q:
|
| 941 |
return []
|
sibyl-memory-client/tests/test_negative_limit_guard.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Regression: a negative ``limit`` must never broaden search results.
|
| 2 |
+
|
| 3 |
+
SQLite treats ``LIMIT -1`` as unbounded, so passing ``limit=-1`` previously
|
| 4 |
+
returned MORE rows, not fewer. Both ``search`` and ``search_entities`` now clamp
|
| 5 |
+
with ``max(0, limit)``. Source: adversarial QA finding
|
| 6 |
+
SEARCH-NEGATIVE-LIMIT-CANNOT-BROADEN-RESULTS (2026-06-01).
|
| 7 |
+
"""
|
| 8 |
+
from sibyl_memory_client import MemoryClient
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def _seed(tmp_path):
|
| 12 |
+
c = MemoryClient.local(tmp_path / "memory.db", tenant_id="qa-sandbox")
|
| 13 |
+
for i in range(6):
|
| 14 |
+
c.set_entity("notes", f"item-{i}", {"text": "alpha beta gamma token"})
|
| 15 |
+
return c
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def test_search_negative_limit_does_not_broaden(tmp_path):
|
| 19 |
+
c = _seed(tmp_path)
|
| 20 |
+
bounded = c.search("token", limit=2)
|
| 21 |
+
negative = c.search("token", limit=-1)
|
| 22 |
+
# negative must never return MORE than a small positive limit, and must not
|
| 23 |
+
# fall through to SQLite's unbounded LIMIT -1.
|
| 24 |
+
assert len(negative) <= len(bounded)
|
| 25 |
+
assert len(negative) == 0
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def test_search_entities_negative_limit_does_not_broaden(tmp_path):
|
| 29 |
+
c = _seed(tmp_path)
|
| 30 |
+
assert c.search_entities("token", limit=-1) == []
|