Goal
Guarantee that retention cannot delete another invocation's live diagnostics or temp files.
Background
Closed issue #62 required active bundles never to be removed. Current discovery treats any status=running bundle older than max_age_seconds as stale without a process, lease, or heartbeat check:
def _discover_run_bundles (
runs_root : Path ,
* ,
protected : set [Path ],
max_age_seconds : float | None ,
now : float ,
) -> list [dict [str , Any ]]:
bundles : list [dict [str , Any ]] = []
try :
children = sorted (runs_root .iterdir (), key = lambda path : path .name )
except OSError :
return bundles
for child in children :
if child .name .startswith ("." ) or child .is_symlink () or not child .is_dir ():
continue
metadata = _read_bundle_metadata (child )
if metadata is None :
# Partial startup directories are deliberately not considered
# complete. They can be diagnosed or cleaned by a consumer's
# explicit maintenance command without retention guessing.
continue
status = str (metadata .get ("status" , "" ))
started_at = _timestamp_to_epoch (metadata .get ("started_at" ))
if started_at is None :
try :
started_at = child .stat ().st_mtime
except OSError :
continue
age = max (0.0 , now - started_at )
running = status == "running"
stale_running = running and max_age_seconds is not None and age >= max_age_seconds
if running and not stale_running :
continue
if status not in {"running" , "ok" , "aborted" , "error" }:
continue
resolved = _safe_resolved_path (child )
try :
size = _bundle_size (child )
except OSError :
continue
retention_metadata = metadata .get ("retention" )
preserve = bool (metadata .get ("preserve" )) or (
isinstance (retention_metadata , dict ) and retention_metadata .get ("preserve" ) is True
)
bundles .append (
{
"path" : child ,
"resolved" : resolved ,
"run_id" : metadata .get ("run_id" ),
"status" : status ,
"started_at" : started_at ,
"age" : age ,
"size" : size ,
"preserve" : preserve ,
"protected" : resolved in protected ,
}
)
. The retention pass then removes it by age:
def _apply_bundle_retention (
runs_root : Path ,
bundles : list [dict [str , Any ]],
* ,
policy : RetentionPolicy ,
protected : set [Path ],
logger : logging .Logger ,
now : float ,
reserved_active_bundles : int ,
) -> None :
del now # retained for a stable extension point in policy implementations
removable = [
bundle
for bundle in bundles
if not bool (bundle ["protected" ])
and not bool (bundle ["preserve" ])
and _safe_resolved_path (bundle ["path" ]) not in protected
]
removable .sort (key = lambda bundle : (float (bundle ["started_at" ]), str (bundle ["path" ])))
def remove (bundle : dict [str , Any ]) -> bool :
path = Path (bundle ["path" ])
try :
_remove_run_bundle (runs_root , path )
except OSError as exc :
logger .warning ("Could not prune run bundle '%s': %s" , path , exc )
removable .remove (bundle )
return False
bundles .remove (bundle )
removable .remove (bundle )
return True
if policy .max_age_seconds is not None :
for bundle in list (removable ):
if float (bundle ["age" ]) >= policy .max_age_seconds :
remove (bundle )
. The current regression test explicitly expects an old running bundle to be deleted:
def test_stale_running_bundle_is_recoverable_with_age_bound (self ) -> None :
with tempfile .TemporaryDirectory () as tmpdir :
root = Path (tmpdir ) / "runs"
root .mkdir ()
stale = _bundle (root , "stale" , status = "running" , started_at = "2020-01-01T00:00:00Z" )
prune_run_bundles (
root ,
root / "active" ,
policy = RetentionPolicy (max_age_seconds = 60 ),
logger = logging .getLogger (__name__ ),
now = 1_600_000_000 ,
)
self .assertFalse (stale .exists ())
.
A legitimate long-running operation can therefore lose its active run bundle when another invocation starts with a short age policy.
Scope
Introduce a cross-platform liveness or lease contract for running bundles.
Separate confirmed crash recovery from age-based terminal retention.
Preserve fail-closed behavior when liveness cannot be established.
Acceptance Criteria
A live concurrent process is never pruned by count, age, or total-size policies.
A crashed run becomes recoverable through a documented, testable mechanism.
PID reuse, host changes, inherited runs, clock skew, and unreadable metadata have safe behavior.
Concurrency tests use separate processes, not only threads.
The acceptance criterion from Implement atomic run bundles and complete-bundle retention policies #62 is restored.
Validation
Run retention, cleanup-security, signal, concurrency, and platform tests.
Non-Goals
Do not remove bounded retention for terminal bundles.
Project Fields
Status: Backlog
Priority: P1
Area: Security
Initiative: v1.0 Readiness
Size: M
Ownership
Goal
Guarantee that retention cannot delete another invocation's live diagnostics or temp files.
Background
Closed issue #62 required active bundles never to be removed. Current discovery treats any
status=runningbundle older thanmax_age_secondsas stale without a process, lease, or heartbeat check:base-cli/lib/python/base_cli/_runtime.py
Lines 355 to 411 in 8a93d22
base-cli/lib/python/base_cli/_runtime.py
Lines 416 to 451 in 8a93d22
base-cli/tests/test_run_bundle_retention.py
Lines 80 to 95 in 8a93d22
A legitimate long-running operation can therefore lose its active run bundle when another invocation starts with a short age policy.
Scope
Acceptance Criteria
Validation
Run retention, cleanup-security, signal, concurrency, and platform tests.
Non-Goals
Do not remove bounded retention for terminal bundles.
Project Fields
Ownership