Skip to content
This repository was archived by the owner on Jul 5, 2026. It is now read-only.

Commit aa1baf8

Browse files
committed
fix(actions): set ActionManager._ongoing_actions_finished_event at init
``ActionManager.__init__`` set ``_ongoing_actions_count = 0`` and built ``_ongoing_actions_finished_event = asyncio.Event()``, which defaults to the clear state. This violates the natural invariant *count=0 ↔ event set*: an immediate ``await event.wait()`` blocks forever even though no action is in flight. The counter helpers (``_increment_ongoing_actions_count`` / ``_decrement_ongoing_actions_count``) flip the event correctly on transition, so the only buggy state was the initial one. Set the event at init time to restore the invariant; behaviour from the first action onward is unchanged. Reproducer: ``await ActionManager(worker, mgr)._ongoing_actions_finished_event.wait()`` hung forever pre-fix; returns immediately post-fix. Added a regression test pinning both ``count == 0`` and ``event.is_set()`` at init plus a ``wait_for(..., timeout=0.1)`` that exercises the path.
1 parent 080d1d9 commit aa1baf8

3 files changed

Lines changed: 30 additions & 0 deletions

File tree

changelog/287.fixed.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- Fixed `ActionManager._ongoing_actions_finished_event` starting in the cleared
2+
state at init while `_ongoing_actions_count` started at 0, violating the
3+
invariant *count=0 ↔ event set*. An immediate `await event.wait()` would
4+
block forever even though no action was in flight. The event is now set at
5+
init time; the counter helpers keep it consistent on increment/decrement.

src/pipecat_flows/actions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ def __init__(self, worker: PipelineWorker, flow_manager: "FlowManager"):
9696
self._worker = worker
9797
self._flow_manager = flow_manager
9898
self._ongoing_actions_count = 0
99+
# Invariant: ``_ongoing_actions_count == 0`` ↔ event SET.
100+
# ``asyncio.Event()`` defaults to clear; combined with count=0 this
101+
# violates the invariant — an immediate ``await event.wait()`` blocks
102+
# forever even though no action is in flight. Setting the event here
103+
# restores the invariant at init time; the counter helpers keep it
104+
# consistent on increment/decrement.
99105
self._ongoing_actions_finished_event = asyncio.Event()
106+
self._ongoing_actions_finished_event.set()
100107
self._deferred_post_actions: list[ActionConfig] = []
101108
self._showed_deprecation_warning_for_legacy_action_handler = False
102109

tests/test_actions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ async def test_initialization(self):
7575
self.assertIn("tts_say", self.action_manager._action_handlers)
7676
self.assertIn("end_conversation", self.action_manager._action_handlers)
7777

78+
async def test_ongoing_actions_finished_event_is_set_at_init(self):
79+
"""``_ongoing_actions_finished_event`` must be set whenever
80+
``_ongoing_actions_count == 0``. At init the count is 0, so the
81+
event must be set — an immediate ``await event.wait()`` must
82+
return without blocking.
83+
84+
Regression: ``asyncio.Event()`` defaults to clear; combined with
85+
``count=0`` this violated the invariant and made an immediate
86+
wait block forever even though no action was in flight.
87+
"""
88+
self.assertEqual(self.action_manager._ongoing_actions_count, 0)
89+
self.assertTrue(self.action_manager._ongoing_actions_finished_event.is_set())
90+
# The wait must return immediately, not hang.
91+
await asyncio.wait_for(
92+
self.action_manager._ongoing_actions_finished_event.wait(),
93+
timeout=0.1,
94+
)
95+
7896
async def test_tts_action(self):
7997
"""Test basic TTS action execution."""
8098
action = {"type": "tts_say", "text": "Hello"}

0 commit comments

Comments
 (0)