Skip to content

fix(turns): dispatch on_user_turn_stop_timeout synchronously so handlers see turn state - #5280

Closed
scttbnsn wants to merge 2 commits into
pipecat-ai:mainfrom
scttbnsn:fix/user-turn-stop-timeout-sync-handler
Closed

fix(turns): dispatch on_user_turn_stop_timeout synchronously so handlers see turn state#5280
scttbnsn wants to merge 2 commits into
pipecat-ai:mainfrom
scttbnsn:fix/user-turn-stop-timeout-sync-handler

Conversation

@scttbnsn

@scttbnsn scttbnsn commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #4848.

The bug

LLMUserAggregator registers on_user_turn_stop_timeout as a regular (async) event, so the user's handler is scheduled as a fire-and-forget task. The watchdog path in UserTurnController._user_turn_stop_timeout_task_handler then goes straight on to _trigger_user_turn_stop(None, ...), which drains the aggregation before that task ever runs. So a handler doing the documented thing, reading aggregation_string() to decide if the turn was empty, sees "" for a turn that produced a full transcript (which then arrives intact in on_user_turn_stopped). The issue's "no transcript" apology double-fire comes straight from that.

The fix

Register the event with sync=True, matching what UserTurnController already does for its own on_user_turn_stop_timeout registration. The handler now runs, and finishes, before the force-stop drains the buffer.

UserTurnProcessor had the identical race: its stop-timeout event was also async-dispatched while _trigger_user_turn_stop synchronously calls handle_user_turn_stopped() on every stop strategy right after, resetting whatever per-turn state a strategy buffers (the same pattern TurnAnalyzerUserTurnStopStrategy uses for self._text). Same one-line fix there.

A tradeoff to point out: the handler now runs inline before the force-stop, so a slow handler delays the stop. Sync events are supposed to be fast anyway, and both docstrings now say so on this event. Seemed better than the alternative, where the event can't do the thing its own example shows.

Tests

Two new regression tests, both red without the fix:

  • test_user_turn_stop_timeout_handler_sees_aggregation: watchdog force-stops a turn with a real transcript; the timeout handler's aggregation_string() must equal it.
  • test_user_turn_stop_timeout_handler_sees_strategy_state (processor): a buffering stop strategy's per-turn state must still be readable in the timeout handler.

Full turn-related suites pass (123 tests across the aggregator, controller, and processor files), ruff clean.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
...t/processors/aggregators/llm_response_universal.py 86.84% <100.00%> (ø)
src/pipecat/turns/user_turn_processor.py 91.56% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@scttbnsn scttbnsn closed this Sep 13, 2026
@scttbnsn
scttbnsn deleted the fix/user-turn-stop-timeout-sync-handler branch September 13, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant