Hindsight cloud client timeout configurability

NousResearch/hermes-agent · cluster 9869

Data as of

Nothing open on GitHub. Fix merged in #15077. Check it covers your case.

The short answer

0open issues
0open PRs
6items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #15077 feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var

    merged to main

    files: plugins/memory/hindsight/__init__.py, tests/plugins/memory/test_hindsight_provider.py

    Show 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)

    merged to main

    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

  • #9882 fix(hindsight): make cloud client timeout configurable · closed without comment
  • #9984 fix: make Hindsight cloud client timeout configurable · stale or abandoned
  • #13067 feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var · duplicate → #10045
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.00 means 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

At a glance

Merged PRs
#15077, #119888
Closed issues
1
Attempts
3 PRs closed without merging
Fix link checked
23 Sep 2026

For your agent

The same facts, typed.