Package- and image-level remote tombstones reach no purger scan
The gap
Three remote cache tables carry a soft_deleted_at writer on main.
No purger scan reads any of the three.
| Table | Writer | Discovery index | Scan level |
|---|---|---|---|
npm_remote_packages |
NpmRemoteEvictor.EvictNpmRemotePackage, in internal/datastore/npm_remote_eviction.go |
index_npm_remote_packages_on_ns_id_soft_deleted_at |
none |
container_remote_images |
ContainerRemoteEvictor.EvictContainerRemoteImage, in internal/datastore/container_remote_eviction.go |
index_container_remote_images_on_ns_id_soft_deleted_at_id |
none |
maven_remote_packages |
MavenRemoteEvictor.EvictMavenRemotePackage, in internal/datastore/maven_remote_eviction.go |
index_maven_remote_packages_on_ns_id_soft_deleted_at |
none |
All three slices exist on main today.
The npm writer landed on 2026-08-14, the container writer on 2026-08-18, and the Maven writer with !1574 (merged) on 2026-08-19.
The three indexes landed with !1581 (merged) on 2026-08-19, in internal/datastore/migrations/sql/20260819135121_add_remote_cache_keyset_and_discovery_indexes.sql.
Each writer marks the named row and no child row. Each leaves every version, file, manifest and attachment under that row live and unmarked. Each doc comment gives the subtree reap to the lifecycle purger.
LifecycleScanStore carries eight scans on main.
Three of them are ScanTombstonedRepositories in internal/datastore/lifecycle_scan.go, plus ScanTombstonedNpmPackages and ScanTombstonedContainerImages in internal/datastore/lifecycle_scan_packages.go.
The other five are the version-level and manifest-level scans in internal/datastore/lifecycle_scan_versions.go.
None of the eight reads a remote package or image table.
A package-level or image-level mark therefore reaches no sweep.
The marked row stays, every row under it stays, and their blob_storage_attachments rows stay.
ADR-025 reclaims a blob when its attachment count reaches zero, so an unreaped subtree blocks reclamation with no error and no metric.
The #### Eviction markers section of docs/specs/S17-rest-management-api.md names the same state "the permanently uncollectable state".
What closes it
Two pieces of work. A scan alone does not close the gap.
1. Three scan levels.
One scan per table, on the index that table already carries, in the shape ScanTombstonedNpmPackages uses.
Each scan is namespace-scoped, prunes to one partition, and reads the oldest tombstone first, as the merged levels do.
2. A downward walk from the parent tombstone. An eviction leaves the versions live, so no version tombstone exists under the mark. The version-level scans reach nothing there, and the upward leg from a version has no row to start from. The S20-A plan gives its steps 8 to 13 the per-format reap arms, and each arm reaps a subtree that a scan hands to it. Nothing hands any arm a package-level or image-level remote tombstone. The purger needs a net-new downward walk from that tombstone, and this walk is the larger half of the work.
Reachability
No production caller outside internal/datastore calls any of the three evictors, so no request produces one of these marks yet.
!1726 (merged) serves the Maven remote package, version and file deletes.
While !1726 (merged) is open, no request path reaches MavenRemoteEvictor.
Once it merges, a request produces a maven_remote_packages mark and the gap becomes reachable from outside internal/datastore.
Whichever merge request serves the npm and container remote deletes does the same for those two tables.
The gap does not wait for a request to matter.
Each of the three evictors is callable from any code in internal/datastore, and the state it writes is unreachable from the moment it commits.
Ownership
Two merged documents give this work to S20-A's author, so this issue records an obligation rather than a new scope decision.
docs/specs/S17-rest-management-api.md, in ## Follow-ups:
S20-A amendment: three eviction scan levels, and the reachable
destructive=falsebranch. Phase 6's evictions write the first package- and image-level remote tombstones, and S20-A's scan inventory stops at version level for the remote tables, reaching a remote package only through its tombstoned versions. The amendment adds themaven_remote_packages,npm_remote_packages, andcontainer_remote_imagesscan levels, backed by the discovery indexes in Eviction markers, and the remote reap logic S20-A already calls net-new walks from them.
docs/plans/2026-08-13-s17-phase6-virtual-remote-repositories.md, in its S20-A dependency bullet:
The three remote package/image-level scan levels are an S20-A spec amendment owned by S20-A's author (Spec Follow-ups); Step 2 here lands only their indexes.
The same #### Eviction markers section of the S17 spec states the consequence directly:
The index alone changes nothing: the purger gains the three scan levels with it, the index-and-scan addition Phase 4 made for
maven_packages, because S20-A's scan inventory reaches a remote package only through its tombstoned versions and calls a remote package with unmarked children unreachable.
Adjacent, and not claimed here
maven_packages sits in the same position on main, and the S17 follow-up does not name it.
MavenPackageStore.SoftDeleteMavenPackage marks the package row and writes no child row, the table carries index_maven_packages_on_ns_id_soft_deleted_at, and no scan level reads that index.
Whether a fourth level belongs here is open. This issue claims the three levels the S17 follow-up names.
No numbered step of docs/plans/2026-08-11-s20a-lifecycle-closed-beta.md owns any of this work.
type::bug. The description documents a clear functional gap: package- and image-level remote tombstones written by the three evictors (npm_remote_packages, container_remote_images, maven_remote_packages) are never picked up by any purger scan, leaving soft-deleted rows and their entire subtrees permanently uncollectable — a broken invariant rather than a missing feature or routine maintenance. The labels Category:Artifact Registry, devops::package, and group::package registry have also been confirmed.
If this classification doesn't look right to you, please feel free to update the labels and let the team know!