Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bdb1594
docs: add r2-orphan-audio-cleanup plan
gianpaj Aug 22, 2026
e6a23b8
refactor(scripts): share maintenance setup
gianpaj Aug 23, 2026
ce6da3d
feat(scripts): add safe R2 audio cleanup
gianpaj Aug 23, 2026
d14c276
docs(scripts): document R2 cleanup [skip deploy]
gianpaj Aug 23, 2026
7528116
docs: design R2 audio backup [skip deploy]
gianpaj Aug 23, 2026
7d6e63a
docs: track R2 backup implementation [skip deploy]
gianpaj Aug 23, 2026
1e15ec1
refactor(scripts): share R2 transfer logic
gianpaj Aug 23, 2026
c2a5c81
feat(scripts): add R2 audio backup
gianpaj Aug 23, 2026
f2b922c
docs: record R2 backup validation [skip deploy]
gianpaj Aug 23, 2026
cd0c271
fix(scripts): evict deleted audio cache
gianpaj Aug 23, 2026
39e76f1
fix(scripts): test cleanup orchestration
gianpaj Aug 23, 2026
0407ca9
docs(scripts): explain cleanup references
gianpaj Aug 23, 2026
af397ae
fix(scripts): stabilize storage key pages
gianpaj Aug 23, 2026
77619d9
fix(scripts): enforce direct backup caps
gianpaj Aug 23, 2026
32b0ab2
fix(scripts): avoid duplicate manifest reads
gianpaj Aug 23, 2026
b2e3588
docs(scripts): clarify manifest review [skip deploy]
gianpaj Aug 23, 2026
afc94b2
fix(scripts): retain partial delete reports
gianpaj Aug 23, 2026
5f54f72
docs(scripts): explain serial cleanup [skip deploy]
gianpaj Aug 23, 2026
48ee744
docs: refresh R2 validation [skip deploy]
gianpaj Aug 23, 2026
d01a662
test(scripts): pin download result order
gianpaj Aug 23, 2026
4e222db
test(scripts): cover checksum mismatch
gianpaj Aug 23, 2026
1f8220b
docs: refresh R2 test count [skip deploy]
gianpaj Aug 23, 2026
a7fe596
fix(scripts): validate cleanup action options
gianpaj Aug 23, 2026
99e79fd
fix(scripts): harden R2 backup startup
gianpaj Aug 23, 2026
9522920
fix(scripts): require existing backup root
gianpaj Aug 23, 2026
8493838
test(scripts): cover backup root permissions
gianpaj Aug 23, 2026
d34d695
docs(scripts): document backup root safety [skip deploy]
gianpaj Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# R2 orphan audio cleanup

## State

The cleanup command and shared script helpers are implemented. The approved plan is
`docs/plans/2026-08-22-r2-orphan-audio-cleanup.md`.

## Decisions

- Inventory and action runs stay separate. An action always consumes a manifest.
- Each allowed location records a mutually exclusive funnel: younger than the cutoff, old and database-referenced, or an orphan candidate. Zero-object locations remain visible.
- R2 has no aggregate bucket-size response. Inventory lists all object metadata once per configured bucket and sums object count and bytes. Objects outside the cleanup prefixes contribute only to bucket totals.
- Candidate checks use one set of live `audio_files.storage_key` values across both R2 buckets.
- The command scans only the three approved bucket and prefix pairs.
- Normal deletion requires a verified local file. `--force` skips only that backup check.
- Existing local files are never overwritten. Finalization uses an atomic no-clobber hard link, with exclusive copy as a fallback for filesystems without hard links. POSIX `rename` can replace an existing destination.
- Objects with opaque or multipart ETags may be downloaded, but they remain ineligible for normal deletion because the local copy cannot be verified against an MD5 checksum.
- Downloads send `If-Match` and stop writing if R2 exceeds the manifest size. This keeps changed objects from consuming the rest of the drive.
- Deletion handles one object at a time. It verifies the local checksum first, then performs an exact database key lookup and a fresh `HeadObject` immediately before the delete request.
- R2 documents conditional reads but not conditional deletes. A replacement or new database reference can still appear in the short gap between the final checks and deletion. Removing that race requires a server-side conditional delete or a coordinated retention state in the database.
- Shared script modules stay narrow: environment loading, Supabase client creation, and R2 cleanup logic.

## Findings

- `scripts/.gitignore` already ignores `*.json`, so inventory manifests and action reports under `scripts/backups/` need no new ignore rule.
- `scripts/tsconfig.json` type-checks the TypeScript maintenance commands and their shared modules.
- `reset-freeloader-credits.mts` is the only migrated command that enables Supabase session persistence.

## Verification

