@@ -173,13 +173,15 @@ pub enum ArtifactStorage {
173173
174174mod artifact_links;
175175mod artifact_resolve;
176+ mod orphaned_artifact_bytes;
176177mod persisted_cleanup;
177178mod run_lookup;
178179mod runner_downloads;
179180pub mod runner_evidence;
180181
181182pub use artifact_links:: * ;
182183pub use artifact_resolve:: * ;
184+ pub use orphaned_artifact_bytes:: * ;
183185pub use persisted_cleanup:: * ;
184186pub use run_lookup:: * ;
185187pub use runner_downloads:: * ;
@@ -204,24 +206,22 @@ pub fn retain_terminal_runs(
204206 ) ) ;
205207 }
206208 let finished_before = ( Utc :: now ( ) - Duration :: days ( options. older_than_days ) ) . to_rfc3339 ( ) ;
209+ // The store and artifact root are resolved once for the whole sweep. Both
210+ // used to be re-derived inside each per-run plan call, which meant one
211+ // connection open plus a full migration ladder per candidate run.
207212 let mut store = ObservationStore :: open_initialized ( ) ?;
213+ let artifact_root = crate :: artifacts:: root ( ) ?;
208214 let candidate_run_ids = store. terminal_run_ids_before ( & finished_before, options. limit ) ?;
209215 let mut artifact_cleanup = Vec :: new ( ) ;
210216 let mut lifecycle_directories = Vec :: new ( ) ;
211217 let mut removable_run_ids = Vec :: new ( ) ;
212218 let mut skipped_run_ids = Vec :: new ( ) ;
213219 for run_id in & candidate_run_ids {
214- let artifacts = cleanup_persisted_artifacts ( PersistedArtifactCleanupOptions {
215- apply : false ,
216- older_than_days : 0 ,
217- run_id : Some ( run_id. clone ( ) ) ,
218- kind : None ,
219- artifact_type : None ,
220- run_kind : None ,
221- component_id : None ,
222- limit : 10_000 ,
223- terminal_only : true ,
224- } ) ?;
220+ let artifacts = cleanup_persisted_artifacts_with_store (
221+ & store,
222+ & artifact_root,
223+ terminal_run_artifact_plan_options ( run_id) ,
224+ ) ?;
225225 let lifecycle_directory = terminal_run_lifecycle_directory ( & store, run_id) ?;
226226 let blocked = artifacts
227227 . rows
@@ -237,21 +237,25 @@ pub fn retain_terminal_runs(
237237 }
238238 }
239239 }
240+ let mut removed_run_count = 0 ;
240241 if options. apply {
241242 // Revalidate and remove artifact bytes before deleting any provenance.
242243 // A blocked resource leaves the terminal run record and lifecycle root intact.
244+ //
245+ // This second plan pass is *not* redundant with the loop above: that
246+ // loop walks every candidate before any deletion happens, so the
247+ // earliest plans are already stale by the time apply begins. Re-reading
248+ // each run's classification immediately before deleting its bytes is
249+ // the only thing that catches a run whose artifacts became unsafe (or
250+ // whose owning run left a terminal state) during the planning window.
251+ // What was redundant, and is now gone, is reopening the store and
252+ // re-resolving the artifact root on every one of these calls.
243253 for run_id in & removable_run_ids {
244- let artifacts = cleanup_persisted_artifacts ( PersistedArtifactCleanupOptions {
245- apply : false ,
246- older_than_days : 0 ,
247- run_id : Some ( run_id. clone ( ) ) ,
248- kind : None ,
249- artifact_type : None ,
250- run_kind : None ,
251- component_id : None ,
252- limit : 10_000 ,
253- terminal_only : true ,
254- } ) ?;
254+ let artifacts = cleanup_persisted_artifacts_with_store (
255+ & store,
256+ & artifact_root,
257+ terminal_run_artifact_plan_options ( run_id) ,
258+ ) ?;
255259 if artifacts
256260 . rows
257261 . iter ( )
@@ -262,7 +266,6 @@ pub fn retain_terminal_runs(
262266 }
263267 let records = store. list_artifacts ( run_id) ?;
264268 let run = store. get_run ( run_id) ?;
265- let artifact_root = crate :: artifacts:: root ( ) ?;
266269 for row in artifacts. rows . iter ( ) . filter ( |row| row. action == "remove" ) {
267270 let artifact = records
268271 . iter ( )
@@ -310,24 +313,39 @@ pub fn retain_terminal_runs(
310313 }
311314 }
312315 store. delete_terminal_runs ( & deleted) ?;
316+ // Count what was actually deleted. Subtracting `skipped_run_ids` from
317+ // `removable_run_ids` under-reported by every run the planning loop had
318+ // already blocked, because those runs are in `skipped_run_ids` but were
319+ // never in `removable_run_ids` to begin with.
320+ removed_run_count = deleted. len ( ) ;
313321 }
314322 Ok ( TerminalRunRetentionOutcome {
315323 dry_run : !options. apply ,
316324 older_than_days : options. older_than_days ,
317- removed_run_count : if options. apply {
318- removable_run_ids
319- . len ( )
320- . saturating_sub ( skipped_run_ids. len ( ) )
321- } else {
322- 0
323- } ,
325+ removed_run_count,
324326 candidate_run_ids,
325327 artifact_cleanup,
326328 lifecycle_directories,
327329 skipped_run_ids,
328330 } )
329331}
330332
333+ /// Per-run artifact plan used by terminal retention. Always a dry-run plan:
334+ /// retention removes bytes itself and releases the rows with the owning run.
335+ fn terminal_run_artifact_plan_options ( run_id : & str ) -> PersistedArtifactCleanupOptions {
336+ PersistedArtifactCleanupOptions {
337+ apply : false ,
338+ older_than_days : 0 ,
339+ run_id : Some ( run_id. to_string ( ) ) ,
340+ kind : None ,
341+ artifact_type : None ,
342+ run_kind : None ,
343+ component_id : None ,
344+ limit : 10_000 ,
345+ terminal_only : true ,
346+ }
347+ }
348+
331349fn terminal_run_lifecycle_directory (
332350 store : & ObservationStore ,
333351 run_id : & str ,
0 commit comments