Python: serialize per-step checkpoint saves in functional workflows - #7676
Open
Ruiming Zhao (uuzzrm) wants to merge 2 commits into
Open
Python: serialize per-step checkpoint saves in functional workflows#7676Ruiming Zhao (uuzzrm) wants to merge 2 commits into
Ruiming Zhao (uuzzrm) wants to merge 2 commits into
Conversation
Concurrent steps (asyncio.gather) could both read the same checkpoint chain head before either wrote it back, creating sibling root checkpoints and leaving part of the history unreachable from the latest checkpoint. Guard the read-save-update with an asyncio.Lock and add a regression test using a storage whose save() yields, which reproduces the interleaving deterministically.
Ruiming Zhao (uuzzrm)
deployed
to
github-app-auth
August 14, 2026 23:48 — with
GitHub Actions
Active
Ruiming Zhao (uuzzrm)
deployed
to
github-app-auth
August 14, 2026 23:48 — with
GitHub Actions
Active
Ruiming Zhao (uuzzrm)
deployed
to
github-app-auth
August 14, 2026 23:48 — with
GitHub Actions
Active
Ruiming Zhao (uuzzrm)
deployed
to
github-app-auth
August 14, 2026 23:49 — with
GitHub Actions
Active
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes a concurrency issue where parallel step completions could fork the checkpoint lineage by serializing checkpoint saves, and adds a regression test that deterministically reproduces the interleaving seen with real checkpoint backends.
Changes:
- Serialize per-step checkpoint save/update with an
asyncio.Lockto prevent sibling-root checkpoints under parallel step completion. - Add a regression test using a yielding in-memory checkpoint storage to force interleaving and validate a single linear checkpoint chain.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| python/packages/core/agent_framework/_workflows/_functional.py | Adds a lock around step-completion checkpoint saving to prevent lineage forks under concurrent step completion. |
| python/packages/core/tests/workflow/test_functional_workflow.py | Adds a deterministic regression test (and storage shim) that exercises the parallel-step checkpoint lineage scenario. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
864
to
870
| # --------------------------------------------------------------------------- | ||
| # Branching / control flow | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Branching / control flow |
Contributor
Author
There was a problem hiding this comment.
Good catch, fixed in f68cb2b - removed the duplicated header block so the section boundary is clean.
Ruiming Zhao (uuzzrm)
deployed
to
github-app-auth
August 14, 2026 23:53 — with
GitHub Actions
Active
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation & Context
Per-step checkpoints in functional workflows assume a single linear history: each new checkpoint links to the previous one via
previous_checkpoint_id. That assumption breaks as soon as two@stepcalls complete around the same time (e.g.asyncio.gather). Both completion callbacks read the same chain head before either writes the updated value back, so both create checkpoints that point at the same parent - two sibling roots instead of one chain. A later final/approval checkpoint continues from only one of them, and the other becomes unreachable when traversing the history from the latest checkpoint.This is the same invariant #4588 / #6695 established for restoration-time ancestry, but on the concurrent save path inside a single run. The final result is unaffected; checkpoint ordering and history traversal are what fork.
Description & Review Guide
The
_on_step_completedclosure inFunctionalWorkflow._run_corenow guards the read-save-update of the chain head with anasyncio.Lock, so concurrent step completions serialize on the checkpoint chain instead of racing on it. The lock is scoped to one run, so it does not couple independent workflows sharing the same storage.Per-step checkpointing behaves the same for sequential workflows (the lock is uncontended). For concurrent steps, each checkpoint now always chains to the one saved just before it, keeping exactly one root and a fully reachable history. The regression test uses an in-memory storage whose
save()yields to the event loop, which reproduces the file/database interleaving deterministically - with the fix it sees one root and a complete chain, without it two sibling roots.Whether guarding
_on_step_completedcovers every path that can write per-step checkpoints, and whether the lock placement (per-run, next to the chain head it protects) reads naturally.Related Issue
Fixes #7647
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.