# Durable operation state

Updated: 2026-08-09

`UserOperations` is the shared persistence foundation for long-running CV, Strategy Snapshot and later AI work. It is not a workflow engine and carries no raw CV, email, prompt, job description or private note. Producers store only bounded task/policy fields plus an opaque subject reference.

## State and ownership

Allowed application states are `queued`, `running`, `waiting_for_retry`, `waiting_for_external_fallback`, `succeeded`, `failed` and `cancelled`. Every row has an owner query filter and a unique `(OwnerUserId, TaskType, IdempotencyKey)` index, so duplicate clicks for one user return the same operation while another user may use the same key safely.

Workers claim from a neutral scope with one conditional database update, receive the owner ID and lease token, then must re-enter that owner scope before heartbeat/completion/failure. Owner mutations use normal query filters plus the unguessable lease token. Claiming from an owner/HTTP scope and completing from a neutral scope are refused.

Expired leases become retryable until `MaxAttempts`; the final expiry fails. Cancellation is immediate before execution and cooperative while running; an expired cancelled lease converges to `cancelled`. Queued deadlines fail closed. Retry delay, attempts, leases, field lengths and progress percentages are bounded.

## Schema ownership

`20260802224646_AddUserOperations` is EF-owned and intentionally absent from `StartupInitializationExtensions`. Its `Up` branches by provider: native SQLite DDL from the model and explicit bounded MariaDB `varchar`/`char`, `datetime(6)` and `int` DDL. Common indexes are generated by the active provider. This incrementally reduces the dual-ownership risk from JT-019 while preserving clean MariaDB types.

Rollback requires stopping operation producers/workers, draining or explicitly cancelling active rows, retaining any referenced results, then applying the migration `Down`. Rolling an old application version against a database that still contains this additive table is safe; dropping it loses operation history and must not be done casually.

Terminal notifications are described in `notifications.md`. Authenticated owner APIs expose bounded list/detail/cancel/retry state under `/api/operations`; DTOs omit idempotency keys, leases, provider/model fields, failure text and result references.

`AiOperationAdmission` now provides the shared AI producer boundary: it rechecks live Pro/AI settings, snapshots `local_only` or `external_allowed`, applies per-user/global capacity, assigns a deadline and returns the stable `/api/operations/{id}` status URL. It stores only subject type/ID, never raw CV/email/prompt text. The current process-local admission semaphore is correct for the documented single-backend deployment; multi-replica rollout requires a database capacity reservation.

`AiOperationWorker` claims only registered task types by priority, enters the explicit owner scope, rechecks entitlement/privacy/cancellation, runs one inference by default, heartbeats the lease, enforces a timeout, classifies bounded retry/permanent failure and commits the existing terminal notification. Its execution scope carries the rechecked privacy/task decision to the shared sidecar client. Successful and failed provider attempts persist bounded provider/model/route provenance in existing operation fields.

`strategy.snapshot` is the first production feature handler. Its producer returns `202`, stores only a bounded job/attachment subject, reuses active work and exposes a context-specific resume lookup. The worker rehydrates owner-filtered data, makes one bounded structured request and publishes one unique `AiWorkspaceNote` only after full response validation. Its GET route is cache-only, so a page read never starts model work.

`Workers:AiOperationsEnabled` still defaults false pending selected-model/browser/MariaDB/restart/production gates. AI-002 supplies sequential local-first routing and a process-local single-model circuit; `strategy.snapshot` is deliberately absent from the external allowlist. AI-004 adds the CV handler. The current one-worker default is the local-model concurrency limit until PROD-003 benchmarks justify anything else.
