Repository navigation
Python: fix get_latest silently returning a stale checkpoint on decode failure - #9485
Open
pratik wayase (PratikWayase) wants to merge 2 commits into
Open
pratik wayase (PratikWayase) wants to merge 2 commits into
pratik wayase (PratikWayase) wants to merge 2 commits into
Conversation
pratik wayase (PratikWayase)
requested review from
SergeyMenshykh,
Tao Chen (TaoChenOSU),
Eduard van Valkenburg (eavanvalkenburg),
Giles Odigwe (giles17),
Jose Alvarez (jpalvarezl),
Evan Mattson (moonbox3),
Roger Barreto (rogerbarreto),
sophia-ramsey and
westey (westey-m)
as code owners
October 18, 2026 05:58
pratik wayase (PratikWayase)
deployed
to
github-app-auth
October 18, 2026 05:58 — with
GitHub Actions
Active
pratik wayase (PratikWayase)
deployed
to
github-app-auth
October 18, 2026 05:58 — with
GitHub Actions
Active
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Duplicate fields in unrelated workflows can block recovery, and Windows mutation tests can pass without exercising scan validation.
2 open issues
What changed in this PR
Fixes #7831 by making Python’s file checkpoint recovery raise when the newest checkpoint cannot be decoded, rather than silently returning older state.
Changes:
- Selects checkpoints from metadata, validates the scan, and decodes only the winner.
- Shares decoding logic with
load()while preserving tolerant listing. - Adds recovery, metadata, concurrency, and deserialization-policy tests.
| File | Description |
|---|---|
| python/packages/core/tests/workflow/test_checkpoint.py | Adds checkpoint recovery regression tests. |
| python/packages/core/agent_framework/_workflows/_checkpoint.py | Implements metadata-first selection and snapshot validation. |
Review effort: Balanced
pratik wayase (PratikWayase)
deployed
to
github-app-auth
October 18, 2026 07:11 — with
GitHub Actions
Active
This branch was successfully deployed
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
FileCheckpointStorage.get_latest()was silently returning stale state. When the newest checkpoint on disk could not be decoded — for example, because itsstatepayload referenced a type not inallowed_checkpoint_types—get_latestwould fall back to the next-newest checkpoint it could read, and return that as if it were the latest. A workflow then resumed from earlier state, having been told its newer state was saved.The root cause was that
get_latestwas built onlist_checkpoints, which catches per-file decode failures, logs a warning, and returns whatever it could read. The only signal of the refusal was alogger.warningon a code path nobody watches during recovery. Issue #7831 describes the bug and the three behaviors that compose to produce it.Description & Review Guide
What are the major changes?
get_latestno longer goes throughlist_checkpoints. It scans the storage directory itself in three phases:.jsonfile under the storage root (recursively, with symlink/cycle protection), read only the plain-JSON envelope —workflow_name,timestamp,checkpoint_id. No payload decoding happens in this phase. Files whoseworkflow_namedoesn't match the requested workflow are skipped; any file that could belong to the requested workflow but is unreadable raisesWorkflowCheckpointException.Path.stat()consistently) and re-scan the directory tree. If anything changed during the scan — a file's mtime/size/ctime, the set of files, the set of directories, or the storage root itself — raise"changed during the scan; retry get_latest". The caller can retry.load(), which could decode a replacement rather than the validated snapshot. If decode fails, raise; do not fall back to an older checkpoint.list_checkpoints()is intentionally left unchanged. It stays tolerant — one bad file should not break listing. The asymmetry is pinned by a new test (test_file_checkpoint_storage_list_checkpoints_still_skips_undecodable_entries). Issue Python: FileCheckpointStorage saves state it cannot restore: silent save/load asymmetry, get_latest invisibility, listing disagreement, and undocumented JSONDecodeError #8181 raises the broader consistency question of whetherlist_checkpointsshould also surface decode errors; this PR does not take a position on that and leaves the door open.A small
_decode_checkpoint()helper is extracted and shared betweenload()andget_latest()so both apply the sameallowed_checkpoint_typespolicy. Behavior ofload()is unchanged.What is the impact of these changes?
get_lateststops decoding every checkpoint in order to return one — only the winner is decoded.get_latestcall are detected and surfaced as a retryable error rather than silently returning a snapshot that may already be stale.list_checkpoints()behavior is unchanged — nothing that lists checkpoints starts failing.What do you want reviewers to focus on?
Path.stat()andos.fstat(fd)use different Win32 APIs (GetFileAttributesExWvsGetFileInformationByHandle) and can return different values forst_dev,st_ino, and evenst_mtime_ns/st_ctime_nson the same stable file. To make the scan work on Windows, the version tuple was reduced to(st_size, st_mtime_ns, st_ctime_ns)and the two intra-fileos.fstat()comparisons (pre-read and post-read) were removed. The post-loop re-stat usesPath.stat()consistently and still detects concurrent writes. Symlink escapes are caught separately by_validate_file_pathand theis_relative_to(root)check in_checkpoint_paths, so droppingst_devdoes not weaken that guard. Reviewers on Linux/macOS should sanity-check that the reduced tuple is still sufficient there.list_checkpointstolerant. This is intentional and pinned by a test, but if maintainers want symmetry withget_latest(per Python: FileCheckpointStorage saves state it cannot restore: silent save/load asymmetry, get_latest invisibility, listing disagreement, and undocumented JSONDecodeError #8181), that's a follow-up PR — not this one.checkpoint_idvalidation.checkpoint_idmust be a string and must resolve to the exact file being read (_validate_file_path(checkpoint_id) != file_path.resolve()→ raise). This catches redirected-ID attacks where a file's metadata claims to be a different checkpoint. Top-level__pickled__/__type__markers are rejected because they could hide different metadata.Related Issue
Fixes #7831
AI Assistance
AI assistance details: used to help understand the issue and the surrounding code.
Contribution Checklist
get_latestpreviously returned a stale checkpoint on decode failure of the newest; it now raises. Callers that depended on the silent-fallback behavior will see a new exception. Nolist_checkpoints/load/save/deletebehavior changes.