{"repo":"NousResearch/hermes-agent","public_id":"15219","display_label":"Memory write metadata, origin tracking, and success gating","summary":"These issues and pull requests concern adding write-origin metadata to external memory provider operations and gating add/replace/remove actions through a MemoryManager bridge with success checks and old_text context.","subsystem":"agent","counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":15346},"resolution":{"state":"partially_resolved","by":"pr-15346"},"freshness":{"state":"stale","last_verified_at":"2026-04-24T21:37:55+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.3933333333333333},"fix_edges":[{"issue_number":15219,"pr_number":15346,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":15219,"title":"Add write-origin metadata for external memory provider writes","url":null,"state":"closed"},{"kind":"issue","number":25526,"title":"MemoryProvider bridge should mirror add/replace/remove with old_text metadata and success gating","url":null,"state":"closed"},{"kind":"pr","number":15315,"title":"fix(memory): add write origin metadata","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#15346","number":15346}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":15346,"title":"fix(memory): add write origin metadata seam","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/memory_manager.py","agent/memory_provider.py","run_agent.py","tests/agent/test_memory_provider.py","tests/run_agent/test_flush_memories_codex.py"],"fix_hunks":[{"file":"agent/memory_manager.py","hunks":["@@ -31,6 +31,7 @@\n import json\n import logging\n import re\n+import inspect\n from typing import Any, Dict, List, Optional\n \n from agent.memory_provider import MemoryProvider","@@ -312,7 +313,39 @@ def on_pre_compress(self, messages: List[Dict[str, Any]]) -> str:\n                 )\n         return \"\\n\\n\".join(parts)\n \n-    def on_memory_write(self, action: str, target: str, content: str) -> None:\n+    @staticmethod\n+    def _provider_memory_write_metadata_mode(provider: MemoryProvider) -> str:\n+        \"\"\"Return how to pass metadata to a provider's memory-write hook.\"\"\"\n+        try:\n+            signature = inspect.signature(provider.on_memory_write)\n+        except (TypeError, ValueError):\n+            return \"keyword\"\n+\n+        params = list(signature.parameters.values())\n+        if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params):\n+            return \"keyword\"\n+        if \"metadata\" in signature.parameters:\n+            return \"keyword\"\n+\n+        accepted = [\n+            p for p in params\n+            if p.kind in (\n+                inspect.Parameter.POSITIONAL_ONLY,\n+                inspect.Parameter.POSITIONAL_OR_KEYWORD,\n+                inspect.Parameter.KEYWORD_ONLY,\n+            )\n+        ]\n+        if len(accepted) >= 4:\n+            return \"positional\"\n+        return \"legacy\"\n+\n+    def on_memory_write(\n+        self,\n+        action: str,\n+        target: str,\n+        content: str,\n+        metadata: Optional[Dict[str, Any]] = None,\n+    ) -> None:\n         \"\"\"Notify external providers when the built-in memory tool writes.\n \n         Skips the builtin provider itself (it's the source of the write).","@@ -321,7 +354,15 @@ def on_memory_write(self, action: str, target: str, content: str) -> None:\n             if provider.name == \"builtin\":\n                 continue\n             try:\n-                provider.on_memory_write(action, target, content)\n+                metadata_mode = self._provider_memory_write_metadata_mode(provider)\n+                if metadata_mode == \"keyword\":\n+                    provider.on_memory_write(\n+                        action, target, content, metadata=dict(metadata or {})\n+                    )\n+                elif metadata_mode == \"positional\":\n+                    provider.on_memory_write(action, target, content, dict(metadata or {}))\n+                else:\n+                    provider.on_memory_write(action, target, content)\n             except Exception as e:\n                 logger.debug(\n                     \"Memory provider '%s' on_memory_write failed: %s\","]},{"file":"agent/memory_provider.py","hunks":["@@ -26,7 +26,7 @@\n   on_turn_start(turn, message, **kwargs) — per-turn tick with runtime context\n   on_session_end(messages)               — end-of-session extraction\n   on_pre_compress(messages) -> str       — extract before context compression\n-  on_memory_write(action, target, content) — mirror built-in memory writes\n+  on_memory_write(action, target, content, metadata=None) — mirror built-in memory writes\n   on_delegation(task, result, **kwargs)  — parent-side observation of subagent work\n \"\"\"\n ","@@ -34,7 +34,7 @@\n \n import logging\n from abc import ABC, abstractmethod\n-from typing import Any, Dict, List\n+from typing import Any, Dict, List, Optional\n \n logger = logging.getLogger(__name__)\n ","@@ -220,12 +220,21 @@ def save_config(self, values: Dict[str, Any], hermes_home: str) -> None:\n           should all have ``env_var`` set and this method stays no-op).\n         \"\"\"\n \n-    def on_memory_write(self, action: str, target: str, content: str) -> None:\n+    def on_memory_write(\n+        self,\n+        action: str,\n+        target: str,\n+        content: str,\n+        metadata: Optional[Dict[str, Any]] = None,\n+    ) -> None:\n         \"\"\"Called when the built-in memory tool writes an entry.\n \n         action: 'add', 'replace', or 'remove'\n         target: 'memory' or 'user'\n         content: the entry content\n+        metadata: structured provenance for the write, when available. Common\n+          keys include ``write_origin``, ``execution_context``, ``session_id``,\n+          ``parent_session_id``, ``platform``, and ``tool_name``.\n \n         Use to mirror built-in memory writes to your backend.\n         \"\"\""]},{"file":"run_agent.py","hunks":["@@ -1437,6 +1437,8 @@ def __init__(\n         \n         # Track conversation messages for session logging\n         self._session_messages: List[Dict[str, Any]] = []\n+        self._memory_write_origin = \"assistant_tool\"\n+        self._memory_write_context = \"foreground\"\n         \n         # Cached system prompt -- built once per session, only rebuilt on compression\n         self._cached_system_prompt: Optional[str] = None","@@ -3075,7 +3077,10 @@ def _run_review():\n                         quiet_mode=True,\n                         platform=self.platform,\n                         provider=self.provider,\n+                        parent_session_id=self.session_id,\n                     )\n+                    review_agent._memory_write_origin = \"background_review\"\n+                    review_agent._memory_write_context = \"background_review\"\n                     review_agent._memory_store = self._memory_store\n                     review_agent._memory_enabled = self._memory_enabled\n                     review_agent._user_profile_enabled = self._user_profile_enabled","@@ -3124,6 +3129,32 @@ def _run_review():\n         t = threading.Thread(target=_run_review, daemon=True, name=\"bg-review\")\n         t.start()\n \n+    def _build_memory_write_metadata(\n+        self,\n+        *,\n+        write_origin: Optional[str] = None,\n+        execution_context: Optional[str] = None,\n+        task_id: Optional[str] = None,\n+        tool_call_id: Optional[str] = None,\n+    ) -> Dict[str, Any]:\n+        \"\"\"Build provenance metadata for external memory-provider mirrors.\"\"\"\n+        metadata: Dict[str, Any] = {\n+            \"write_origin\": write_origin or getattr(self, \"_memory_write_origin\", \"assistant_tool\"),\n+            \"execution_context\": (\n+                execution_context\n+                or getattr(self, \"_memory_write_context\", \"foreground\")\n+            ),\n+            \"session_id\": self.session_id or \"\",\n+            \"parent_session_id\": self._parent_session_id or \"\",\n+            \"platform\": self.platform or os.environ.get(\"HERMES_SESSION_SOURCE\", \"cli\"),\n+            \"tool_name\": \"memory\",\n+        }\n+        if task_id:\n+            metadata[\"task_id\"] = task_id\n+        if tool_call_id:\n+            metadata[\"tool_call_id\"] = tool_call_id\n+        return {k: v for k, v in metadata.items() if v not in (None, \"\")}\n+\n     def _apply_persist_user_message_override(self, messages: List[Dict]) -> None:\n         \"\"\"Rewrite the current-turn user message before persistence/return.\n ","@@ -7812,6 +7843,19 @@ def _codex_output_tool_calls(resp):\n                             old_text=args.get(\"old_text\"),\n                             store=self._memory_store,\n                         )\n+                        if self._memory_manager and args.get(\"action\") in (\"add\", \"replace\"):\n+                            try:\n+                                self._memory_manager.on_memory_write(\n+                                    args.get(\"action\", \"\"),\n+                                    flush_target,\n+                                    args.get(\"content\", \"\"),\n+                                    metadata=self._build_memory_write_metadata(\n+                                        write_origin=\"memory_flush\",\n+                                        execution_context=\"flush_memories\",\n+                                    ),\n+                                )\n+                            except Exception:\n+                                pass\n                         if not self.quiet_mode:\n                             print(f\"  🧠 Memory flush: saved to {args.get('target', 'memory')}\")\n                     except Exception as e:","@@ -8043,6 +8087,10 @@ def _invoke_tool(self, function_name: str, function_args: dict, effective_task_i\n                         function_args.get(\"action\", \"\"),\n                         target,\n                         function_args.get(\"content\", \"\"),\n+                        metadata=self._build_memory_write_metadata(\n+                            task_id=effective_task_id,\n+                            tool_call_id=tool_call_id,\n+                        ),\n                     )\n                 except Exception:\n                     pass","@@ -8554,6 +8602,10 @@ def _execute_tool_calls_sequential(self, assistant_message, messages: list, effe\n                             function_args.get(\"action\", \"\"),\n                             target,\n                             function_args.get(\"content\", \"\"),\n+                            metadata=self._build_memory_write_metadata(\n+                                task_id=effective_task_id,\n+                                tool_call_id=getattr(tool_call, \"id\", None),\n+                            ),\n                         )\n                     except Exception:\n                         pass"]},{"file":"tests/agent/test_memory_provider.py","hunks":["@@ -77,6 +77,13 @@ def on_memory_write(self, action, target, content):\n         self.memory_writes.append((action, target, content))\n \n \n+class MetadataMemoryProvider(FakeMemoryProvider):\n+    \"\"\"Provider that opts into write metadata.\"\"\"\n+\n+    def on_memory_write(self, action, target, content, metadata=None):\n+        self.memory_writes.append((action, target, content, metadata or {}))\n+\n+\n # ---------------------------------------------------------------------------\n # MemoryProvider ABC tests\n # ---------------------------------------------------------------------------","@@ -862,6 +869,51 @@ def test_on_memory_write_add(self):\n         mgr.on_memory_write(\"add\", \"memory\", \"new fact\")\n         assert p.memory_writes == [(\"add\", \"memory\", \"new fact\")]\n \n+    def test_on_memory_write_metadata_passed_to_opt_in_provider(self):\n+        \"\"\"Providers that accept metadata receive structured write provenance.\"\"\"\n+        mgr = MemoryManager()\n+        p = MetadataMemoryProvider(\"ext\")\n+        mgr.add_provider(p)\n+\n+        mgr.on_memory_write(\n+            \"add\",\n+            \"memory\",\n+            \"new fact\",\n+            metadata={\n+                \"write_origin\": \"assistant_tool\",\n+                \"execution_context\": \"foreground\",\n+                \"session_id\": \"sess-1\",\n+            },\n+        )\n+\n+        assert p.memory_writes == [\n+            (\n+                \"add\",\n+                \"memory\",\n+                \"new fact\",\n+                {\n+                    \"write_origin\": \"assistant_tool\",\n+                    \"execution_context\": \"foreground\",\n+                    \"session_id\": \"sess-1\",\n+                },\n+            )\n+        ]\n+\n+    def test_on_memory_write_metadata_keeps_legacy_provider_compatible(self):\n+        \"\"\"Old 3-arg providers keep working when the manager receives metadata.\"\"\"\n+        mgr = MemoryManager()\n+        p = FakeMemoryProvider(\"ext\")\n+        mgr.add_provider(p)\n+\n+        mgr.on_memory_write(\n+            \"add\",\n+            \"user\",\n+            \"legacy provider fact\",\n+            metadata={\"write_origin\": \"assistant_tool\"},\n+        )\n+\n+        assert p.memory_writes == [(\"add\", \"user\", \"legacy provider fact\")]\n+\n     def test_on_memory_write_replace(self):\n         \"\"\"on_memory_write fires for 'replace' actions.\"\"\"\n         mgr = MemoryManager()"]},{"file":"tests/run_agent/test_flush_memories_codex.py","hunks":["@@ -233,6 +233,31 @@ def test_flush_executes_memory_tool_calls(self, monkeypatch):\n         assert call_kwargs.kwargs[\"target\"] == \"notes\"\n         assert \"dark mode\" in call_kwargs.kwargs[\"content\"]\n \n+    def test_flush_bridges_memory_write_metadata(self, monkeypatch):\n+        \"\"\"Flush memory writes notify external providers with flush provenance.\"\"\"\n+        agent = _make_agent(monkeypatch, api_mode=\"chat_completions\", provider=\"openrouter\")\n+        agent._memory_manager = MagicMock()\n+        agent.session_id = \"sess-flush\"\n+        agent.platform = \"cli\"\n+\n+        mock_response = _chat_response_with_memory_call()\n+\n+        with patch(\"agent.auxiliary_client.call_llm\", return_value=mock_response):\n+            messages = [\n+                {\"role\": \"user\", \"content\": \"Hello\"},\n+                {\"role\": \"assistant\", \"content\": \"Hi\"},\n+                {\"role\": \"user\", \"content\": \"Note this\"},\n+            ]\n+            with patch(\"tools.memory_tool.memory_tool\", return_value=\"Saved.\"):\n+                agent.flush_memories(messages)\n+\n+        agent._memory_manager.on_memory_write.assert_called_once()\n+        call_kwargs = agent._memory_manager.on_memory_write.call_args\n+        assert call_kwargs.args[:3] == (\"add\", \"notes\", \"User prefers dark mode.\")\n+        assert call_kwargs.kwargs[\"metadata\"][\"write_origin\"] == \"memory_flush\"\n+        assert call_kwargs.kwargs[\"metadata\"][\"execution_context\"] == \"flush_memories\"\n+        assert call_kwargs.kwargs[\"metadata\"][\"session_id\"] == \"sess-flush\"\n+\n     def test_flush_strips_artifacts_from_messages(self, monkeypatch):\n         \"\"\"After flush, the flush prompt and any response should be removed from messages.\"\"\"\n         agent = _make_agent(monkeypatch, api_mode=\"chat_completions\", provider=\"openrouter\")\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}