# NousResearch/hermes-agent · OpenViking plugin auth, headers, and API fixes

## Open now

- Nothing open on GitHub.

These issues and PRs address multiple interrelated problems in the OpenViking plugin: incorrect API endpoint paths, broken authentication header behavior with API key auth, tenant-scoping header mismatches causing memory search failures, and compatibility issues with OpenViking v0.4.1+.

## TL;DR

- 19 member(s): 0 open issue(s), 8 closed issue(s), 0 open PR(s), 3 merged PR(s), 8 PR(s) closed without merging.
- A fix was merged to main in PR #4936. Verify it addresses your case.
- **Recommended:** read PR #4936 (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: `partially_resolved` · deciding PR(s): `pr-4936`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.08`: 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

- #4740: fix(openviking): plugin uses non-existent API endpoints, browse/read tools broken
- #4825: fix(openviking memory): send tenant headers for multi-tenant OpenViking
- #8739: Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
- #8742: Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
- #8743: Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch
- #21130: OpenViking plugin: 403 with API key auth + env vars not reloaded after /reload
- #47344: OpenViking plugin always sends X-OpenViking-Account header, broken on OV v0.4.1+ with api_key auth
- #47527: fix(openviking): v0.4.1 compatibility - auth headers and search limit

## Prior & competing PRs

- **#4936: fix(openviking): send tenant-scoping headers for multi-tenant servers (salvage #4863)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/memory/openviking/__init__.py`
  - fix hunks:
  - `plugins/memory/openviking/__init__.py`

    ```diff
    @@ -10,6 +10,8 @@
     Config via environment variables (profile-scoped via each profile's .env):
       OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)
       OPENVIKING_API_KEY   — API key (required for authenticated servers)
    +  OPENVIKING_ACCOUNT   — Tenant account (default: root)
    +  OPENVIKING_USER      — Tenant user (default: default)
     
     Capabilities:
       - Automatic memory extraction on session commit (6 categories)
    ```

    ```diff
    @@ -51,15 +53,22 @@ def _get_httpx():
     class _VikingClient:
         """Thin HTTP client for the OpenViking REST API."""
     
    -    def __init__(self, endpoint: str, api_key: str = ""):
    +    def __init__(self, endpoint: str, api_key: str = "",
    +                 account: str = "", user: str = ""):
             self._endpoint = endpoint.rstrip("/")
             self._api_key = [redacted:secret-assignment]
    +        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "root")
    +        self._user = user or os.environ.get("OPENVIKING_USER", "default")
             self._httpx = _get_httpx()
             if self._httpx is None:
                 raise ImportError("httpx is required for OpenViking: pip install httpx")
     
         def _headers(self) -> dict:
    -        h = {"Content-Type": "application/json"}
    +        h = {
    +            "Content-Type": "application/json",
    +            "X-OpenViking-Account": self._account,
    +            "X-OpenViking-User": self._user,
    +        }
             if self._api_key:
                 h["X-API-Key"] = self._api_key
             return h
    ```
