fix(byterover): honor auto extract config

This commit is contained in:
LeonSGP43 2026-05-07 14:24:17 +08:00 committed by Teknium
parent f062cf076b
commit 52a09d8faf
2 changed files with 127 additions and 1 deletions

View file

@ -12,6 +12,11 @@ curl -fsSL https://byterover.dev/install.sh | sh).
Config via environment variables (profile-scoped via each profile's .env):
BRV_API_KEY ByteRover API key (for cloud features, optional for local)
Config via config.yaml:
memory:
byterover:
auto_extract: false # disable automatic brv curate hooks
Working directory: $HERMES_HOME/byterover/ (profile-scoped context tree)
"""
@ -40,6 +45,49 @@ _MIN_QUERY_LEN = 10
_MIN_OUTPUT_LEN = 20
def _coerce_bool(value: Any, default: bool = False) -> bool:
if isinstance(value, bool):
return value
if value is None:
return default
if isinstance(value, (int, float)):
return bool(value)
if isinstance(value, str):
text = value.strip().lower()
if text in {"1", "true", "yes", "on"}:
return True
if text in {"0", "false", "no", "off"}:
return False
return default
def _load_plugin_config() -> Dict[str, Any]:
"""Read ByteRover's profile-scoped memory config.
New memory-provider setup stores non-secret provider settings under
``memory.<provider>``. Some users also set ``memory.provider_config`` from
early docs/issues, so accept it as a compatibility fallback.
"""
try:
from hermes_cli.config import load_config
config = load_config()
memory_config = config.get("memory", {})
if not isinstance(memory_config, dict):
return {}
provider_config = memory_config.get("byterover", {})
if isinstance(provider_config, dict) and provider_config:
return dict(provider_config)
legacy_config = memory_config.get("provider_config", {})
if isinstance(legacy_config, dict):
return dict(legacy_config)
except Exception:
pass
return {}
# ---------------------------------------------------------------------------
# brv binary resolution (cached, thread-safe)
# ---------------------------------------------------------------------------
@ -172,7 +220,9 @@ STATUS_SCHEMA = {
class ByteRoverMemoryProvider(MemoryProvider):
"""ByteRover persistent memory via the brv CLI."""
def __init__(self):
def __init__(self, config: Optional[Dict[str, Any]] = None):
self._config = dict(config) if config is not None else _load_plugin_config()
self._auto_extract = _coerce_bool(self._config.get("auto_extract"), True)
self._cwd = ""
self._session_id = ""
self._turn_count = 0
@ -195,6 +245,12 @@ class ByteRoverMemoryProvider(MemoryProvider):
"env_var": "BRV_API_KEY",
"url": "https://app.byterover.dev",
},
{
"key": "auto_extract",
"description": "Automatically curate completed turns and compression/memory hooks",
"default": "true",
"choices": ["true", "false"],
},
]
def initialize(self, session_id: str, **kwargs) -> None:
@ -238,6 +294,9 @@ class ByteRoverMemoryProvider(MemoryProvider):
def sync_turn(self, user_content: str, assistant_content: str, *, session_id: str = "") -> None:
"""Curate the conversation turn in background (non-blocking)."""
self._turn_count += 1
if not self._auto_extract:
logger.debug("ByteRover sync_turn skipped (auto_extract disabled)")
return
# Only curate substantive turns
if len(user_content.strip()) < _MIN_QUERY_LEN:
@ -264,6 +323,9 @@ class ByteRoverMemoryProvider(MemoryProvider):
def on_memory_write(self, action: str, target: str, content: str) -> None:
"""Mirror built-in memory writes to ByteRover."""
if not self._auto_extract:
logger.debug("ByteRover memory mirror skipped (auto_extract disabled)")
return
if action not in {"add", "replace"} or not content:
return
@ -282,6 +344,9 @@ class ByteRoverMemoryProvider(MemoryProvider):
def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:
"""Extract insights before context compression discards turns."""
if not self._auto_extract:
logger.debug("ByteRover pre-compression flush skipped (auto_extract disabled)")
return ""
if not messages:
return ""

View file

@ -0,0 +1,61 @@
"""Tests for the ByteRover memory provider config gates."""
from plugins.memory.byterover import ByteRoverMemoryProvider
def test_auto_extract_false_skips_sync_turn(monkeypatch):
calls = []
provider = ByteRoverMemoryProvider({"auto_extract": False})
provider.initialize("session-1")
monkeypatch.setattr("plugins.memory.byterover._run_brv", lambda *args, **kwargs: calls.append((args, kwargs)))
provider.sync_turn("please remember this detail", "acknowledged")
assert calls == []
assert provider._sync_thread is None
def test_auto_extract_false_skips_memory_write(monkeypatch):
calls = []
provider = ByteRoverMemoryProvider({"auto_extract": "false"})
provider.initialize("session-1")
monkeypatch.setattr("plugins.memory.byterover._run_brv", lambda *args, **kwargs: calls.append((args, kwargs)))
provider.on_memory_write("add", "user", "User prefers concise responses")
assert calls == []
def test_auto_extract_false_skips_pre_compress(monkeypatch):
calls = []
provider = ByteRoverMemoryProvider({"auto_extract": "off"})
provider.initialize("session-1")
monkeypatch.setattr("plugins.memory.byterover._run_brv", lambda *args, **kwargs: calls.append((args, kwargs)))
result = provider.on_pre_compress([
{"role": "user", "content": "remember this"},
{"role": "assistant", "content": "stored"},
])
assert result == ""
assert calls == []
def test_auto_extract_false_keeps_explicit_curate_tool(monkeypatch):
calls = []
provider = ByteRoverMemoryProvider({"auto_extract": False})
provider.initialize("session-1")
def fake_run(args, **kwargs):
calls.append(args)
return {"success": True, "output": "ok"}
monkeypatch.setattr("plugins.memory.byterover._run_brv", fake_run)
result = provider.handle_tool_call("brv_curate", {"content": "Important project fact"})
assert "Memory curated successfully" in result
assert calls == [["curate", "--", "Important project fact"]]