{"repo":"NousResearch/hermes-agent","public_id":"4740","display_label":"OpenViking plugin auth, headers, and API fixes","summary":"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+.","subsystem":"agent","counts":{"members":19,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":8,"merged_prs":3,"attempt_prs":8},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":4936},"resolution":{"state":"partially_resolved","by":"pr-4936"},"freshness":{"state":"stale","last_verified_at":"2026-04-15T18:28:45+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.08116959064327486},"fix_edges":[{"issue_number":4740,"pr_number":5078,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":4825,"pr_number":4936,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":8705,"pr_number":10463,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":8742,"pr_number":10463,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":4740,"title":"fix(openviking): plugin uses non-existent API endpoints, browse/read tools broken","url":null,"state":"closed"},{"kind":"issue","number":4825,"title":"fix(openviking memory): send tenant headers for multi-tenant OpenViking","url":null,"state":"closed"},{"kind":"issue","number":8739,"title":"Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch","url":null,"state":"closed"},{"kind":"issue","number":8742,"title":"Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch","url":null,"state":"closed"},{"kind":"issue","number":8743,"title":"Memory search returns 0 results: OpenViking _tenant_filter account_id mismatch","url":null,"state":"closed"},{"kind":"issue","number":21130,"title":"OpenViking plugin: 403 with API key auth + env vars not reloaded after /reload","url":null,"state":"closed"},{"kind":"issue","number":47344,"title":"OpenViking plugin always sends X-OpenViking-Account header, broken on OV v0.4.1+ with api_key auth","url":null,"state":"closed"},{"kind":"issue","number":47527,"title":"fix(openviking): v0.4.1 compatibility - auth headers and search limit","url":null,"state":"closed"},{"kind":"pr","number":4742,"title":"fix(openviking): correct API endpoint paths, response parsing, and auth headers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#4743","number":4743}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":4770,"title":"fix(openviking): correct API endpoints validated against live server OpenAPI spec","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#5078","number":5078},{"kind":"pr","value":"#4742","number":4742}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":4863,"title":"fix(openviking): send tenant-scoping headers for multi-tenant servers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#4936","number":4936}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":4936,"title":"fix(openviking): send tenant-scoping headers for multi-tenant servers (salvage #4863)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/openviking/__init__.py"],"fix_hunks":[{"file":"plugins/memory/openviking/__init__.py","hunks":["@@ -10,6 +10,8 @@\n Config via environment variables (profile-scoped via each profile's .env):\n   OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)\n   OPENVIKING_API_KEY   — API key (required for authenticated servers)\n+  OPENVIKING_ACCOUNT   — Tenant account (default: root)\n+  OPENVIKING_USER      — Tenant user (default: default)\n \n Capabilities:\n   - Automatic memory extraction on session commit (6 categories)","@@ -51,15 +53,22 @@ def _get_httpx():\n class _VikingClient:\n     \"\"\"Thin HTTP client for the OpenViking REST API.\"\"\"\n \n-    def __init__(self, endpoint: str, api_key: str = \"\"):\n+    def __init__(self, endpoint: str, api_key: str = \"\",\n+                 account: str = \"\", user: str = \"\"):\n         self._endpoint = endpoint.rstrip(\"/\")\n         self._api_key = [redacted:secret-assignment]\n+        self._account = account or os.environ.get(\"OPENVIKING_ACCOUNT\", \"root\")\n+        self._user = user or os.environ.get(\"OPENVIKING_USER\", \"default\")\n         self._httpx = _get_httpx()\n         if self._httpx is None:\n             raise ImportError(\"httpx is required for OpenViking: pip install httpx\")\n \n     def _headers(self) -> dict:\n-        h = {\"Content-Type\": \"application/json\"}\n+        h = {\n+            \"Content-Type\": \"application/json\",\n+            \"X-OpenViking-Account\": self._account,\n+            \"X-OpenViking-User\": self._user,\n+        }\n         if self._api_key:\n             h[\"X-API-Key\"] = self._api_key\n         return h\n"]}]},{"kind":"pr","number":5078,"title":"fix(openviking): correct API endpoint paths and response parsing","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/openviking/__init__.py"]},{"kind":"pr","number":9103,"title":"fix(memory): align OpenViking default account with server extraction default","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10463","number":10463},{"kind":"pr","value":"#9807","number":9807}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":10463,"title":"fix(openviking): correct account default + commit session on /new and compress","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","plugins/memory/openviking/__init__.py","run_agent.py","scripts/release.py","tests/agent/test_memory_provider.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -4100,6 +4100,8 @@ def new_session(self, silent=False):\n                 self.agent.flush_memories(self.conversation_history)\n             except (Exception, KeyboardInterrupt):\n                 pass\n+            # Trigger memory extraction on the old session before session_id rotates.\n+            self.agent.commit_memory_session(self.conversation_history)\n             self._notify_session_boundary(\"on_session_finalize\")\n         elif self.agent:\n             # First session or empty history — still finalize the old session"]},{"file":"plugins/memory/openviking/__init__.py","hunks":["@@ -10,8 +10,9 @@\n Config via environment variables (profile-scoped via each profile's .env):\n   OPENVIKING_ENDPOINT  — Server URL (default: http://127.0.0.1:1933)\n   OPENVIKING_API_KEY   — API key (required for authenticated servers)\n-  OPENVIKING_ACCOUNT   — Tenant account (default: root)\n+  OPENVIKING_ACCOUNT   — Tenant account (default: default)\n   OPENVIKING_USER      — Tenant user (default: default)\n+  OPENVIKING_AGENT   — Tenant agent (default: hermes)\n \n Capabilities:\n   - Automatic memory extraction on session commit (6 categories)","@@ -80,11 +81,12 @@ class _VikingClient:\n     \"\"\"Thin HTTP client for the OpenViking REST API.\"\"\"\n \n     def __init__(self, endpoint: str, api_key: str = \"\",\n-                 account: str = \"\", user: str = \"\"):\n+                 account: str = \"\", user: str = \"\", agent: str = \"\"):\n         self._endpoint = endpoint.rstrip(\"/\")\n         self._api_key = [redacted:secret-assignment]\n-        self._account = account or os.environ.get(\"OPENVIKING_ACCOUNT\", \"root\")\n+        self._account = account or os.environ.get(\"OPENVIKING_ACCOUNT\", \"default\")\n         self._user = user or os.environ.get(\"OPENVIKING_USER\", \"default\")\n+        self._agent = agent or os.environ.get(\"OPENVIKING_AGENT\", \"hermes\")\n         self._httpx = _get_httpx()\n         if self._httpx is None:\n             raise ImportError(\"httpx is required for OpenViking: pip install httpx\")","@@ -94,6 +96,7 @@ def _headers(self) -> dict:\n             \"Content-Type\": \"application/json\",\n             \"X-OpenViking-Account\": self._account,\n             \"X-OpenViking-User\": self._user,\n+            \"X-OpenViking-Agent\": self._agent,\n         }\n         if self._api_key:\n             h[\"X-API-Key\"] = self._api_key","@@ -282,20 +285,44 @@ def get_config_schema(self):\n             },\n             {\n                 \"key\": \"api_key\",\n-                \"description\": \"OpenViking API key\",\n+                \"description\": \"OpenViking API key (leave blank for local dev mode)\",\n                 \"secret\": True,\n                 \"env_var\": \"OPENVIKING_API_KEY\",\n             },\n+            {\n+                \"key\": \"account\",\n+                \"description\": \"OpenViking tenant account ID ([default], used when local mode, OPENVIKING_API_KEY is empty)\",\n+                \"default\": \"default\",\n+                \"env_var\": \"OPENVIKING_ACCOUNT\",\n+            },\n+            {\n+                \"key\": \"user\",\n+                \"description\": \"OpenViking user ID within the account ([default], used when local mode, OPENVIKING_API_KEY is empty)\",\n+                \"default\": \"default\",\n+                \"env_var\": \"OPENVIKING_USER\",\n+            },\n+            {\n+                \"key\": \"agent\",\n+                \"description\": \"OpenViking agent ID within the account ([hermes], useful in multi-agent mode)\",\n+                \"default\": \"hermes\",\n+                \"env_var\": \"OPENVIKING_AGENT\",\n+            },\n         ]\n \n     def initialize(self, session_id: str, **kwargs) -> None:\n         self._endpoint = os.environ.get(\"OPENVIKING_ENDPOINT\", _DEFAULT_ENDPOINT)\n         self._api_key = [redacted:secret-assignment]\"OPENVIKING_API_KEY\", \"\")\n+        self._account = os.environ.get(\"OPENVIKING_ACCOUNT\", \"default\")\n+        self._user = os.environ.get(\"OPENVIKING_USER\", \"default\")\n+        self._agent = os.environ.get(\"OPENVIKING_AGENT\", \"hermes\")\n         self._session_id = session_id\n         self._turn_count = 0\n \n         try:\n-            self._client = _VikingClient(self._endpoint, self._api_key)\n+            self._client = _VikingClient(\n+                self._endpoint, self._api_key,\n+                account=self._account, user=self._user, agent=self._agent,\n+            )\n             if not self._client.health():\n                 logger.warning(\"OpenViking server at %s is not reachable\", self._endpoint)\n                 self._client = None","@@ -325,7 +352,8 @@ def system_prompt_block(self) -> str:\n                 \"(abstract/overview/full), viking_browse to explore.\\n\"\n                 \"Use viking_remember to store facts, viking_add_resource to index URLs/docs.\"\n             )\n-        except Exception:\n+        except Exception as e:\n+            logger.warning(\"OpenViking system_prompt_block failed: %s\", e)\n             return (\n                 \"# OpenViking Knowledge Base\\n\"\n                 f\"Active. Endpoint: {self._endpoint}\\n\"","@@ -351,7 +379,10 @@ def queue_prefetch(self, query: str, *, session_id: str = \"\") -> None:\n \n         def _run():\n             try:\n-                client = _VikingClient(self._endpoint, self._api_key)\n+                client = _VikingClient(\n+                    self._endpoint, self._api_key,\n+                    account=self._account, user=self._user, agent=self._agent,\n+                )\n                 resp = client.post(\"/api/v1/search/find\", {\n                     \"query\": query,\n                     \"top_k\": 5,","@@ -386,7 +417,10 @@ def sync_turn(self, user_content: str, assistant_content: str, *, session_id: st\n \n         def _sync():\n             try:\n-                client = _VikingClient(self._endpoint, self._api_key)\n+                client = _VikingClient(\n+                    self._endpoint, self._api_key,\n+                    account=self._account, user=self._user, agent=self._agent,\n+                )\n                 sid = self._session_id\n \n                 # Add user message","@@ -442,7 +476,10 @@ def on_memory_write(self, action: str, target: str, content: str) -> None:\n \n         def _write():\n             try:\n-                client = _VikingClient(self._endpoint, self._api_key)\n+                client = _VikingClient(\n+                    self._endpoint, self._api_key,\n+                    account=self._account, user=self._user, agent=self._agent,\n+                )\n                 # Add as a user message with memory context so the commit\n                 # picks it up as an explicit memory during extraction\n                 client.post(f\"/api/v1/sessions/{self._session_id}/messages\", {"]},{"file":"run_agent.py","hunks":["@@ -3040,6 +3040,18 @@ def shutdown_memory_provider(self, messages: list = None) -> None:\n             except Exception:\n                 pass\n     \n+    def commit_memory_session(self, messages: list = None) -> None:\n+        \"\"\"Trigger end-of-session extraction without tearing providers down.\n+        Called when session_id rotates (e.g. /new, context compression);\n+        providers keep their state and continue running under the old\n+        session_id — they just flush pending extraction now.\"\"\"\n+        if not self._memory_manager:\n+            return\n+        try:\n+            self._memory_manager.on_session_end(messages or [])\n+        except Exception:\n+            pass\n+\n     def close(self) -> None:\n         \"\"\"Release all resources held by this agent instance.\n ","@@ -6826,6 +6838,8 @@ def _compress_context(self, messages: list, system_message: str, *, approx_token\n             try:\n                 # Propagate title to the new session with auto-numbering\n                 old_title = self._session_db.get_session_title(self.session_id)\n+                # Trigger memory extraction on the old session before it rotates.\n+                self.commit_memory_session(messages)\n                 self._session_db.end_session(self.session_id, \"compression\")\n                 old_session_id = self.session_id\n                 self.session_id = f\"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:6]}\""]},{"file":"scripts/release.py","hunks":["@@ -197,6 +197,8 @@\n     \"[redacted:email]\": \"zhouboli\",\n     \"[redacted:email]\": \"tomqiaozc\",\n     \"[redacted:email]\": \"xinbenlv\",\n+    \"[redacted:email]\": \"ZaynJarvis\",\n+    \"[redacted:email]\": \"ZaynJarvis\",\n }\n \n "]},{"file":"tests/agent/test_memory_provider.py","hunks":["@@ -695,3 +695,44 @@ def test_fenced_block_separates_user_from_recall(self):\n         fence_end = combined.index(\"</memory-context>\")\n         assert \"Alice\" in combined[fence_start:fence_end]\n         assert combined.index(\"weather\") < fence_start\n+\n+\n+# ---------------------------------------------------------------------------\n+# AIAgent.commit_memory_session — routes to MemoryManager.on_session_end\n+# ---------------------------------------------------------------------------\n+\n+\n+class _CommitRecorder(FakeMemoryProvider):\n+    \"\"\"Provider that records on_session_end calls for assertions.\"\"\"\n+\n+    def __init__(self, name=\"recorder\"):\n+        super().__init__(name)\n+        self.end_calls = []\n+\n+    def on_session_end(self, messages):\n+        self.end_calls.append(list(messages or []))\n+\n+\n+class TestCommitMemorySessionRouting:\n+    def test_on_session_end_fans_out(self):\n+        mgr = MemoryManager()\n+        builtin = _CommitRecorder(\"builtin\")\n+        external = _CommitRecorder(\"openviking\")\n+        mgr.add_provider(builtin)\n+        mgr.add_provider(external)\n+\n+        msgs = [{\"role\": \"user\", \"content\": \"hi\"}]\n+        mgr.on_session_end(msgs)\n+\n+        assert builtin.end_calls == [msgs]\n+        assert external.end_calls == [msgs]\n+\n+    def test_on_session_end_tolerates_failure(self):\n+        mgr = MemoryManager()\n+        builtin = FakeMemoryProvider(\"builtin\")\n+        bad = _CommitRecorder(\"bad-provider\")\n+        bad.on_session_end = lambda m: (_ for _ in ()).throw(RuntimeError(\"boom\"))\n+        mgr.add_provider(builtin)\n+        mgr.add_provider(bad)\n+\n+        mgr.on_session_end([])  # must not raise\n"]}]},{"kind":"pr","number":12937,"title":"fix(openviking): viking_read fails on file URIs — route to /content/read","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"5d253e65b","number":0},{"kind":"pr","value":"#17869","number":17869},{"kind":"pr","value":"#5886","number":5886},{"kind":"pr","value":"#12757","number":12757}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":21136,"title":"fix(openviking): resolve 403 with API key auth and late .env loading","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#51952","number":51952},{"kind":"pr","value":"#21130","number":21130}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":47355,"title":"fix(openviking): skip tenant headers when api_key is set","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"4af16b5da24801858818ccf07a6bdb8027ba3387","number":0},{"kind":"pr","value":"#48206","number":48206},{"kind":"pr","value":"#48206","number":48206}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":47528,"title":"fix(openviking): v0.4.1 compatibility - auth headers and search limit","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#47527","number":47527},{"kind":"pr","value":"#46532","number":46532},{"kind":"pr","value":"#47528","number":47528},{"kind":"pr","value":"#46532","number":46532}],"category":"wontfix_bydesign","implication":"wontfix"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}