Skip to content
Registry StackDocsDevelopment (unreleased)

Review retention and audit

For the operator

View as Markdown

Decide how long Registry Casework keeps a review, what survives erasure, and what the audit journal records, so a producer collects its result in time and a caller recovers safely when an audit write fails.

A retention window is how long Casework keeps a part of a review after the request settles. The policy author sets two of them per ReviewPolicy, in casework.yaml:

retention:
terminalRetentionDays: 90
accountabilityRetentionDays: 365
  • terminalRetentionDays keeps the review itself: submitted context, tasks, notes, decisions and the result. It is from 1 to 3,650 days.
  • accountabilityRetentionDays keeps the accountability record: who decided and the private reason. It is at least as long as the terminal window and at most 3,650 days.

Both run from the request’s terminal settlement; a request still under review has no expiry. Each trusted producer also sets how long its admission receipt can be recovered:

retention: {receiptRetentionDays: 30}

Erasure runs inside casework serve; there is no separate job or command. The maintenance worker runs a retention pass about once a minute, and each pass erases at most 100 eligible requests and skips rows another transaction holds, so a backlog drains over several passes. Erasure starts only while the audit writer is ready, and each erasure is recorded in the audit journal.

When the terminal window closes, Casework deletes the ReviewResult, the decisions, private drafts, history and notes, the ReviewTasks, clock occurrences, and pending completion calls, and drops every stored response it would have replayed. It also scrubs the ReviewRequest: the submitted context, the subject and source fields, the requester reference, the result constraints, and the completion destination are removed, and the subject is replaced by the submission digest.

If accountabilityRetentionDays is longer, these remain until it closes:

  1. the accountability record: who decided, the private reason, and the public reason flag;
  2. the scrubbed ReviewRequest: its producer, policy binding, lifecycle state, submission digest, and timestamps;
  3. pseudonymous stand-ins for the producer and the initiating caller, so the initiator can still learn that its result expired;
  4. each idempotency record: the caller’s issuer and subject, access profile, operation, resource, Idempotency-Key, and request hash, without the stored response, so a resend is refused with idempotency.expired instead of admitted again;
  5. the submission reservation, keyed by the digest: the same submission is answered review.result-expired, and different content for the same producer, subject, version, and policy is review.submission-conflict.

Then all of it is deleted.

The result carries its own deadline, availableUntil: its settlement time plus terminalRetentionDays. Until then the producer reads it, and a request that is still under review answers 202. After it, the result read answers 410 with no body.

A producer that lost the admission response retries the same body under the same Idempotency-Key within its receipt window; Casework answers 200 with the request it already accepted, rather than 201. A changed body under that key is idempotency.key-reused, and a resend after the response was erased is idempotency.expired.

A completion destination is the producer’s URL Casework calls when a result is ready. The call carries no decision; the producer then reads the result. Each destination is declared under reviewCompletionDestinations in the runtime configuration, keyed by the destination id the producer names in casework.yaml.

A failed call returns to the queue and is tried again after retryDelaySeconds (default 30), up to maximumAttempts (default 12), each attempt bounded by attemptTimeoutMilliseconds (default 5000). Delivery stops when the attempts run out or the result window closes; the result stays readable until availableUntil.

An attempt interrupted by a crash or restart counts as a failed attempt, so an interrupted final attempt is not tried again. The producer still learns the result from the result feed, GET /v1/review-results, which the Base Registry Engine polls. With maximumAttempts: 1, any interrupted attempt ends the delivery as dead-lettered.

The audit journal is the file Casework appends one line to before each admitted operation and one after it commits. On a deployment it is the file audit.path names; a development session keeps its own under .casework/dev/audit/ in the project until the session is removed. Search it for the decisions reviewers made:

Terminal window
rg '"casework.review-decided"' /var/lib/registry-casework/audit/casework.ndjson

The first matching line is the request entry; the next has "phase":"response" and adds the pseudonymous ReviewRequest and ReviewTask, the accountability event id, and whether a public reason was given. This is the request entry of the decision in Decide your first ReviewRequest:

{"schema":"registry-casework-audit/v1","eventId":"<id>","time":"<time>","phase":"request","correlation":"<id>","record":{"event":"casework.review-decided","principalPseudonym":"<pseudonym>","profileId":"staff"}}

The journal names people and records only by keyed pseudonym and carries no context, reason or result. Its location, key, rotation and local retention are audit.path, audit.hashKeyRef, audit.rotateBytes and audit.retentionDays in the runtime configuration. Entries are neither chained nor signed, so ship sealed files to an archive you control, and keep the key as long as you need to recompute pseudonyms; audit key handover covers rotation.

A refused audit write answers 503 with service.unavailable, and /ready answers 503 until you repair the destination and restart. The caller cannot tell from the response which entry failed, and the two cases differ:

  • The request entry is written before the database transaction opens. If it is refused, nothing changed.
  • The response entry is written after PostgreSQL commits. If it is refused, the change stands: the decision is recorded, the claim is held, or the ReviewRequest is admitted, and the journal lacks its response line.

So a caller that receives 503 on a mutation must not assume it failed. Once Casework is ready, resend the same Idempotency-Key with the same body and If-Match: Casework returns the stored response if the change committed and runs it if it did not. For a route without an idempotency key, such as grant revocation or a caseload apply, read the resource instead. Only then choose another operation; a fresh key on a stale picture can apply a second change.

A database restore can bring back data that was already erased; Back up and restore Casework gives the order of recovery.

Registry Casework API