fix(runtime): close API clients on shutdown

Close API clients when the runtime shuts down to release HTTP connection pool resources, and surface the active session ID in /session output.

Cherry-picked from PR #273. Excluded the embedded-wrapper session resolver and memory parser API changes because they need a narrower public API design.
This commit is contained in:
guyuming
2026-05-27 10:19:21 +00:00
committed by tjb-tech
parent a68d0f984d
commit bf5931e7c1
7 changed files with 50 additions and 0 deletions
+4
View File
@@ -150,6 +150,10 @@ class AnthropicApiClient:
kwargs["base_url"] = self._base_url
return AsyncAnthropic(**kwargs)
async def close(self) -> None:
"""Close the underlying HTTP client."""
await self._client.close()
def _refresh_client_auth(self) -> None:
if not self._claude_oauth or self._auth_token_resolver is None:
return
+4
View File
@@ -128,3 +128,7 @@ class CopilotClient:
)
async for event in self._inner.stream_message(patched):
yield event
async def close(self) -> None:
"""Close the underlying OpenAI-compatible client."""
await self._inner.close()
+4
View File
@@ -276,6 +276,10 @@ class OpenAICompatibleClient:
kwargs["timeout"] = timeout
self._client = AsyncOpenAI(**kwargs)
async def close(self) -> None:
"""Close the underlying HTTP client."""
await self._client.close()
async def stream_message(self, request: ApiMessageRequest) -> AsyncIterator[ApiStreamEvent]:
"""Yield text deltas and the final message, matching the Anthropic client interface."""
last_error: Exception | None = None
+1
View File
@@ -867,6 +867,7 @@ def create_default_command_registry(
latest = session_dir / "latest.json"
transcript = session_dir / "transcript.md"
lines = [
f"Session ID: {context.session_id or '(none)'}",
f"Session directory: {session_dir}",
f"Latest snapshot: {'present' if latest.exists() else 'missing'}",
f"Transcript export: {'present' if transcript.exists() else 'missing'}",
+3
View File
@@ -496,6 +496,9 @@ async def close_runtime(bundle: RuntimeBundle) -> None:
HookEvent.SESSION_END,
{"cwd": bundle.cwd, "event": HookEvent.SESSION_END.value},
)
close_api_client = getattr(bundle.api_client, "close", None)
if close_api_client is not None:
await close_api_client()
def _last_user_text(messages: list[ConversationMessage]) -> str:
+2
View File
@@ -1155,7 +1155,9 @@ async def test_agents_session_files_and_reload_plugins_commands(tmp_path: Path,
(tmp_path / "src" / "app.py").write_text("print('hi')\n", encoding="utf-8")
session_command, session_args = registry.lookup("/session")
context.session_id = "session-smoke"
session_result = await session_command.handler(session_args, context)
assert "Session ID: session-smoke" in session_result.message
assert "Session directory:" in session_result.message
session_path_command, session_path_args = registry.lookup("/session path")
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
from pathlib import Path
import pytest
from openharness.ui.runtime import build_runtime, close_runtime
class _ClosableApiClient:
def __init__(self) -> None:
self.closed = False
async def stream_message(self, request):
del request
if False:
yield None
async def close(self) -> None:
self.closed = True
@pytest.mark.asyncio
async def test_close_runtime_closes_api_client(tmp_path: Path, monkeypatch):
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
client = _ClosableApiClient()
bundle = await build_runtime(cwd=str(tmp_path), api_client=client)
await close_runtime(bundle)
assert client.closed is True