# Automatic translation recovery

Automatic translation follows the session's explicit `auto_translate` setting,
or the workspace default when the session has no override. It operates on
committed source revisions and is independent of audio capture. The CLI, MCP,
API, browser and terminal read the same persisted result and recovery state.

## What continues automatically

A committed turn or source correction schedules a coalesced translation pass.
The controller writes a pending marker before trying the ordinary job executor.
If the executor is full, the marker retains ownership and admission is retried
without requiring another utterance or user action. Capture does not fail merely
because translation cannot currently obtain a job slot.

Retryable provider failures receive delays of **30, 60 and 120 seconds**, giving
an initial attempt and at most three automatic retries. An attempt is one
translation pass, which can contain several bounded provider requests. A
successful pass resets the consecutive failure budget, including when a later
coalesced pass starts within the same job. The controller yields the executor
after four successful coalesced passes if more source has arrived.

HTTP 408, 429 and 5xx errors, connection failures and request deadlines are
retryable. Other errors retain the provider's existing typed retryability;
configuration, privacy-policy and authentication failures normally require a
repair. The four-attempt limit also applies to an interrupted attempt resumed
after service restart. It is a pass-attempt limit, not a promise of four total
HTTP requests for a long transcript.

The retry deadline is persisted as an explicit UTC timestamp. Restarting Olenji
resumes owned `pending`, `running` and `retry_wait` records after rereading current
preferences and privacy policy. A future deadline is preserved. Startup does
not start audio capture or sweep unmarked historical sessions into a provider.
Completed, disabled, cancelled and blocked records do not automatically replay.

## Reading state

`session.metadata.translation_automation` is service-owned metadata. It appears
in session reads and JSON exports, but the public metadata-write schema rejects
attempts to set it directly. A waiting example is:

```json
{
  "state": "retry_wait",
  "attempts": 1,
  "max_attempts": 4,
  "next_retry_at": "2026-10-25T16:00:30.000+00:00",
  "reason": "TRANSLATION_PROVIDER_ERROR",
  "job_id": "job_example"
}
```

| State | Meaning |
| --- | --- |
| `pending` | Work is owned and waiting for admission or another coalesced pass. |
| `running` | An automatic pass is processing committed source. |
| `retry_wait` | A retryable failure is waiting until `next_retry_at`. |
| `blocked` | A nonretryable failure or exhausted budget requires a relevant explicit repair. |
| `cancelled` | This automatic generation was explicitly cancelled. |
| `disabled` | Automatic translation has been disabled for this session. |
| `idle` | The scheduled work has completed; further source can schedule more. |

`attempts` counts consecutive pass attempts in the current failure chain;
successful completion resets it to zero. `reason` is a stable code, including
`RETRY_EXHAUSTED` when the budget is exhausted. The existing `translation_error`
contains the explanatory error. `job_id` identifies the latest admitted owner
when one exists; a marker awaiting its first admission can have `null` here.
Historical job records retain their actual completed/failed/cancelled result.

Polling for capacity and coalescing source commits do not repeatedly emit the
same visible marker. The browser and terminal display these states without
creating their own retry timers.

## Repair and cancellation

An explicit edit of `auto_translate`, `translation_language`,
`translation_provider`, `translation_model` or `local_only` reconsiders owned
work with a fresh retry budget. Selecting `auto_translate: true` again is a
deliberate retry request even if the stored value is already true. Tags,
retention and presentation edits do not reset a blocked budget.

Provider repairs and relevant workspace-setting edits wake previously owned
unfinished/blocked work. A provider-specific repair matches the session's
explicit provider, the applicable workspace default, or an implicit provider
selection that the repair can affect. It does not replay an unrelated explicit
provider, completed session or legacy unmarked session. A disabled session
continues to require explicit enabling.

An ordinary manual `translate` request remains independent. It can translate
committed source after a provider repair without changing the automatic retry
budget. To resume blocked future automatic processing, explicitly reenable
automatic translation or edit the relevant provider/preferences.

