Skip to content

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

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

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

Conversation

@ktz03

@ktz03 LI (ktz03) commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Motivation & Context

State.set() stages writes in _pending, but a failed or cancelled superstep previously left those writes intact. Because a Workflow reuses the same runner/state across run() calls, a later successful run could commit() stale pending writes from a prior failed/cancelled superstep.

Fixes #7859.

Description & Review Guide

  • What are the major changes? Discard pending State on fail/cancel (including finally so raising executor cleanup cannot skip it); defer executor_failed yields until after discard; block Workflow.run reuse while cleanup is in progress; lock cancel_pending_requests with the same active/cleanup hold as run().
  • What is the impact of these changes? Failed/cancelled/dropped streams cannot leak staged state into a later run.
  • What do you want reviewers to focus on? RunnerImpl.run_until_convergence cleanup ordering and Workflow.cancel_pending_requests / _is_run_active.

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. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

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.

- 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
- Discard in finally so raising executor cleanup cannot skip it
- Defer executor_failed yields until after discard
- Block Workflow.run reuse while superstep cleanup is in progress
- Discard in finally so raising executor cleanup cannot skip it
- Defer executor_failed yields until after discard
- Block Workflow.run reuse while superstep cleanup is in progress
Reject concurrent cancel/run while ResponseStream cleanup or another exclusive entry holds the workflow, and hold _exclusive_run_hold for the cancel continuation.
@ktz03

Copy link
Copy Markdown
Contributor Author

Eduard van Valkenburg (@eavanvalkenburg) Updated PR body to the repository template and addressed the latest review feedback on this head — please re-review when convenient.

@moonbox3

Copy link
Copy Markdown
Contributor

Please fix the failing CI/CD checks.

@eavanvalkenburg

Copy link
Copy Markdown
Member

Thanks for the contribution. The current head is not green: Python - Tests, Python - Test Coverage, Test Typing Checks, and Merge Gatekeeper are failing, with additional cancellations in the Python matrix. Could you please get the checks passing, then re-request review?

Commit declarative response headers before DeclarativeActionError so Runner State.discard (microsoft#7859) does not drop them. Type WorkflowContext in the discard regression tests so ty accepts yield_output.
@ktz03

Copy link
Copy Markdown
Contributor Author

Addressed on 07c4334e:

Workflow runs for this head are currently waiting on approval-gated Actions.

…staging

Keep cancel_pending_requests setup inside try/finally so normalize_tools failures cannot leave _exclusive_run_hold stuck. Discard residual pending State when checkpoint preparation does not complete, and only mark the superstep committed after checkpoint prep returns.
Rebase microsoft#7859 discard/exclusive-hold semantics onto main's asyncio.wait runner loop so cancelled supersteps release iteration/event tasks instead of hanging, while still discarding uncommitted State before the next run.
self._assign_response_headers(state, result)
# Runner discards pending State when the superstep fails (#7859 / #8306).
# Commit first so diagnostic headers remain readable after the error.
state.commit()

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.

LI (@ktz03), state.commit() runs even when this action has no responseHeaders; in that path _assign_response_headers() is a no-op, so this commits all pending workflow state immediately before the error is raised. Those failed-superstep writes can then leak into a later run, which is the bug this PR is intended to prevent. Please only preserve diagnostic header state when headers were actually configured or written, and add a regression asserting that a failed HTTP action without responseHeaders discards pending state.

@eavanvalkenburg

Copy link
Copy Markdown
Member

LI (@ktz03) Thanks for the contribution. The failed-superstep state-leak concern remains outstanding without updates for more than three business days, so I’m closing this for now. Please reopen once failed HTTP actions without responseHeaders discard pending state and the regression is covered.

@ktz03

Copy link
Copy Markdown
Contributor Author

The requested fix is available on the source branch in commit 8fd977f. Failed HTTP actions now discard pending state, including when responseHeaders is not configured. All four regression cases fail before the fix and pass afterward. Fresh validation: 1,095 declarative tests and all 50 HTTP tests passed; Ruff and strict Pyright checks passed. I don’t see a Reopen button on this PR. Could a maintainer please reopen it so the updated head can be reviewed?

This branch was previously deployed

1 inactive deployment
github-app-auth — efb2156b Deployed Sep 17, 2026 by ktz03 via add_label #23075
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

4 participants