Repository navigation
Description
_interrupt_audio_playback gates the whole truncation path on elapsed_ms > 0, and elapsed_ms is wall-clock derived. On a Windows host at the default timer resolution that value is exactly 0.0 for the first ~16 ms after audio starts, so an interrupt in that window silently skips conversation.item.truncate.
The path
src/agents/realtime/openai_realtime.py:995
elapsed_ms = (time.monotonic() - audio_state.initial_received_time) * 1000and the gate at :1072
if elapsed_ms > 0:
... # emits audio-interrupted, computes truncated_ms,
# and sends conversation.item.truncatetime.monotonic() on Windows is GetTickCount64():
monotonic impl : GetTickCount64()
monotonic resolution: 0.015625 s
identical back-to-back reads: 20000/20000
So two reads inside the same tick are the same number, and the subtraction is exactly zero.
Measured on the real object, not in the abstract
m = OpenAIRealtimeWebSocketModel()
m._audio_state_tracker.set_audio_format("pcm16")
m._audio_state_tracker.on_audio_delta("item_1", 0, b"\x00" * 4800)
m._get_playback_state()playback_state elapsed_ms : 0.0
current_item_id : item_1
-> the guard is if elapsed_ms > 0: so this path is SKIPPED
iterations until elapsed_ms > 0: 9916 | wall: 16.0 ms
16.0 ms is one GetTickCount64 tick.
Why this matters beyond the tests
Truncation is what keeps the model's conversation state in step with what the user actually heard. When it is skipped, the item keeps its full audio, so the model believes it said more than was played — which is the exact condition truncation exists to prevent. A barge-in that lands promptly is more likely to hit this window, not less, so the failure is biased toward the responsive case.
Note the guard is > 0 rather than >= 0: zero elapsed is treated as "nothing to truncate", when on this platform it means "less than one tick has been measurable".
Four tests are load-bearing on ambient timer resolution
On main at 9aba900, uv run pytest tests/realtime/test_openai_realtime.py::TestSendEventAndConfig -p no:randomly:
FAILED ... ::test_normal_interrupt_targets_response_and_interrupts_audio - assert 0 == 1
FAILED ... ::test_interrupt_respects_auto_cancellation_when_not_forced - assert 0 == 1
FAILED ... ::test_interrupt_force_cancel_overrides_auto_cancellation
FAILED ... ::test_interrupt_honors_falsy_present_session_auto_cancellation
4 failed, 58 passed
Each seeds audio and interrupts on the next statement, so each assumes the clock advanced in between.
These pass in CI — tests-windows is green on main. I take that to mean the hosted runner is running with the system timer raised (some process calling timeBeginPeriod), which is common on Windows CI. That is the uncomfortable part: whether this code path runs at all depends on a global system setting any other process on the machine can change, and the suite passes or fails accordingly. It is not a property of the code under test.
Possible directions
Not proposing one, since the right call depends on how you want the clock treated:
- Inject the clock. The tests could then advance it explicitly and assert the truncate deterministically, on every platform.
- Use a higher-resolution source —
time.perf_counter()is ~100 ns on Windows and monotonic, and this is a duration measurement, not a timestamp. - Reconsider
> 0. If the intent is "skip when no audio has played", zero-elapsed-but-audio-present is not that case.
1 and 3 are independent and both look worth doing; 2 alone would make the window much smaller without removing the class.
Happy to send a PR for whichever shape you prefer — I would default to 2 plus 3, and leave clock injection to you since it touches the test contract.
Environment
Windows 11, Python 3.12.10, main at 9aba900. uv sync only (the dev group cannot install on this host — see #4442), so voice/litellm extras are absent; the failures above are unrelated to those, and reproduce with the four named tests alone.
Activity
test(realtime): backdate audio start in the interrupt tests