Resource Cleanup and Retention Runbook
This runbook covers smoke Project cleanup, Project deletion, the daily fork sweep, and retention. Cleanup keeps the git-data, objects-data, and core-data volumes small (ADR 0010).
1. Smoke Project cleanup
Section titled “1. Smoke Project cleanup”scripts/smoke.ts makes temporary Projects in the stack. scripts/cleanup-projects.ts deletes them through DELETE /v1/projects/:id on https://api.rhumbatron.localhost, as the Clerk smoke user.
- The script selects the active Projects of the smoke user with a slug that matches
/^(smoke|sandbox)-\d+$/. - It keeps every other Project, for example a demo Project.
--allselects every active Project of the smoke user.scripts/e2e.tsandscripts/journeys.tsdelete their own Projects at the end of each run.
Do this procedure:
-
Set
CLERK_SECRET_KEYandCLERK_SMOKE_USER_IDfor the Clerk development instance. -
Set
NODE_EXTRA_CA_CERTS=.local-ca.crt(local-stack.md). -
List the matches. This command changes nothing.
Terminal window bun run cleanup:projects --dry-run -
Delete the matches.
Terminal window bun run cleanup:projects
2. Project deletion
Section titled “2. Project deletion”Only the owner can delete a Project. DELETE /v1/projects/:id does three steps:
- It sets the Project to
archivedin the product database. - It purges the
ProjectRootDOandProjectViewDOactors of the Project. - It queues
project.cleanupon theintegrationcommand queue.
Then cleanupProject in workers/integration/src/project-cleanup.ts deletes all the data that the Project made.
flowchart TB
del[DELETE /v1/projects/:id] --> arch[API: status archived,<br/>purge root and view]
arch --> q[integration queue: project.cleanup]
q --> f[Delete ws-*, cand-*, and edit-* forks<br/>20 per message]
f --> more{More forks?}
more -->|yes| q
more -->|no| canon[Delete canonical repo]
canon --> obj[Delete the object prefixes of the Project]
obj --> apps[Wait for app removal]
apps --> tasks[25 Tasks per message:<br/>purge AgentDO, delete Task rows]
tasks --> rows[Delete content rows]
rows --> shards[Purge 32 ResourceShardDO shards]
shards --> done[Delete the policy copy, scrub name and slug,<br/>set cleaned_at]
cron[Daily maintenance 04:17 UTC] -.->|re-queue leftovers| q
sweep[Event router, every 5 minutes] -.->|index.purge after cleaned_at| idx[Indexer: vectors, index objects, index rows]
Cleanup skips a Project that is not archived. Each queue message does a bounded part of the work. The next delivery continues from the point where the last delivery stopped. Every step is safe to repeat. Cleanup does the steps in this order:
- Purge
ProjectRootDOandProjectViewDOagain. Mark everyapp_deploymentsrowremoved. - Delete every Workspace fork (
ws-*), Candidate fork (cand-*), and code edit session fork (edit-*) thattask_runs,candidates, andcode_sessionsrecord, 20 per message. - Delete the canonical repository (
ARTIFACTS.delete(projectId)). - Delete every object in the evidence bucket under these prefixes, each followed by the Project ID:
source/,context/,apps/,app-previews/,evidence/,work-graphs/, andworkspace/. - Wait until
removeAppon the apps service clears the app data stores. - For each batch of 25 Tasks, purge the
AgentDOof each Task, then delete thetask_dependencies,decision_receipts,task_runs, andtasksrows. - Delete the rows of
evidence_index,approvals,conflicts,changesets,candidates,change_decisions,live_removals,changes,request_conflicts,goal_suggestions,project_policies,project_members,project_invitations,agents,dead_letters,releases,project_events,code_session_files,code_session_commands,code_sessions, andsandbox_awake. Each statement deletes at most 500 rows. Each message runs at most 40 statements. - Purge the 32
ResourceShardDOshards (Claims, Intents, Challenges, outbox). This step is the last step, so a late Claim cannot fill a shard again. - Delete the policy copy in
CONFIG. Set the name toDeleted projectand the slug todeleted-<id>. Setcleaned_at.
The indexer deletes the vectors in MEMORY, the index/ objects, and the project_indexes and project_vectors rows (index.purge). Every 5 minutes, the event router sends index.purge for each archived Project that has cleaned_at and an index row. The daily maintenance also sends it. The indexer deletes the rows last, so the sweep stops when the purge is complete.
Cleanup keeps these records.
| Kept | Reason |
|---|---|
The projects row (ID, organization, status, generation, timestamps), with the name and slug scrubbed |
Archive record. The slug is free to use again. |
One app_deployments row for the production app host |
The host answers 410. The row has the host label and the removed status only. |
usage_counters |
The Anthropic, Sandbox, browser, and build meters are global rows. A deletion cannot free budget. The budget meters of the Project and its Changes (project-usd-micro:<id>, change-usd-micro:<id>) hold only an ID and an amount, and they stay. |
The events/ archive in the object store, and the event streams |
All Projects share them. The event streams keep Events for 7 days. Core deletes archive objects after 365 days. |
Organization, user, and membership rows; notification_reads; processed_events |
They are not Project data. |
| The Project Sandbox | sandboxd removes it with its disk after 2 idle minutes, so it holds no data. |
3. Daily fork sweep
Section titled “3. Daily fork sweep”sweepForks in workers/integration/src/project-cleanup.ts deletes the forks of active Projects that no Candidate can need again. It deletes a fork 30 days after its run or Candidate finishes (FORK_RETENTION_DAYS).
- Workspace forks (
ws-*): runs that failed, runs that completed without a ChangeSet, and runs of a Change that iscompleted,cancelled, orfailed. - Candidate forks (
cand-*): Candidates that arerejected,stale, orpromoted. The sweep deletes a promoted fork only after it confirms that canonicalmaincontains the composed commit. - Code edit sessions: the sweep ends each open session that is 10 minutes idle or 2 hours old (
expireCodeSessionsinworkers/integration/src/code-sessions.ts). It ends at most 50 sessions and deletes at most 50 left forks per run. It deletes the session fork and the edits. It also deletes a session fork that a failed delete left. A session that passes a limit also ends at its next request.
The sweep deletes at most 20 repositories per message. It sets fork_deleted_at in the product database, so a retry deletes nothing twice.
The event router’s per-minute sweep queues one forks.sweep each day at 04:17 UTC (workers/event-router/src/maintenance.ts). The same tick queues project.cleanup again for at most 10 archived Projects with leftovers. It also queues index.build for at most 10 active Projects without a ready index. Core runs the sweep at the start of each minute (services/core/src/main.ts). The sweep runs only while the stack runs.
4. Retention
Section titled “4. Retention”Core runs the object expiry of services/core/src/expiry.ts on the object store (objects, versitygw). The expiry deletes an object by its key prefix and its upload time:
| Prefix | Age at deletion |
|---|---|
app-previews/ |
7 days |
tmp/ |
7 days |
events/ |
365 days |
Project cleanup and app removal delete the other objects.
| Resource | Scope | Retention | Managed by |
|---|---|---|---|
| Task and Candidate forks | ws-*, cand-* |
30 days after the run or Candidate finishes | sweepForks |
| Code edit session forks | edit-* |
Until the session ends (at most 2 hours, or the next daily sweep) | endSession, expireCodeSessions |
| Project repositories | One per Project, named by the Project ID | Until the Project is deleted | cleanupProject |
| Preview rows and app data stores | app_deployments rows of preview:* channels |
7 days | expirePreviews in workers/integration/src/app-release.ts |
| Preview bundles | app-previews/ |
7 days, or sooner when the Project is deleted or its app is removed | expireObjects, cleanupProject, removeApp |
| Temporary objects | tmp/ |
7 days | expireObjects |
| Evidence | evidence/ |
Until the Project is deleted | cleanupProject |
| Project objects | source/, context/, apps/, work-graphs/, workspace/ |
Until the Project is deleted | cleanupProject |
| Event archive | events/ |
365 days | expireObjects |
| Event streams | EVENTS_00 to EVENTS_07 |
7 days | JetStream max_age |
| Command stream | COMMANDS |
Until a consumer acknowledges the command, at most 14 days | JetStream work-queue retention, max_age |
| Admin audit rows | admin_audit |
365 days | workers/api/src/access.ts |
| Metrics rows | METRICS, EVENT_METRICS |
30 days | packages/platform/src/sql/metrics.ts, at core start |
docker compose down -v deletes every volume, and with it all of the data above (local-stack.md).