feat(openviking): use user memory by default
Remove the implicit hermes peer and the peer question from new connection setup. Preserve explicit peer settings and keep memory paths consistent with the captured client identity. Add setup, configuration, request, recall, and session regression tests, plus upgrade guidance.
This commit is contained in:
@@ -50,7 +50,6 @@ OPENVIKING_ENDPOINT=http://127.0.0.1:1933
|
||||
# OPENVIKING_API_KEY=...
|
||||
# OPENVIKING_ACCOUNT=default
|
||||
# OPENVIKING_USER=default
|
||||
# OPENVIKING_AGENT=hermes
|
||||
```
|
||||
|
||||
## Config
|
||||
@@ -73,7 +72,7 @@ profile's `.env`:
|
||||
| `OPENVIKING_API_KEY` | (none) | User/admin API key for authenticated servers |
|
||||
| `OPENVIKING_ACCOUNT` | `default` | Tenant account for local/trusted mode |
|
||||
| `OPENVIKING_USER` | `default` | Tenant user for local/trusted mode |
|
||||
| `OPENVIKING_AGENT` | `hermes` | Hermes peer ID in OpenViking, used for peer-scoped memories |
|
||||
| `OPENVIKING_AGENT` | (none) | Optional peer ID for separate assistant context |
|
||||
|
||||
When `OPENVIKING_API_KEY` is set, Hermes lets OpenViking derive account/user
|
||||
identity from the key. In local or trusted deployments without an API key,
|
||||
@@ -82,6 +81,33 @@ Hermes also sends `User-Agent: openviking-memory-hermes/<version>` on
|
||||
OpenViking requests. This standard harness identifier contains the Hermes
|
||||
version, but no per-user identifier, and does not add a separate request.
|
||||
|
||||
### Optional peer identity
|
||||
|
||||
New connections use the OpenViking user's memory directory by default. Setup
|
||||
does not ask for a peer ID. Without a configured peer, Hermes sends neither
|
||||
`X-OpenViking-Actor-Peer` nor assistant-message `peer_id`.
|
||||
|
||||
For separate assistant context, set the existing `agent` field in the active
|
||||
profile's `config.yaml`:
|
||||
|
||||
```yaml
|
||||
memory:
|
||||
openviking:
|
||||
agent: work-assistant
|
||||
```
|
||||
|
||||
Existing non-empty `OPENVIKING_AGENT`, YAML `agent`, and linked OpenViking
|
||||
`actor_peer_id` or legacy `agent_id` values retain their behavior. Resolution
|
||||
order remains environment, linked OpenViking config, then Hermes YAML. To use
|
||||
no peer, remove the peer value from each configured source and start a new
|
||||
Hermes session.
|
||||
|
||||
Upgrades do not move or delete existing memories. Installations that relied
|
||||
on the old implicit `hermes` peer now use user memory for new writes. Set
|
||||
`agent: hermes` to retain that previous behavior. Without a peer ID, default
|
||||
OpenViking search can still find old peer memories, including other peers
|
||||
under the same OpenViking user. Keep a peer ID if you need the narrower view.
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
@@ -96,8 +122,9 @@ version, but no per-user identifier, and does not add a separate request.
|
||||
## Memory Writes And Deletes
|
||||
|
||||
`viking_remember` writes directly to OpenViking with `POST /api/v1/content/write`
|
||||
and `mode=create`. It creates peer-scoped memory files under explicit-uid
|
||||
`viking://user/<user>/peers/${OPENVIKING_AGENT}/memories/...` URIs, where
|
||||
and `mode=create`. By default it creates files under explicit-uid
|
||||
`viking://user/<user>/memories/...` URIs. When a peer ID is configured, it keeps
|
||||
the existing `viking://user/<user>/peers/<peer>/memories/...` path. In both cases,
|
||||
`<user>` is resolved client-side from `/api/v1/system/status` (server-asserted
|
||||
current user). Hermes caches a confirmed user only for the active connection.
|
||||
If the probe fails, Hermes uses the configured user, or `default`, for that
|
||||
@@ -111,7 +138,7 @@ local memory operation succeeds:
|
||||
|
||||
| Hermes action | OpenViking operation |
|
||||
|---------------|----------------------|
|
||||
| `add` | `content/write` with `mode=create` under the configured peer memory namespace |
|
||||
| `add` | `content/write` with `mode=create` under user memory, or the configured peer memory directory |
|
||||
|
||||
Built-in `replace` and `remove` operations are not mirrored because Hermes
|
||||
native memory entries do not yet carry stable OpenViking file URIs. Use
|
||||
|
||||
@@ -13,7 +13,7 @@ or a linked OpenViking CLI config:
|
||||
OPENVIKING_API_KEY — API key (required for authenticated servers)
|
||||
OPENVIKING_ACCOUNT — Tenant account for local/trusted mode (default: default)
|
||||
OPENVIKING_USER — Tenant user for local/trusted mode (default: default)
|
||||
OPENVIKING_AGENT — Hermes peer ID in OpenViking (default: hermes)
|
||||
OPENVIKING_AGENT — Optional peer ID (default: no peer)
|
||||
|
||||
Capabilities:
|
||||
- Automatic memory extraction on session commit (6 categories)
|
||||
@@ -65,9 +65,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
|
||||
_OPENVIKING_SERVICE_ENDPOINT = "https://api.vikingdb.cn-beijing.volces.com/openviking"
|
||||
_DEFAULT_AGENT = "hermes"
|
||||
_DEFAULT_AGENT = ""
|
||||
_OPENVIKING_USER_AGENT = f"openviking-memory-hermes/{_HERMES_VERSION}"
|
||||
_AGENT_PROMPT_LABEL = "Hermes peer ID in OpenViking"
|
||||
_OVCLI_CONFIG_ENV = "OPENVIKING_CLI_CONFIG_FILE"
|
||||
_OVCLI_DEFAULT_RELATIVE_PATH = ".openviking/ovcli.conf"
|
||||
_OVCLI_SAVED_PREFIX = "ovcli.conf."
|
||||
@@ -1835,7 +1834,6 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
||||
is_local = _is_local_openviking_url(endpoint)
|
||||
api_key_type = "user" if service else ""
|
||||
prefilled_api_key = ""
|
||||
prefilled_agent = ""
|
||||
while True:
|
||||
values = {
|
||||
"endpoint": endpoint,
|
||||
@@ -1859,9 +1857,6 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
||||
if credential_choice == cancelled:
|
||||
return _SETUP_CANCELLED
|
||||
if credential_choice == 2:
|
||||
values["agent"] = _clean_config_value(
|
||||
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
||||
) or _DEFAULT_AGENT
|
||||
_print_validation_progress("Validating OpenViking local dev access...")
|
||||
valid, message, _role = _validate_openviking_setup_values(values)
|
||||
if valid:
|
||||
@@ -1975,13 +1970,6 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
||||
prefilled_api_key = values["api_key"]
|
||||
continue
|
||||
|
||||
if prefilled_agent:
|
||||
values["agent"] = prefilled_agent
|
||||
prefilled_agent = ""
|
||||
else:
|
||||
values["agent"] = _clean_config_value(
|
||||
prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
|
||||
) or _DEFAULT_AGENT
|
||||
_print_validation_progress("Validating OpenViking API access...")
|
||||
valid, message, role = _validate_openviking_setup_values(
|
||||
values,
|
||||
@@ -2003,7 +1991,6 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
|
||||
)
|
||||
if route_choice == 0:
|
||||
prefilled_api_key = values["api_key"]
|
||||
prefilled_agent = values["agent"]
|
||||
api_key_type = "root"
|
||||
continue
|
||||
if route_choice == 1:
|
||||
@@ -2063,6 +2050,9 @@ def _save_hermes_only_config(
|
||||
) -> None:
|
||||
provider_config["use_ovcli_config"] = False
|
||||
provider_config.pop("ovcli_config_path", None)
|
||||
# A newly selected connection must not inherit the previous YAML peer.
|
||||
# Non-empty peer values, if supplied, are saved with the connection below.
|
||||
provider_config.pop("agent", None)
|
||||
_set_openviking_provider(config, provider_config)
|
||||
_write_env_vars(
|
||||
env_path,
|
||||
@@ -2386,10 +2376,10 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
||||
{
|
||||
"key": "agent",
|
||||
"description": (
|
||||
"Hermes peer ID in OpenViking, sent as the actor peer and "
|
||||
"used for peer-scoped memories"
|
||||
"Optional peer ID for separate assistant context. "
|
||||
"Uses user memory when no peer is configured."
|
||||
),
|
||||
"default": "hermes",
|
||||
"default": _DEFAULT_AGENT,
|
||||
"env_var": "OPENVIKING_AGENT",
|
||||
},
|
||||
{
|
||||
@@ -4881,20 +4871,21 @@ class OpenVikingMemoryProvider(MemoryProvider):
|
||||
)
|
||||
|
||||
def _build_memory_uri(self, subdir: str, *, client=None, timeout: Optional[float] = None) -> str:
|
||||
"""Build a viking:// memory URI under the configured peer namespace."""
|
||||
"""Build a user memory URI, optionally under the configured peer."""
|
||||
slug = uuid.uuid4().hex[:12]
|
||||
# Explicit-uid URIs are canonical under every auth mode; the uid-less
|
||||
# `viking://user/peers/...` shorthand was removed upstream (#4196) and
|
||||
# `viking://~/...` only expands for USER/ADMIN roles, not dev/ROOT.
|
||||
active_client = client if client is not None else getattr(self, "_client", None)
|
||||
# An empty peer on a captured client is intentional. Do not borrow a
|
||||
# later peer from the provider after a configuration reload.
|
||||
agent = str(
|
||||
getattr(active_client, "_agent", "")
|
||||
or getattr(self, "_agent", "")
|
||||
or _DEFAULT_AGENT
|
||||
getattr(active_client, "_agent", getattr(self, "_agent", "")) or ""
|
||||
).strip()
|
||||
peer_prefix = f"peers/{agent}/" if agent else ""
|
||||
return _user_scoped_uri(
|
||||
self._user_space(active_client, timeout=timeout),
|
||||
f"peers/{agent}/memories/{subdir}/mem_{slug}.md",
|
||||
f"{peer_prefix}memories/{subdir}/mem_{slug}.md",
|
||||
)
|
||||
|
||||
def on_memory_write(
|
||||
|
||||
@@ -8,6 +8,8 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from typing import Any, cast
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import pytest
|
||||
|
||||
import plugins.memory.openviking as openviking_plugin
|
||||
from hermes_cli import __version__ as _HERMES_VERSION
|
||||
from plugins.memory.openviking import OpenVikingMemoryProvider
|
||||
@@ -635,7 +637,8 @@ class TestOpenVikingRead:
|
||||
|
||||
|
||||
class TestOpenVikingAutoRecallPrefetch:
|
||||
def test_prefetch_e2e_sends_limit_and_reads_l2_content(self, monkeypatch):
|
||||
@pytest.mark.parametrize("peer", ["", "hermes"])
|
||||
def test_prefetch_e2e_sends_limit_and_reads_l2_content(self, monkeypatch, peer):
|
||||
records = {"searches": [], "reads": [], "listings": [], "headers": []}
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
@@ -655,6 +658,7 @@ class TestOpenVikingAutoRecallPrefetch:
|
||||
if parsed.path == "/health":
|
||||
self._send_json({"status": "ok", "healthy": True, "version": "test"})
|
||||
return
|
||||
records["headers"].append(dict(self.headers))
|
||||
if parsed.path == "/api/v1/system/status":
|
||||
self._send_json({"status": "ok", "result": {"user": "user"}})
|
||||
return
|
||||
@@ -709,7 +713,7 @@ class TestOpenVikingAutoRecallPrefetch:
|
||||
"result": {
|
||||
"memories": [
|
||||
{
|
||||
"uri": "viking://user/peers/hermes/memories/e2e-full.md",
|
||||
"uri": "viking://user/user/peers/hermes/memories/e2e-full.md",
|
||||
"score": 0.9,
|
||||
"level": 2,
|
||||
"category": "events",
|
||||
@@ -742,7 +746,7 @@ class TestOpenVikingAutoRecallPrefetch:
|
||||
monkeypatch.setenv("OPENVIKING_ENDPOINT", endpoint)
|
||||
monkeypatch.setenv("OPENVIKING_ACCOUNT", "acct")
|
||||
monkeypatch.setenv("OPENVIKING_USER", "user")
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", "hermes")
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", peer)
|
||||
|
||||
provider = OpenVikingMemoryProvider()
|
||||
try:
|
||||
@@ -760,7 +764,7 @@ class TestOpenVikingAutoRecallPrefetch:
|
||||
assert "people/ada.md — Ada is the project owner." in block
|
||||
assert "E2E full L2 memory content." in block
|
||||
assert "E2E abstract should not be injected." not in block
|
||||
assert records["reads"] == ["viking://user/peers/hermes/memories/e2e-full.md"]
|
||||
assert records["reads"] == ["viking://user/user/peers/hermes/memories/e2e-full.md"]
|
||||
assert [listing["uri"] for listing in records["listings"]] == [
|
||||
"viking://user/user/memories/preferences",
|
||||
"viking://user/user/memories/entities",
|
||||
@@ -781,7 +785,7 @@ class TestOpenVikingAutoRecallPrefetch:
|
||||
{key.lower(): value for key, value in headers.items()}
|
||||
for headers in records["headers"]
|
||||
]
|
||||
assert all(headers.get("x-openviking-actor-peer") == "hermes" for headers in normalized_headers)
|
||||
assert all(headers.get("x-openviking-actor-peer", "") == peer for headers in normalized_headers)
|
||||
assert all(
|
||||
headers.get("user-agent") == f"openviking-memory-hermes/{_HERMES_VERSION}"
|
||||
for headers in normalized_headers
|
||||
@@ -907,6 +911,7 @@ class TestEnsureClientReloadsEnv:
|
||||
monkeypatch.setenv("OPENVIKING_ENDPOINT", "http://srv:31933")
|
||||
monkeypatch.setenv("OPENVIKING_API_KEY", "")
|
||||
monkeypatch.setenv("OPENVIKING_USER", "alice")
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", "hermes")
|
||||
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider._env_refresh_enabled = True
|
||||
@@ -946,6 +951,7 @@ class TestEnsureClientReloadsEnv:
|
||||
monkeypatch.setattr("plugins.memory.openviking._VikingClient", _StubClient)
|
||||
monkeypatch.setenv("OPENVIKING_ENDPOINT", "https://openviking.example")
|
||||
monkeypatch.setenv("OPENVIKING_API_KEY", "sk-test")
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", "hermes")
|
||||
|
||||
provider = OpenVikingMemoryProvider()
|
||||
provider.initialize("session-1")
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
"""Optional peer identity must agree across setup, requests and memory writes."""
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
import plugins.memory.openviking as ov
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_config(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes"))
|
||||
for key in (*ov._OPENVIKING_ENV_KEYS, "OPENVIKING_CLI_CONFIG_FILE"):
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", ["env", "yaml", "actor_peer_id", "agent_id"])
|
||||
@pytest.mark.parametrize("peer", ["", "hermes", "work-assistant"])
|
||||
def test_configured_peer_routing_is_preserved(tmp_path, monkeypatch, source, peer):
|
||||
config = {}
|
||||
if source == "env":
|
||||
monkeypatch.setenv("OPENVIKING_AGENT", peer)
|
||||
elif source == "yaml":
|
||||
config["agent"] = peer
|
||||
else:
|
||||
path = tmp_path / "ovcli.conf"
|
||||
path.write_text(
|
||||
json.dumps({"url": "http://localhost:1933", source: peer}), encoding="utf-8"
|
||||
)
|
||||
config = {"use_ovcli_config": True, "ovcli_config_path": str(path)}
|
||||
|
||||
settings = ov._resolve_connection_settings(config)
|
||||
client = ov._VikingClient("http://localhost:1933", agent=settings["agent"])
|
||||
monkeypatch.setattr(client, "get", lambda *a, **kw: {"result": {"user": "alice"}})
|
||||
provider = ov.OpenVikingMemoryProvider()
|
||||
uri = provider._build_memory_uri("preferences", client=client)
|
||||
|
||||
assert settings["agent"] == peer
|
||||
assert client._headers().get("X-OpenViking-Actor-Peer", "") == peer
|
||||
prefix = f"peers/{peer}/" if peer else ""
|
||||
assert uri.startswith(f"viking://user/alice/{prefix}memories/preferences/mem_")
|
||||
|
||||
|
||||
def test_unconfigured_client_and_schema_do_not_supply_a_peer():
|
||||
settings = ov._resolve_connection_settings({})
|
||||
client = ov._VikingClient("http://localhost:1933")
|
||||
schema = {
|
||||
field["key"]: field
|
||||
for field in ov.OpenVikingMemoryProvider().get_config_schema()
|
||||
}
|
||||
|
||||
assert settings["agent"] == ""
|
||||
assert schema["agent"]["default"] == ""
|
||||
assert "X-OpenViking-Actor-Peer" not in client._headers()
|
||||
assert "X-OpenViking-Actor-Peer" not in client._multipart_headers()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("peer", ["", "hermes"])
|
||||
def test_memory_uri_uses_captured_peer_even_when_empty(monkeypatch, peer):
|
||||
client = ov._VikingClient("http://localhost:1933", agent=peer)
|
||||
monkeypatch.setattr(client, "get", lambda *a, **kw: {"result": {"user": "alice"}})
|
||||
provider = ov.OpenVikingMemoryProvider()
|
||||
provider._agent = "later-peer"
|
||||
|
||||
uri = provider._build_memory_uri("preferences", client=client)
|
||||
|
||||
prefix = f"peers/{peer}/" if peer else ""
|
||||
assert uri.startswith(f"viking://user/alice/{prefix}memories/preferences/mem_")
|
||||
assert "later-peer" not in uri
|
||||
|
||||
|
||||
@pytest.mark.parametrize("save_to_store", [False, True])
|
||||
@pytest.mark.parametrize("credential", ["dev", "user", "root", "service"])
|
||||
def test_new_setup_does_not_ask_for_or_save_peer(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
save_to_store,
|
||||
credential,
|
||||
):
|
||||
from hermes_cli import memory_setup
|
||||
|
||||
home = tmp_path / "hermes"
|
||||
home.mkdir()
|
||||
(home / ".env").write_text(
|
||||
"OPENVIKING_AGENT=old-peer\nOTHER_KEY=keep\n", encoding="utf-8"
|
||||
)
|
||||
config = {"memory": {"openviking": {"agent": "old-peer", "recall_limit": 9}}}
|
||||
validations = []
|
||||
|
||||
def validate(values, **kwargs):
|
||||
validations.append(dict(values))
|
||||
role = (
|
||||
"root"
|
||||
if credential == "root"
|
||||
else "user"
|
||||
if values.get("api_key")
|
||||
else None
|
||||
)
|
||||
return True, "", role
|
||||
|
||||
def prompt(label, default=None, secret=False):
|
||||
values = {
|
||||
"OpenViking server URL": "http://localhost:1933",
|
||||
"OpenViking user API key": "test-user-key",
|
||||
"OpenViking root API key": "test-root-key",
|
||||
"OpenViking API key": "test-service-key",
|
||||
"OpenViking account": "account",
|
||||
"OpenViking user": "alice",
|
||||
"OpenViking profile name": "personal",
|
||||
}
|
||||
assert label in values, f"Unexpected setup question: {label}"
|
||||
return values[label]
|
||||
|
||||
def select(title, options, **kwargs):
|
||||
choices = {
|
||||
" OpenViking connection": 0 if credential == "service" else 1,
|
||||
" OpenViking credential": {"dev": 2, "user": 0, "root": 1}.get(
|
||||
credential, 0
|
||||
),
|
||||
" Save OpenViking config": int(save_to_store),
|
||||
}
|
||||
assert title in choices, f"Unexpected setup menu: {title}"
|
||||
return choices[title]
|
||||
|
||||
monkeypatch.setattr(memory_setup, "_prompt", prompt)
|
||||
monkeypatch.setattr(memory_setup, "_curses_select", select)
|
||||
monkeypatch.setattr(ov, "_validate_openviking_reachability", lambda *a: (True, ""))
|
||||
monkeypatch.setattr(ov, "_validate_openviking_setup_values", validate)
|
||||
|
||||
ov.OpenVikingMemoryProvider().post_setup(str(home), config)
|
||||
|
||||
assert validations
|
||||
assert all(values["agent"] == "" for values in validations)
|
||||
assert "OPENVIKING_AGENT" not in (home / ".env").read_text(encoding="utf-8")
|
||||
assert "OTHER_KEY=keep" in (home / ".env").read_text(encoding="utf-8")
|
||||
saved_config = ov._load_hermes_openviking_config()
|
||||
assert saved_config["recall_limit"] == 9
|
||||
assert ov._resolve_connection_settings(saved_config)["agent"] == ""
|
||||
if save_to_store:
|
||||
saved = json.loads(
|
||||
Path(saved_config["ovcli_config_path"]).read_text(encoding="utf-8")
|
||||
)
|
||||
assert "actor_peer_id" not in saved
|
||||
assert "agent_id" not in saved
|
||||
|
||||
|
||||
@pytest.mark.parametrize("peer", ["", "hermes"])
|
||||
def test_wire_requests_keep_writes_and_session_messages_in_the_selected_scope(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
peer,
|
||||
):
|
||||
records = []
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
def respond(self, payload):
|
||||
body = json.dumps(payload).encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self.respond({"status": "ok", "healthy": True, "version": "test"})
|
||||
elif self.path == "/api/v1/system/status":
|
||||
self.respond({"result": {"user": "alice"}})
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def do_POST(self):
|
||||
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
|
||||
records.append((self.path, dict(self.headers), payload))
|
||||
self.respond({"status": "ok", "result": {"written_bytes": 10}})
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
home = tmp_path / "hermes"
|
||||
home.mkdir()
|
||||
provider_config = {"endpoint": f"http://127.0.0.1:{server.server_port}"}
|
||||
if peer:
|
||||
provider_config["agent"] = peer
|
||||
(home / "config.yaml").write_text(
|
||||
yaml.safe_dump({
|
||||
"memory": {"provider": "openviking", "openviking": provider_config}
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
provider = ov.OpenVikingMemoryProvider()
|
||||
try:
|
||||
provider.initialize("peer-test", hermes_home=str(home))
|
||||
assert provider._client is not None
|
||||
result = json.loads(
|
||||
provider.handle_tool_call("viking_remember", {"content": "I like tea"})
|
||||
)
|
||||
assert result["status"] == "stored"
|
||||
provider.on_memory_write("add", "user", "I like coffee")
|
||||
provider.sync_turn("hello", "hi", session_id="peer-test")
|
||||
assert provider._drain_writers("peer-test", timeout=5.0)
|
||||
provider.sync_turn(
|
||||
"next",
|
||||
"reply",
|
||||
session_id="peer-test",
|
||||
messages=[
|
||||
{"role": "user", "content": "next"},
|
||||
{"role": "assistant", "content": "reply"},
|
||||
],
|
||||
)
|
||||
assert provider._drain_writers("peer-test", timeout=5.0)
|
||||
provider.on_session_end([])
|
||||
finally:
|
||||
provider.shutdown()
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=3.0)
|
||||
|
||||
assert records
|
||||
for _path, headers, _payload in records:
|
||||
if peer:
|
||||
assert headers["X-OpenViking-Actor-Peer"] == peer
|
||||
else:
|
||||
assert "X-OpenViking-Actor-Peer" not in headers
|
||||
writes = [
|
||||
payload for path, _, payload in records if path == "/api/v1/content/write"
|
||||
]
|
||||
prefix = f"peers/{peer}/" if peer else ""
|
||||
assert {write["content"] for write in writes} == {"I like tea", "I like coffee"}
|
||||
assert all(
|
||||
write["uri"].startswith(f"viking://user/alice/{prefix}memories/")
|
||||
for write in writes
|
||||
)
|
||||
batches = [
|
||||
payload["messages"]
|
||||
for path, _, payload in records
|
||||
if path.endswith("/messages/batch")
|
||||
]
|
||||
assert len(batches) == 2
|
||||
for batch in batches:
|
||||
assert "peer_id" not in batch[0]
|
||||
if peer:
|
||||
assert batch[1]["peer_id"] == peer
|
||||
else:
|
||||
assert "peer_id" not in batch[1]
|
||||
assert any(path.endswith("/commit") for path, _, _ in records)
|
||||
@@ -274,7 +274,10 @@ def test_link_ovcli_profile_removes_stale_inline_config(tmp_path):
|
||||
assert "OTHER_KEY=keep" in env_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_post_setup_existing_profile_picker_validates_and_links_saved_profile(tmp_path, monkeypatch):
|
||||
@pytest.mark.parametrize("peer_key", [None, "actor_peer_id", "agent_id"])
|
||||
def test_post_setup_existing_profile_picker_validates_and_links_saved_profile(
|
||||
tmp_path, monkeypatch, peer_key,
|
||||
):
|
||||
_clear_openviking_env(monkeypatch)
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
@@ -285,10 +288,10 @@ def test_post_setup_existing_profile_picker_validates_and_links_saved_profile(tm
|
||||
active_path = openviking_home / "ovcli.conf"
|
||||
saved_path = openviking_home / "ovcli.conf.VPS"
|
||||
active_path.write_text(json.dumps({"url": "http://active.test"}), encoding="utf-8")
|
||||
saved_path.write_text(
|
||||
json.dumps({"url": "https://vps.example", "api_key": "user-key"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
saved_values = {"url": "https://vps.example", "api_key": "user-key"}
|
||||
if peer_key:
|
||||
saved_values[peer_key] = "existing-peer"
|
||||
saved_path.write_text(json.dumps(saved_values), encoding="utf-8")
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
monkeypatch.setattr(openviking_module.Path, "home", staticmethod(lambda: tmp_path))
|
||||
|
||||
@@ -318,7 +321,7 @@ def test_post_setup_existing_profile_picker_validates_and_links_saved_profile(tm
|
||||
"root_api_key": "",
|
||||
"account": "",
|
||||
"user": "",
|
||||
"agent": "",
|
||||
"agent": "existing-peer" if peer_key else "",
|
||||
}]
|
||||
assert config["memory"]["provider"] == "openviking"
|
||||
assert config["memory"]["openviking"] == {
|
||||
@@ -328,6 +331,9 @@ def test_post_setup_existing_profile_picker_validates_and_links_saved_profile(tm
|
||||
env_text = env_path.read_text(encoding="utf-8")
|
||||
assert "OPENVIKING_" not in env_text
|
||||
assert "OTHER_KEY=keep" in env_text
|
||||
settings = openviking_module._resolve_connection_settings(config["memory"]["openviking"])
|
||||
assert settings["agent"] == ("existing-peer" if peer_key else "")
|
||||
assert json.loads(saved_path.read_text(encoding="utf-8")) == saved_values
|
||||
|
||||
|
||||
def test_local_setup_recommends_user_api_key_before_unauthenticated_mode(monkeypatch):
|
||||
@@ -355,8 +361,6 @@ def test_local_setup_recommends_user_api_key_before_unauthenticated_mode(monkeyp
|
||||
if label == "OpenViking user API key":
|
||||
assert secret is True
|
||||
return "user-key"
|
||||
if label == openviking_module._AGENT_PROMPT_LABEL:
|
||||
return default
|
||||
raise AssertionError(f"Unexpected prompt: {label}")
|
||||
|
||||
values = openviking_module._prompt_manual_connection_values(
|
||||
|
||||
@@ -315,7 +315,6 @@ OPENVIKING_ENDPOINT=http://127.0.0.1:1933
|
||||
# OPENVIKING_API_KEY=...
|
||||
# OPENVIKING_ACCOUNT=default
|
||||
# OPENVIKING_USER=default
|
||||
# OPENVIKING_AGENT=hermes
|
||||
```
|
||||
|
||||
OpenViking server settings live in `ov.conf` (`--config`,
|
||||
@@ -329,7 +328,18 @@ live in `ovcli.conf` (`OPENVIKING_CLI_CONFIG_FILE` or
|
||||
- `viking://` URI scheme for hierarchical knowledge browsing
|
||||
|
||||
`OPENVIKING_ACCOUNT` and `OPENVIKING_USER` are used for local/trusted mode.
|
||||
`OPENVIKING_AGENT` is Hermes' peer ID in OpenViking for peer-scoped memories.
|
||||
Peer identity is optional. By default, Hermes sends no peer ID and writes
|
||||
explicit memories to `viking://user/<user>/memories/...`. Setup does not ask
|
||||
for a peer ID. For separate assistant context, set
|
||||
`memory.openviking.agent: work-assistant` in `config.yaml`.
|
||||
|
||||
Existing non-empty peer settings keep their peer-scoped writes and recall.
|
||||
This includes `OPENVIKING_AGENT` and `actor_peer_id` or legacy `agent_id` in a
|
||||
linked OpenViking config. Existing memories are not moved or deleted.
|
||||
Installations that relied on the old implicit `hermes` peer can retain it by
|
||||
setting `memory.openviking.agent: hermes`. With no peer ID, default search can
|
||||
include all existing peer memories under the same OpenViking user.
|
||||
|
||||
Hermes sends `User-Agent: openviking-memory-hermes/<version>` on OpenViking
|
||||
requests. This standard harness identifier contains no per-user identifier and
|
||||
does not add a separate request.
|
||||
|
||||
Reference in New Issue
Block a user