# NousResearch/hermes-agent · Hindsight cloud client timeout configurability

## Open now

- 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.

## TL;DR

- 6 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 2 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #15077. Verify it addresses your case.
- **Recommended:** read PR #15077 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.33`: the share of member 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.

## Closed / duplicate issues

- #9869: \[Bug\]: Hindsight plugin hardcoded 30s timeout causes hindsight_reflect to fail

## Prior & competing PRs

- **#15077: feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `plugins/memory/hindsight/__init__.py`, `tests/plugins/memory/test_hindsight_provider.py`
  - fix hunks:
  - `plugins/memory/hindsight/__init__.py`

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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",
    ```

    ```diff
    @@ -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
     # ---------------------------------------------------------------------------
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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)
     
    ```

    ```diff
    @@ -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):
    ```

    ```diff
    @@ -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()
     
    ```

    ```diff
    @@ -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"
    ```

    ```diff
    @@ -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 = {
    ```

    ```diff
    @@ -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 ""
    ```

    ```diff
    @@ -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,
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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:
    ```

    ```diff
    @@ -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 ""))
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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 · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - 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)
- **#9882: fix(hindsight): make cloud client timeout configurable**. closed without merge
  - maintainer direction: category `silent_close`; implication `active`
- **#9984: fix: make Hindsight cloud client timeout configurable**. closed without merge
  - maintainer direction: category `stale_abandoned`; implication `active`
- **#13067: feat(hindsight): add configurable HINDSIGHT_TIMEOUT env var**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#10045`

## Fix status

- Merged to main in PR #15077 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-09-23`.
