Lifecycle & maintenance
Everything here runs in cavs-worker, off the request path. Nothing on this page
blocks a push or a pull.
The maintenance stages
Section titled “The maintenance stages”| Stage | Trigger | What it does | Status |
|---|---|---|---|
| Usage reconciliation | upload.session.finalized, repository.usage.changed |
Sums real stored bytes per namespace, writes the daily usage snapshot, evaluates quota thresholds | live |
| Cleanup | every 5 minutes | Expires stale upload sessions and invitations, sweeps orphaned index sessions | live |
| Notifications | invitation, quota, billing and storage-health events | In-app notifications and email | live |
| Webhooks | events with a public mapping | Signed outbound deliveries with retries | live |
| Storage health | every ~10 minutes | Probes BYOS connections, flips health status, notifies on transitions | live |
| Snapshots | snapshot.requested |
Captures a point-in-time storage record | live |
| Health & recommendations | scheduled | Recomputes repository health checks and recommendations | live |
| Garbage collection | gc.requested |
Reclaims chunks nothing references | coordinator only — see below |
| Repack | repack.requested |
Compacts dead bytes out of packfiles | coordinator only |
| Index compaction | index.compaction.requested |
Compacts the Git-index snapshots | coordinator only |
Every stage is idempotent — events are deduplicated by id before handling, so at-least-once delivery never double-counts.
Upload session expiry
Section titled “Upload session expiry”An upload session lives for 6 hours. The cleanup loop expires sessions past that and sweeps the objects that were authorized but never finalized.
Consequences:
- A job that dies mid-upload leaves nothing permanent. Re-run it.
- Re-opening with the same
Idempotency-Keyreturns the same session, so a retry resumes rather than restarting. - Objects only become visible in the repository at finalize. A push killed before finalize publishes nothing — there is no half-published state.
Invitations expire on the same loop.
Usage reconciliation
Section titled “Usage reconciliation”Finalizing a push publishes repository.usage.changed. The worker then:
- Sums the real stored bytes under the repository’s namespace, directly from the object store.
- Writes the daily usage snapshot that the dashboard and
GET …/usageread. - Compares against the plan and emits
storage.quota.warningorstorage.quota.exceededwhen a threshold is crossed.
If the object store is briefly unreachable it falls back to a conservative estimate and retries. So a figure can be approximate for a short window — never silently wrong for long.
Latency is seconds. Object count and generation update immediately at finalize; byte totals follow.
Quota enforcement
Section titled “Quota enforcement”| Event | Effect |
|---|---|
storage.quota.warning |
Notification + email + webhook. Nothing is blocked. |
storage.quota.exceeded |
Notification + email + webhook, and the organization is marked PAST_DUE. |
Upload authorization checks storage quota before presigning, using logical
sizes as an upper bound — so a batch that would clearly overflow is refused up
front with 402 quota_exceeded rather than half-written.
BYOS repositories skip the storage-quota check entirely (your bucket, your capacity) but object-size limits still apply.
Deletion semantics
Section titled “Deletion semantics”| You delete | Immediately | Eventually |
|---|---|---|
| An object reference (a commit, a branch) | index updated | chunks it uniquely held become unreferenced |
| A repository | metadata gone, repository disappears from the API | bytes queued for GC |
| An organization | all metadata gone | all managed bytes queued for GC |
| A BYOS storage connection | connection record and encrypted credentials gone | nothing — your bucket is untouched |
Retention
Section titled “Retention”CAVS does not automatically expire your data. Objects live until the repository or
organization is deleted. Custom retention policies are a Business-plan feature
(features.custom_retention).
If you want time-based cleanup today, drive it yourself: list objects sorted by age, decide what is dead, delete the referencing refs.
# Objects, oldest last — inspect the tailcurl -sS "$API/repositories/$REPO/objects?sort=recent&limit=200" -H "$H" \ | jq -r '.objects[] | "\(.created_at)\t\(.size)\t\(.oid)"' | tail -50Index maintenance
Section titled “Index maintenance”The Git index is snapshotted per generation and swapped atomically, so a read never sees a half-written tree. Old generations are swept on finalize and on index-session expiry.
Re-index from scratch after a history rewrite:
cav repo index --fullIntegrity
Section titled “Integrity”Integrity is structural rather than scheduled:
- Chunks are addressed by BLAKE3-256; objects by SHA-256 with a Merkle root.
- Every download verifies its oid client-side and refuses to write a mismatch.
- A corrupt local chunk cache is detected, quarantined and repaired in place.
- Reconstruction is byte-identical or it fails — never partial.
Watching maintenance
Section titled “Watching maintenance”# Maintenance entries appear in the repository activity feedcurl -sS "$API/repositories/$REPO/activity?limit=50" -H "$H" \ | jq '.events[] | select(.type | test("gc|repack|snapshot|index"))'maintenance.gc.completed is a public webhook event if
you want to react to it.
Self-hosted operators get per-stage metrics — consumer lag, handler duration, failures, reconciliation outcomes. See Observability.
- Storage insights — the numbers reconciliation produces.
- Usage & metering — how usage becomes a bill.
- Architecture — how the worker is wired.

