Nothing open on GitHub. Fix merged in #15077. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #15077. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
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.
Open issues0
Open PRs0
Fixes on record
-
#15077 feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var
files:
plugins/memory/hindsight/__init__.py,tests/plugins/memory/test_hindsight_provider.pyShow the diff
-
plugins/memory/hindsight/__init__.py@@ -3,6 +3,8 @@ Long-term memory with knowledge graph, entity resolution, and multi-strategy retrieval. Supports cloud (API key) and local modes. +Configurable timeout via HINDSIGHT_TIMEOUT env var or config.json. + Original PR #1811 by benfrank241, adapted to MemoryProvider ABC. Config via environment variables: @@ -11,6 +13,7 @@ HINDSIGHT_BUDGET — recall budget: low/mid/high (default: mid) HINDSIGHT_API_URL — API endpoint HINDSIGHT_MODE — cloud or local (default: cloud) + HINDSIGHT_TIMEOUT — API request timeout in seconds (default: 120) HINDSIGHT_RETAIN_TAGS — comma-separated tags attached to retained memories HINDSIGHT_RETAIN_SOURCE — metadata source value attached to retained memories HINDSIGHT_RETAIN_USER_PREFIX — label used before user turns in retained transcripts @@ -41,6 +44,7 @@ _DEFAULT_API_URL = "https://api.hindsight.vectorize.io" _DEFAULT_LOCAL_URL = "http://localhost:8888" _MIN_CLIENT_VERSION = "0.4.22" +_DEFAULT_TIMEOUT = 120 # seconds — cloud API can take 30-40s per request _VALID_BUDGETS = {"low", "mid", "high"} _PROVIDER_DEFAULT_MODELS = { "openai": "gpt-4o-mini", @@ -98,13 +102,18 @@ def _run(): return _loop -def _run_sync(coro, timeout: float = 120.0): +def _run_sync(coro, timeout: float = _DEFAULT_TIMEOUT): """Schedule *coro* on the shared loop and block until done.""" loop = _get_loop() future = asyncio.run_coroutine_threadsafe(coro, loop) return future.result(timeout=timeout) +# --------------------------------------------------------------------------- +# Backward-compatible alias — instances use self._run_sync() instead. +# --------------------------------------------------------------------------- + + # --------------------------------------------------------------------------- # Tool schemas # --------------------------------------------------------------------------- @@ -347,6 +356,7 @@ def __init__(self): self._agent_identity = "" self._turn_index = 0 self._client = None + self._timeout = _DEFAULT_TIMEOUT self._prefetch_result = "" self._prefetch_lock = threading.Lock() self._prefetch_thread = None @@ -532,6 +542,11 @@ def post_setup(self, hermes_home: str, config: dict) -> None: # Step 4: Save everything provider_config["bank_id"] = "hermes" provider_config["recall_budget"] = "mid" + # Read existing timeout from config if present, otherwise use default + existing_timeout = self._config.get("timeout") if self._config else None + timeout_val = existing_timeout if existing_timeout else _DEFAULT_TIMEOUT + provider_config["timeout"] = timeout_val + env_writes["HINDSIGHT_TIMEOUT"] = str(timeout_val) config["memory"]["provider"] = "hindsight" save_config(config) @@ -618,6 +633,7 @@ def get_config_schema(self): {"key": "recall_max_tokens", "description": "Maximum tokens for recall results", "default": 4096}, {"key": "recall_max_input_chars", "description": "Maximum input query length for auto-recall", "default": 800}, {"key": "recall_prompt_preamble", "description": "Custom preamble for recalled memories in context"}, + {"key": "timeout", "description": "API request timeout in seconds", "default": _DEFAULT_TIMEOUT}, ] def _get_client(self): @@ -648,14 +664,19 @@ def _get_client(self): self._client = HindsightEmbedded(**kwargs) else: from hindsight_client import Hindsight - kwargs = {"base_url": self._api_url, "timeout": 30.0} + timeout = self._timeout or _DEFAULT_TIMEOUT + kwargs = {"base_url": self._api_url, "timeout": float(timeout)} if self._api_key: [redacted:secret-assignment]"api_key"] = self._api_key - logger.debug("Creating Hindsight cloud client (url=%s, has_key=%s)", - self._api_url, bool(self._api_key)) + logger.debug("Creating Hindsight cloud client (url=%s, has_key=%s, timeout=%s)", + self._api_url, bool(self._api_key), kwargs["timeout"]) self._client = Hindsight(**kwargs) return self._client + def _run_sync(self, coro): + """Schedule *coro* on the shared loop using the configured timeout.""" + return _run_sync(coro, timeout=self._timeout) + def initialize(self, session_id: str, **kwargs) -> None: self._session_id = str(session_id or "").strip() @@ -699,6 +720,8 @@ def initialize(self, session_id: str, **kwargs) -> None: self._turn_index = 0 self._session_turns = [] self._mode = self._config.get("mode", "cloud") + # Read timeout from config or env var, fall back to default + self._timeout = self._config.get("timeout") or int(os.environ.get("HINDSIGHT_TIMEOUT", str(_DEFAULT_TIMEOUT))) # "local" is a legacy alias for "local_embedded" if self._mode == "local": self._mode = "local_embedded" @@ -878,7 +901,7 @@ def _run(): client = self._get_client() if self._prefetch_method == "reflect": logger.debug("Prefetch: calling reflect (bank=%s, query_len=%d)", self._bank_id, len(query)) - resp = _run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget)) + resp = self._run_sync(client.areflect(bank_id=self._bank_id, query=query, budget=self._budget)) text = resp.text or "" else: recall_kwargs: dict = { @@ -892,7 +915,7 @@ def _run(): recall_kwargs["types"] = self._recall_types logger.debug("Prefetch: calling recall (bank=%s, query_len=%d, budget=%s)", self._bank_id, len(query), self._budget) - resp = _run_sync(client.arecall(**recall_kwargs)) + resp = self._run_sync(client.arecall(**recall_kwargs)) num_results = len(resp.results) if resp.results else 0 logger.debug("Prefetch: recall returned %d results", num_results) text = "\n".join(f"- {r.text}" for r in resp.results if r.text) if resp.results else "" @@ -1018,7 +1041,7 @@ def _sync(): item.pop("retain_async", None) logger.debug("Hindsight retain: bank=%s, doc=%s, async=%s, content_len=%d, num_turns=%d", self._bank_id, self._session_id, self._retain_async, len(content), len(self._session_turns)) - _run_sync(client.aretain_batch( + self._run_sync(client.aretain_batch( bank_id=self._bank_id, items=[item], document_id=self._session_id, @@ -1058,7 +1081,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: ) logger.debug("Tool hindsight_retain: bank=%s, content_len=%d, context=%s", self._bank_id, len(content), context) - _run_sync(client.aretain(**retain_kwargs)) + self._run_sync(client.aretain(**retain_kwargs)) logger.debug("Tool hindsight_retain: success") return json.dumps({"result": "Memory stored successfully."}) except Exception as e: @@ -1081,7 +1104,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: recall_kwargs["types"] = self._recall_types logger.debug("Tool hindsight_recall: bank=%s, query_len=%d, budget=%s", self._bank_id, len(query), self._budget) - resp = _run_sync(client.arecall(**recall_kwargs)) + resp = self._run_sync(client.arecall(**recall_kwargs)) num_results = len(resp.results) if resp.results else 0 logger.debug("Tool hindsight_recall: %d results", num_results) if not resp.results: @@ -1099,7 +1122,7 @@ def handle_tool_call(self, tool_name: str, args: dict, **kwargs) -> str: try: logger.debug("Tool hindsight_reflect: bank=%s, query_len=%d, budget=%s", self._bank_id, len(query), self._budget) - resp = _run_sync(client.areflect( + resp = self._run_sync(client.areflect( bank_id=self._bank_id, query=query, budget=self._budget )) logger.debug("Tool hindsight_reflect: response_len=%d", len(resp.text or "")) @@ -1126,7 +1149,7 @@ def shutdown(self) -> None: except RuntimeError: pass else: - _run_sync(self._client.aclose()) + self._run_sync(self._client.aclose()) except Exception: pass self._client = None -
tests/plugins/memory/test_hindsight_provider.py@@ -270,7 +270,7 @@ def test_local_embedded_setup_materializes_profile_env(self, tmp_path, monkeypat provider.post_setup(str(hermes_home), {"memory": {}}) assert saved_configs[-1]["memory"]["provider"] == "hindsight" - assert (hermes_home / ".env").read_text() == "HINDSIGHT_LLM_API_KEY=[redacted:secret-assignment]" + assert (hermes_home / ".env").read_text() == "HINDSIGHT_LLM_API_KEY=[redacted:secret-assignment]" profile_env = user_home / ".hindsight" / "profiles" / "hermes.env" assert profile_env.exists()
-
-
#119888 hindsight moves to the plugin catalog (auto-migrated on first start / hermes update)
files:
.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(+30 more)
3 PRs closed without merging
From the close comments: closed without comment 1 · duplicate 1 · stale or abandoned 1
Closed issues · 1
- Closed#9869[Bug]: Hindsight plugin hardcoded 30s timeout causes hindsight_reflect to fail
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.33: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the Lab