Skip to content

fix(realtime): restore agent after update failure - #4652

Open
hsusul wants to merge 2 commits into
openai:mainfrom
hsusul:fix/realtime-update-agent-send-rollback
Open

fix(realtime): restore agent after update failure#4652
hsusul wants to merge 2 commits into
openai:mainfrom
hsusul:fix/realtime-update-agent-send-rollback

Conversation

@hsusul

@hsusul hsusul commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This pull request fixes RealtimeSession.update_agent() leaving its local agent and dispatch snapshot changed when the outbound session update fails. It restores the previous state only while the failed update still owns the current transition, so a newer concurrent update is never rolled back.

Test plan

  • Added deterministic coverage for an outbound update failure.
  • Added concurrent coverage proving an older failed update cannot overwrite a newer successful update.
  • Ran .agents/skills/code-change-verification/scripts/run.sh after make sync; formatting, lint, type checking, and the full test suite passed.

Issue number

None.

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass
  • If using Codex, I've run /review before submitting this PR

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ea26f7e986

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/realtime/session.py Outdated
Comment thread src/agents/realtime/session.py Outdated

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. The underlying state-desynchronization bug is real, but the current rollback is not safe for all supported async outcomes. CancelledError bypasses except Exception, and when two overlapping updates both fail, the newer call restores the older failed call's snapshot rather than the last successfully committed agent.

Please replace the identity-based rollback with a session-owned serialized update_agent() transaction that snapshots and restores only the last successfully committed agent and dispatch snapshot. On cancellation, the outbound send must be allowed to settle: keep the new state if it succeeds, restore the committed predecessor if it fails, and then propagate cancellation. Please add deterministic regressions for the two-failure case and cancellation whose send later succeeds or fails. The current green CI run does not exercise these interleavings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 000eaf1b37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

agent=agent,
)
updated_snapshot = self._dispatch_snapshot_from_settings(agent, updated_settings)
async with self._update_agent_lock:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid deadlocking reentrant agent updates

When async_tool_calls=False and a custom or ScriptedRealtimeModel emits a function call while processing this session-update send, a tool that invokes the public update_agent() API blocks on this lock. The original update is simultaneously awaiting send_event(), which cannot finish delivering the function call until that tool returns, so both tasks deadlock; serialize transitions without holding a non-reentrant lock across listener delivery.

AGENTS.md reference: AGENTS.md:L149-L149

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the follow-up at exact head 000eaf1. I independently reproduced the requested two-failure and repeated-cancellation send-success/send-failure cases; those now settle correctly on Python 3.10 and 3.13. The full realtime session file is also green (203 tests on each version), along with Ruff, mypy, and Pyright for the changed files.

I reproduced this reentrancy issue with a cleanup-safe public-path probe: async_tool_calls=False, a ScriptedRealtimeModel emits a function call while handling the outer session-update send, and the tool awaits session.update_agent(nested). The outer update holds _update_agent_lock while send_event() awaits listener delivery, and the nested update waits on the same lock.

One caution from prototyping: a ContextVar logical owner is not sufficient by itself. A detached tool task inherits it and can bypass serialization, then overlap a third independent update after the outer transaction releases the lock. Making the nested send inline preserves Scripted transport reentrancy, but a cancelled nested public update can then return before its wire operation settles. Moving that send back to a shielded child task restores cancellation semantics but reintroduces the Scripted delivery-worker deadlock.

I suggest freezing the narrow protocol before adding more owner state: serialize independent outer updates; separate outbound wire commit/failure from synchronous listener completion (or explicitly register listener-originated transitions); shield and drain every outer or nested send before commit/rollback; and enqueue inherited background work as an independent transaction. The identity fence should remain while handoff still mutates the same state outside this protocol. I have deterministic probes for all three boundaries and can contribute them once the intended transport/session contract is agreed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants