Skip to content

Python: discard pending State on failed/cancelled superstep (#7859) - #8306

Open
LI (ktz03) wants to merge 2 commits into
microsoft:mainfrom
ktz03:fix/discard-pending-state-on-failed-superstep
Open

Python: discard pending State on failed/cancelled superstep (#7859)#8306
LI (ktz03) wants to merge 2 commits into
microsoft:mainfrom
ktz03:fix/discard-pending-state-on-failed-superstep

Conversation

@ktz03

Copy link
Copy Markdown

Motivation & Context

State.set() stages writes in _pending, but the runner never called State.discard() on failure/cancellation. Because a Workflow reuses the same RunnerImpl/State across run() calls, a later successful run could commit() stale pending writes from a prior failed superstep.

Fixes #7859.

Description & Review Guide

  • Call self._state.discard() on the failed-iteration and cancellation paths in RunnerImpl.run_until_convergence().
  • Add a regression test that fails a run after set_state, then succeeds on a second run and asserts the leaked key is absent from committed state.

Related Issue

Fixes #7859

No other open PR targets this issue.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change.

Call State.discard() on runner failure and cancellation paths so staged
writes cannot leak into a later successful run on the same Workflow.

Fixes microsoft#7859.

Copilot AI left a comment

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.

🟡 Changes recommended

Cleanup can still be skipped during event streaming and cancellation outside the polling block.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Prevents failed workflow supersteps from leaking staged Python state into later runs.

Changes:

  • Discards pending state on failure and cancellation paths.
  • Adds a workflow-reuse regression test for failures.
File summaries
File Description
_runner.py Adds pending-state cleanup paths.
test_runner.py Tests state isolation after failure.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread python/packages/core/agent_framework/_workflows/_runner.py Outdated
Comment thread python/packages/core/agent_framework/_workflows/_runner.py Outdated
Comment thread python/packages/core/tests/workflow/test_runner.py
- Scope cancel/BaseException cleanup across the full superstep until commit
- Discard pending state before yielding failure events
- Add cancellation regression that stages state then reuses the workflow
@ktz03
LI (ktz03) deployed to github-app-auth September 12, 2026 11:19 — with GitHub Actions Active
Comment on lines +181 to +186
if not iteration_task.done():
iteration_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await iteration_task
if not committed:
self._state.discard()

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.

What happens if cancellation triggers executor cleanup that raises? At _runner.py:181-186, await iteration_task propagates that cleanup error before discard() executes, so the staged writes survive and the next run commits them. I reproduced this with an executor that sets secret and raises RuntimeError from finally. Could discard() run before the await or from a finally block so executor cleanup cannot bypass it?

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.

Following up on the earlier failure-event comment: could we keep the run reserved until runner cleanup finishes? _runner.py:135-151 can yield a live executor_failed event while iteration_task is still cancelling siblings, so dropping the ResponseStream releases the weak run lock before discard() runs. A successor then reaches the run-kwargs commit and commits the failed superstep state; I reproduced secret: stale at this commit. Could cleanup complete before reuse is allowed, while still permitting a never-started stream to be dropped?

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

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Pending State writes from a failed superstep leak into a later successful run

3 participants