# feat(proxy): reclaim space from the blob store

## Motivation

The blob store only grows. Every cache miss adds a blob and nothing ever takes one away except an operator purging a digest by hand, so a proxy in front of a busy monorepo fills whatever volume it was given and then starts failing writes. `docs/operations.md` has carried a section admitting this, and the manual intervention it recommends is a SQL query against `versions` plus a loop of `DELETE` calls against the purge route.

Content addressing is what makes the fix cheap: deleting a blob loses no metadata, the registry still holds the digest, and the next request for it fetches again and verifies against the same digest. So the store can be trimmed on a timer without anybody having to decide whether a particular artifact is still wanted.

## Implementation

- `sweep.rs` holds the walk and the reclamation loop. One pass clears the partial writes left in `incoming` by fetches that died, removes blobs no index entry points at, and then removes the oldest blobs until the store is back within its ceiling. `Reclaimed` is what a pass did, and it is the route's response body.
- Three settings, read the way the existing ones are: `CAIRN_CACHE_MAX_BYTES` is the ceiling, `CAIRN_CACHE_MIN_AGE` is how long a blob is left alone before a sweep may take it, and `CAIRN_SWEEP_INTERVAL` is how often one runs. Both durations go through the existing `parse_duration`, so they need a unit, and the ceiling goes through `parse_bytes` and understands `32GiB`. Defaults are 32 GiB, an hour and fifteen minutes, so a fresh deployment needs no new configuration.
- A background task in `main.rs` sweeps on the interval. `Sweeper` holds a lock so two sweeps cannot be in flight over one directory at once, since each would decide what to remove from a total the other was already changing.
- `POST /v1/admin/cache/sweep` runs one now, behind the same bearer token the purge route asks for. `?dry_run=true` reports what a sweep would reclaim without removing anything, which is what makes a new ceiling safe to try on a live deployment before it goes into the environment. It sits outside the route timeout for the same reason the cache route does.
- Two counters, `cairn_proxy_sweep_blobs_removed_total` and `cairn_proxy_sweep_bytes_reclaimed_total`. Blobs as well as bytes, because forty thousand tiny blobs and one large one look identical in bytes alone.
- `tokio`'s `time` and `sync` features are declared for the interval and the lock. No new crate, and `Cargo.lock` is unchanged.

`.env.example` gains the three variables with the comments that file's other settings carry. `docs/operations.md` and `docs/architecture.md` lose the known-gap wording and gain the runbook for sizing the ceiling.

No tests. The reclamation loop needs a populated store on a real filesystem, which is the same reason nothing tests the store paths today; this is covered by running the stack.
