From 9cf0a802f5153090987539adfd4b189393f91009 Mon Sep 17 00:00:00 2001 From: DK09876 Date: Thu, 25 Jun 2026 23:40:54 -0700 Subject: [PATCH] fix(aider,openhands): close client on exit + OpenHands Docker MCP docs From real-app integration testing: - aider: close the Hindsight client when the wrapper owns it, so aiohttp no longer prints 'Unclosed connector' warnings after aider exits. Test-injected clients are left to the caller. Bump 0.1.1. - openhands: document that the OpenHands Docker app loads MCP from UI settings (not the project config.toml), and that the server must be added as a Streamable HTTP server (not SSE) reachable via host.docker.internal. Same hint printed by 'init'. Bump 0.1.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../aider/hindsight_aider/runner.py | 55 +++++++++++++------ hindsight-integrations/aider/pyproject.toml | 2 +- .../aider/tests/test_runner.py | 18 ++++++ hindsight-integrations/aider/uv.lock | 2 +- hindsight-integrations/openhands/README.md | 14 +++++ .../openhands/hindsight_openhands/cli.py | 5 ++ .../openhands/pyproject.toml | 2 +- hindsight-integrations/openhands/uv.lock | 2 +- 8 files changed, 80 insertions(+), 20 deletions(-) diff --git a/hindsight-integrations/aider/hindsight_aider/runner.py b/hindsight-integrations/aider/hindsight_aider/runner.py index 5a7fba0f5..ce382c998 100644 --- a/hindsight-integrations/aider/hindsight_aider/runner.py +++ b/hindsight-integrations/aider/hindsight_aider/runner.py @@ -97,6 +97,20 @@ def do_retain(client: Any, config: AiderConfig, bank_id: str, transcript: str) - logger.warning("Hindsight retain failed: %s", e) +def _close_client(client: Any) -> None: + """Best-effort close of the Hindsight client's HTTP session. + + The client wraps an aiohttp session; leaving it open prints + "Unclosed connector" warnings after aider exits. + """ + close = getattr(client, "close", None) + if callable(close): + try: + close() + except Exception: # pragma: no cover - cleanup best-effort + pass + + def _default_run_aider(cmd: list[str]) -> int: """Run aider, inheriting stdio so the session is interactive.""" try: @@ -119,25 +133,34 @@ def run( ) -> int: """Recall -> run aider -> retain. Returns Aider's exit code.""" config = config or load_config() + # Track whether we created the client so we can close it on exit. A + # test-injected client is left for the caller to manage. + owns_client = client is None client = client or resolve_client(config) bank_id = resolve_bank_id(config) memory_path = Path(config.memory_filename) history_path = Path(config.chat_history_file) - injected = False - if config.auto_recall: - query = compose_recall_query(aider_args, config.recall_default_query) - injected = do_recall(client, config, bank_id, query, memory_path) - - prev_size = history_size(history_path) - - cmd = build_aider_command(config, aider_args, memory_path if injected else None) - code = (run_aider or _default_run_aider)(cmd) - - if config.auto_retain: - transcript = format_transcript(read_history_delta(history_path, prev_size)) - if transcript: - do_retain(client, config, bank_id, transcript) - - return code + try: + injected = False + if config.auto_recall: + query = compose_recall_query(aider_args, config.recall_default_query) + injected = do_recall(client, config, bank_id, query, memory_path) + + prev_size = history_size(history_path) + + cmd = build_aider_command(config, aider_args, memory_path if injected else None) + code = (run_aider or _default_run_aider)(cmd) + + if config.auto_retain: + transcript = format_transcript(read_history_delta(history_path, prev_size)) + if transcript: + do_retain(client, config, bank_id, transcript) + + return code + finally: + # Close the HTTP session we opened so aiohttp doesn't print + # "Unclosed connector" warnings after aider exits. + if owns_client: + _close_client(client) diff --git a/hindsight-integrations/aider/pyproject.toml b/hindsight-integrations/aider/pyproject.toml index 64eb5e1be..a5a55cf5b 100644 --- a/hindsight-integrations/aider/pyproject.toml +++ b/hindsight-integrations/aider/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hindsight-aider" -version = "0.1.0" +version = "0.1.1" description = "Aider integration for Hindsight - persistent long-term memory across pair-programming sessions" readme = "README.md" requires-python = ">=3.10" diff --git a/hindsight-integrations/aider/tests/test_runner.py b/hindsight-integrations/aider/tests/test_runner.py index 4c6f25229..f435c7c0d 100644 --- a/hindsight-integrations/aider/tests/test_runner.py +++ b/hindsight-integrations/aider/tests/test_runner.py @@ -157,3 +157,21 @@ def test_passes_through_exit_code(self, tmp_path): cfg = _config(tmp_path) client = make_client([]) assert run([], config=cfg, client=client, run_aider=lambda cmd: 3) == 3 + + def test_closes_client_it_created(self, tmp_path, monkeypatch): + """A client run() resolves itself is closed on exit (avoids the aiohttp + 'Unclosed connector' warning).""" + import hindsight_aider.runner as runner_mod + + client = make_client([]) + monkeypatch.setattr(runner_mod, "resolve_client", lambda config: client) + cfg = _config(tmp_path, auto_recall=False, auto_retain=False) + run([], config=cfg, run_aider=lambda cmd: 0) # no client= -> run() owns it + client.close.assert_called_once() + + def test_injected_client_not_closed(self, tmp_path): + """A test/caller-injected client is left for the caller to manage.""" + client = make_client([]) + cfg = _config(tmp_path, auto_recall=False, auto_retain=False) + run([], config=cfg, client=client, run_aider=lambda cmd: 0) + client.close.assert_not_called() diff --git a/hindsight-integrations/aider/uv.lock b/hindsight-integrations/aider/uv.lock index ee2d3414c..c9036997a 100644 --- a/hindsight-integrations/aider/uv.lock +++ b/hindsight-integrations/aider/uv.lock @@ -344,7 +344,7 @@ wheels = [ [[package]] name = "hindsight-aider" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "hindsight-client" }, diff --git a/hindsight-integrations/openhands/README.md b/hindsight-integrations/openhands/README.md index 13006110d..1e083ffad 100644 --- a/hindsight-integrations/openhands/README.md +++ b/hindsight-integrations/openhands/README.md @@ -40,6 +40,20 @@ open local server). > snippet to paste instead of touching the file. `hindsight-openhands init > --print-only` shows the snippet + rule anytime. +### Running the OpenHands Docker app? + +The containerized OpenHands app loads MCP servers from its **UI settings**, not +from a project `config.toml`. So add the server in **Settings → MCP** as a +**Streamable HTTP** server (not SSE — Hindsight's endpoint is streamable HTTP): + +- **URL:** `http://host.docker.internal:8888/mcp//` (use `host.docker.internal`, + not `localhost`, so the container can reach a Hindsight server on your host; + launch the app with `--add-host host.docker.internal:host-gateway`) +- **API key:** your `hsk_...` for Cloud, or none for an open local server + +The MCP tools load when a conversation starts. The `AGENTS.md` rule still applies +when you open the project as a repository. + ## Commands | Command | Description | diff --git a/hindsight-integrations/openhands/hindsight_openhands/cli.py b/hindsight-integrations/openhands/hindsight_openhands/cli.py index 183b74430..6f697f524 100644 --- a/hindsight-integrations/openhands/hindsight_openhands/cli.py +++ b/hindsight-integrations/openhands/hindsight_openhands/cli.py @@ -25,6 +25,7 @@ apply_to_config, build_shttp_server, default_config_path, + mcp_endpoint_url, remove_from_config, render_snippet, ) @@ -106,6 +107,10 @@ def cmd_init(args: argparse.Namespace) -> None: print(f" Wrote recall/retain rule to {outcome.agents_md_path}") print("\nDone. Start OpenHands in this project — the hindsight MCP tools") print("(recall/retain/reflect) are available and used automatically.") + print("\nRunning the OpenHands Docker app? It reads MCP servers from its UI") + print("settings, not this config.toml — add the server in Settings -> MCP as") + print(f"a Streamable HTTP (shttp) server, URL {mcp_endpoint_url(cfg.hindsight_api_url, cfg.bank_id)}") + print("(use http://host.docker.internal: so the container can reach a local server).") def cmd_status(args: argparse.Namespace) -> None: diff --git a/hindsight-integrations/openhands/pyproject.toml b/hindsight-integrations/openhands/pyproject.toml index 5d1ddecb9..6573fde2b 100644 --- a/hindsight-integrations/openhands/pyproject.toml +++ b/hindsight-integrations/openhands/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "hindsight-openhands" -version = "0.1.0" +version = "0.1.1" description = "OpenHands integration for Hindsight - persistent long-term memory via MCP" readme = "README.md" requires-python = ">=3.10" diff --git a/hindsight-integrations/openhands/uv.lock b/hindsight-integrations/openhands/uv.lock index fb4598fb5..36607d786 100644 --- a/hindsight-integrations/openhands/uv.lock +++ b/hindsight-integrations/openhands/uv.lock @@ -25,7 +25,7 @@ wheels = [ [[package]] name = "hindsight-openhands" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "tomlkit" },