Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: the "so console and web dialogs can continue without losing history" part already happens today regardless of is_retryable — both dialog call sites roll back the user turn and continue on any exception, not just retryable ones. Might be worth tightening this to just describe the timeout fix itself.


## [0.1.23](https://github.com/microsoft/conductor/compare/v0.1.22...v0.1.23) - 2026-07-20

### Added
Expand Down
7 changes: 4 additions & 3 deletions src/conductor/providers/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bumps the default dialog-turn timeout from 120s to 1800s for anyone who hasn't set runtime.max_session_seconds, and it's a single flat wait_for with none of the periodic idle-check/recovery logic the main agent loop has around this same config value. If a dialog session genuinely hangs, a console or web user could be staring at nothing for 30 minutes before seeing an error.

Would a shorter, dialog-specific default (or at least a log line once idle_timeout_seconds has elapsed) be worth adding here, so a stall isn't completely silent until the full deadline hits?

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither gates/dialog.py nor dialog_evaluator.py look at .is_retryable on this error — both just catch a bare Exception and move on. So this flip doesn't change any observable behavior today, and it puts this raise at odds with the main execution loop, which treats the identical max_session_seconds timeout as non-retryable on purpose (see the # Don't retry — same root cause will recur comment a bit further up in this file).

If there's a future retry path planned for dialog turns, a short comment here explaining the divergence would help the next person who reads this. Otherwise I'd lean toward keeping this False to match the main loop.

Suggested change
is_retryable=True,
is_retryable=False,

) from exc

if error_message:
Expand Down
37 changes: 36 additions & 1 deletion tests/test_providers/test_copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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."""
Expand Down
Loading