You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Option to start a reply on time when caller noise began while the agent was thinking (1.6.6 behavior) #7837
An InterruptionOptions setting to start a reply's audio on time when caller noise began while the agent was thinking, as 1.6.6 did. For example hold_reply_while_user_speaking: bool = True; when false, _reconcile_playout_pause would resume at forwarding start.
What changed (1.6.6 → 1.8.6, unchanged in 1.8.7 and 2.0.0b1)
When VAD start-of-speech arrives while the agent is thinking, on_start_of_speech pauses the current speech handle. That is the same in both versions.
1.6.6: generation._audio_forwarding_task began with audio_output.resume(), so the reply played as soon as its audio was ready.
1.8.x: _reconcile_playout_pause keeps that pause ("a paused playout stays paused regardless of forwarding status"). The reply waits for VAD end of speech, which is the sound's length plus min_silence_duration. A sound of 0.5 s or more also counts as an interruption, so the reply then waits for false_interruption_timeout too.
Impact (telephony, Deepgram Flux with turn_detection="stt", interruption mode "vad")
Scripted VAD and STT through a real AgentSession. LLM first token after 0.7 s. First audio at 2.48 s with no noise:
Caller sound near reply start
1.6.6 first audio
1.8.6 first audio
none
2.48 s
2.48 s
0.1–0.3 s
2.48 s
3.05–3.25 s
0.6 s
2.48 s
5.35 s
On phone lines, breaths, line noise and short "mm" sounds near reply start are common. Each adds 0.6 s or more of dead air before the agent answers.
Workarounds / Alternatives
We currently monkeypatch AgentActivity._reconcile_playout_pause to call audio_output.resume() unconditionally, matching 1.6.6, and pin its source in a test so upgrades surface changes.
Setting false_interruption_timeout=None removes the hold, but also turns off resuming after a false interruption mid-reply, which we want to keep.
Making VAD less sensitive also changes endpointing and interruptions.
Additional Context
The setting could apply only to pauses recorded while the agent was thinking (_PausedSpeechInfo.agent_state == "thinking"). Interruptions during playback would keep today's behavior.
@31groot you said three days ago you wanted to take it and I did not want to tread on that, so: if you have something in progress, say so and I will close mine in favour of yours. No hard feelings either way — I only picked it up because nothing had appeared and the measurement here made it straightforward.
Implemented as the issue suggests: hold_reply_while_user_speaking, default True so nothing changes by default, and scoped to pauses recorded while the agent was thinking — an interruption during playback keeps today's behaviour.
One detail for @assembled-caden, since you are monkeypatching _reconcile_playout_pause: resuming unconditionally there is not quite enough on current main. Releasing the pause and falling through lets the block below re-pause it, because the agent is still not "speaking" and the caller is still not silent. The PR returns straight after resuming.
The hold is a trade-off: before the first audio plays, a breath and the start of a word look the same, so we accept some dead air on noise to keep a reply from starting and then being cut off by the final transcript.
Would a shorter false_interruption_timeout only for pauses that start while the agent is thinking cover your case? if so we may separate the timeout for speaking and thinking states.
I measured this before answering, because a shorter timeout is what I assumed too, and on 3c6e65124 there turns out to be nothing to shorten for most of the rows in the table.
A pause recorded while the agent is thinking already carries timeout=0. Both paths that create one hardcode it:
false_interruption_timeout reaches one of those only when _interrupt_by_audio_activity upgrades it at agent_activity.py:2404, and that needs on_vad_inference_done to have seen speech_duration >= min_duration — 0.5 s by default.
Driving those hooks directly, agent in thinking, false_interruption_timeout=2.0:
So separating the timeout by state acts on exactly one row of @assembled-caden's table — the 0.6 s one that lands at 5.35 s, where it would take out most of two seconds. The 0.1–0.3 s rows already run at timeout zero and still cost +0.57–0.77 s. That cost is the wait for VAD end-of-speech, because the pause is released from on_end_of_speech; their numbers are consistent with a min_silence_duration near the 0.55 s Silero default. No timeout value reduces it — it is already zero.
That is the half I care about, because the sounds that actually show up near a reply's start on a phone line are breaths, line clicks and short "mm"s, and those are all under min_duration.
Your trade-off framing is right, and I would rather concede it than argue around it. Before the first frame a breath and a word onset are the same signal, and the price of guessing wrong is a reply that starts and is then cut by the final transcript. That is why the option defaults to True — current behaviour unless someone asks for the other one — and when they do ask, the downside is bounded: a reply interrupted ~100 ms in, which is what happens anyway if the caller interrupts a moment later.
On the PR's third change, in case it reads as unrelated: the authorization gather also waits on _user_silence_event (3207, 3743, 4171, 4402). That gate is not tied to EOS — on_vad_inference_done sets it once accumulated silence passes min_endpointing_delay / 2, at 2505-2512 — but it is clear while the caller is still making noise, so releasing the pause alone left the reply waiting there and the option still did nothing. Hence both.
If you would still rather have a timeout than a flag, say so and I will open it; it is the smaller change. I would only want it described as covering the >= min_duration case, because the sub-0.5 s rows need the pause not to be taken, or released at forwarding, which is what the boolean does.
@assembled-caden a heads-up, since the design decision here is live and it determines whether you get to drop that _reconcile_playout_pause monkeypatch or keep carrying it.
@longcw's counter-proposal above is a shorter false_interruption_timeout for pauses that start while the agent is thinking, rather than the boolean in #7919. I measured it against 3c6e65124 before answering, because it was what I assumed too, and it only covers part of your table:
A pause recorded while the agent is thinking already carries timeout=0 — agent_activity.py:2451 (on_start_of_speech) and :5010 (_reconcile_playout_pause) both hardcode it.
The configured value reaches such a pause only when _interrupt_by_audio_activity upgrades it at :2404, and that needs on_vad_inference_done to have seen speech_duration >= min_duration — 0.5 s by default.
So separating the timeout by state acts on the 0.6 s / 5.35 s row of your table and does nothing for the 0.1–0.3 s rows, which already run at timeout zero and still cost you +0.57–0.77 s waiting for VAD end-of-speech.
The thing that would settle this is your data, not mine. If the sounds that actually trigger this on your calls are mostly under min_duration — breaths, line clicks, short "mm"s — then the timeout split leaves your problem in place and the argument for a flag is straightforward. If they are mostly longer than 0.5 s, longcw's version is the smaller change and probably the right one, and I would rather implement that than argue for mine.
Do you have a rough duration distribution for them? Even "most are short" or "most are around half a second" would be more persuasive than my synthetic numbers, since you are the one running this on real calls.
Also worth flagging for your patch specifically, if you have not hit it yet: resuming unconditionally in _reconcile_playout_pause is not quite enough on current main. Releasing the pause and falling through lets the block below re-pause it, because the agent is still not "speaking" and the caller is still not silent. #7919 returns straight after resuming for that reason.
Thanks both. To answer @longcw : no a thinking only timeout wouldn't cover most of it. I confirmed on 1.8.6 that pauses recorded while thinking already carry timeout=0 (both on_start_of_speech and _reconcile_playout_pause). The 0.1-0.3 s rows are the wait for VAD end-of-speech, not a timeout.
@Rehansanjay : I don't have a measured duration breakdown from real calls yet. The table came from scripted VAD/STT runs. From listening, the common triggers are breaths, line clicks and short "mm"s, which would mostly be under min_duration. I'll post numbers if I get them.
In terms ofthe patch: ours replaces _reconcile_playout_pause entirely and doesn't fall through, so the re-pause doesn't happen. A test checks first-audio timing with noise at reply start. Good catch for anyone copying it, though.
I'm in favor of the boolean in #7919, default True, scoped to pauses taken while thinking.
@assembled-caden thank you — "mostly under min_duration" is the part that actually decides this, and it is the one thing neither @longcw nor I could supply. Scripted runs can show the mechanism but not which sounds really turn up on a call.
Noted on the patch: replacing _reconcile_playout_pause outright sidesteps the fall-through entirely, so that warning was not news to you. It only bites the narrower version that releases the pause and continues.
So where this stands, as I read it: the thinking-state timeout split would help the ≥0.5 s case and leave the sub-0.5 s one where it is, and the sub-0.5 s one is what you actually hear. That is an argument for the flag rather than against the timeout — if you also want a separate timeout for thinking vs speaking states, @longcw, the two are not exclusive and I am happy to send that as well.
#7919 now has current main (3c6e65124) merged in and still defaults to True, so nothing changes for anyone who does not ask. (Correcting myself: when I first posted this it was 49 commits behind and I said it was rebased, which it was not. It is up to date now.) The three gates are in the description above if it helps review: the forwarding pause, the authorization gather, and the block that re-creates the pause with timeout zero when start-of-speech lands before the handle becomes current.
No rush from my side. If you would rather have the timeout shape than the boolean, say so and I will open it instead — I would rather the dead air go away than own the patch that removes it.
@assembled-caden you said the duration breakdown was the missing piece and that you did not
have one from real calls. I cannot get your calls, but the boundary itself is measurable, so
here it is against 3a6248cb7 using this repo's own fake harness (tests/fake_session.py),
with min_duration at its default 0.5s.
A real turn, then a burst with an empty transcript beginning while the agent is thinking --
VAD start/end-of-speech fire and STT emits nothing, which is what a cough or a door is.
burst
reply starts
held vs baseline
registers as an interruption?
none
3.03s
—
—
0.05s
3.46s
+0.43s
no
0.10s
3.50s
+0.48s
no
0.20s
3.60s
+0.58s
no
0.30s
3.71s
+0.68s
no
0.40s
3.81s
+0.79s
no
0.50s
5.91s
+2.89s
yes
0.60s
6.01s
+2.98s
yes
0.80s
6.22s
+3.19s
yes
The sub-threshold rows are a pure VAD wait. Every one of them fits burst + min_silence_duration, measured from when the reply was ready: the harness VAD uses min_silence_duration = 0.5, the burst opens at 2.9s and the reply is ready at 3.03s, so the
predicted hold is 0.37 + burst — 0.42, 0.47, 0.57, 0.67, 0.77 against 0.43, 0.48, 0.58, 0.68,
0.79 measured. Within 20ms on all five. That is your "the 0.1-0.3s rows are the wait for VAD
end-of-speech, not a timeout", with a number on it.
This is what decides @longcw's proposal. A shorter false_interruption_timeout for pauses
that begin while thinking cannot touch those five rows, because no timeout runs in them. The
reply is held until the VAD says the noise stopped, and that is min_silence_duration, not a
timeout. It is the same reason I gave on 21 October — a pause recorded while thinking already
carries timeout=0 — now visible as a duration rather than as a code path.
Where a shorter timeout would help is the bottom three rows: +2.89s at a 0.5s burst is the
VAD wait plus false_interruption_timeout of 2.0, and each of those is 0.37 + burst + 2.0 to
within 20ms. So the counter-proposal covers exactly the bursts long enough to register as real
interruptions -- which, per @assembled-caden's listening, are not the common case. The common
case gets nothing from it.
Caveats, since this is synthetic. Fake VAD and STT, not real calls, so it shows the
mechanism and the boundary rather than your population of noises. The timing proxy is the first agent_state_changed to speaking, not a literal audio frame. The hold scales with the VAD's min_silence_duration, so a deployment that tunes it away from 0.5 shifts every sub-threshold
row with it.
One methodological note worth stating, because it nearly produced the opposite conclusion. My
first run opened the burst at 2.0s and reported +0.00s for every sub-threshold duration,
which reads as "short noise is already free" and would have argued for the counter-proposal.
It was an artefact: VAD end-of-speech lands at burst end + 0.5, so a short burst at 2.0s was
already over before the reply was ready at 3.03s, and the two never overlapped. Five rows at
exactly 0.00s was the tell. The burst has to still be open when the reply becomes ready, which
is why it starts at 2.9s above.
@assembled-caden if you can tell me roughly where your noises sit relative to 0.5s -- even
"nearly all under" versus "a mix" -- that is enough to say which half of this table your calls
live in.
Activity
31groot commentedon Oct 15, 2026
hello, i want to take this
Rehansanjay commentedon Oct 18, 2026
Opened #7919 for this.
@31groot you said three days ago you wanted to take it and I did not want to tread on that, so: if you have something in progress, say so and I will close mine in favour of yours. No hard feelings either way — I only picked it up because nothing had appeared and the measurement here made it straightforward.
Implemented as the issue suggests:
hold_reply_while_user_speaking, defaultTrueso nothing changes by default, and scoped to pauses recorded while the agent was thinking — an interruption during playback keeps today's behaviour.One detail for @assembled-caden, since you are monkeypatching
_reconcile_playout_pause: resuming unconditionally there is not quite enough on current main. Releasing the pause and falling through lets the block below re-pause it, because the agent is still not "speaking" and the caller is still not silent. The PR returns straight after resuming.longcw commentedon Oct 19, 2026
The hold is a trade-off: before the first audio plays, a breath and the start of a word look the same, so we accept some dead air on noise to keep a reply from starting and then being cut off by the final transcript.
Would a shorter
false_interruption_timeoutonly for pauses that start while the agent is thinking cover your case? if so we may separate the timeout for speaking and thinking states.Rehansanjay commentedon Oct 21, 2026
I measured this before answering, because a shorter timeout is what I assumed too, and on
3c6e65124there turns out to be nothing to shorten for most of the rows in the table.A pause recorded while the agent is thinking already carries
timeout=0. Both paths that create one hardcode it:on_start_of_speech→_update_paused_speech(current_speech, timeout=0)—agent_activity.py:2451_reconcile_playout_pause→_update_paused_speech(speech_handle, timeout=0)—agent_activity.py:5010false_interruption_timeoutreaches one of those only when_interrupt_by_audio_activityupgrades it atagent_activity.py:2404, and that needson_vad_inference_doneto have seenspeech_duration >= min_duration— 0.5 s by default.Driving those hooks directly, agent in
thinking,false_interruption_timeout=2.0:So separating the timeout by state acts on exactly one row of @assembled-caden's table — the 0.6 s one that lands at 5.35 s, where it would take out most of two seconds. The 0.1–0.3 s rows already run at timeout zero and still cost +0.57–0.77 s. That cost is the wait for VAD end-of-speech, because the pause is released from
on_end_of_speech; their numbers are consistent with amin_silence_durationnear the 0.55 s Silero default. No timeout value reduces it — it is already zero.That is the half I care about, because the sounds that actually show up near a reply's start on a phone line are breaths, line clicks and short "mm"s, and those are all under
min_duration.Your trade-off framing is right, and I would rather concede it than argue around it. Before the first frame a breath and a word onset are the same signal, and the price of guessing wrong is a reply that starts and is then cut by the final transcript. That is why the option defaults to
True— current behaviour unless someone asks for the other one — and when they do ask, the downside is bounded: a reply interrupted ~100 ms in, which is what happens anyway if the caller interrupts a moment later.On the PR's third change, in case it reads as unrelated: the authorization gather also waits on
_user_silence_event(3207,3743,4171,4402). That gate is not tied to EOS —on_vad_inference_donesets it once accumulated silence passesmin_endpointing_delay / 2, at2505-2512— but it is clear while the caller is still making noise, so releasing the pause alone left the reply waiting there and the option still did nothing. Hence both.If you would still rather have a timeout than a flag, say so and I will open it; it is the smaller change. I would only want it described as covering the
>= min_durationcase, because the sub-0.5 s rows need the pause not to be taken, or released at forwarding, which is what the boolean does.the probe, if you want to re-run it
Rehansanjay commentedon Oct 21, 2026
@assembled-caden a heads-up, since the design decision here is live and it determines whether you get to drop that
_reconcile_playout_pausemonkeypatch or keep carrying it.@longcw's counter-proposal above is a shorter
false_interruption_timeoutfor pauses that start while the agent is thinking, rather than the boolean in #7919. I measured it against3c6e65124before answering, because it was what I assumed too, and it only covers part of your table:timeout=0—agent_activity.py:2451(on_start_of_speech) and:5010(_reconcile_playout_pause) both hardcode it._interrupt_by_audio_activityupgrades it at:2404, and that needson_vad_inference_doneto have seenspeech_duration >= min_duration— 0.5 s by default.So separating the timeout by state acts on the 0.6 s / 5.35 s row of your table and does nothing for the 0.1–0.3 s rows, which already run at timeout zero and still cost you +0.57–0.77 s waiting for VAD end-of-speech.
The thing that would settle this is your data, not mine. If the sounds that actually trigger this on your calls are mostly under
min_duration— breaths, line clicks, short "mm"s — then the timeout split leaves your problem in place and the argument for a flag is straightforward. If they are mostly longer than 0.5 s, longcw's version is the smaller change and probably the right one, and I would rather implement that than argue for mine.Do you have a rough duration distribution for them? Even "most are short" or "most are around half a second" would be more persuasive than my synthetic numbers, since you are the one running this on real calls.
Also worth flagging for your patch specifically, if you have not hit it yet: resuming unconditionally in
_reconcile_playout_pauseis not quite enough on current main. Releasing the pause and falling through lets the block below re-pause it, because the agent is still not"speaking"and the caller is still not silent. #7919 returns straight after resuming for that reason.assembled-caden commentedon Oct 21, 2026
Thanks both. To answer @longcw : no a thinking only timeout wouldn't cover most of it. I confirmed on 1.8.6 that pauses recorded while thinking already carry
timeout=0(bothon_start_of_speechand_reconcile_playout_pause). The 0.1-0.3 s rows are the wait for VAD end-of-speech, not a timeout.@Rehansanjay : I don't have a measured duration breakdown from real calls yet. The table came from scripted VAD/STT runs. From listening, the common triggers are breaths, line clicks and short "mm"s, which would mostly be under
min_duration. I'll post numbers if I get them.In terms ofthe patch: ours replaces
_reconcile_playout_pauseentirely and doesn't fall through, so the re-pause doesn't happen. A test checks first-audio timing with noise at reply start. Good catch for anyone copying it, though.I'm in favor of the boolean in #7919, default
True, scoped to pauses taken while thinking.Rehansanjay commentedon Oct 22, 2026
@assembled-caden thank you — "mostly under
min_duration" is the part that actually decides this, and it is the one thing neither @longcw nor I could supply. Scripted runs can show the mechanism but not which sounds really turn up on a call.Noted on the patch: replacing
_reconcile_playout_pauseoutright sidesteps the fall-through entirely, so that warning was not news to you. It only bites the narrower version that releases the pause and continues.So where this stands, as I read it: the thinking-state timeout split would help the ≥0.5 s case and leave the sub-0.5 s one where it is, and the sub-0.5 s one is what you actually hear. That is an argument for the flag rather than against the timeout — if you also want a separate timeout for thinking vs speaking states, @longcw, the two are not exclusive and I am happy to send that as well.
#7919 now has current
main(3c6e65124) merged in and still defaults toTrue, so nothing changes for anyone who does not ask. (Correcting myself: when I first posted this it was 49 commits behind and I said it was rebased, which it was not. It is up to date now.) The three gates are in the description above if it helps review: the forwarding pause, the authorization gather, and the block that re-creates the pause with timeout zero when start-of-speech lands before the handle becomes current.No rush from my side. If you would rather have the timeout shape than the boolean, say so and I will open it instead — I would rather the dead air go away than own the patch that removes it.
Rehansanjay commentedon Oct 27, 2026
@assembled-caden you said the duration breakdown was the missing piece and that you did not
have one from real calls. I cannot get your calls, but the boundary itself is measurable, so
here it is against
3a6248cb7using this repo's own fake harness (tests/fake_session.py),with
min_durationat its default 0.5s.A real turn, then a burst with an empty transcript beginning while the agent is thinking --
VAD start/end-of-speech fire and STT emits nothing, which is what a cough or a door is.
The sub-threshold rows are a pure VAD wait. Every one of them fits
burst + min_silence_duration, measured from when the reply was ready: the harness VAD usesmin_silence_duration = 0.5, the burst opens at 2.9s and the reply is ready at 3.03s, so thepredicted hold is
0.37 + burst— 0.42, 0.47, 0.57, 0.67, 0.77 against 0.43, 0.48, 0.58, 0.68,0.79 measured. Within 20ms on all five. That is your "the 0.1-0.3s rows are the wait for VAD
end-of-speech, not a timeout", with a number on it.
This is what decides @longcw's proposal. A shorter
false_interruption_timeoutfor pausesthat begin while thinking cannot touch those five rows, because no timeout runs in them. The
reply is held until the VAD says the noise stopped, and that is
min_silence_duration, not atimeout. It is the same reason I gave on 21 October — a pause recorded while thinking already
carries
timeout=0— now visible as a duration rather than as a code path.Where a shorter timeout would help is the bottom three rows:
+2.89sat a 0.5s burst is theVAD wait plus
false_interruption_timeoutof 2.0, and each of those is0.37 + burst + 2.0towithin 20ms. So the counter-proposal covers exactly the bursts long enough to register as real
interruptions -- which, per @assembled-caden's listening, are not the common case. The common
case gets nothing from it.
Caveats, since this is synthetic. Fake VAD and STT, not real calls, so it shows the
mechanism and the boundary rather than your population of noises. The timing proxy is the first
agent_state_changedtospeaking, not a literal audio frame. The hold scales with the VAD'smin_silence_duration, so a deployment that tunes it away from 0.5 shifts every sub-thresholdrow with it.
One methodological note worth stating, because it nearly produced the opposite conclusion. My
first run opened the burst at 2.0s and reported +0.00s for every sub-threshold duration,
which reads as "short noise is already free" and would have argued for the counter-proposal.
It was an artefact: VAD end-of-speech lands at
burst end + 0.5, so a short burst at 2.0s wasalready over before the reply was ready at 3.03s, and the two never overlapped. Five rows at
exactly 0.00s was the tell. The burst has to still be open when the reply becomes ready, which
is why it starts at 2.9s above.
@assembled-caden if you can tell me roughly where your noises sit relative to 0.5s -- even
"nearly all under" versus "a mix" -- that is enough to say which half of this table your calls
live in.