diff --git a/CHANGELOG.md b/CHANGELOG.md index b3705fc7..edbb51ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/microsoft/conductor/compare/v0.1.23...HEAD) +### Fixed + +- **Copilot dialog turns no longer fail after a hidden 120-second deadline** — + lightweight dialog sessions now honor `runtime.max_session_seconds` (or the + Copilot provider's 1800-second default), and timeout errors are classified as + retryable so console and web dialogs can continue without losing history. + ## [0.1.23](https://github.com/microsoft/conductor/compare/v0.1.22...v0.1.23) - 2026-07-20 ### Added diff --git a/src/conductor/providers/copilot.py b/src/conductor/providers/copilot.py index b80393ef..aaac3642 100644 --- a/src/conductor/providers/copilot.py +++ b/src/conductor/providers/copilot.py @@ -2468,12 +2468,13 @@ def on_event(event: Any) -> None: session.on(on_event) await session.send(full_prompt) + dialog_timeout = self._idle_recovery_config.max_session_seconds try: - await asyncio.wait_for(done.wait(), timeout=120.0) + await asyncio.wait_for(done.wait(), timeout=dialog_timeout) except TimeoutError as exc: raise ProviderError( - "Dialog turn timed out after 120s", - is_retryable=False, + f"Dialog turn timed out after {dialog_timeout:g}s", + is_retryable=True, ) from exc if error_message: diff --git a/tests/test_providers/test_copilot.py b/tests/test_providers/test_copilot.py index 19298330..9278f27c 100644 --- a/tests/test_providers/test_copilot.py +++ b/tests/test_providers/test_copilot.py @@ -8,7 +8,12 @@ from conductor.config.schema import AgentDef, ProviderSettings, ToolOutputConfig from conductor.exceptions import ProviderError -from conductor.providers.copilot import CopilotProvider, RetryConfig, SDKResponse +from conductor.providers.copilot import ( + CopilotProvider, + IdleRecoveryConfig, + RetryConfig, + SDKResponse, +) def stub_handler(agent: AgentDef, prompt: str, context: dict[str, Any]) -> dict[str, Any]: @@ -1242,6 +1247,36 @@ async def create_session(**kwargs: Any) -> Any: history=[], ) + @pytest.mark.asyncio + async def test_dialog_turn_honors_configured_session_timeout(self) -> None: + """Dialog turns use the configured session limit, not a fixed 120s cap.""" + from unittest.mock import AsyncMock as _AsyncMock + + provider = CopilotProvider( + mock_handler=stub_handler, + idle_recovery_config=IdleRecoveryConfig(max_session_seconds=0.01), + ) + provider._started = True + + session = _AsyncMock() + session.on = lambda callback: None + session.send = _AsyncMock() + session.destroy = _AsyncMock() + + client = _AsyncMock() + client.create_session = _AsyncMock(return_value=session) + provider._client = client + + with pytest.raises(ProviderError, match="timed out after 0.01s") as exc_info: + await provider.execute_dialog_turn( + system_prompt="sys", + user_message="hi", + history=[], + ) + + assert exc_info.value.is_retryable is True + session.destroy.assert_awaited_once() + class TestCopilotProviderLargeOutput: """Tests for ``large_output`` forwarding to the Copilot SDK."""