- **#5078: fix(openviking): correct API endpoint paths and response parsing**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/memory/openviking/__init__.py`
- **#10463: fix(openviking): correct account default + commit session on /new and compress**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `plugins/memory/openviking/__init__.py`, `run_agent.py`, `scripts/release.py`, `tests/agent/test_memory_provider.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -4100,6 +4100,8 @@ def new_session(self, silent=False):
                     self.agent.flush_memories(self.conversation_history)
                 except (Exception, KeyboardInterrupt):
                     pass
    +            # Trigger memory extraction on the old session before session_id rotates.
    +            self.agent.commit_memory_session(self.conversation_history)
                 self._notify_session_boundary("on_session_finalize")
             elif self.agent:
                 # First session or empty history — still finalize the old session
    ```
  - `plugins/memory/openviking/__init__.py`

    ```diff
    @@ -10,8 +10,9 @@
     Config via environment variables (profile-scoped via each profile's .env):
       OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)
       OPENVIKING_API_KEY   — API key (required for authenticated servers)
    -  OPENVIKING_ACCOUNT   — Tenant account (default: root)
    +  OPENVIKING_ACCOUNT   — Tenant account (default: default)
       OPENVIKING_USER      — Tenant user (default: default)
    +  OPENVIKING_AGENT   — Tenant agent (default: hermes)
     
     Capabilities:
       - Automatic memory extraction on session commit (6 categories)
    ```

    ```diff
    @@ -80,11 +81,12 @@ class _VikingClient:
         """Thin HTTP client for the OpenViking REST API."""
     
         def __init__(self, endpoint: str, api_key: str = "",
    -                 account: str = "", user: str = ""):
    +                 account: str = "", user: str = "", agent: str = ""):
             self._endpoint = endpoint.rstrip("/")
             self._api_key = [redacted:secret-assignment]
    -        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "root")
    +        self._account = account or os.environ.get("OPENVIKING_ACCOUNT", "default")
             self._user = user or os.environ.get("OPENVIKING_USER", "default")
    +        self._agent = agent or os.environ.get("OPENVIKING_AGENT", "hermes")
             self._httpx = _get_httpx()
             if self._httpx is None:
                 raise ImportError("httpx is required for OpenViking: pip install httpx")
    ```

    ```diff
    @@ -94,6 +96,7 @@ def _headers(self) -> dict:
                 "Content-Type": "application/json",
                 "X-OpenViking-Account": self._account,
                 "X-OpenViking-User": self._user,
    +            "X-OpenViking-Agent": self._agent,
             }
             if self._api_key:
                 h["X-API-Key"] = self._api_key
    ```

    ```diff
    @@ -282,20 +285,44 @@ def get_config_schema(self):
                 },
                 {
                     "key": "api_key",
    -                "description": "OpenViking API key",
    +                "description": "OpenViking API key (leave blank for local dev mode)",
                     "secret": True,
                     "env_var": "OPENVIKING_API_KEY",
                 },
    +            {
    +                "key": "account",
    +                "description": "OpenViking tenant account ID ([default], used when local mode, OPENVIKING_API_KEY is empty)",
    +                "default": "default",
    +                "env_var": "OPENVIKING_ACCOUNT",
    +            },
    +            {
    +                "key": "user",
    +                "description": "OpenViking user ID within the account ([default], used when local mode, OPENVIKING_API_KEY is empty)",
    +                "default": "default",
    +                "env_var": "OPENVIKING_USER",
    +            },
    +            {
    +                "key": "agent",
    +                "description": "OpenViking agent ID within the account ([hermes], useful in multi-agent mode)",
    +                "default": "hermes",
    +                "env_var": "OPENVIKING_AGENT",
    +            },
             ]
     
         def initialize(self, session_id: str, **kwargs) -> None:
             self._endpoint = os.environ.get("OPENVIKING_ENDPOINT", _DEFAULT_ENDPOINT)
             self._api_key = [redacted:secret-assignment]"OPENVIKING_API_KEY", "")
    +        self._account = os.environ.get("OPENVIKING_ACCOUNT", "default")
    +        self._user = os.environ.get("OPENVIKING_USER", "default")
    +        self._agent = os.environ.get("OPENVIKING_AGENT", "hermes")
             self._session_id = session_id
             self._turn_count = 0
     
             try:
    -            self._client = _VikingClient(self._endpoint, self._api_key)
    +            self._client = _VikingClient(
    +                self._endpoint, self._api_key,
    +                account=self._account, user=self._user, agent=self._agent,
    +            )
                 if not self._client.health():
                     logger.warning("OpenViking server at %s is not reachable", self._endpoint)
                     self._client = None
    ```

    ```diff
    @@ -325,7 +352,8 @@ def system_prompt_block(self) -> str:
                     "(abstract/overview/full), viking_browse to explore.\n"
                     "Use viking_remember to store facts, viking_add_resource to index URLs/docs."
                 )
    -        except Exception:
    +        except Exception as e:
    +            logger.warning("OpenViking system_prompt_block failed: %s", e)
                 return (
                     "# OpenViking Knowledge Base\n"
                     f"Active. Endpoint: {self._endpoint}\n"
    ```

    ```diff
    @@ -351,7 +379,10 @@ def queue_prefetch(self, query: str, *, session_id: str = "") -> None:
     
             def _run():
                 try:
    -                client = _VikingClient(self._endpoint, self._api_key)
    +                client = _VikingClient(
    +                    self._endpoint, self._api_key,
    +                    account=self._account, user=self._user, agent=self._agent,
    +                )
                     resp = client.post("/api/v1/search/find", {
                         "query": query,
                         "top_k": 5,
    ```

    ```diff
    @@ -386,7 +417,10 @@ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: st
     
             def _sync():
                 try:
    -                client = _VikingClient(self._endpoint, self._api_key)
    +                client = _VikingClient(
    +                    self._endpoint, self._api_key,
    +                    account=self._account, user=self._user, agent=self._agent,
    +                )
                     sid = self._session_id
     
                     # Add user message
    ```

    ```diff
    @@ -442,7 +476,10 @@ def on_memory_write(self, action: str, target: str, content: str) -> None:
     
             def _write():
                 try:
    -                client = _VikingClient(self._endpoint, self._api_key)
    +                client = _VikingClient(
    +                    self._endpoint, self._api_key,
    +                    account=self._account, user=self._user, agent=self._agent,
    +                )
                     # Add as a user message with memory context so the commit
                     # picks it up as an explicit memory during extraction
                     client.post(f"/api/v1/sessions/{self._session_id}/messages", {
    ```
  - `run_agent.py`

    ```diff
    @@ -3040,6 +3040,18 @@ def shutdown_memory_provider(self, messages: list = None) -> None:
                 except Exception:
                     pass
         
    +    def commit_memory_session(self, messages: list = None) -> None:
    +        """Trigger end-of-session extraction without tearing providers down.
    +        Called when session_id rotates (e.g. /new, context compression);
    +        providers keep their state and continue running under the old
    +        session_id — they just flush pending extraction now."""
    +        if not self._memory_manager:
    +            return
    +        try:
    +            self._memory_manager.on_session_end(messages or [])
    +        except Exception:
    +            pass
    +
         def close(self) -> None:
             """Release all resources held by this agent instance.
     
    ```

    ```diff
    @@ -6826,6 +6838,8 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token
                 try:
                     # Propagate title to the new session with auto-numbering
                     old_title = self._session_db.get_session_title(self.session_id)
    +                # Trigger memory extraction on the old session before it rotates.
    +                self.commit_memory_session(messages)
                     self._session_db.end_session(self.session_id, "compression")
                     old_session_id = self.session_id
                     self.session_id = f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}"
    ```
  - `scripts/release.py`

    ```diff
    @@ -197,6 +197,8 @@
         "[redacted:email]": "zhouboli",
         "[redacted:email]": "tomqiaozc",
         "[redacted:email]": "xinbenlv",
    +    "[redacted:email]": "ZaynJarvis",
    +    "[redacted:email]": "ZaynJarvis",
     }
     
     
    ```
  - `tests/agent/test_memory_provider.py`

    ```diff
    @@ -695,3 +695,44 @@ def test_fenced_block_separates_user_from_recall(self):
             fence_end = combined.index("</memory-context>")
             assert "Alice" in combined[fence_start:fence_end]
             assert combined.index("weather") < fence_start
    +
    +
    +# ---------------------------------------------------------------------------
    +# AIAgent.commit_memory_session — routes to MemoryManager.on_session_end
    +# ---------------------------------------------------------------------------
    +
    +
    +class _CommitRecorder(FakeMemoryProvider):
    +    """Provider that records on_session_end calls for assertions."""
    +
    +    def __init__(self, name="recorder"):
    +        super().__init__(name)
    +        self.end_calls = []
    +
    +    def on_session_end(self, messages):
    +        self.end_calls.append(list(messages or []))
    +
    +
    +class TestCommitMemorySessionRouting:
    +    def test_on_session_end_fans_out(self):
    +        mgr = MemoryManager()
    +        builtin = _CommitRecorder("builtin")
    +        external = _CommitRecorder("openviking")
    +        mgr.add_provider(builtin)
    +        mgr.add_provider(external)
    +
    +        msgs = [{"role": "user", "content": "hi"}]
    +        mgr.on_session_end(msgs)
    +
    +        assert builtin.end_calls == [msgs]
    +        assert external.end_calls == [msgs]
    +
    +    def test_on_session_end_tolerates_failure(self):
    +        mgr = MemoryManager()
    +        builtin = FakeMemoryProvider("builtin")
    +        bad = _CommitRecorder("bad-provider")
    +        bad.on_session_end = lambda m: (_ for _ in ()).throw(RuntimeError("boom"))
    +        mgr.add_provider(builtin)
    +        mgr.add_provider(bad)
    +
    +        mgr.on_session_end([])  # must not raise
    ```
- **#4742: fix(openviking): correct API endpoint paths, response parsing, and auth headers**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#4743`
- **#4770: fix(openviking): correct API endpoints validated against live server OpenAPI spec**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#5078`, `#4742`
- **#4863: fix(openviking): send tenant-scoping headers for multi-tenant servers**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#4936`
- **#9103: fix(memory): align OpenViking default account with server extraction default**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#10463`, `#9807`
- **#12937: fix(openviking): viking_read fails on file URIs — route to /content/read**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `5d253e65b`, `#17869`, `#5886`, `#12757`
- **#21136: fix(openviking): resolve 403 with API key auth and late .env loading**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#51952`, `#21130`
- **#47355: fix(openviking): skip tenant headers when api_key is set**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `4af16b5da24801858818ccf07a6bdb8027ba3387`, `#48206`, `#48206`
- **#47528: fix(openviking): v0.4.1 compatibility - auth headers and search limit**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#47527`, `#46532`, `#47528`, `#46532`

## Fix status

- Merged to main in PR #4936 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-15`. This confirmation is old and may be out of date (a revert/reopen would not have invalidated it here); re-verify.
- Subsystem: `agent`
