Problem
Run-bundle retention has no native-Windows implementation. RetentionPolicy — including
RetentionPolicy.safe_defaults(), which every default App gets — is silently inoperative
on a platform the support matrix lists as Supported, so run bundles and their temp contents
grow without bound and every invocation past the cap logs a warning per removable bundle.
The only destructive path is descriptor-relative POSIX:
_remove_run_bundle() calls os.stat(candidate, dir_fd=root_fd, ...) and _remove_tree_at()
(lib/python/base_cli/_runtime.py:765-795);
_remove_tree_at() calls _directory_open_flags(), which dereferences os.O_DIRECTORY
and os.O_NOFOLLOW unconditionally (lib/python/base_cli/_runtime.py:297).
There is a guard asymmetry: _open_directory_nofollow() degrades gracefully with
if not hasattr(os, "O_DIRECTORY") or not hasattr(os, "O_NOFOLLOW")
(lib/python/base_cli/_runtime.py:801), but _directory_open_flags() and _remove_tree_at()
have no such guard. _cleanup.py handles the same platform question correctly via
_supports_fd_relative_cleanup() (lib/python/base_cli/_cleanup.py:110-119), and
docs/platform-support.md documents that temp-content erasure falls back on Windows. Retention
has no equivalent fallback and no documentation of the gap.
Verified evidence
Reviewed 2026-09-30 at a58ec109349fa3f3d03eae5b0de078b39ea361a2 (macOS, Python 3.14.6, Click 8.4.2).
Faithful native-Windows model — os.O_DIRECTORY/os.O_NOFOLLOW absent,
os.supports_dir_fd empty, and os.open() on a directory raising PermissionError (the
Windows CRT behaviour). Five finished status: "ok" bundles, RetentionPolicy(max_bundles=1):
Could not prune run bundle '.../runs/bundle0': [Errno 13] Permission denied
Could not prune run bundle '.../runs/bundle1': [Errno 13] Permission denied
Could not prune run bundle '.../runs/bundle2': [Errno 13] Permission denied
Could not prune run bundle '.../runs/bundle3': [Errno 13] Permission denied
Could not prune run bundle '.../runs/bundle4': [Errno 13] Permission denied
prune returned normally (no exception raised to the caller)
bundles remaining: 5 -> ['bundle0'..'bundle4'] (policy wanted 1)
prune_run_bundles() does not fail the command — except (OSError, RuntimeError) and
remove()'s except OSError absorb it — so the failure is silent apart from warning spam,
bounded only by _RETENTION_REMOVAL_BUDGET = 256 warnings per invocation.
Secondary, harder failure. On a platform where a directory os.open() succeeds but
O_DIRECTORY is absent, _remove_tree_at() raises AttributeError, which is not caught by
prune_run_bundles()'s except (OSError, RuntimeError). It escapes _create_context() and
fails the command. Reproduced by deleting only those two attributes on macOS with
RetentionPolicy(max_bundles=3):
--- POSIX (real platform) --- run 1..5: exit=0
--- no O_DIRECTORY/O_NOFOLLOW --- run 1..3: exit=1 ("Error: Unexpected internal error.")
Native-Windows confirmation on a real runner is still needed to establish which manifestation
occurs there; both follow from the same missing fallback.
Why CI does not catch it. windows-latest is in the matrix, but the unit suite uses a fresh
temp home per test and never exceeds max_bundles, and no test mocks a dir_fd-less platform
for this path (tests/test_platform_edge_paths.py mocks os.name == "nt" only for
_private_files). The Windows benchmark profile runs 31 persistence iterations against one home,
which does cross the default cap — worth checking whether that job is currently reporting warnings.
Proposal
- Add a
_supports_fd_relative_bundle_removal() guard mirroring _cleanup.py, and a
pathname-based Windows removal fallback that keeps the existing symlink/mount/identity
refusals (validate each component, refuse reparse points, stay on one volume).
- If no safe removal is possible, fail closed once per pass with a single actionable
warning naming the platform limitation — not one warning per bundle.
- Guard
_directory_open_flags() (or its callers) so an unsupported platform raises a typed
error prune_run_bundles() already catches, never AttributeError.
- Document the retention boundary in
docs/platform-support.md next to the existing
temp-erasure note.
Acceptance criteria
- On a platform without descriptor-relative directory operations, retention either removes
bundles through the audited fallback or fails closed with exactly one warning per pass.
- No code path in
_runtime.py dereferences os.O_DIRECTORY/os.O_NOFOLLOW without a guard.
- A regression test simulates a
dir_fd-less platform (absent flags and empty
os.supports_dir_fd) and asserts the invocation still exits 0 and the warning count is bounded.
- A native-Windows test asserts
max_bundles is actually enforced.
docs/platform-support.md states the retention boundary explicitly.
Non-goals
- Do not weaken the POSIX identity/mount/symlink guarantees to share one code path.
- Do not perform race-prone pathname recursion on POSIX where descriptor operations exist.
Problem
Run-bundle retention has no native-Windows implementation.
RetentionPolicy— includingRetentionPolicy.safe_defaults(), which every defaultAppgets — is silently inoperativeon a platform the support matrix lists as Supported, so run bundles and their temp contents
grow without bound and every invocation past the cap logs a warning per removable bundle.
The only destructive path is descriptor-relative POSIX:
_remove_run_bundle()callsos.stat(candidate, dir_fd=root_fd, ...)and_remove_tree_at()(
lib/python/base_cli/_runtime.py:765-795);_remove_tree_at()calls_directory_open_flags(), which dereferencesos.O_DIRECTORYand
os.O_NOFOLLOWunconditionally (lib/python/base_cli/_runtime.py:297).There is a guard asymmetry:
_open_directory_nofollow()degrades gracefully withif not hasattr(os, "O_DIRECTORY") or not hasattr(os, "O_NOFOLLOW")(
lib/python/base_cli/_runtime.py:801), but_directory_open_flags()and_remove_tree_at()have no such guard.
_cleanup.pyhandles the same platform question correctly via_supports_fd_relative_cleanup()(lib/python/base_cli/_cleanup.py:110-119), anddocs/platform-support.mddocuments that temp-content erasure falls back on Windows. Retentionhas no equivalent fallback and no documentation of the gap.
Verified evidence
Reviewed 2026-09-30 at
a58ec109349fa3f3d03eae5b0de078b39ea361a2(macOS, Python 3.14.6, Click 8.4.2).Faithful native-Windows model —
os.O_DIRECTORY/os.O_NOFOLLOWabsent,os.supports_dir_fdempty, andos.open()on a directory raisingPermissionError(theWindows CRT behaviour). Five finished
status: "ok"bundles,RetentionPolicy(max_bundles=1):prune_run_bundles()does not fail the command —except (OSError, RuntimeError)andremove()'sexcept OSErrorabsorb it — so the failure is silent apart from warning spam,bounded only by
_RETENTION_REMOVAL_BUDGET = 256warnings per invocation.Secondary, harder failure. On a platform where a directory
os.open()succeeds butO_DIRECTORYis absent,_remove_tree_at()raisesAttributeError, which is not caught byprune_run_bundles()'sexcept (OSError, RuntimeError). It escapes_create_context()andfails the command. Reproduced by deleting only those two attributes on macOS with
RetentionPolicy(max_bundles=3):Native-Windows confirmation on a real runner is still needed to establish which manifestation
occurs there; both follow from the same missing fallback.
Why CI does not catch it.
windows-latestis in the matrix, but the unit suite uses a freshtemp home per test and never exceeds
max_bundles, and no test mocks adir_fd-less platformfor this path (
tests/test_platform_edge_paths.pymocksos.name == "nt"only for_private_files). The Windows benchmark profile runs 31 persistence iterations against one home,which does cross the default cap — worth checking whether that job is currently reporting warnings.
Proposal
_supports_fd_relative_bundle_removal()guard mirroring_cleanup.py, and apathname-based Windows removal fallback that keeps the existing symlink/mount/identity
refusals (validate each component, refuse reparse points, stay on one volume).
warning naming the platform limitation — not one warning per bundle.
_directory_open_flags()(or its callers) so an unsupported platform raises a typederror
prune_run_bundles()already catches, neverAttributeError.docs/platform-support.mdnext to the existingtemp-erasure note.
Acceptance criteria
bundles through the audited fallback or fails closed with exactly one warning per pass.
_runtime.pydereferencesos.O_DIRECTORY/os.O_NOFOLLOWwithout a guard.dir_fd-less platform (absent flags and emptyos.supports_dir_fd) and asserts the invocation still exits 0 and the warning count is bounded.max_bundlesis actually enforced.docs/platform-support.mdstates the retention boundary explicitly.Non-goals