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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'}",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user