# Preserved replay assets

New sites start with public replay asset preservation enabled. Existing sites keep their live-asset behavior until an owner enables preservation in Settings, Recording. The setting applies to new recordings. Existing recordings are never backfilled and labelled as preserved originals.

The recorder still captures permitted page structure and interaction events. Preservation copies supported public images, stylesheets, and fonts shortly after intake, so replacing or deleting those files later does not change the saved copy. It does not turn DOM replay into screen video or add canvas, WebGL, video, authenticated resources, or cross-origin frame capture.

## Site settings

The API field is `replayAssets: {preserve: boolean, origins: string[]}`. Registered site origins are eligible automatically when preservation is enabled. Owners can approve up to 20 additional exact public HTTPS origins for CDNs. Wildcards, credentials, paths, private hosts, and IP literals are not accepted as additional origins.

Only files already referenced by accepted, privacy-filtered recording events are candidates. Strict recordings do not fetch external assets. The worker sends no visitor cookies, authorization, or Referer headers. DNS pinning, redirect validation, size limits, decompression limits, content sniffing, and supported MIME types follow the replay proxy's safety rules.

Changing preservation or approved origins applies to future recordings. A recording keeps the policy it began with, so changing a setting during a visit cannot silently change its preservation policy. An unapproved file in an old recording is not fetched later and presented as its original copy.

## Coverage in replay

The player and heatmap reference show preservation coverage, with a refresh action for work still in progress:

| Status | Meaning |
| --- | --- |
| Preserved | The supported external references found within the capture limits have saved copies. |
| Pending | Durable work is still waiting or running. |
| Partial | Some references were unavailable, blocked, outside capture limits, or copied late. |
| Legacy | This recording predates preservation and uses live files. |
| Disabled | Preservation was off when this recording began, so it uses live files. |

Saved files reflect the bytes the worker fetched, with capture timestamps. They cannot establish what a browser saw if a file had already changed before the copy. Copies more than 30 seconds after their recorded reference remain marked partial. New incomplete archives block missing files instead of silently loading today's version. A missing manifest on a new recording is also partial, rather than a reason to use live files.

Public shares carry the same coverage counts, but omit the private approved-origin review list. Agents receive coverage through recording and replay responses. A report must not describe a legacy or partial replay as an exact historical screen.

## Durable work and storage

The accepted recording batch and its archive work are committed together. Exact batch retries do not create new work. Intake schedules the accepted batch through Cloud Tasks and waits for the scheduling result before acknowledging it. A scheduling failure is retryable without counting the recording twice.

Workers use leases, bounded attempts, and immutable per-recording manifests. A URL hash maps to one selected content hash. Selected bytes are pinned before saving; an interrupted save recovers that same copy or exposes a gap, rather than downloading a newer file under the old reference. CSS retains its encoding and dependencies resolve against the final URL after redirects. Stored CSS is rewritten to fresh signed archive links only when served.

Objects live under `recordings/<project>/<recording>/assets/<content-hash>`. This deduplicates equal files within a recording. Files are not deduplicated across recordings. Deleting or expiring a recording denies subsequent asset reads and removes its object prefix with the recording. Provider soft-delete recovery settings can retain deleted object generations for their configured recovery period.

Asset links bind the project, recording, asset identifier, and expiry. Every asset read checks the current project, recording, tombstone, and retention state. Storage client rules still deny direct object reads. A valid old signature cannot resurrect a deleted recording.

## Limits and operations

Preservation is bounded to 64 unique URLs, four dependency levels, 32 intake batches, and 20 MB of copied files per recording. Later references or exhausted bounds produce partial coverage. A project has a UTC-day budget of 1 GB and 10,000 fetch requests. Failed or interrupted fetches reserve budget conservatively. The UI must not promise that every file will be preserved.

API instances share four file-processing slots across preservation, saved-asset reads, and legacy fetches, with a bounded waiting queue. Individual network and storage operations have deadlines. Intake schedules only its accepted batch. Recovery schedules at most four jobs concurrently within a ten-second dispatch budget and advances a persisted dispatch generation, so an exhausted task's old name cannot prevent recovery. Work older than its capture window cannot fetch a fresh file and call it original. The scheduled retention job recovers orphaned outbox work; it is a recovery mechanism, not the normal near-intake capture path.

`infra/replay-assets.sh` provisions the queue and task identity. `infra/deploy.sh` configures a tagged worker target for candidate verification and updates the retention recovery job. Production requires:

- `HOOTLENS_ARCHIVE_QUEUE`: the full Cloud Tasks queue resource.
- `HOOTLENS_ARCHIVE_WORKER_URL`: the HTTPS origin serving this worker revision.
- `HOOTLENS_ARCHIVE_TASK_AUDIENCE`: the configured service audience.
- `HOOTLENS_ARCHIVE_TASK_EMAIL`: the exact Google service account allowed to call the worker.
- `ASSET_PROXY_KEY`: the existing signing key shared by API instances.

The internal worker verifies a Google ID token, its audience, issuer, verified email, and exact configured worker identity. Cloud Tasks headers alone confer no access. Runtime and retention accounts can enqueue tasks on the dedicated queue and attach that worker identity. The task identity has no separate recording-data role; storage runs under the API's existing private runtime identity.

Local development can dispatch inline using the same durable repository. Cloud tests must separately verify queue configuration, IAM, delivery, indexes, and object permissions. Emulator success does not establish that those cloud resources are configured.