Cancelling an active or queued automatic job ends that generation. Cancelling
the latest failed automatic job can also cancel the deferred retry it still
owns, including a durable record outside the bounded in-memory controller.
The historical failed job remains failed. Cancelling an older failed job does
not cancel a newer admitted retry with a different job ID.

An admission reservation temporarily cannot know whether the executor accepted
a newer job. It retains one cancellation intent for the previous owner and
applies that intent if admission rejects. A successfully accepted newer job has
its own owner. This prevents a full executor from reviving a cancelled retry
without allowing an old job ID to cancel new work.

Cancellation ends work already coalesced into that generation. A source or
explicit preference request after cancellation completes can create another
generation. To prevent future automatic work, set `auto_translate` to `false`.
Disabling is checked before provider resolution and before every further batch.
An already transmitted provider request cannot be retracted; its valid returned
track can still be saved. No subsequent batch starts while disabled.

## Ownership and bounds

The controller owns one sleeping thread per runtime and at most **128 small
session entries**. The database owns overflow. Startup, repair and overflow
scans use stable ID pages of **128 metadata rows**; they do not materialize a
workspace's transcript text. Unmarked and idle rows are skipped before full
session/statistics or configuration reads. Default provider lookup is sampled
once per candidate page and checked again at actual admission.

Job admission occurs outside the controller's condition lock. Each reservation
has a private generation token, so a delayed earlier submission return cannot
replace a later retry's job ID. Queued-cancellation callbacks, provider results
and cleanup act only on their owning entry. Repair pages recheck current durable
eligibility before admission; a page fetched before cancellation cannot revive
that cancelled record. Metadata is rechecked under the controller's ordering
lock so a stale source-triggered read cannot republish an older disabled state
after a newer explicit enable.

Shutdown first stops and joins the controller, then drains owned job callbacks
before closing SQLite. Shutdown interruption retains pending ownership;
explicit cancellation retains cancellation. No retry timer, provider client or
executor worker remains owned by the recovery controller after runtime closure.

The translator still serializes manual and automatic passes per session. It
rechecks source revisions when saving, preserves human-authored tracks, checks
local-only policy for each provider batch and does not republish an obsolete
selected target. Provider responses never become new source transcript text.

## Verification and limits

`tests/test_translation_recovery.py` owns the retry, capacity, restart and
cancellation behavior. Its controlled HTTP providers return literal known
Turkish/German content, and the assertions check actual source revisions and
saved tracks. Concurrent operations use events at request, snapshot, lock,
admission and submission-return boundaries. Fixture teardown releases blocked
operations, drains jobs and checks that the controller thread has stopped.
The adjacent lifecycle tests retain manual target ownership, automatic retarget
and agent-appended source coverage.

The [review record](/docs/1.1.0/source/#source-f360a30b62a8a5899778) records
executed tests, preceding-build failures and isolated fault variants. A separate
installed-package acceptance harness, `tests/smoke_translation_recovery.py`,
uses a real loopback HTTP provider and the production retry delay to check
service restart outside the source checkout. Its actual execution belongs to
the current validation index, not to the unit-test result.

Recovery begins after the pending marker is persisted. Source commit and
automatic scheduling are not one transaction: a process killed in that small
interval can leave newly committed source requiring an explicit enable or
translation request. Recovery does not claim exactly-once provider delivery:
an interrupted request may have reached a remote provider before Olenji could
persist its response. Successful revision-bound tracks are not retransmitted
on a later pass.

The translator still snapshots all committed source and existing tracks per
pass. This controller bounds scheduling memory, not that separate long-session
translation snapshot. Provider retry hints such as `Retry-After` are not yet
used; the documented bounded local backoff remains the policy. Paid-provider
availability, translation quality and hardware capture are separate acceptance
gates. No model, native runtime or network access is silently installed or
enabled by recovery.
