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:
ehz0ah
2026-08-28 01:59:44 +08:00
committed by kshitij
parent 8fd144c502
commit 823bcc887a
6 changed files with 335 additions and 43 deletions
+32 -5
View File
@@ -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
+14 -23
View File
@@ -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(
+11 -5
View File
@@ -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.