Repository navigation
Python: Fence agent_server-history conversation turns with a pre-dispatch claim - #9737
Darren Cohen (dargilco) wants to merge 3 commits into
Conversation
…atch claim Two concurrent requests replaying the same approval to one named conversation could each load a stale session copy, consume the same pending approval, and dispatch the approved tool twice. The pre-dispatch conversation claim was skipped for history_source="agent_server" (the default), and AgentServer only serializes same-conversation turns when its task subsystem is enabled. Apply the conditional claim write to agent_server history too, so a losing turn fails before agent.run(). Because no provider thread is mutated in this mode, release the claim when a turn fails or is cancelled and restore the pre-turn state. Skip the claim when steering already owns admission. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Failure cleanup can enable repeated tool effects, while cancellation during persistence can bypass claim release.
2 open issues
What changed in this PR
This PR extends pre-dispatch conversation claims to the default agent_server history mode, preventing concurrent turns from dispatching tools using stale session state.
Changes:
- Claim stored, named conversations before agent execution, except when steering controls admission.
- Release owned claims after uncommitted turns and clarify busy-conversation errors.
- Add concurrency, failure, cancellation, and claim-ownership regression tests.
| File | Description |
|---|---|
python/packages/foundry_hosting/tests/test_responses.py |
Adds claim lifecycle tests and updates commit-conflict simulation. |
python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py |
Extends admission claims and adds claim-release handling. |
Review effort: Balanced
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (1 commit(s)): 47645fd503f3
Model: gpt-5.6-sol
Overview
This PR extends the existing conditional session claim to stored agent_server conversations and adds ownership-checked release for uncommitted turns. The production Foundry store's ETag writes, pre-dispatch busy checks, and concurrency/ownership regressions provide strong guards against simultaneous approval dispatch. One cancellation window remains: a remotely committed pre-dispatch claim can be stranded because release eligibility is recorded only after the claim-write await returns.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/foundry_hosting/agent_framework_foundry_hosting/_responses.py
- Do not reopen a consumed approval: an approval turn that fails after its tool may have run now persists the consumed state (without the claim) instead of restoring the pre-turn snapshot, so a retry cannot repeat the tool effect. Other turns still restore the pre-turn state. - Release the claim from a finally clause, shielded, so cancellation during the response-snapshot write no longer strands the conversation as in-flight. - Record the releasable claim before awaiting the claim write so a cancellation after the backend commits it can still be cleaned up (the release is ownership-checked). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Motivation & Context
With the default
history_source="agent_server",ResponsesHostServerdid not establish exclusive ownership of a named conversation before dispatching a turn. The pre-dispatch conversation claim only applied to theservice/agenthistory modes, and AgentServer serializes same-conversation turns only when its task subsystem is enabled (not the default). Two concurrent requests presenting the same pendingmcp_approval_responseto one conversation could therefore each load a stale copy of the session, each consume the same pending approval, and each dispatch the approved tool. The stale-session conflict was only reported after both tool effects had already happened.Description & Review Guide
_responses.py: apply the existing conditional conversation claim write toagent_serverhistory as well, so a losing concurrent turn fails beforeagent.run()and never reaches a tool.agent_serverhistory mutates no provider thread, a turn that fails or is cancelled releases its claim (_release_conversation_claim) from a shieldedfinally, leaving the conversation usable. A claim owned by another response is never touched. A non-approval turn restores the pre-turn state; an approval turn (which may already have run its tool) persists the consumed approval instead, so a retry cannot repeat the tool effect.agent_serverhistory no longer refers to a provider thread._ConflictingConversationStorenow rejects only the commit write, since a conflict is now detected at the claim.agent_serverhistory (vs. leaving it claimed as the service path does).steering_owns_admission) and the claim-release ordering in thefinallyblock.Related Issue
N/A - no tracked issue yet; this is a correctness fix for a concurrency defect in the default Responses host path.
AI Assistance
AI assistance details: implementation, tests, and this PR description were drafted with GitHub Copilot.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.