{"repo":"NousResearch/hermes-agent","public_id":"9869","display_label":"Hindsight cloud client timeout configurability","summary":"These issues and PRs address a hardcoded 30-second timeout in the Hindsight plugin's cloud client, proposing or implementing a configurable HINDSIGHT_TIMEOUT environment variable as a fix.","subsystem":null,"counts":{"members":6,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":3},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":15077},"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.3333333333333333},"fix_edges":[{"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":15077,"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":9869,"title":"[Bug]: Hindsight plugin hardcoded 30s timeout causes hindsight_reflect to fail","url":null,"state":"closed"},{"kind":"pr","number":9882,"title":"fix(hindsight): make cloud client timeout configurable","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":9984,"title":"fix: make Hindsight cloud client timeout configurable","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":13067,"title":"feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#10045","number":10045}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":15077,"title":"feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var","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,6 +3,8 @@\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+\n Original PR #1811 by benfrank241, adapted to MemoryProvider ABC.\n \n Config via environment variables:","@@ -11,6 +13,7 @@\n   HINDSIGHT_BUDGET                 — recall budget: low/mid/high (default: mid)\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_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","@@ -41,6 +44,7 @@\n _DEFAULT_API_URL = \"https://api.hindsight.vectorize.io\"\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 _VALID_BUDGETS = {\"low\", \"mid\", \"high\"}\n _PROVIDER_DEFAULT_MODELS = {\n     \"openai\": \"gpt-4o-mini\",","@@ -98,13 +102,18 @@ def _run():\n         return _loop\n \n \n-def _run_sync(coro, timeout: float = 120.0):\n+def _run_sync(coro, timeout: float = _DEFAULT_TIMEOUT):\n     \"\"\"Schedule *coro* on the shared loop and block until done.\"\"\"\n     loop = _get_loop()\n     future = asyncio.run_coroutine_threadsafe(coro, loop)\n     return future.result(timeout=timeout)\n \n \n+# ---------------------------------------------------------------------------\n+# Backward-compatible alias — instances use self._run_sync() instead.\n+# ---------------------------------------------------------------------------\n+\n+\n # ---------------------------------------------------------------------------\n # Tool schemas\n # ---------------------------------------------------------------------------","@@ -347,6 +356,7 @@ def __init__(self):\n         self._agent_identity = \"\"\n         self._turn_index = 0\n         self._client = None\n+        self._timeout = _DEFAULT_TIMEOUT\n         self._prefetch_result = \"\"\n         self._prefetch_lock = threading.Lock()\n         self._prefetch_thread = None","@@ -532,6 +542,11 @@ def post_setup(self, hermes_home: str, config: dict) -> None:\n         # Step 4: Save everything\n         provider_config[\"bank_id\"] = \"hermes\"\n         provider_config[\"recall_budget\"] = \"mid\"\n+        # Read existing timeout from config if present, otherwise use default\n+        existing_timeout = self._config.get(\"timeout\") if self._config else 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         config[\"memory\"][\"provider\"] = \"hindsight\"\n         save_config(config)\n ","@@ -618,6 +633,7 @@ def get_config_schema(self):\n             {\"key\": \"recall_max_tokens\", \"description\": \"Maximum tokens for recall results\", \"default\": 4096},\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         ]\n \n     def _get_client(self):","@@ -648,14 +664,19 @@ def _get_client(self):\n                 self._client = HindsightEmbedded(**kwargs)\n             else:\n                 from hindsight_client import Hindsight\n-                kwargs = {\"base_url\": self._api_url, \"timeout\": 30.0}\n+                timeout = self._timeout or _DEFAULT_TIMEOUT\n+                kwargs = {\"base_url\": self._api_url, \"timeout\": float(timeout)}\n                 if self._api_key:\n                     [redacted:secret-assignment]\"api_key\"] = self._api_key\n-                logger.debug(\"Creating Hindsight cloud client (url=%s, has_key=%s)\",\n-                             self._api_url, bool(self._api_key))\n+                logger.debug(\"Creating Hindsight cloud client (url=%s, has_key=%s, timeout=%s)\",\n+                             self._api_url, bool(self._api_key), kwargs[\"timeout\"])\n                 self._client = Hindsight(**kwargs)\n         return self._client\n \n+    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 initialize(self, session_id: str, **kwargs) -> None:\n         self._session_id = str(session_id or \"\").strip()\n ","@@ -699,6 +720,8 @@ def initialize(self, session_id: str, **kwargs) -> None:\n         self._turn_index = 0\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         # \"local\" is a legacy alias for \"local_embedded\"\n         if self._mode == \"local\":\n             self._mode = \"local_embedded\"","@@ -878,7 +901,7 @@ def _run():\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 = _run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))\n+                    resp = self._run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget))\n                     text = resp.text or \"\"\n                 else:\n                     recall_kwargs: dict = {","@@ -892,7 +915,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 = _run_sync(client.arecall(**recall_kwargs))\n+                    resp = self._run_sync(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 \"\"","@@ -1018,7 +1041,7 @@ 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._session_id, self._retain_async, len(content), len(self._session_turns))\n-                _run_sync(client.aretain_batch(\n+                self._run_sync(client.aretain_batch(\n                     bank_id=self._bank_id,\n                     items=[item],\n                     document_id=self._session_id,","@@ -1058,7 +1081,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-                _run_sync(client.aretain(**retain_kwargs))\n+                self._run_sync(client.aretain(**retain_kwargs))\n                 logger.debug(\"Tool hindsight_retain: success\")\n                 return json.dumps({\"result\": \"Memory stored successfully.\"})\n             except Exception as e:","@@ -1081,7 +1104,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 = _run_sync(client.arecall(**recall_kwargs))\n+                resp = self._run_sync(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:","@@ -1099,7 +1122,7 @@ 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 = _run_sync(client.areflect(\n+                resp = self._run_sync(client.areflect(\n                     bank_id=self._bank_id, query=query, budget=self._budget\n                 ))\n                 logger.debug(\"Tool hindsight_reflect: response_len=%d\", len(resp.text or \"\"))","@@ -1126,7 +1149,7 @@ def shutdown(self) -> None:\n                     except RuntimeError:\n                         pass\n                 else:\n-                    _run_sync(self._client.aclose())\n+                    self._run_sync(self._client.aclose())\n             except Exception:\n                 pass\n             self._client = None"]},{"file":"tests/plugins/memory/test_hindsight_provider.py","hunks":["@@ -270,7 +270,7 @@ 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+        assert (hermes_home / \".env\").read_text() == \"HINDSIGHT_LLM_API_KEY=[redacted:secret-assignment]\"\n \n         profile_env = user_home / \".hindsight\" / \"profiles\" / \"hermes.env\"\n         assert profile_env.exists()\n"]}]},{"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}}