- `pnpm --filter @sexyvoice/scripts test` passes 23 tests.
- `pnpm --filter @sexyvoice/scripts type-check` passes.
- The focused Biome check for the cleanup command, helpers, and tests passes.
- `pnpm fixall` passes with five existing Sentry namespace-import warnings.
- `pnpm type-check` passes for all packages.
- The normal `pnpm test` run passes all 63 web test files and all script tests, then waits in Vitest watch mode until the command timeout.
- `CI=1 pnpm test` exits but fails the 35 Stripe webhook tests because the shared Redis connection closes. The Stripe webhook file passes all 35 tests when run alone.
- Read-only inventory completed against both configured buckets. `sv-audio-files` contained 29,290 objects and 34,825,931,807 bytes. `sv-api-speech-audio-files` contained 5,132 objects and 2,226,824,512 bytes. The free prefixes contained only objects younger than 45 days, and the manifest contained zero orphan candidates.
42 changes: 42 additions & 0 deletions .agents/notes/implemented/operations/2026-08-23-r2-audio-backup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# R2 audio backup implementation

## Scope

Implement the approved design in `docs/plans/2026-08-23-r2-audio-backup-design.md` without changing the cleanup command's deletion policy.

## Invariants

- Backup never reads Supabase or deletes remote or local data.
- Existing local paths are verified but never replaced.
- Cleanup keeps its bucket/prefix allowlist, 45-day cutoff, manifest validation, database checks, and fresh pre-delete R2 check.
- Opaque ETags count as size-verified for backup but remain insufficient to authorize cleanup deletion.
- Shared R2 modules stay Promise-based. Effect remains in the backup command.

## Implementation sequence

1. Extract the AWS SDK adapter and generic transfer helpers. Keep cleanup tests green.
2. Add source parsing, planning, downloads, reports, and offline tests for the backup command.
3. Add `effective-progress@0.12.0` and `effect@4.0.0-rc.111`, wire the root command, and document operation.
4. Run focused checks, repository checks, and one narrow production dry run.

## Decisions and learnings

- Shared metadata stores normalized ETags. The AWS adapter adds quotes for `If-Match`.
- The adapter classifies conditional GET failures as `changed` or `missing`; other failures reject.
- Safe path resolution inspects existing components with `lstat` and rejects keys with empty or dot segments or backslashes because filesystem normalization could collapse distinct R2 keys.
- Node does not expose an `openat`-style API for no-follow directory traversal. The command rechecks path components before writes and finalization, but it assumes no hostile process mutates the destination tree during a run.
- `effective-progress@0.12.0` does not support `mode: 'result'` on `Progress.forEach`. It supports that mode on `Progress.all`. Use `Progress.all` for TTY runs and `Effect.all` for non-TTY runs so all downloads finish and the progress bar reports success and failure counts accurately.
- The approved spec was moved by the user from `docs/superpowers/specs/` to `docs/plans/`; preserve the move.
- Effect's optional `msgpackr-extract` dependency does not need native acceleration for this command, so its install script is disabled in `allowBuilds`.
- Main-bucket cleanup deletion evicts the object's Redis URL cache key after R2 confirms deletion. Cache eviction failures return a nonzero result because a stale URL would break repeated generation requests.

## Verification record

- Shared extraction: cleanup tests pass, scripts type-check passes, and focused Biome checks pass.
- Backup implementation: 39 offline tests pass, scripts type-check passes, and focused Biome checks pass.
- Repository gates: `pnpm fixall` passes with five existing Sentry namespace-import warnings. `pnpm type-check` passes.
- Full tests: all 39 scripts tests and 62 of 63 web test files pass. The 35 Stripe webhook tests fail on the existing closed Redis connection in `tests/utils/redis-test-utils.ts`; the focused file fails for the same reason.
- Production dry run: `sv-api-speech-audio-files/generated-audio/` lists 5,132 objects and 2.1 GiB, selects all objects without downloading, and writes the ignored report.
- Review fix: 41 scripts tests pass, scripts type-check passes, focused Biome checks pass, and `Redis.fromEnv()` accepts the project's `KV_REST_API_URL` and `KV_REST_API_TOKEN` variables.

Do not run a full backup.
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ Routes under `apps/web/app/api/v1/*` are API-key authenticated except
- For Gemini TTS error or credit complaints, follow
`skills/investigate-gemini-tts-credit-report/SKILL.md`; keep the investigation
read-only and require separate human approval for any refund.
- Dashboard TTS may use Redis URL caching; external API speech must not.
- Dashboard TTS may use Redis URL caching; external API speech must not. R2
cleanup deletion must evict the matching dashboard Redis cache key after it
deletes a main-bucket object.
- Dashboard audio uses `R2_BUCKET_NAME`; external API audio uses
`R2_SPEECH_API_BUCKET_NAME` and `R2_SPEECH_API_PUBLIC_URL`.
- Voice generation can involve Replicate, Google Gemini TTS (models `gpro` for
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"@tiptap/starter-kit": "^3.22.4",
"@tiptap/suggestion": "^3.22.4",
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "1.37.0",
"@upstash/redis": "catalog:",
"@vercel/analytics": "^1.6.1",
"@vercel/edge-config": "^1.4.3",
"@vercel/speed-insights": "2.0.0",
Expand Down
1 change: 1 addition & 0 deletions docs/devops.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ Used for:
- caching
- rate limiting
- fast lookups
- evicting dashboard audio URL cache entries after R2 cleanup deletion

### Cloudflare R2

Expand Down
Loading
Loading