Python: raise instead of silently returning a stale checkpoint from get_latest - #7832
Open
Oleg Solozobov (dev404ai) wants to merge 1 commit into
Open
Conversation
Oleg Solozobov (dev404ai)
temporarily deployed
to
github-app-auth
August 23, 2026 11:28 — with
GitHub Actions
Inactive
Oleg Solozobov (dev404ai)
temporarily deployed
to
github-app-auth
August 23, 2026 11:28 — with
GitHub Actions
Inactive
Oleg Solozobov (dev404ai)
temporarily deployed
to
github-app-auth
August 23, 2026 11:29 — with
GitHub Actions
Inactive
Oleg Solozobov (dev404ai)
temporarily deployed
to
github-app-auth
August 23, 2026 11:29 — with
GitHub Actions
Inactive
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
Closes #7831
FileCheckpointStorage.get_latestwas built onlist_checkpoints, which catches every exception per file, logs a warning and returns whatever it could read. A checkpoint thatsaveaccepted butloadrefuses therefore disappeared from the listing, andget_latestreturned the newest of the survivors. A workflow resumed from earlier state after being told its newer state was saved, and the only trace was alogger.warningon a path nobody watches during recovery.Two encoding rules make that reachable rather than theoretical.
encode_checkpoint_valueapplies no allow list, sosaveaccepts any state value.decode_checkpoint_valuedoes apply one, soloadcan refuse the same checkpointsaveaccepted. Restricting deserialization is a deliberate security control and nothing here changes it: the problem is only that a refusal is currently converted into a plausible wrong answer.Closed issue #3529 asked for the same thing from the other side. Its
list_checkpointsrow readsTODO: improve this API .get_latest(), error, human in the list. The issue closed onceget_latest()existed; becauseget_latest()was built onlist_checkpoints, the error half of that line stayed open. This closes it.Description
get_latestnow identifies the newest checkpoint from stored metadata and loads only that one.workflow_name,timestampandcheckpoint_idare plain JSON in the stored file, so the newest checkpoint can be found without decoding any payload. Once found, it is loaded throughload, which means an undecodable newest checkpoint raisesWorkflowCheckpointExceptioncarrying the decoder's own actionable message namingallowed_checkpoint_types, instead of being skipped in favor of an older one.Two alternatives were rejected on their merits.
Validating encodable types at
saveis the wrong layer. The allow list is a read-time policy configured per storage instance throughallowed_checkpoint_types, so the writer cannot know the reader's policy, which may live in a different process. Enforcing it on write would break both the intent of the security model and existing workflows that widen the list on the reading side.Making
list_checkpointsraise is breaking, and it destroys the robustness itstry/exceptexists to provide: one unreadable file would break every listing.list_checkpointsis therefore left exactly as it is, so nothing that lists checkpoints starts failing.A side effect worth noting:
get_latestno longer decodes every checkpoint in order to return one.The same catch-and-warn shape exists in
agent_framework_azure_cosmos._checkpoint_storage, whoseget_latestalso builds on its own listing. This pull request deliberately does not touch it, because that backend cannot be exercised here without an Azure account and shipping an unverified change to a backend I cannot run would be worse than scoping honestly.Contribution Checklist
Four tests were added to
python/packages/core/tests/workflow/test_checkpoint.py:Nonelist_checkpointsstill skips unreadable entries, so its tolerance is pinned rather than assumedOn
mainatd9d3fb62, with the tests applied and the fix reversed, the second of those fails withFailed: DID NOT RAISE WorkflowCheckpointExceptionwhile the other three pass. With the fix applied, that file passes 48 of 48.The full workflow suite reports 966 collected, 962 passed, 0 failed, 0 errors, 4 skipped in 42.30s:
ruff checkandruff format --checkare clean on both changed files. The change is 89 insertions and 5 deletions across two files.