# NousResearch/hermes-agent · OpenViking integration updates and end-user isolation

## Open now

- Nothing open on GitHub.

These members cover adapting the memory provider to the current OpenViking API and adding configurable end-user isolation using the peer_id feature introduced in OpenViking v0.4.1.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #48206. Verify it addresses your case.
- **Recommended:** read PR #48206 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #48464: feat(openviking): add configurable end-user isolation via OpenViking v0.4.1 peer_id

## Prior & competing PRs

- **#48206: fix(openviking): adapt memory provider for current api**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `plugins/memory/openviking/README.md`, `plugins/memory/openviking/__init__.py`, `scripts/release.py`, `tests/openviking_plugin/test_openviking.py`, `tests/plugins/memory/test_openviking_provider.py`, `website/docs/user-guide/features/memory-providers.md`
  - fix hunks:
  - `plugins/memory/openviking/README.md`

    ```diff
    @@ -31,10 +31,14 @@ All config via environment variables in `.env`:
     | Env Var | Default | Description |
     |---------|---------|-------------|
     | `OPENVIKING_ENDPOINT` | `http://127.0.0.1:1933` | Server URL |
    -| `OPENVIKING_API_KEY` | (none) | API key (optional) |
    -| `OPENVIKING_ACCOUNT` | (none) | Tenant account override |
    -| `OPENVIKING_USER` | (none) | Tenant user override |
    -| `OPENVIKING_AGENT` | `hermes` | Tenant agent namespace |
    +| `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 |
    +
    +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,
    +Hermes sends `OPENVIKING_ACCOUNT` and `OPENVIKING_USER` as identity headers.
     
     ## Tools
     
    ```
  - `plugins/memory/openviking/__init__.py`

    ```diff
    @@ -11,9 +11,9 @@
     or a linked OpenViking CLI config:
       OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)
       OPENVIKING_API_KEY   — API key (required for authenticated servers)
    -  OPENVIKING_ACCOUNT   — Optional tenant account override
    -  OPENVIKING_USER      — Optional tenant user override
    -  OPENVIKING_AGENT     — Tenant agent (default: hermes)
    +  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)
     
     Capabilities:
       - Automatic memory extraction on session commit (6 categories)
    ```

    ```diff
    @@ -55,6 +55,7 @@
     _DEFAULT_ENDPOINT = "http://127.0.0.1:1933"
     _OPENVIKING_SERVICE_ENDPOINT = "https://api.vikingdb.cn-beijing.volces.com/openviking"
     _DEFAULT_AGENT = "hermes"
    +_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."
    ```

    ```diff
    @@ -200,10 +201,9 @@ def __init__(self, endpoint: str, api_key: str = "",
                      agent: Optional[str] = None):
             self._endpoint = endpoint.rstrip("/")
             self._api_key = [redacted:secret-assignment]
    -        # Empty account/user fall back to "default" and the tenant headers are
    -        # always sent — ROOT API keys require them (preserves the merged
    -        # contract from #22414/#21232; an empty string must NOT omit the
    -        # header). Use `or` (not `is not None`) so "" also falls back.
    +        # Account/user are local/trusted-mode tenant identity. API-key requests
    +        # omit these headers by default; trusted-mode retry may send them only
    +        # after OpenViking explicitly asks for asserted tenant identity.
             self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
             self._user = user or os.environ.get("OPENVIKING_USER", "default")
             self._agent = agent if agent is not None else os.environ.get("OPENVIKING_AGENT", _DEFAULT_AGENT)
    ```

    ```diff
    @@ -211,15 +211,18 @@ def __init__(self, endpoint: str, api_key: str = "",
             if self._httpx is None:
                 raise ImportError("httpx is required for OpenViking: pip install httpx")
     
    -    def _headers(self) -> dict:
    +    def _headers(self, *, include_tenant: bool | None = None) -> dict:
    +        if include_tenant is None:
    +            include_tenant = not bool(self._api_key)
    +
             h = {"Content-Type": "application/json"}
             if self._agent:
                 h["X-OpenViking-Actor-Peer"] = self._agent
    -            h["X-OpenViking-Agent"] = self._agent
    -        if self._account:
    -            h["X-OpenViking-Account"] = self._account
    -        if self._user:
    -            h["X-OpenViking-User"] = self._user
    +        if include_tenant:
    +            if self._account:
    +                h["X-OpenViking-Account"] = self._account
    +            if self._user:
    +                h["X-OpenViking-User"] = self._user
             if self._api_key:
                 h["X-API-Key"] = self._api_key
                 h["Authorization"] = "Bearer " + self._api_key
    ```

    ```diff
    @@ -228,11 +231,33 @@ def _headers(self) -> dict:
         def _url(self, path: str) -> str:
             return f"{self._endpoint}{path}"
     
    -    def _multipart_headers(self) -> dict:
    -        headers = self._headers()
    +    def _multipart_headers(self, *, include_tenant: bool | None = None) -> dict:
    +        headers = self._headers(include_tenant=include_tenant)
             headers.pop("Content-Type", None)
             return headers
     
    +    @staticmethod
    +    def _needs_trusted_identity_retry(exc: Exception) -> bool:
    +        message = str(exc)
    +        return (
    +            "Trusted mode requests must include X-OpenViking-Account" in message
    +            or "Trusted mode requests must include X-OpenViking-User" in message
    +            or "Trusted mode requests must include X-OpenViking-Account or explicit account_id" in message
    +        )
    +
    +    def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:
    +        try:
    +            headers = self._multipart_headers() if multipart else self._headers()
    +            return self._parse_response(send(headers))
    +        except Exception as exc:
    +            if not self._api_key or not self._needs_trusted_identity_retry(exc):
    +                raise
    +            headers = (
    +                self._multipart_headers(include_tenant=True)
    +                if multipart else self._headers(include_tenant=True)
    +            )
    +            return self._parse_response(send(headers))
    +
         def _parse_response(self, resp) -> dict:
             try:
                 data = resp.json()
    ```

    ```diff
    @@ -267,28 +292,33 @@ def _parse_response(self, resp) -> dict:
             return data
     
         def get(self, path: str, **kwargs) -> dict:
    -        resp = self._httpx.get(
    -            self._url(path), headers=self._headers(), timeout=_TIMEOUT, **kwargs
    +        return self._send_with_trusted_identity_retry(
    +            lambda headers: self._httpx.get(
    +                self._url(path), headers=headers, timeout=_TIMEOUT, **kwargs
    +            )
             )
    -        return self._parse_response(resp)
     
         def post(self, path: str, payload: dict = None, **kwargs) -> dict:
    -        resp = self._httpx.post(
    -            self._url(path), json=payload or {}, headers=self._headers(),
    -            timeout=_TIMEOUT, **kwargs
    +        return self._send_with_trusted_identity_retry(
    +            lambda headers: self._httpx.post(
    +                self._url(path), json=payload or {}, headers=headers,
    +                timeout=_TIMEOUT, **kwargs
    +            )
             )
    -        return self._parse_response(resp)
     
         def upload_temp_file(self, file_path: Path) -> str:
             mime_type = mimetypes.guess_type(file_path.name)[0] or "application/octet-stream"
    -        with file_path.open("rb") as f:
    -            resp = self._httpx.post(
    -                self._url("/api/v1/resources/temp_upload"),
    -                files={"file": (file_path.name, f, mime_type)},
    -                headers=self._multipart_headers(),
    -                timeout=_TIMEOUT,
    -            )
    -        data = self._parse_response(resp)
    +
    +        def _send(headers):
    +            with file_path.open("rb") as f:
    +                return self._httpx.post(
    +                    self._url("/api/v1/resources/temp_upload"),
    +                    files={"file": (file_path.name, f, mime_type)},
    +                    headers=headers,
    +                    timeout=_TIMEOUT,
    +                )
    +
    +        data = self._send_with_trusted_identity_retry(_send, multipart=True)
             result = data.get("result", {})
             temp_file_id = result.get("temp_file_id", "")
             if not temp_file_id:
    ```

    ```diff
    @@ -1219,7 +1249,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
                     return _SETUP_CANCELLED
                 if credential_choice == 0:
                     values["agent"] = _clean_config_value(
    -                    prompt("OpenViking agent", default=_DEFAULT_AGENT)
    +                    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)
    ```

    ```diff
    @@ -1339,7 +1369,7 @@ def _prompt_manual_connection_values(prompt, select, cancelled, *, service: bool
                 prefilled_agent = ""
             else:
                 values["agent"] = _clean_config_value(
    -                prompt("OpenViking agent", default=_DEFAULT_AGENT)
    +                prompt(_AGENT_PROMPT_LABEL, default=_DEFAULT_AGENT)
                 ) or _DEFAULT_AGENT
             _print_validation_progress("Validating OpenViking API access...")
             valid, message, role = _validate_openviking_setup_values(
    ```

    ```diff
    @@ -1697,7 +1727,10 @@ def get_config_schema(self):
                 },
                 {
                     "key": "agent",
    -                "description": "OpenViking agent ID within the account ([hermes], useful in multi-agent mode)",
    +                "description": (
    +                    "Hermes peer ID in OpenViking, sent as the actor peer and "
    +                    "used for peer-scoped memories"
    +                ),
                     "default": "hermes",
                     "env_var": "OPENVIKING_AGENT",
                 },
    ```

    ```diff
    @@ -2129,18 +2162,22 @@ def _new_client(self) -> _VikingClient:
         def _text_part(content: str) -> Dict[str, str]:
             return {"type": "text", "text": content}
     
    -    @classmethod
    -    def _turn_batch_payload(cls, user_content: str, assistant_content: str) -> Dict[str, Any]:
    +    def _turn_batch_payload(self, user_content: str, assistant_content: str) -> Dict[str, Any]:
    +        assistant_message: Dict[str, Any] = {
    +            "role": "assistant",
    +            "parts": [self._text_part(assistant_content)],
    +        }
    +        if self._agent:
    +            assistant_message["peer_id"] = self._agent
             return {
                 "messages": [
    -                {"role": "user", "parts": [cls._text_part(user_content)]},
    -                {"role": "assistant", "parts": [cls._text_part(assistant_content)]},
    +                {"role": "user", "parts": [self._text_part(user_content)]},
    +                assistant_message,
                 ]
             }
     
    -    @classmethod
         def _post_session_turn(
    -        cls,
    +        self,
             client: _VikingClient,
             sid: str,
             user_content: str,
    ```

    ```diff
    @@ -2148,7 +2185,7 @@ def _post_session_turn(
         ) -> None:
             client.post(
                 f"/api/v1/sessions/{sid}/messages/batch",
    -            cls._turn_batch_payload(user_content, assistant_content),
    +            self._turn_batch_payload(user_content, assistant_content),
             )
     
         def _session_has_pending_tokens(self, sid: str) -> bool:
    ```

    ```diff
    @@ -2402,9 +2439,9 @@ def on_session_switch(
             )
     
         def _build_memory_uri(self, subdir: str) -> str:
    -        """Build a viking:// memory URI under the configured user/agent/subdir."""
    +        """Build a viking:// memory URI under the configured peer namespace."""
             slug = uuid.uuid4().hex[:12]
    -        return f"viking://user/{self._user}/agent/{self._agent}/memories/{subdir}/mem_{slug}.md"
    +        return f"viking://user/peers/{self._agent}/memories/{subdir}/mem_{slug}.md"
     
         def on_memory_write(
             self,
    ```

    ```diff
    @@ -2535,14 +2572,16 @@ def _tool_search(self, args: dict) -> str:
     
             payload: Dict[str, Any] = {"query": query}
             mode = args.get("mode", "auto")
    -        if mode != "auto":
    -            payload["mode"] = mode
             if args.get("scope"):
                 payload["target_uri"] = args["scope"]
             if args.get("limit"):
                 payload["limit"] = args["limit"]
     
    -        resp = self._client.post("/api/v1/search/find", payload)
    +        endpoint = "/api/v1/search/search" if mode == "deep" else "/api/v1/search/find"
    +        if endpoint == "/api/v1/search/search" and self._session_id:
    +            payload["session_id"] = self._session_id
    +
    +        resp = self._client.post(endpoint, payload)
             result = resp.get("result", {})
     
             # Format results for the model — keep it concise
    ```
  - `scripts/release.py`

    ```diff
    @@ -91,6 +91,7 @@
         "[redacted:email]": "randomsnowflake",
         "[redacted:email]": "zakame",
         "152110621+jiangkoumo@users.noreply.github.com": "jiangkoumo",
    +    "[redacted:email]": "qin-ctx",
         "[redacted:email]": "ViewWay",
         "[redacted:email]": "m4dni5",
         "[redacted:email]": "harjothkhara",
    ```
  - `tests/openviking_plugin/test_openviking.py`

    ```diff
    @@ -239,6 +239,7 @@ def test_sync_turn_stores_only_slash_skill_user_instruction(self, monkeypatch):
                             {
                                 "role": "assistant",
                                 "parts": [{"type": "text", "text": "Done."}],
    +                            "peer_id": "hermes",
                             },
                         ]
                     },
    ```

    ```diff
    @@ -474,8 +475,8 @@ def test_list_browse_unwraps_and_normalizes_entry_shapes(self):
     class TestOpenVikingMemoryUriBuilder:
         """Regression tests for _build_memory_uri — fixes #36969.
     
    -    Before the fix the URI omitted /agent/{agent}/, causing all agents
    -    under the same user to share the same memory namespace.
    +    OpenViking's current memory layout stores peer-scoped memories under
    +    viking://user/peers/{peer_id}/...
         """
     
         def _make_provider(self, user="alice", agent="coder"):
    ```

    ```diff
    @@ -484,19 +485,19 @@ def _make_provider(self, user="alice", agent="coder"):
             p._agent = agent
             return p
     
    -    def test_uri_layout_includes_agent_segment(self):
    -        """URI must contain /agent/{agent}/ between user and memories."""
    +    def test_uri_layout_includes_peer_segment(self):
    +        """URI must contain /peers/{peer_id}/ between user and memories."""
             p = self._make_provider(user="alice", agent="coder")
             uri = p._build_memory_uri("preferences")
    -        assert uri.startswith("viking://user/alice/agent/coder/memories/preferences/mem_")
    +        assert uri.startswith("viking://user/peers/coder/memories/preferences/mem_")
             assert uri.endswith(".md")
     
    -    def test_uri_uses_configured_agent_not_default(self):
    -        """_agent value must be interpolated — not hardcoded to 'hermes'."""
    +    def test_uri_uses_configured_peer_not_default(self):
    +        """_agent value is the OpenViking actor peer ID, not hardcoded to 'hermes'."""
             p = self._make_provider(user="alice", agent="research-bot")
             uri = p._build_memory_uri("entities")
    -        assert "/agent/research-bot/" in uri
    -        assert "/agent/hermes/" not in uri
    +        assert "/peers/research-bot/" in uri
    +        assert "/peers/hermes/" not in uri
     
         def test_uri_slug_is_twelve_hex_chars_and_unique(self):
             """Slug must be 12 hex chars and differ between calls."""
    ```
  - `tests/plugins/memory/test_openviking_provider.py`

    ```diff
    @@ -369,7 +369,7 @@ def test_post_setup_create_remote_user_profile_can_mirror_to_openviking_store(tm
             _prompt_from_values({
                 "OpenViking server URL": "https://openviking.example",
                 "OpenViking user API key": "[redacted:secret-assignment]",
    -            "OpenViking agent": "hermes",
    +            "Hermes peer ID in OpenViking": "hermes",
                 "OpenViking profile name": "VPS",
             }),
         )
    ```

    ```diff
    @@ -411,7 +411,7 @@ def test_post_setup_create_remote_user_can_keep_hermes_only(tmp_path, monkeypatc
             _prompt_from_values({
                 "OpenViking server URL": "https://openviking.example",
                 "OpenViking user API key": "[redacted:secret-assignment]",
    -            "OpenViking agent": "agent",
    +            "Hermes peer ID in OpenViking": "agent",
             }),
         )
         config = {"memory": {}}
    ```

    ```diff
    @@ -455,7 +455,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]
             _prompt_from_values(
                 {
                     "OpenViking API key": "[redacted:secret-assignment]",
    -                "OpenViking agent": "agent",
    +                "Hermes peer ID in OpenViking": "agent",
                 },
                 forbidden={"OpenViking server URL", "OpenViking user API key", "OpenViking root API key"},
             ),
    ```

    ```diff
    @@ -540,7 +540,7 @@ def fake_prompt(label, default=None, secret=[redacted:secret-assignment]
                 "OpenViking user API key": "[redacted:secret-assignment]",
                 "OpenViking account": "acct",
                 "OpenViking user": "alice",
    -            "OpenViking agent": "agent",
    +            "Hermes peer ID in OpenViking": "agent",
             }
             return values.get(label, default or "")
     
    ```

    ```diff
    @@ -549,7 +549,7 @@ def fake_prompt(label, default=None, secret=[redacted:secret-assignment]
     
         OpenVikingMemoryProvider().post_setup(str(hermes_home), config)
     
    -    assert prompt_events.count("OpenViking agent") == 1
    +    assert prompt_events.count("Hermes peer ID in OpenViking") == 1
         env_text = (hermes_home / ".env").read_text(encoding="utf-8")
         assert "OPENVIKING_API_KEY=[redacted:secret-assignment]" in env_text
         assert "OPENVIKING_ACCOUNT=acct" in env_text
    ```

    ```diff
    @@ -580,7 +580,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]
                 {
                     "OpenViking server URL": "https://openviking.example",
                     "OpenViking root API key": "[redacted:secret-assignment]",
    -                "OpenViking agent": "agent",
    +                "Hermes peer ID in OpenViking": "agent",
                 },
                 forbidden={"OpenViking user API key", "OpenViking account", "OpenViking user"},
             ),
    ```

    ```diff
    @@ -616,7 +616,7 @@ def validate_values(values, *, require_api_key=[redacted:secret-assignment]
                 "OpenViking root API key": "[redacted:secret-assignment]",
                 "OpenViking account": "acct",
                 "OpenViking user": "alice",
    -            "OpenViking agent": "agent",
    +            "Hermes peer ID in OpenViking": "agent",
             }),
             lambda *args, **kwargs: next(choices),
             -1,
    ```

    ```diff
    @@ -1091,7 +1091,7 @@ def validate_reachability(endpoint):
             "_prompt",
             _prompt_from_values({
                 "OpenViking server URL": "localhost",
    -            "OpenViking agent": "agent",
    +            "Hermes peer ID in OpenViking": "agent",
             }),
         )
         config = {"memory": {}}
    ```

    ```diff
    @@ -1126,7 +1126,7 @@ def test_post_setup_invalid_env_profile_can_create_new_config(tmp_path, monkeypa
             _prompt_from_values({
                 "OpenViking server URL": "https://openviking.example",
                 "OpenViking user API key": "[redacted:secret-assignment]",
    -            "OpenViking agent": "agent",
    +            "Hermes peer ID in OpenViking": "agent",
             }),
         )
         config = {"memory": {}}
    ```

    ```diff
    @@ -1210,6 +1210,36 @@ def test_tool_search_sends_limit_not_legacy_top_k():
         assert "top_k" not in payload
     
     
    +def test_tool_search_uses_find_for_normal_search():
    +    provider = OpenVikingMemoryProvider()
    +    provider._client = MagicMock()
    +    provider._client.post.return_value = {
    +        "result": {"memories": [], "resources": [], "skills": [], "total": 0}
    +    }
    +
    +    provider._tool_search({"query": "simple lookup", "mode": "fast"})
    +
    +    provider._client.post.assert_called_once_with("/api/v1/search/find", {
    +        "query": "simple lookup",
    +    })
    +
    +
    +def test_tool_search_uses_session_search_for_deep_search():
    +    provider = OpenVikingMemoryProvider()
    +    provider._client = MagicMock()
    +    provider._session_id = "session-123"
    +    provider._client.post.return_value = {
    +        "result": {"memories": [], "resources": [], "skills": [], "total": 0}
    +    }
    +
    +    provider._tool_search({"query": "connect facts", "mode": "deep"})
    +
    +    provider._client.post.assert_called_once_with("/api/v1/search/search", {
    +        "query": "connect facts",
    +        "session_id": "session-123",
    +    })
    +
    +
     def test_tool_add_resource_uploads_existing_local_file(tmp_path):
         sample = tmp_path / "sample.md"
         sample.write_text("# Local resource\n", encoding="utf-8")
    ```

    ```diff
    @@ -1457,10 +1487,10 @@ def capture_httpx_post(url, **kwargs):
         assert "files" in captured_kwargs
         assert "json" not in captured_kwargs
         headers = captured_kwargs["headers"]
    -    assert headers["X-OpenViking-Account"] == "test-account"
    -    assert headers["X-OpenViking-User"] == "test-user"
    +    assert "X-OpenViking-Account" not in headers
    +    assert "X-OpenViking-User" not in headers
         assert headers["X-OpenViking-Actor-Peer"] == "test-agent"
    -    assert headers["X-OpenViking-Agent"] == "test-agent"
    +    assert "X-OpenViking-Agent" not in headers
         assert headers["X-API-Key"] == "test-key"
         assert "Content-Type" not in headers
     
    ```

    ```diff
    @@ -1517,16 +1547,17 @@ def test_viking_client_headers_include_bearer_when_api_key_set():
         headers = client._headers()
         assert headers["X-API-Key"] == "test-key"
         assert headers["Authorization"] == "Bearer test-key"
    +    assert headers["X-OpenViking-Actor-Peer"] == "hermes"
    +    assert "X-OpenViking-Agent" not in headers
    +    assert "X-OpenViking-Account" not in headers
    +    assert "X-OpenViking-User" not in headers
     
     
    -def test_viking_client_headers_send_tenant_when_default():
    -    # account/user set to the literal string "default". OpenViking 0.3.x
    -    # requires X-OpenViking-Account and X-OpenViking-User for ROOT API key
    -    # requests to tenant-scoped APIs — omitting them causes
    -    # INVALID_ARGUMENT errors even when account="default".
    +def test_viking_client_headers_send_tenant_in_local_mode():
    +    # Local/trusted mode needs explicit tenant identity headers.
         client = _VikingClient(
             "https://example.com",
    -        api_key="[redacted:secret-assignment]",
    +        api_key="",
             account="default",
             user="default",
             agent="hermes",
    ```

    ```diff
    @@ -1535,14 +1566,13 @@ def test_viking_client_headers_send_tenant_when_default():
         assert headers["X-OpenViking-Account"] == "default"
         assert headers["X-OpenViking-User"] == "default"
         assert headers["X-OpenViking-Actor-Peer"] == "hermes"
    -    assert headers["X-OpenViking-Agent"] == "hermes"
    -    assert headers["Authorization"] == "Bearer test-key"
    +    assert "X-OpenViking-Agent" not in headers
    +    assert "Authorization" not in headers
     
     
     def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monkeypatch):
         _clear_openviking_tenant_env(monkeypatch)
    -    # Empty account/user strings fall back to "default" via the constructor.
    -    # Headers are sent even for the default value — ROOT API keys need them.
    +    # Empty account/user strings fall back to "default" in local mode.
         client = _VikingClient(
             "https://example.com",
             api_key="",
    ```

    ```diff
    @@ -1553,11 +1583,13 @@ def test_viking_client_headers_send_tenant_when_empty_falls_back_to_default(monk
         headers = client._headers()
         assert headers["X-OpenViking-Account"] == "default"
         assert headers["X-OpenViking-User"] == "default"
    +    assert headers["X-OpenViking-Actor-Peer"] == "hermes"
    +    assert "X-OpenViking-Agent" not in headers
         assert "Authorization" not in headers
         assert "X-API-Key" not in headers
     
     
    -def test_viking_client_headers_sent_with_real_tenant_values():
    +def test_viking_client_headers_can_include_tenant_for_trusted_retry():
         client = _VikingClient(
             "https://example.com",
             api_key="[redacted:secret-assignment]",
    ```

    ```diff
    @@ -1565,9 +1597,54 @@ def test_viking_client_headers_sent_with_real_tenant_values():
             user="real-user",
             agent="hermes",
         )
    -    headers = client._headers()
    +    headers = client._headers(include_tenant=True)
         assert headers["X-OpenViking-Account"] == "real-account"
         assert headers["X-OpenViking-User"] == "real-user"
    +    assert headers["Authorization"] == "Bearer test-key"
    +
    +
    +def test_viking_client_retries_with_tenant_headers_for_trusted_mode(monkeypatch):
    +    client = _VikingClient(
    +        "https://example.com",
    +        api_key="[redacted:secret-assignment]",
    +        account="acct",
    +        user="usr",
    +        agent="hermes",
    +    )
    +    captured_headers = []
    +
    +    def capture_get(url, **kwargs):
    +        captured_headers.append(kwargs.get("headers") or {})
    +        if len(captured_headers) == 1:
    +            return SimpleNamespace(
    +                status_code=400,
    +                text="",
    +                json=lambda: {
    +                    "status": "error",
    +                    "error": {
    +                        "code": "INVALID_ARGUMENT",
    +                        "message": "Trusted mode requests must include X-OpenViking-Account.",
    +                    },
    +                },
    +                raise_for_status=lambda: None,
    +            )
    +        return SimpleNamespace(
    +            status_code=200,
    +            text="",
    +            json=lambda: {"status": "ok", "result": {"ok": True}},
    +            raise_for_status=lambda: None,
    +        )
    +
    +    monkeypatch.setattr(client._httpx, "get", capture_get)
    +
    +    assert client.get("/api/v1/system/status") == {
    +        "status": "ok",
    +        "result": {"ok": True},
    +    }
    +    assert "X-OpenViking-Account" not in captured_headers[0]
    +    assert "X-OpenViking-User" not in captured_headers[0]
    +    assert captured_headers[1]["X-OpenViking-Account"] == "acct"
    +    assert captured_headers[1]["X-OpenViking-User"] == "usr"
     
     
     def test_viking_client_health_sends_auth_headers(monkeypatch):
    ```

    ```diff
    @@ -1590,6 +1667,10 @@ def capture_get(url, **kwargs):
         assert client.health() is True
         assert captured["url"] == "https://example.com/health"
         assert captured["headers"]["Authorization"] == "Bearer test-key"
    +    assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
    +    assert "X-OpenViking-Agent" not in captured["headers"]
    +    assert "X-OpenViking-Account" not in captured["headers"]
    +    assert "X-OpenViking-User" not in captured["headers"]
     
     
     def test_viking_client_validate_auth_uses_authenticated_system_status(monkeypatch):
    ```

    ```diff
    @@ -1620,8 +1701,9 @@ def capture_get(url, **kwargs):
         }
         assert captured["url"] == "https://example.com/api/v1/system/status"
         assert captured["headers"]["Authorization"] == "Bearer test-key"
    -    assert captured["headers"]["X-OpenViking-Account"] == "acct"
    -    assert captured["headers"]["X-OpenViking-User"] == "alice"
    +    assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
    +    assert "X-OpenViking-Account" not in captured["headers"]
    +    assert "X-OpenViking-User" not in captured["headers"]
     
     
     def test_viking_client_validate_root_access_uses_admin_accounts(monkeypatch):
    ```

    ```diff
    @@ -1650,10 +1732,9 @@ def capture_get(url, **kwargs):
         assert client.validate_root_access() == {"status": "ok", "result": []}
         assert captured["url"] == "https://example.com/api/v1/admin/accounts"
         assert captured["headers"]["Authorization"] == "Bearer root-key"
    -    # Empty account/user fall back to "default" and the tenant headers are
    -    # always sent — ROOT API keys require them (#22414/#21232 contract).
    -    assert captured["headers"]["X-OpenViking-Account"] == "default"
    -    assert captured["headers"]["X-OpenViking-User"] == "default"
    +    assert captured["headers"]["X-OpenViking-Actor-Peer"] == "hermes"
    +    assert "X-OpenViking-Account" not in captured["headers"]
    +    assert "X-OpenViking-User" not in captured["headers"]
     
     
     def test_validate_openviking_reachability_uses_health_only(monkeypatch):
    ```

    ```diff
    @@ -2055,7 +2136,7 @@ def post(self, path, payload=None, **kwargs):
         assert captured_payloads == [{
             "messages": [
                 {"role": "user", "parts": [{"type": "text", "text": "u"}]},
    -            {"role": "assistant", "parts": [{"type": "text", "text": "a"}]},
    +            {"role": "assistant", "parts": [{"type": "text", "text": "a"}], "peer_id": "hermes"},
             ]
         }]
     
    ```

    ```diff
    @@ -2099,7 +2180,7 @@ def post(self, path, payload=None, **kwargs):
             {
                 "messages": [
                     {"role": "user", "parts": [{"type": "text", "text": "u"}]},
    -                {"role": "assistant", "parts": [{"type": "text", "text": "a"}]},
    +                {"role": "assistant", "parts": [{"type": "text", "text": "a"}], "peer_id": "hermes"},
                 ]
             },
         )]
    ```

    ```diff
    @@ -2453,7 +2534,7 @@ def post(self, path, payload=None, **kwargs):
         assert captured_payloads[0]["content"] == "remember this"
         assert captured_payloads[0]["mode"] == "create"
         assert captured_payloads[0]["uri"].startswith(
    -        "viking://user/usr/agent/hermes/memories/preferences/mem_"
    +        "viking://user/peers/hermes/memories/preferences/mem_"
         )
     
     
    ```
  - `website/docs/user-guide/features/memory-providers.md`

    ```diff
    @@ -299,6 +299,8 @@ hermes memory setup    # select "openviking"
     # Or manually:
     hermes config set memory.provider openviking
     echo "OPENVIKING_ENDPOINT=http://localhost:1933" >> ~/.hermes/.env
    +# Authenticated servers should use a user/admin API key:
    +echo "OPENVIKING_API_KEY=..." >> ~/.hermes/.env
     ```
     
     **Key features:**
    ```

    ```diff
    @@ -306,6 +308,9 @@ echo "OPENVIKING_ENDPOINT=http://localhost:1933" >> ~/.hermes/.env
     - Automatic memory extraction on session commit (profile, preferences, entities, events, cases, patterns)
     - `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.
    +
     ---
     
     ### Mem0
    ```
- **#48465: feat(openviking): add configurable end-user isolation via OpenViking v0.4.1 peer_id**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#48206`

## Fix status

- Merged to main in PR #48206 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-18`.
- Subsystem: `agent`
