Repository navigation
Conversation
A speech ended on "listening" when a finished tool's deferred reply, or a realtime tool called inside a preamble, still had to speak, so clients closed the turn early and the user-away timer armed. GPT-Live also let the user go away while its backend model was still working. A speech now ends on "thinking" when another speech is already committed to follow it. The session holds the user-away timer through counted holds that running tools and a realtime model's background work take, and realtime models report that work with a new background_work_changed event, which GPT-Live emits from its backend chain state.
A reused realtime session reports background work only when it changes, so an agent that took over a busy session never held the user-away timer. The detaching activity now records its state in the reusable resources, and the activity that reuses the session takes the hold.
…sion reuse" This reverts commit fa4016b.
background_work_changed fires only on change, so an activity that attaches to a busy session, or one that finishes while no listener is attached during a handoff, held a wrong state. Realtime and duplex sessions now expose background_work_running, which the adapters pass through and GPT-Live implements, and the activity reads it after it subscribes and on reconnect.
There was a problem hiding this comment.
Note
Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.
Devin Review found 1 new potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| @property | ||
| def background_work_running(self) -> bool: | ||
| """Whether background work is running, as the last ``background_work_changed`` said.""" | ||
| return False |
There was a problem hiding this comment.
🟡 Busy custom sessions lose away hold
On handoff, background_work_running returns false for custom sessions that emit busy events but do not override the property. The reused session emits no repeat event, so the user-away timer can fire while work remains active.
Learn more
A realtime session reports model work through background_work_changed. The base property's constant false disagrees with a subclass that emits this event but does not override the property. During handoff, AgentActivity._detach_reusable_resources releases the old hold. AgentActivity._start_session_impl reads the reused session's property, so it cannot restore that hold. The same default exists on DuplexSession, whose events pass through the duplex adapter.
Example: A custom session emits background_work_changed(running=True) when a backend reply starts. The agent hands off to another agent using the same model while that reply runs. The new activity reads false, starts the user-away timer, and receives no new event until the reply ends.
Recommended fix: Cache background_work_changed in both session base classes when emitted, or explicitly require event-emitting implementations to expose their current state and preserve the activity's event-derived state across reuse for implementations without that property.
Was this helpful? React with 👍 or 👎 to provide feedback.
An edge event for background work lost its state whenever no listener was attached, such as during a handoff that reuses the realtime session. The session now exposes background_work_running only, GPT-Live returns its backend state from it, and the user-away timer reads it when it would start. The answer that follows the work restarts the timer.
| if ( | ||
| self._activity is not None | ||
| and (rt_session := self._activity.realtime_llm_session) is not None | ||
| and rt_session.background_work_running | ||
| ): | ||
| # a tool that can speak when it lands holds the window; a paused agent's can't (#6883) | ||
| # the answer speaks when the work lands, and that speech restarts the window | ||
| return |
There was a problem hiding this comment.
🔴 User marked away during backend reply
If response.created arrives after the user stops speaking, background_work_running turns true after the away timer starts. No state change cancels that timer, so a slow GPT-Live reply marks the waiting user away.
Learn more
The away timer starts when both user and agent are listening. GPT-Live ends the user's turn through _end_speech, which can start that timer before the backend sends its first response event. Later, _handle_response_event marks the backend busy without changing either voice state. This property check only runs when the timer is armed, so it cannot stop an existing countdown.
Example: With a 15-second timeout, the user stops speaking at second 0 and response.created arrives at second 1. If backend reasoning lasts 20 seconds, the user becomes away at second 15 while the reply is still pending.
Recommended fix: Cancel an armed away timer when backend work starts, using a state-change notification or equivalent callback. Keep the property read for timer starts and session reuse, and handle the case where work begins after the timer was armed.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
related to this, we start the timer here before activity.start() installs the reused realtime session. The guard here will see None for rt_session. a busy session could survive the handoff and then setting the user as away
| self.send_event(types.ResponseCreateEvent(event_id=utils.shortuuid("response_create_"))) | ||
|
|
||
| @property | ||
| def _backend_busy(self) -> bool: |
There was a problem hiding this comment.
do we need a separate property here? or just move the implementation to background_work_running?
| if ( | ||
| self._activity is not None | ||
| and (rt_session := self._activity.realtime_llm_session) is not None | ||
| and rt_session.background_work_running | ||
| ): | ||
| # a tool that can speak when it lands holds the window; a paused agent's can't (#6883) | ||
| # the answer speaks when the work lands, and that speech restarts the window | ||
| return |
| self._chat_ctx._upsert_item(item) | ||
|
|
||
| def _tool_execution_updated(self, ev: ToolExecutionUpdatedEvent) -> None: | ||
| def _hold_user_away(self, owner: AgentActivity | None) -> None: |
There was a problem hiding this comment.
| def _hold_user_away(self, owner: AgentActivity | None) -> None: | |
| def _create_away_timer_hold(self, owner: AgentActivity | None) -> None: |
creating a hold on the timer.
| if owner in (None, self._activity): | ||
| self._cancel_user_away_timer() | ||
|
|
||
| def _release_user_away(self, owner: AgentActivity | None) -> None: |
There was a problem hiding this comment.
| def _release_user_away(self, owner: AgentActivity | None) -> None: | |
| def _release_away_timer_hold(self, owner: AgentActivity | None) -> None: |
| return not self._speech_q and (not self._current_speech or self._current_speech.done()) | ||
|
|
||
| @property | ||
| def _speech_follows(self) -> bool: |
There was a problem hiding this comment.
| def _speech_follows(self) -> bool: | |
| def _has_followup_work(self) -> bool: |
| if ( | ||
| self._activity is not None | ||
| and (rt_session := self._activity.realtime_llm_session) is not None | ||
| and rt_session.background_work_running | ||
| ): | ||
| # a tool that can speak when it lands holds the window; a paused agent's can't (#6883) | ||
| # the answer speaks when the work lands, and that speech restarts the window | ||
| return |
There was a problem hiding this comment.
related to this, we start the timer here before activity.start() installs the reused realtime session. The guard here will see None for rt_session. a busy session could survive the handoff and then setting the user as away
Problem: A speech ends on
listeningwhile another speech is already owed, such as a finished tool's deferred reply or a realtime tool called inside a preamble, so clients close the turn early and the user-away timer arms. GPT-Live also marks the userawaywhile its backend model still works, because the voice layer cannot see that work.Fix: A speech now ends on
thinkingwhen another speech is already committed to follow it, and running tools hold the user-away timer through counted holds. Realtime and duplex sessions expose a read-onlybackground_work_running, which GPT-Live returns from its backend chain state and the user-away timer reads before it starts.Fixes #8362. Fixes #8317. Supersedes #8379 and #8345.
Context for reviewing and coding agents