{"repo":"NousResearch/hermes-agent","public_id":"7149","display_label":"Hindsight daemon idle timeout and recovery fixes","summary":"These items address a bug where the Hindsight daemon shuts down after an idle timeout and is not restarted, along with related configuration wiring and memory plugin changes.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":16284},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-09-23T08:16:42+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":7149,"pr_number":16284,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":7149,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":7718,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9869,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9948,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":18774,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":21809,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":23307,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":34415,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":35195,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":39424,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":43891,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":44055,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":44804,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":45676,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":46424,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":52153,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":70089,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":70606,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":78064,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":88944,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":93568,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":105353,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":110838,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":117266,"pr_number":119888,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":7149,"title":"[Bug]: Hindsight daemon shuts down after idle timeout and isn't brought back","url":null,"state":"closed"},{"kind":"pr","number":7435,"title":"fix(hindsight): restart daemon after idle timeout shutdown","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#16284","number":16284},{"kind":"pr","value":"#7149","number":7149},{"kind":"pr","value":"#16284","number":16284}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":16284,"title":"fix(hindsight): recover embedded daemon after idle shutdown","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["plugins/memory/hindsight/__init__.py","tests/plugins/memory/test_hindsight_provider.py"],"fix_hunks":[{"file":"plugins/memory/hindsight/__init__.py","hunks":["@@ -3,7 +3,9 @@\n Long-term memory with knowledge graph, entity resolution, and multi-strategy\n retrieval. Supports cloud (API key) and local modes.\n \n-Configurable timeout via HINDSIGHT_TIMEOUT env var or config.json.\n+Configurable request timeout via HINDSIGHT_TIMEOUT env var or config.json.\n+Configurable embedded daemon idle timeout via HINDSIGHT_IDLE_TIMEOUT env var\n+or config.json idle_timeout.\n \n Original PR #1811 by benfrank241, adapted to MemoryProvider ABC.\n ","@@ -14,6 +16,7 @@\n   HINDSIGHT_API_URL                — API endpoint\n   HINDSIGHT_MODE                   — cloud or local (default: cloud)\n   HINDSIGHT_TIMEOUT                — API request timeout in seconds (default: 120)\n+  HINDSIGHT_IDLE_TIMEOUT           — embedded daemon idle timeout seconds; 0 disables shutdown (default: 300)\n   HINDSIGHT_RETAIN_TAGS            — comma-separated tags attached to retained memories\n   HINDSIGHT_RETAIN_SOURCE          — metadata source value attached to retained memories\n   HINDSIGHT_RETAIN_USER_PREFIX     — label used before user turns in retained transcripts","@@ -45,6 +48,7 @@\n _DEFAULT_LOCAL_URL = \"http://localhost:8888\"\n _MIN_CLIENT_VERSION = \"0.4.22\"\n _DEFAULT_TIMEOUT = 120  # seconds — cloud API can take 30-40s per request\n+_DEFAULT_IDLE_TIMEOUT = 300  # seconds — Hindsight embedded daemon default\n _VALID_BUDGETS = {\"low\", \"mid\", \"high\"}\n _PROVIDER_DEFAULT_MODELS = {\n     \"openai\": \"gpt-4o-mini\",","@@ -59,6 +63,17 @@\n }\n \n \n+def _parse_int_setting(value: Any, default: int) -> int:\n+    \"\"\"Parse an integer config/env value, falling back on invalid input.\"\"\"\n+    if value is None or value == \"\":\n+        return default\n+    try:\n+        return int(value)\n+    except (TypeError, ValueError):\n+        logger.warning(\"Invalid integer Hindsight setting %r; using default %s\", value, default)\n+        return default\n+\n+\n def _check_local_runtime() -> tuple[bool, str | None]:\n     \"\"\"Return whether local embedded Hindsight imports cleanly.\n ","@@ -203,6 +218,8 @@ def _load_config() -> dict:\n     return {\n         \"mode\": os.environ.get(\"HINDSIGHT_MODE\", \"cloud\"),\n         \"apiKey\": os.environ.get(\"HINDSIGHT_API_KEY\", \"\"),\n+        \"timeout\": _parse_int_setting(os.environ.get(\"HINDSIGHT_TIMEOUT\"), _DEFAULT_TIMEOUT),\n+        \"idle_timeout\": _parse_int_setting(os.environ.get(\"HINDSIGHT_IDLE_TIMEOUT\"), _DEFAULT_IDLE_TIMEOUT),\n         \"retain_tags\": os.environ.get(\"HINDSIGHT_RETAIN_TAGS\", \"\"),\n         \"retain_source\": os.environ.get(\"HINDSIGHT_RETAIN_SOURCE\", \"\"),\n         \"retain_user_prefix\": os.environ.get(\"HINDSIGHT_RETAIN_USER_PREFIX\", \"User\"),","@@ -304,6 +321,16 @@ def _build_embedded_profile_env(config: dict[str, Any], *, llm_api_key: str | No\n     }\n     if current_base_url:\n         env_values[\"HINDSIGHT_API_LLM_BASE_URL\"] = str(current_base_url)\n+\n+    idle_timeout = (\n+        config.get(\"idle_timeout\")\n+        if config.get(\"idle_timeout\") is not None\n+        else os.environ.get(\"HINDSIGHT_IDLE_TIMEOUT\")\n+    )\n+    if idle_timeout is not None and idle_timeout != \"\":\n+        env_values[\"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT\"] = str(\n+            _parse_int_setting(idle_timeout, _DEFAULT_IDLE_TIMEOUT)\n+        )\n     return env_values\n \n ","@@ -412,6 +439,7 @@ def __init__(self):\n         self._turn_index = 0\n         self._client = None\n         self._timeout = _DEFAULT_TIMEOUT\n+        self._idle_timeout = _DEFAULT_IDLE_TIMEOUT\n         self._prefetch_result = \"\"\n         self._prefetch_lock = threading.Lock()\n         self._prefetch_thread = None","@@ -592,10 +620,17 @@ def post_setup(self, hermes_home: str, config: dict) -> None:\n             sys.stdout.write(\"  LLM API key: \")\n             sys.stdout.flush()\n             llm_key = getpass.getpass(prompt=\"\") if sys.stdin.isatty() else sys.stdin.readline().strip()\n-            # Always write explicitly (including empty) so the provider sees \"\"\n-            # rather than a missing variable.  The daemon reads from .env at\n-            # startup and fails when HINDSIGHT_LLM_API_KEY is unset.\n-            env_writes[\"HINDSIGHT_LLM_API_KEY\"] = llm_key\n+            if llm_key:\n+                env_writes[\"HINDSIGHT_LLM_API_KEY\"] = llm_key\n+            else:\n+                env_path = Path(hermes_home) / \".env\"\n+                existing_llm_key = \"\"\n+                if env_path.exists():\n+                    for line in env_path.read_text().splitlines():\n+                        if line.startswith(\"HINDSIGHT_LLM_API_KEY=\"):\n+                            existing_llm_key = line.split(\"=\", 1)[1]\n+                            break\n+                env_writes[\"HINDSIGHT_LLM_API_KEY\"] = existing_llm_key\n \n         # Step 4: Save everything\n         provider_config[\"bank_id\"] = \"hermes\"","@@ -605,6 +640,11 @@ def post_setup(self, hermes_home: str, config: dict) -> None:\n         timeout_val = existing_timeout if existing_timeout else _DEFAULT_TIMEOUT\n         provider_config[\"timeout\"] = timeout_val\n         env_writes[\"HINDSIGHT_TIMEOUT\"] = str(timeout_val)\n+        if mode == \"local_embedded\":\n+            existing_idle_timeout = self._config.get(\"idle_timeout\") if self._config else None\n+            idle_timeout_val = existing_idle_timeout if existing_idle_timeout is not None else _DEFAULT_IDLE_TIMEOUT\n+            provider_config[\"idle_timeout\"] = idle_timeout_val\n+            env_writes[\"HINDSIGHT_IDLE_TIMEOUT\"] = str(idle_timeout_val)\n         config[\"memory\"][\"provider\"] = \"hindsight\"\n         save_config(config)\n ","@@ -693,6 +733,7 @@ def get_config_schema(self):\n             {\"key\": \"recall_max_input_chars\", \"description\": \"Maximum input query length for auto-recall\", \"default\": 800},\n             {\"key\": \"recall_prompt_preamble\", \"description\": \"Custom preamble for recalled memories in context\"},\n             {\"key\": \"timeout\", \"description\": \"API request timeout in seconds\", \"default\": _DEFAULT_TIMEOUT},\n+            {\"key\": \"idle_timeout\", \"description\": \"Embedded daemon idle timeout in seconds (0 disables auto-shutdown)\", \"default\": _DEFAULT_IDLE_TIMEOUT, \"when\": {\"mode\": \"local_embedded\"}},\n         ]\n \n     def _get_client(self):","@@ -720,6 +761,14 @@ def _get_client(self):\n                 )\n                 if self._llm_base_url:\n                     kwargs[\"llm_base_url\"] = self._llm_base_url\n+                idle_timeout = _parse_int_setting(\n+                    self._config.get(\"idle_timeout\")\n+                    if self._config.get(\"idle_timeout\") is not None\n+                    else os.environ.get(\"HINDSIGHT_IDLE_TIMEOUT\", self._idle_timeout),\n+                    _DEFAULT_IDLE_TIMEOUT,\n+                )\n+                self._idle_timeout = idle_timeout\n+                kwargs[\"idle_timeout\"] = idle_timeout\n                 self._client = HindsightEmbedded(**kwargs)\n             else:\n                 from hindsight_client import Hindsight","@@ -736,6 +785,38 @@ def _run_sync(self, coro):\n         \"\"\"Schedule *coro* on the shared loop using the configured timeout.\"\"\"\n         return _run_sync(coro, timeout=self._timeout)\n \n+    def _is_retriable_embedded_connection_error(self, exc: Exception) -> bool:\n+        \"\"\"Return True for stale embedded-daemon connection failures.\"\"\"\n+        if self._mode != \"local_embedded\":\n+            return False\n+        text = f\"{type(exc).__name__}: {exc}\".lower()\n+        return any(\n+            marker in text\n+            for marker in (\n+                \"cannot connect to host\",\n+                \"connection refused\",\n+                \"connect call failed\",\n+                \"clientconnectorerror\",\n+            )\n+        )\n+\n+    def _run_hindsight_operation(self, operation):\n+        \"\"\"Run an async Hindsight client operation, retrying once after idle shutdown.\"\"\"\n+        client = self._get_client()\n+        try:\n+            return self._run_sync(operation(client))\n+        except Exception as exc:\n+            if not self._is_retriable_embedded_connection_error(exc):\n+                raise\n+            logger.info(\n+                \"Hindsight embedded daemon appears unreachable; recreating client and retrying once: %s\",\n+                exc,\n+            )\n+            self._client = None\n+            client = self._get_client()\n+            self._client = client\n+            return self._run_sync(operation(client))\n+\n     def initialize(self, session_id: str, **kwargs) -> None:\n         self._session_id = str(session_id or \"\").strip()\n         self._parent_session_id = str(kwargs.get(\"parent_session_id\", \"\") or \"\").strip()","@@ -790,7 +871,14 @@ def initialize(self, session_id: str, **kwargs) -> None:\n         self._session_turns = []\n         self._mode = self._config.get(\"mode\", \"cloud\")\n         # Read timeout from config or env var, fall back to default\n-        self._timeout = self._config.get(\"timeout\") or int(os.environ.get(\"HINDSIGHT_TIMEOUT\", str(_DEFAULT_TIMEOUT)))\n+        self._timeout = _parse_int_setting(\n+            self._config.get(\"timeout\") if self._config.get(\"timeout\") is not None else os.environ.get(\"HINDSIGHT_TIMEOUT\"),\n+            _DEFAULT_TIMEOUT,\n+        )\n+        self._idle_timeout = _parse_int_setting(\n+            self._config.get(\"idle_timeout\") if self._config.get(\"idle_timeout\") is not None else os.environ.get(\"HINDSIGHT_IDLE_TIMEOUT\"),\n+            _DEFAULT_IDLE_TIMEOUT,\n+        )\n         # \"local\" is a legacy alias for \"local_embedded\"\n         if self._mode == \"local\":\n             self._mode = \"local_embedded\"","@@ -981,10 +1069,9 @@ def queue_prefetch(self, query: str, *, session_id: str = \"\") -> None:\n \n         def _run():\n             try:\n-                client = self._get_client()\n                 if self._prefetch_method == \"reflect\":\n                     logger.debug(\"Prefetch: calling reflect (bank=%s, query_len=%d)\", self._bank_id, len(query))\n-                    resp = self._run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))\n+                    resp = self._run_hindsight_operation(lambda client: client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))\n                     text = resp.text or \"\"\n                 else:\n                     recall_kwargs: dict = {","@@ -998,7 +1085,7 @@ def _run():\n                         recall_kwargs[\"types\"] = self._recall_types\n                     logger.debug(\"Prefetch: calling recall (bank=%s, query_len=%d, budget=%s)\",\n                                  self._bank_id, len(query), self._budget)\n-                    resp = self._run_sync(client.arecall(**recall_kwargs))\n+                    resp = self._run_hindsight_operation(lambda client: client.arecall(**recall_kwargs))\n                     num_results = len(resp.results) if resp.results else 0\n                     logger.debug(\"Prefetch: recall returned %d results\", num_results)\n                     text = \"\\n\".join(f\"- {r.text}\" for r in resp.results if r.text) if resp.results else \"\"","@@ -1131,12 +1218,14 @@ def _sync():\n                 item.pop(\"retain_async\", None)\n                 logger.debug(\"Hindsight retain: bank=%s, doc=%s, async=%s, content_len=%d, num_turns=%d\",\n                              self._bank_id, self._document_id, self._retain_async, len(content), len(self._session_turns))\n-                self._run_sync(client.aretain_batch(\n-                    bank_id=self._bank_id,\n-                    items=[item],\n-                    document_id=self._document_id,\n-                    retain_async=self._retain_async,\n-                ))\n+                self._run_hindsight_operation(\n+                    lambda client: client.aretain_batch(\n+                        bank_id=self._bank_id,\n+                        items=[item],\n+                        document_id=self._document_id,\n+                        retain_async=self._retain_async,\n+                    )\n+                )\n                 logger.debug(\"Hindsight retain succeeded\")\n             except Exception as e:\n                 logger.warning(\"Hindsight sync failed: %s\", e, exc_info=True)","@@ -1152,12 +1241,6 @@ def get_tool_schemas(self) -> List[Dict[str, Any]]:\n         return [RETAIN_SCHEMA, RECALL_SCHEMA, REFLECT_SCHEMA]\n \n     def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n-        try:\n-            client = self._get_client()\n-        except Exception as e:\n-            logger.warning(\"Hindsight client init failed: %s\", e)\n-            return tool_error(f\"Hindsight client unavailable: {e}\")\n-\n         if tool_name == \"hindsight_retain\":\n             content = args.get(\"content\", \"\")\n             if not content:","@@ -1171,7 +1254,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n                 )\n                 logger.debug(\"Tool hindsight_retain: bank=%s, content_len=%d, context=%s\",\n                              self._bank_id, len(content), context)\n-                self._run_sync(client.aretain(**retain_kwargs))\n+                self._run_hindsight_operation(lambda client: client.aretain(**retain_kwargs))\n                 logger.debug(\"Tool hindsight_retain: success\")\n                 return json.dumps({\"result\": \"Memory stored successfully.\"})\n             except Exception as e:","@@ -1194,7 +1277,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n                     recall_kwargs[\"types\"] = self._recall_types\n                 logger.debug(\"Tool hindsight_recall: bank=%s, query_len=%d, budget=%s\",\n                              self._bank_id, len(query), self._budget)\n-                resp = self._run_sync(client.arecall(**recall_kwargs))\n+                resp = self._run_hindsight_operation(lambda client: client.arecall(**recall_kwargs))\n                 num_results = len(resp.results) if resp.results else 0\n                 logger.debug(\"Tool hindsight_recall: %d results\", num_results)\n                 if not resp.results:","@@ -1212,9 +1295,11 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str:\n             try:\n                 logger.debug(\"Tool hindsight_reflect: bank=%s, query_len=%d, budget=%s\",\n                              self._bank_id, len(query), self._budget)\n-                resp = self._run_sync(client.areflect(\n-                    bank_id=self._bank_id, query=query, budget=self._budget\n-                ))\n+                resp = self._run_hindsight_operation(\n+                    lambda client: client.areflect(\n+                        bank_id=self._bank_id, query=query, budget=self._budget\n+                    )\n+                )\n                 logger.debug(\"Tool hindsight_reflect: response_len=%d\", len(resp.text or \"\"))\n                 return json.dumps({\"result\": resp.text or \"No relevant memories found.\"})\n             except Exception as e:"]},{"file":"tests/plugins/memory/test_hindsight_provider.py","hunks":["@@ -7,6 +7,7 @@\n \n import json\n import re\n+import sys\n from types import SimpleNamespace\n from unittest.mock import AsyncMock, MagicMock\n ","@@ -18,6 +19,7 @@\n     REFLECT_SCHEMA,\n     RETAIN_SCHEMA,\n     _load_config,\n+    _build_embedded_profile_env,\n     _normalize_retain_tags,\n     _resolve_bank_id_template,\n     _sanitize_bank_segment,","@@ -34,7 +36,8 @@ def _clean_env(monkeypatch):\n     \"\"\"Ensure no stale env vars leak between tests.\"\"\"\n     for key in (\n         \"HINDSIGHT_API_KEY\", \"HINDSIGHT_API_URL\", \"HINDSIGHT_BANK_ID\",\n-        \"HINDSIGHT_BUDGET\", \"HINDSIGHT_MODE\", \"HINDSIGHT_LLM_API_KEY\",\n+        \"HINDSIGHT_BUDGET\", \"HINDSIGHT_MODE\", \"HINDSIGHT_TIMEOUT\",\n+        \"HINDSIGHT_IDLE_TIMEOUT\", \"HINDSIGHT_LLM_API_KEY\",\n         \"HINDSIGHT_RETAIN_TAGS\", \"HINDSIGHT_RETAIN_SOURCE\",\n         \"HINDSIGHT_RETAIN_USER_PREFIX\", \"HINDSIGHT_RETAIN_ASSISTANT_PREFIX\",\n     ):","@@ -251,6 +254,51 @@ def test_config_from_env_fallback(self, tmp_path, monkeypatch):\n         assert cfg[\"banks\"][\"hermes\"][\"bankId\"] == \"env-bank\"\n         assert cfg[\"banks\"][\"hermes\"][\"budget\"] == \"high\"\n \n+    def test_embedded_profile_env_includes_idle_timeout_from_config(self):\n+        env = _build_embedded_profile_env({\n+            \"llm_provider\": \"openai\",\n+            \"llm_model\": \"gpt-4o-mini\",\n+            \"idle_timeout\": 0,\n+        })\n+\n+        assert env[\"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT\"] == \"0\"\n+\n+    def test_embedded_profile_env_includes_idle_timeout_from_env(self, monkeypatch):\n+        monkeypatch.setenv(\"HINDSIGHT_IDLE_TIMEOUT\", \"42\")\n+\n+        env = _build_embedded_profile_env({\n+            \"llm_provider\": \"openai\",\n+            \"llm_model\": \"gpt-4o-mini\",\n+        })\n+\n+        assert env[\"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT\"] == \"42\"\n+\n+    def test_get_client_passes_idle_timeout_to_hindsight_embedded(self, monkeypatch):\n+        captured = {}\n+\n+        class FakeHindsightEmbedded:\n+            def __init__(self, **kwargs):\n+                captured.update(kwargs)\n+\n+        monkeypatch.setitem(sys.modules, \"hindsight\", SimpleNamespace(HindsightEmbedded=FakeHindsightEmbedded))\n+        monkeypatch.setattr(\"plugins.memory.hindsight._check_local_runtime\", lambda: (True, \"\"))\n+\n+        p = HindsightMemoryProvider()\n+        p._mode = \"local_embedded\"\n+        p._config = {\n+            \"profile\": \"hermes\",\n+            \"llm_provider\": \"openai_compatible\",\n+            \"llm_api_key\": \"test-key\",\n+            \"llm_model\": \"test-model\",\n+            \"idle_timeout\": 0,\n+        }\n+        p._llm_base_url = \"http://localhost:8060/v1\"\n+\n+        p._get_client()\n+\n+        assert captured[\"idle_timeout\"] == 0\n+        assert captured[\"llm_provider\"] == \"openai\"\n+\n \n class TestPostSetup:\n     def test_local_embedded_setup_materializes_profile_env(self, tmp_path, monkeypatch):","@@ -272,7 +320,10 @@ def test_local_embedded_setup_materializes_profile_env(self, tmp_path, monkeypat\n         provider.post_setup(str(hermes_home), {\"memory\": {}})\n \n         assert saved_configs[-1][\"memory\"][\"provider\"] == \"hindsight\"\n-        assert (hermes_home / \".env\").read_text() == \"HINDSIGHT_LLM_API_KEY=[redacted:secret-assignment]\"\n+        env_text = (hermes_home / \".env\").read_text()\n+        assert \"HINDSIGHT_LLM_API_KEY=[redacted:secret-assignment]\" in env_text\n+        assert \"HINDSIGHT_TIMEOUT=120\\n\" in env_text\n+        assert \"HINDSIGHT_IDLE_TIMEOUT=300\\n\" in env_text\n \n         profile_env = user_home / \".hindsight\" / \"profiles\" / \"hermes.env\"\n         assert profile_env.exists()","@@ -281,6 +332,7 @@ def test_local_embedded_setup_materializes_profile_env(self, tmp_path, monkeypat\n             \"HINDSIGHT_API_LLM_API_KEY=[redacted:secret-assignment]\"\n             \"HINDSIGHT_API_LLM_MODEL=gpt-4o-mini\\n\"\n             \"HINDSIGHT_API_LOG_LEVEL=info\\n\"\n+            \"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT=300\\n\"\n         )\n \n     def test_local_embedded_setup_respects_existing_profile_name(self, tmp_path, monkeypatch):","@@ -446,6 +498,28 @@ def test_recall_error_handling(self, provider):\n         ))\n         assert \"error\" in result\n \n+    def test_local_embedded_recall_reconnects_after_idle_shutdown(self, provider, monkeypatch):\n+        first_client = _make_mock_client()\n+        first_client.arecall.side_effect = RuntimeError(\"Cannot connect to host 127.0.0.1:8888\")\n+        second_client = _make_mock_client()\n+        second_client.arecall.return_value = SimpleNamespace(\n+            results=[SimpleNamespace(text=\"Recovered memory\")]\n+        )\n+        clients = iter([first_client, second_client])\n+\n+        provider._mode = \"local_embedded\"\n+        provider._client = first_client\n+        monkeypatch.setattr(provider, \"_get_client\", lambda: next(clients))\n+\n+        result = json.loads(provider.handle_tool_call(\n+            \"hindsight_recall\", {\"query\": \"test\"}\n+        ))\n+\n+        assert result[\"result\"] == \"1. Recovered memory\"\n+        assert provider._client is second_client\n+        first_client.arecall.assert_called_once()\n+        second_client.arecall.assert_called_once()\n+\n \n # ---------------------------------------------------------------------------\n # Prefetch tests\n"]}]},{"kind":"pr","number":42279,"title":"fix(hindsight): wire daemonIdleTimeout config to HindsightEmbedded client","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9d799e0531c","number":0},{"kind":"commit","value":"4cbf862abe4","number":0},{"kind":"commit","value":"dc75038866a3966b5bf5c82ff57c7a758bb4070a","number":0},{"kind":"commit","value":"4cbf862abe4","number":0},{"kind":"commit","value":"4cbf862abe4","number":0},{"kind":"pr","value":"#119888","number":119888}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":119888,"title":"hindsight moves to the plugin catalog (auto-migrated on first start / hermes update)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":[".github/workflows/tests-os.yml",".github/workflows/tests.yml","COMPAT_MANIFEST.md","CONTRIBUTING.es.md","CONTRIBUTING.md","Dockerfile","compat_manifest.json","hermes_cli/config_defaults.py","hermes_cli/memory_setup.py","hermes_cli/subcommands/memory.py","hermes_cli/tips.py","hermes_cli/update_cmd.py","hermes_cli/web_server_memory.py","nix/checks.nix","nix/hermes-agent.nix","nix/moduleCommon.nix","nix/packages.nix","plugins/AGENTS.md","plugins/memory/__init__.py","plugins/memory/hindsight/README.md","plugins/memory/hindsight/__init__.py","plugins/memory/hindsight/config_schema.py","plugins/memory/hindsight/embedded.py","plugins/memory/hindsight/plugin.yaml","plugins/memory/hindsight/settings.py","plugins/memory/hindsight/setup.py","plugins/memory/hindsight/templates.py","pyproject.toml","tests/agent/test_memory_session_switch.py","tests/hermes_cli/test_web_server.py","tests/plugins/memory/test_bom_tolerant_config_reads.py","tests/plugins/memory/test_hindsight_config_schema.py","tests/plugins/memory/test_hindsight_env_perms.py","tests/plugins/memory/test_hindsight_local_runtime_hint.py","tests/plugins/memory/test_hindsight_provider.py","tests/plugins/memory/test_hindsight_templates.py","tests/plugins/memory/test_multiplex_memory_identity_scope.py","tests/plugins/memory/test_provider_threads_inherit_profile.py","tests/plugins/test_hindsight_health_grace_timeout.py","tests/plugins/test_hindsight_root_guard.py","tests/test_project_metadata.py","tests/tools/test_lazy_deps.py","tools/lazy_deps.py","website/docs/getting-started/nix-setup.md","website/docs/integrations/index.md","website/docs/reference/cli-commands.md","website/docs/user-guide/docker.md","website/docs/user-guide/features/memory-providers.md","website/docs/user-guide/features/memory.md","website/docs/user-guide/features/overview.md"]}],"trust":{"default_tier":"verified","includes_suggested":false}}