# Olenji API and integration contract

This document describes Olenji's local HTTP interface (schema 1.0) and internal adapter contracts, including the 1.4.0 configurable dictation and live textbox additions. The executable OpenAPI definition from `olenji schema --json` is authoritative for request validation. The Python 3.11+ package runs a FastAPI service and serves its bundled browser assets; the CLI and MCP adapter use the same HTTP interface.

## HTTP contract

All normal /api endpoints return `{ok:true,schema_version:'1.0',data:...}`. Errors are `{ok:false,error:{code,message,details?,retryable?}}`. Lists: data `{items:[...]}`. API host is same origin. Authentication is an HttpOnly cookie after `POST /api/pair` with `{token}` from URL fragment `#pair=...`; JS removes fragment immediately. CLI uses Authorization Bearer. `/api/health` public basic state. API helper unwraps data. No external frontend dependencies/fonts/CDNs. SSE uses cookie, URL `/api/events?after=0&session_id=...`; type `change`, data `{seq,type,session_id,payload,created_at}`. Replay cursor is global seq. `heartbeat` may be ignored. Throttle refreshes. Transient event type `partial`, data `{session_id,text,speaker_id?,start,end}`. Current state available by snapshot.

The local client authenticates service ownership through its workspace identity and requires the health response's application `version` to equal its own before normal operations or startup reuse. A mismatch raises `SERVICE_VERSION_MISMATCH`, including the reported client/service versions. Status and explicit stop remain available so an agent can recover without an automatic recording restart. Application version comes from `olenji.__version__`; API `schema_version` and database schema are independent version domains.

Help/version do not need third-party imports or a workspace. Raw export stdout and MCP stdout stay reserved for their document/protocol even when startup fails; such failures use stderr and a nonzero exit. A closed downstream pipe exits quietly. Interrupting a CLI that owns an unready service startup reaps that unreported child, while interrupting a wait on already admitted work leaves the durable job's lifecycle unchanged.

Job waits and `events --follow` may establish the local service once before beginning their observation budget. Initial service connection/startup is separate from `--wait-timeout` / `--seconds`; view-application waits begin after their view mutation, and the interactive terminal timer begins after its initial snapshot. Subsequent observer polls verify the existing service's workspace/version without starting a replacement. Losing that service returns retryable `SERVICE_UNAVAILABLE`; event-follow errors include the last consumed `cursor` and selected `session_id` so an agent can inspect recovery and resume. Job expiry returns retryable `WAIT_TIMEOUT` with `job_id`, without cancelling the job. Finite event follow ends cleanly, and an expired view wait returns the last known view with `applied:false` and `wait_timed_out:true` rather than claiming a late acknowledgement.

The browser defers incoming view changes while an action is pressed or a dialog is open. A note opened before a requested session change saves to its original session; the pending view applies after the draft closes. Background refresh does not replace the pressed control before activation finishes, and deferred presentation is not acknowledged as rendered. Cancellation, focus loss and hidden-page release resume deferred work; explicit detachment still takes effect.

Observer deadlines use monotonic time, bound sleeps and each network phase's timeout to the remaining budget, and are checked before/after blocking phases and between yielded response chunks. These synchronous deadlines are not hard real-time interruption guarantees: OS scheduling, a currently blocked read and response decoding can finish after the nominal deadline; a result received after expiry is not accepted. Observer JSON bodies are limited to 8 MiB, with a separate 16 KiB health-probe limit. Reads request identity encoding, close their response on success/error/cancellation, and fail with `RESPONSE_TOO_LARGE` when a normal observer body exceeds its limit. Reduce the event page/window or use export for larger content. These limits do not cap normal one-shot API reads or streamed exports.

CLI JSON document inputs (`--body`, `--params`, `--metadata`, `--config`, `--query`, `--context`, `--inference` and `--file`, including their `@file`/stdin forms) accept at most 8 MiB per document and 256 nested array/object container levels. Files and binary stdin use their encoded byte size, preserving UTF-8/UTF-16/UTF-32 BOM decoding; text-only embedded stdin uses a UTF-8 byte budget. Readers stop at the budget plus the byte/Unicode character needed to detect overflow, close files they opened, and leave caller-owned stdin open. Oversized input returns `INPUT_TOO_LARGE` with `limit_bytes`; invalid JSON, excessive nesting and nonfinite numbers (including exponent overflow) return `INVALID_JSON`. The explicit depth check reports `limit_container_depth` and uses an iterative ancestor stack, independent of the Python decoder's recursion policy. These argument errors exit with code 2 before contacting the service and do not echo input content. This input-read budget is separate from the HTTP service's 2 MiB non-upload request-body limit; an accepted input document must still satisfy its API schema and encoded request-body limit.

### Entities

- Session: `{id,title,state,language,model_id,source_kind,created_at,updated_at,duration,metadata,segment_count,word_count,speaker_count}`. state idle/processing/live/paused/ready/failed/interrupted. metadata includes retain_audio, translation_language, source_name, media_available, capture_owner, diarize.
- Segment: `{id,session_id,start,end,text,speaker_id,revision,is_final,language,confidence?,words:[]}`. API times are seconds; persistence may quantize. Speaker IDs stable, unknown allowed.
- Speaker: `{id,session_id,label,color}`.
- Translation: `{id,session_id,source_refs:[{segment_id,revision}],target_language,text,status,producer,updated_at}` status current/stale/draft. Explicit revision preconditions.
- Annotation: `{id,session_id,kind,text,segment_ids,created_at}`.
- Job: `{id,kind,state,progress,phase,message,result,error,created_at,updated_at}` state queued/running/completed/failed/cancelled. progress 0..1.
- View: `{id,session_id,config,desired_revision,applied_revision,visibility,connected,last_seen}`.

### Main routes

- GET /api/doctor -> host/capabilities; GET /api/models -> items; GET /api/models/{id}; POST /api/models/install `{model_id?,profile?,compatible_only?}` -> job; POST /api/models/{id}/verify; POST /api/models/{id}/remove. GET /api/models/parity (declare before id).
- GET /api/jobs; GET /api/jobs/{id}; POST /api/jobs/{id}/cancel.
- GET /api/sessions?q=; POST /api/sessions `{title?,language?,model_id?,source_kind?,metadata?}`; GET /api/sessions/{id} -> snapshot `{session,segments,speakers,translations,annotations,cursor,views}`; PATCH /api/sessions/{id} title/metadata; DELETE /api/sessions/{id}.
- POST /api/transcribe multipart `file`, model_id, language, diarize bool, title, retain_audio bool, inference JSON object as text -> `{session_id,job_id}`. POST /api/transcribe/path JSON same with path (CLI local only).
- GET /api/sources -> devices/capabilities; POST /api/live `{model_id,source:'microphone'|'system'|'browser'|device id,title?,language?,diarize?,retain_audio?,inference?}` -> `{session_id}`; POST /api/sessions/{id}/pause|resume|stop.
- WS /api/live/{id}?sample_rate=48000: browser PCM float32 mono binary frames, authorized cookie or bearer token; frame ack not required. Only one source may supply an active browser-owned session. The browser uses getUserMedia and AudioContext+AudioWorklet and stops its tracks on stop. Pause, resume and finish control messages share the same ordered socket and acknowledge their request IDs after preceding audio has been handled.
- PATCH /api/sessions/{id}/segments/{segment_id} `{text?,speaker_id?,expected_revision}` -> segment; POST /api/sessions/{id}/speakers `{label}`; PATCH /api/sessions/{id}/speakers/{speaker_id} `{label?,color?}`.
- POST /api/sessions/{id}/translations `{translations:[{source_refs,target_language,text,producer?,id?}]}`; POST /api/sessions/{id}/translate `{target_language?,provider?,model?,base_url?}` -> job; GET /api/providers -> configured metadata only; POST /api/providers `{id,base_url,api_key?,model}` stores protected config; no keys returned.
- POST /api/sessions/{id}/annotations `{kind,text,segment_ids?}`; DELETE /api/sessions/{id}/annotations/{id}.
- POST /api/sessions/{id}/refine `{model_id?,diarize?,inference?}` -> job; GET /api/sessions/{id}/export?format=json|jsonl|txt|md|srt|vtt|html&language=original|tr|bilingual -> file; GET /api/sessions/{id}/media -> Range-capable local source/preview.
- POST /api/sessions/{id}/export/prepare with the same export query parameters -> `{download_url,filename,bytes,cursor,expires_in}`. GET the root-relative `download_url` with the existing cookie/bearer credential for a one-use native download; the URL does not carry an API credential.
- POST /api/views `{session_id,config?}` -> View; PATCH /api/views/{id} `{config,expected_revision?}` -> View; POST /api/views/{id}/ack `{applied_revision,visibility}`; GET /api/views; POST /api/views/{id}/detach.
- GET /api/settings -> nonsensitive settings; PATCH /api/settings.

Omitted or null model, recognition-language and audio-retention options inherit the workspace defaults. Explicit `false` and `"auto"` override those defaults. Translation operations inherit `default_translation_language` when the target is omitted. Bounded read projections are an exception: the `SessionWindow.target_language` schema and `/tail` query default to literal `tr`; callers should supply the selected track explicitly. The TUI resolves its omitted language from settings before requesting its tail. New views inherit layout, theme and target-language preferences; explicit view fields override them. Idempotency fingerprints the original request, so changing preferences does not change a completed retry result.

GET `/api/sessions/{id}/metadata` returns metadata, counts, speakers, views and cursor without loading all transcript tracks. Use paginated segment/event routes for long-running agent workflows. JSON and JSONL exports retain the complete snapshot's tracks; readable formats apply their language selection.

### Inference request ownership

File-path transcription, multipart upload and refinement additionally accept `speaker_model_id` and `speaker_preset`. Either field enables speaker processing; omission preserves the existing `diarize`/WeSpeaker route and default idempotency shape. Presets must be advertised by the selected speaker catalog entry. `GET /api/speaker-models` returns speaker-only catalog/readiness records. New ONNX/Sherpa timeline choices are offline-only and reject live requests before capture. Session metadata records selected speaker model, verified identity, preset and method independently of the ASR model. Failed refinement restores that metadata with its source-preservation transaction. Timelines retain overlap uncertainty rather than duplicate source words; processing is bounded to one hour.

### Global push-to-talk

`GET /api/hotkey` reports requested configuration, native binding/approval, `listening`, `effective_state`, connected capture, owned session/job, delivery, feedback and recovery. Top-level `error` includes a nested native binding fault; `operation_error` separately retains manager/cleanup failures. A failed stop can leave `capture.connected:true` and owned audio despite suspension, blocks recovery and requires another disable attempt. `enabled` means saved requested intent and is not readiness. States distinguish disabled, ready, starting, recording, stopping, processing, inserting, inserted, saved_only, no_speech, suspended, permission_needed, failed, delivery_uncertain and unavailable. Recording requires the owned session's `capture_connected=true`; an accepted press or created session is insufficient. Processing follows the exact owned queued/running finalization job.

`POST /api/hotkey` requires a bearer credential even when a browser cookie is paired. Strict fields include primary `shortcut`, `activation` (`hold` by default or `toggle`), up to eight additional `bindings` of `{shortcut, activation}`, `delivery_timing` (`final` by default or explicit `live`), and optional speech `model_id`/`language`/`inference`, `retain_audio` (false), `local_only` (true), `max_hold_seconds` (strict integer 1–600, default 120), `output` (`transcript` by default, explicit `insert`), `feedback` (true), optional strict nonnegative `input_device` and optional `input_device_name` guard. Explicit inputs resolve to an observed name at configuration and are rechecked before every activation; missing/changed identity fails with `DICTATION_INPUT_CHANGED` rather than substituting another device. Omission retains the OS default. `GET /api/hotkey/devices` only enumerates service-host inputs/default; it opens no stream and reports permission/audio flow untested. The paired browser may read status/devices and prepare a CLI command but cannot change the binding through its cookie.

Model/language validation never silently installs dependencies, records, accesses the clipboard or enables login startup. `DELETE /api/hotkey` unbinds, cancels recovery/delivery and stops only its owned capture; incomplete cleanup is retryable and retains ownership. An enabled saved binding can restore at service startup without resuming audio. Native scope is the service host's desktop. Legacy canonical chords remain unchanged. Typed shortcut status includes `kind`, `physical_key`, `side_policy`, `display_label`, `modifiers`, `canonical` and the physical edge identity; configured activation is reported per binding and as `active_activation` during capture. Duplicate or overlapping physical bindings are refused before replacing listeners: `alt` overlaps `right_alt`, `alt+a` overlaps `right_alt+a`, and modifier order alone does not distinguish `ctrl+alt` from `alt+ctrl`. Hold ends on its owning release; toggle starts on a fresh press and finishes on a second fresh press after release. A logical recording deadline applies even when the key is up. Interruption/fault of any owned listener cancels logical capture and pending delivery, including toggle captures with no physical key held. Both gestures share one capture owner; busy presses cannot queue surprise recordings. `right_alt` is Right Option/Alt alone, distinct from `left_alt` and `alt+right`. Either-side modifiers permit both sides; a sided trigger rejects the opposite side and extra modifier families. Modifier-only chords use the final stated modifier as primary. Repeats, preheld primaries and mid-hold modifier correction cannot start capture; all primary sides must release before a new deliberate press. Disconnect/timeout/disable finalize only owned audio.

macOS uses listen-only key/flags edges with Input Monitoring, including physical modifier/Fn keys and F1–F20. X11 uses passive grabs and consumes the trigger key, including family-grab conflicts before side validation. Windows physical modifier/sided bindings are listen-only; legacy registered chords consume the shortcut and debugger F12 is refused. Windows numpad Enter/equal/clear are unrepresentable here. Ordinary ASCII letter/digit chords resolve through the current layout; the browser recorder uses `KeyboardEvent.key` rather than US letter/digit positions and refuses non-ASCII output. Mac modifier callbacks snapshot HID state instead of toggling a cache that asynchronous polls can advance; only an event with its primary still held can press, while polls may release/rearm. Forbidden opposite-side/family events end and block a physical hold even when a brief tap already ended in the hardware snapshot; either-side bindings retain their permitted opposite-side behavior. Portal numpad triggers use XKB names and Clear never aliases keypad5. The parser accepts F1–F24 and modified navigation/numpad/ordinary keys; capabilities describe platform prerequisites rather than successful registration or hardware tests. Media and mouse keys are unsupported. Wayland uses GlobalShortcuts consent/approved descriptions and refuses physical sides/modifier-only keys before registration; it never falls back to XWayland.

Secure Input preserves enabled intent with `effective_state:suspended`. Recovery waits 1, 2, 4, 8, 16 and 30 seconds for at most six fresh handshake attempts per episode. `recovery` exposes state, attempts, budget and retry delay. A fault is cleared only after a healthy replacement handshake; exhaustion needs explicit re-enable. Disable/rebind/shutdown cancel waits. Recovery never replays edges, resumes a microphone session or retries insertion, and requires release/new press. Permission revocation, layout change, disconnect and resource faults are distinct and require explicit repair/re-enable.

On macOS, optional feedback owns an isolated nonactivating click-through AppKit panel. The bounded acknowledged pipe sends only allowlisted state, numeric level/elapsed and a selected-input flag at most five times per second, never transcript/draft text. A stale level becomes unknown after 0.8 seconds; 2.5 seconds without acknowledged updates fails/ends the companion. Feedback failure is reported independently and does not interrupt capture. Other desktops report unsupported native feedback. Native focus, display, key, microphone and field behavior require their own physical verification.

With `output:insert` and `delivery_timing:final`, insertion captures an opaque supported target before microphone acquisition and keeps native focus details in an owned helper. After the exact owned `live.finalize` job succeeds, it observes a clean ready transcript, bounds source/event/text counts, rejects gaps or edits and marks one attempt before the external side effect. It rechecks source identity and the native helper rechecks the same focused field. The two processes are not an atomic database/desktop transaction; no exactly-once crash guarantee is claimed. Failed, uncertain and interrupted delivery is never replayed. Disabling cancels a pending delivery and joins an already claimed bounded native write; it cannot retract inserted text. No Enter/Send or clipboard operation is performed. macOS uses only positively writable `AXSelectedText` on an ordinary field, requires one empty typed selection and working `AXStringForRange`, and confirms the new UTF-16 span/caret after one write. An explicit disabled state is rejected. Only `AXTextArea` may omit `AXEnabled` with the native attribute-unsupported result; this relies on positive selected-text writability rather than declaring the missing state enabled. No-value, malformed, permission and transport failures remain errors. It never reads the surrounding draft. The focused application and field/PIDs must remain the captured identities. Linux uses AT-SPI insertion without an existing selection, and Windows insertion is unsupported. Native permissions and actual field behavior require their own verification.

With `output:insert` and `delivery_timing:live`, macOS captures a progressive target before opening the microphone. `delivery.capabilities.live_supported` is explicit; unsupported desktops refuse live configuration before recording. Cumulative source plus the latest non-overlapping hypothesis replaces only Olenji's previously inserted span. Hypotheses can change or shrink; they are not appended repeatedly. Finalization replaces the provisional text with the clean owned final result, without duplicating identical text. Buffered recognition latency depends on the selected model; this is not instantaneous per-word streaming.

Live mode privately reads a bounded full AXValue (at most 200,000 UTF-16 units), original caret and expected field value in the native helper. It checks foreground app/instance, field, secure state, full expected value and selection before each update, selects only its own span, rechecks and verifies the write. Human edits, caret/focus changes, failed/uncertain replies or capture gaps stop further delivery. AX does not provide atomic compare-and-replace against a human edit racing the setter; no atomic desktop transaction is claimed. Target contents never enter status, logs or durable delivery metadata. On interruption/cancellation, the last displayed draft may remain for review; there is no automatic rollback, retarget, Send or retry. Successful no-speech finalization can clear only Olenji's owned provisional span. Final-only mode keeps its existing draft-free, single-write contract.

### Recognition inference settings

File-path transcription, multipart upload, live start and refinement accept optional `inference` settings. Typed fields are `device`, `compute_type`, `cpu_threads` (strict integer 1–64) and `native_backend`; each model's `inference_options` capability object identifies accepted fields/defaults. CLI flags and MCP map to that same object. JSON CLI input and individual flags are mutually exclusive. Unsupported backend fields fail before job/session/capture/upload ownership; actual hardware/precision availability is checked by the inference process. Explicit device requests reject unavailable runtime devices/providers; explicit native backends must match the loaded model's reported backend. ONNX CUDA applies to encoder/frontend sessions and does not promise that every operation executes on a GPU. Explicit native `auto` remains an automatic selection policy.

Only requested settings persist in session metadata and each recognized source's `provenance.inference_options`; they do not certify actual hardware utilization or effective precision. Refine omission/null inherits saved requests only when the selected backend matches the prior backend. An explicit object replaces the full request; `{}` restores adapter defaults. Failed refinement restores prior model/language/settings with its existing source-preservation transaction. Default requests retain pre-option idempotency fingerprints; explicit settings are part of the request identity. Cache keys include relevant requested settings and verified installation identity. Full supported values, errors and native-host boundaries are in [INFERENCE_OPTIONS.md](/docs/1.4.0/inference-options.md).

### Hardware-aware model recommendations

`POST /api/models/recommend`, `olenji models recommend` and MCP `olenji_recommend_models` share `ModelRecommendationRequest`: `workload` is `dictation` (default), `live` or `file`; `priority` is `latency`, `balanced` (default) or `quality`; `language` defaults to `auto`; `word_timestamps` defaults to false; optional `memory_budget_mb` is a strict integer from 128 through 1048576, in MiB; `probe_runtimes` defaults to true. CLI `--no-runtime-probe` sets the last field to false. Invalid typed values and unknown fields fail before probing.

The report ranks at most five exact compatible ASR identities and returns `best_fit`, `ready_now`, `excluded`, `hardware`, `runtime_probes`, `memory_budget` and `limitations`. Candidates expose `inference`, `execution_language`, `estimated_memory_bytes`, `installed`, `runtime_observed`, `ready`, `accelerator`, `accelerator_memory_fit`, `cpu_fallback`, `install_command`, `sample_required` and reasons. Language and word-timing requirements are filters, not permission to substitute a model or fabricate timestamps. A model using automatic language detection can require `execution_language: "auto"` even when its language support satisfied a specific-language request.

`ready_now` is independently reranked among ready candidates and usable CPU fallbacks, so it can differ from `best_fit`. CUDA/Vulkan recommendations with unknown accelerator-memory fit remain unready even when the device is observed; helper executable presence alone never makes a Core ML model ready. CPU/Metal fit uses the host-budget estimate rather than certifying actual model execution.

The recommendation observes the service host and makes bounded capability probes in already installed inference runtimes when enabled. It does not download models/runtimes, select a default, start recording or benchmark speech recognition. Missing or skipped probe evidence stays unknown; a successful probe is not successful inference. Memory figures are advisory estimates and a budget is not a reservation. Explicit models and saved defaults retain their existing semantics. Applying a candidate is a separate explicit operation using its proposed inference settings and execution language.

Unspecified CPU thread defaults respect detected process affinity and Linux cgroup CPU quotas, capped at eight; explicit supported `cpu_threads` values remain overrides. These limits reduce oversubscription rather than promise maximal utilization or an optimal thread count.

### Long sessions and consistency

- `POST /api/sessions/{id}/window` reads a bounded source/translation projection and atomic cursor. The CLI exposes it through `sessions window SESSION_ID --query JSON`; MCP exposes the same `SessionWindow` fields through `olenji_session_window`. Query input supports literal JSON, `@file` and `-` stdin. Its typed body selects source offset/limit, literal full-session search, translation language, fixed/recent speakers, committed turns, seek/source anchor and independently paged notes/speakers. Render only `window_ids`; additional `segments` are connected translation context. `expected_cursor` rejects intervening content edits with retryable `SNAPSHOT_CHANGED`; `after` is valid only for an identical window/query. Limits are 500 visible turns, 1,000 connected nondeleted/nonsuperseded translation tracks, 5,000 additional source IDs and 8 MiB JSON; `WINDOW_TOO_COMPLEX` fails explicitly. Full field and ownership semantics are in [BROWSER_WINDOWS.md](/docs/1.4.0/browser-windows.md).
- `GET /api/sessions/{id}/segments/{segment_id}/neighbors` returns `{segment,previous,next,cursor}` from one read transaction. Neighbors follow the full session's `(start,id)` order, independent of browser filters or translation context. Missing source/session returns 404; merges must still supply current source revisions.
- `GET /api/sessions/{id}?after=CURSOR` returns `{unchanged:true,cursor}` if no selected-session content event occurred after the supplied checkpoint. Otherwise it returns the complete atomic snapshot. View/job state is independent of this content check; use the appropriate endpoints/events for those domains. A cursor ahead of the database produces a complete snapshot.
- `GET /api/sessions/{id}/segments?offset=0&limit=100` returns `{items,cursor,total,offset,next_offset}` from one database read transaction. Pass the first page's `cursor` as `expected_cursor` on later pages. Selected-session changes produce retryable `SNAPSHOT_CHANGED` (409); restart rather than merging inconsistent pages. Timing filters must be finite, nonnegative and ordered. Unrelated-session events do not invalidate the read.
- `GET /api/sessions/{id}/tail?limit=15&target_language=tr&after=CURSOR` returns the snapshot shape plus `tail_ids`. The source list also contains the context referenced by current or human-owned translation groups connected to those turns. Render only `tail_ids` while using the full context for revision/ownership validation. Limits are 1–100 recent turns, 1,000 connected tracks and 5,000 additional source IDs; excessive grouping returns `WINDOW_TOO_COMPLEX` (409). Unchanged content uses the same compact response as a full snapshot. Opt-in `include_partials=true` additionally returns transient `partials`, including when durable content is unchanged. Individual transient records carry their runtime `epoch` and `draft_revision`. Drafts and levels are not committed source rows; transient generation/revision fields do not replace the durable cursor.
- `GET /api/events/poll` atomically pairs `items`, `cursor` and `head`. Empty filtered pages advance to the current global head. A checkpoint ahead of the database returns `{items:[],cursor:head,head,reset:true}`. SSE emits `event: reset` with `{cursor,reason:'cursor_ahead'}`; a client must close/recreate EventSource and refresh its snapshot so an obsolete Last-Event-ID does not repeat the reset.
- `segments.replacement_started` is a durable event inside the same commit as refinement, turn merge or speaker merge. Consumers that render individual change events should resnapshot at this marker before applying constituents; the snapshot cursor lets them discard events already represented in that atomic state.
- Translation language matching is case-insensitive; new tags receive consistent language/script/region casing. Source revisions, entire sentence groups and human corrections keep their existing priority rules.

Database schema version 4 adds derived counters and indexed translation references without changing the public schema version `1.0`. Upgrading an existing older database first creates a consistent SQLite backup beside it. `doctor` reports `database_schema_version` and `sqlite_version`; runtime startup requires SQLite 3.24 or newer. Public rows do not expose internal counter columns.

When a source producer commits an earlier speaker ID that has since been merged, the source transaction follows that session's durable `speakers.merged` chain to its surviving speaker. This applies to file and live recognition, including a merge between acoustic attribution and commit. Never-existing and other-session IDs still return `SPEAKER_NOT_FOUND`. Existing source revisions and translation staleness change only through the normal merge/edit transaction. A partial merge-event index bounds lookup work to merge history; its additive creation does not change database format version 4.

Snapshot/export list ordering is explicit: segments by `(start,id)`, translations by `(updated_at,id)`, annotations by `(created_at,id)`, views by `id`, and speakers by stored insertion order. Lists of jobs use creation time then `id`; view discovery uses last-seen time then `id`. Equal timestamps therefore do not depend on a SQLite query plan. Translation selection keeps human/current/latest precedence, then uses ascending stable ID for an otherwise tied group in the browser, terminal and export readers. ID comparison uses Unicode code-point order rather than browser locale collation or UTF-16 unit order. This defines previously unspecified ties without a database schema migration.

### Admission, live ordering and export lifetime

An explicit session metadata patch with `retain_audio:false` requests owned-audio cleanup even when the preference is already false. Retention changes and cleanup serialize with job reservation and require the session to have no active capture/transcription/refinement work. A failed OS unlink returns retryable `AUDIO_CLEANUP_FAILED` (503); retention remains disabled and media access is denied, but owned files may remain on disk. After resolving the file-access problem, repeat the same disable request to retry cleanup. Success is returned only after cleanup completes. This does not delete the external original input, transcript text, prior exports or backups.

`doctor.capacity.jobs` reports `ordinary_pending`, `ordinary_limit` and `finalizers_pending`; `doctor.capacity.live_sources` reports `active` and `limit`. Ordinary active+queued jobs are bounded at 16 times the startup `max_parallel_jobs` value (16–128); cleanup finalizers remain admissible. Up to eight live sources can be owned simultaneously, including sources still starting or finalizing. An unadmitted job returns retryable `JOB_QUEUE_FULL` (503), and an unadmitted source returns retryable `CAPTURE_CAPACITY` (429). Newly created, unstarted sessions/uploads are cleaned up. These two definite admission failures release their pending idempotency reservation, so the same key and arguments may be retried after capacity becomes available. Completed results and uncertain crash outcomes retain their existing idempotency protection.

The browser PCM socket also accepts `{type:'gap',seconds,reason:'browser_processing_overrun',request_id?}`. The duration must be a finite positive number no greater than 3,600 seconds, measured from samples the producer actually discarded. The pipeline records the gap in source order, advances timing and retains it even if stop immediately follows. An optional acknowledgment is `{type:'gap_recorded',session_id,request_id?}`. Gaps are not guesses about network delay. Pause/resume acknowledgments include `state`, `updated_at` and an atomic database `cursor`; clients must not allow an older snapshot to restart a paused producer.

Pause, resume, stop and terminal-state publication share per-session control ordering. A delayed pause cannot publish after a later resume or completed finalization. Ending/cancelled capture returns `CAPTURE_STOPPING`; a released source returns `CAPTURE_NOT_ACTIVE` for pause/resume. Stop returns its finalization job after ending capture; inference drains outside the control lock. An earlier bounded control-enqueue wait can delay a concurrent control. See [live control ordering](/docs/1.4.0/source/#source-08fcb73bd054a6c115c5) for ownership and acceptance evidence.

Measured native or capture-queue loss produces `source.gap` with exact `start`, `end`, `duration_known:true` and `recoverable_from_retained_audio:false`, even before the first accepted frame or immediately before stop. One pending contiguous interval transfers with the next accepted frame/control or finalization; PCM queue admission stays nonblocking; measured-loss capture callbacks do not write SQLite. A single cause is `device_clock_gap` or `capture_queue_overflow`; mixed contiguous loss is `capture_loss` with a `causes` array. Intentional pause time advances the timeline without increasing the lost-audio count. Retained media uses silence at the missing positions and preserves accepted samples. Recognition backlog remains a distinct `transcription.gap`; see [live replay](/docs/1.4.0/source/#source-ae9db70b94e9747f9869).

Appending agent source serializes its source-writer admission with job reservation. Translation jobs may read revisions while new source is appended; capture, refinement and other source-owning jobs still prevent competing appends. A successful append wakes explicitly enabled automatic translation without requiring a later metadata edit. Appended source remains human/agent-owned through ordinary revision and refinement rules.

Exports read one SQLite snapshot and write a private temporary file before the HTTP transfer begins. Source capture and edits can continue while it is prepared; the snapshot's transaction is closed before a slow client downloads. All seven formats are encoded incrementally, preserving grouped translation ownership and source revisions. JSON/JSONL include original tracks, stale translations, notes, views and provenance.

Two exports may prepare concurrently; at most 16 prepared/sending files are owned at once. `EXPORT_BUSY` (503) is retryable. Prepared links expire after five minutes and are single-use. Completion, disconnect, preparation failure and shutdown remove the temporary file; expired files are reclaimed periodically. If deletion fails, a non-downloadable cleanup record retains ownership and its capacity slot until retry succeeds. Startup clears abandoned files with Olenji's private spool prefix. Deleting a session revokes its unclaimed exports and prevents an in-progress preparation from publishing. An already transferring download cannot retract bytes the client has received. `X-Olenji-Cursor` identifies the export snapshot; a one-use download always transfers the complete file and does not support byte ranges. Retained media continues to support ranges.

CLI/MCP destination exports stream into a temporary file beside the destination, flush it and atomically replace the destination only after a complete successful transfer. Inline MCP output is limited to 200,000 bytes without buffering an unlimited response. Export storage is proportional to file size on disk, while Python memory is bounded by a row batch/translation group rather than total transcript length.

## Terminal and agent caller semantics

### Passive environment advice

This additive interface is included from application version **1.1.0**. Earlier 1.0.2 artifacts and their documentation remain immutable and do not expose this command/tool. Registry publication and installed acceptance require their own exact-artifact receipts.

`olenji environment --json` and `olenji_environment` accept optional `task` and `context`. `task` is one of `inspect` (default), `live_transcription`, `file_transcription` or `text_translation`. `context` is an object with only these optional enum fields: `host_relation` (`unknown`, `same_device`, `remote`); `audio_access` and `file_access` (`unknown`, `available`, `unavailable`); `presentation` (`unknown`, `browser`, `artifact`, `text`). Omitted fields remain unknown. Invalid task/context values and unknown fields are rejected before work. These declarations guide recommendations and do not establish access or authorization.

The passive report distinguishes observed process facts from supplied context and unknown facts about the user, audio, file and presentation. It supplies advice, not a governing policy, task replacement, permission grant or executable prompt. A product name, process hint, package installation or MCP client name cannot prove that the process is on the user's computer. A source listed as available is not proof of granted permission or flowing audio; a browser launcher success is not proof of a visible user tab.

The CLI early branch runs before Config and needs no workspace, credentials, service connection, device/model probe, recording, job, download or browser launch. The MCP tool makes no service request or active probe and uses the same report logic; `mcp serve` retains its existing startup workspace setup. Runtime `doctor` includes environment context alongside its explicit heavier readiness discovery. MCP capabilities includes environment advice and reuses `doctor.capture` instead of enumerating sources twice. Process scope must remain explicit: passive CLI/MCP facts describe their answering process; doctor facts describe the service.

MCP initialization/discovery returns concise advisory guidance and points to the environment tool and `olenji://agent-guide` resource; reading that resource returns the full conditional workflow. Source discovery adds evidence metadata with `scope:service_host`, `availability:adapter_or_prerequisites`, `permissions:not_checked`, `audio_flow:not_checked` and `user_device_relation:unknown`. Existing source availability booleans are unchanged and must not be relabelled as successful capture or permission checks.

Task advice is conditional. Accessible files may be processed on a remote host when authorized. Missing live audio calls for a targeted connection check or a concise local-agent handoff; unknown access alone is not a reason to force a platform switch. Text-only translation does not require capture, a speech model or FFmpeg. The selected session's `local_only` policy controls Olenji provider egress; the doctor's workspace-default value does not establish that session's policy or external-agent data handling.

MCP file imports expand and resolve `path` in the MCP process working directory before sending it to the persistent service. This matches CLI local-path behavior even when the service was launched elsewhere.

`tui SESSION_ID --json` and `--jsonl` return one versioned CLI envelope whose `data` is a bounded recent-turn snapshot, with complete translation-group context and opt-in ephemeral drafts/levels. Redirected stdout defaults to JSON. These machine modes do not enter the interactive loop; use `events --follow --jsonl` for durable streaming. `live start --tui --json` returns normal live-start data plus a `terminal` snapshot and leaves capture with its declared owner. `--seconds` bounds an interactive viewer only. Explicit human mode requires a TTY and fails with `TERMINAL_REQUIRED` before starting capture when one is absent.

The interactive viewport gives newest turns and provisional drafts the available rows, preserves speaker/group attribution and keeps capture/detach status visible. It polls bounded state, applies transient updates even when the durable cursor is unchanged and shows an explicit reconnect state during bounded retryable failures. Refresh/reconnect never starts a replacement service or capture. Authentication, missing-session and nonretryable errors remain errors. See [TERMINAL.md](/docs/1.4.0/terminal.md) for geometry and ownership limits.

## Model/engine ABI

Live sessions hold a shared ASR model pin from startup through inference teardown, including silence and pause. Model removal returns `model_in_use` while that ownership remains. Recognition dispatches read current session `local_only` metadata; changing policy cannot retract an already dispatched request.

`catalog.MODELS` list of dicts with id,name,family,description,languages,task,streaming,platforms,download_bytes,license,source,backend,artifact,parity,availability,capabilities. All 17 selectable ASR identities in the pinned 25 October FluidVoice reference, plus functional portable Whisper and extra families. The dated `fluidvoice-2026-10-22` profile preserves its original sixteen selections. Preserve exact-vs-alternative identity. Apple and Pico limitations explicit.

`ModelManager(config)` config has `.data_dir:Path`, `.models_dir:Path`, `.runtime_dir:Path`.
Methods `list()->list[dict]`, `get(id)->dict`, `plan(model_id=None,profile=None,compatible_only=False)->dict`, `install(model_id,progress=None,cancel=None)->dict`, `verify(id)->dict`, `remove(id)->dict`, `parity()->dict`, `path(id)->Path|None`, `doctor()->dict`. progress callable `(fraction, phase, message)`; cancel threading.Event. Methods raise `OlenjiError(code,message,details=None,retryable=False)` from errors.py. Include readiness/platform status in list. Installation must execute real downloads/runtime setup; never claim installed just from catalog. Runtime bootstrap changes only managed runtime env, not global Python.

`engines.transcribe(path, model_id, manager, language=None, on_segment=None, cancel=None, collect=True, **kwargs)->list[dict]` records `{start,end,text,language?,words?,confidence?}`. Backend calls actual inference. on_segment(segment) streams committed outputs when supported. `collect=False` requires that callback and returns an empty list while delivering every record through it, avoiding duplicate long-transcript retention. The default return contract is unchanged. File code and pipeline may call it repeatedly for buffered live chunks. No fake fixtures in production. Arbitrary speech backend capability unsupported must be explicit. `engines.available_backends(manager)->dict`.

## Audio/pipeline ABI

`Pipeline(runtime)` runtime has `.store`, `.models`, `.config`, `.emit_partial(session_id,payload)`, `.submit_job(kind,fn,session_id=None)->job`; fn signature `(job_id, progress, cancel)`.
Pipeline methods `transcribe_file(session_id,path,model_id,language=None,diarize=False,job_id=None,progress=None,cancel=None,refine=False,inference=None)->dict`, `start_live(session_id,source,model_id,language=None,diarize=False,retain_audio=False,inference=None)->dict`, `feed_audio(session_id,pcm:bytes,sample_rate:int)->None`, `record_browser_gap(session_id,seconds,reason='browser_processing_overrun')`, `pause(session_id)`, `resume(session_id)`, `stop(session_id)->dict`, `sources()->dict`, `shutdown()`.

Store methods shared: `get_session(id)`, `update_session(id,**fields)`, `add_segment(session_id, segment)->dict`, `list_segments(session_id)`, `ensure_speaker(session_id,label, speaker_id=None)->dict`, `update_speaker(session_id,id,**fields)`, `add_event(type,session_id=None,payload={})`, `update_job(id,**fields)`, `snapshot(id)`, `clear_machine_segments(session_id)` (must preserve user edits), `put_translations(session_id, records)->list`, `translations(session_id)`. Session metadata updates merge. Core knows errors. Media saved in config.sessions_dir / session_id.

`translations.translate_session(runtime,session_id,target_language,provider=None,model=None,base_url=None,progress=None,cancel=None)->dict`. Providers are resolved from `config.get_providers(include_secrets=True)`, a map keyed by provider ID. Translation uses configured OpenAI-compatible endpoints under the session's local-only policy; external agents may instead write revision-bound tracks directly. Acoustic diarization produces anonymous voice clusters and preserves uncertainty when attribution is insufficient.

Provider responses must honor `Accept-Encoding: identity`. A compressed success response returns `TRANSLATION_RESPONSE_INVALID` before its body is read, preventing decompression from bypassing the 2 MiB response limit. Configure a provider or proxy that forces compression before retrying.

Automatic translation consumes the selected target and never republishes an older target when its provider call finishes. New committed source or a preference edit during a pass retains a coalesced continuation. A manual translation may select its completed track only if no later explicit target selection has superseded the intent captured at API job admission (or direct-call entry), including a same-language edit or a change away and back. Source-linked tracks and human corrections retain their ordinary ownership rules regardless of which target is selected.

Automatic work persists a service-owned `translation_automation` marker before admission. One owned controller thread holds at most 128 session entries; stable ID pages recover overflow from persisted metadata without loading transcript text. A full job queue retains pending work and retries admission independently. Retryable provider failures receive delays of 30, 60 and 120 seconds, with at most four consecutive attempts. Success resets the failure chain; permanent rejection or exhaustion blocks further automatic attempts until relevant explicit reconfiguration or provider repair. New speech and unrelated metadata edits do not replenish a blocked budget.

On startup, only previously marked pending/running/retry-wait work is eligible to continue, subject to current enablement and privacy settings. Legacy/unmarked, idle, disabled, cancelled and blocked sessions are not automatically replayed. Capture is never restarted. Disablement is checked before provider resolution and each additional batch. A call already sent may finish and preserve its revision-bound track. Explicit cancellation ends its owned generation; a stale scan or an older job cannot revive it or cancel a newer owner. Read-only status includes state, attempt count, retry deadline and owning job ID. Browser/terminal displays consume that state without submitting work.

An explicit `translate` command remains independent of automatic scheduling. To resume blocked automatic work, repair its provider or explicitly re-enable/reconfigure automatic translation. Relevant session keys are `auto_translate`, `translation_language`, `translation_provider`, `translation_model` and `local_only`; unrelated tags/retention changes do not reset the budget. Provider/default edits reconsider only previously owned unfinished/blocked work, not every saved transcript. Detailed states, cancellation and recovery ownership are in [translation recovery](/docs/1.4.0/translation-recovery.md).

## Offline help

`olenji docs` returns the installed handbook file URL and section list. `--open`
requests the default browser; it reports `browser_launch` as `not_requested`,
`requested`, or `unavailable`, not a claim of visible rendering. `--list` prints
section IDs; `--section ID` must match an actual heading in the bundled document.
Unknown IDs return `INVALID_ARGUMENT` without launching. Missing or malformed
help returns `DOCS_UNAVAILABLE`. This path creates no Config, Client, workspace,
credentials or service and needs no third-party modules. `--data-dir` does not
change the installed help location. The browser footer serves the identical HTML
through `/assets/handbook.html`; it contains documentation, not private session
data. Existing origin/security checks still apply.


### Editing and export corrections (28 October source update)

A split expands annotation source links to both resulting turns. A merge replaces
links to removed turns with the surviving ID, preserving order and removing
duplicates. Changed notes emit `annotation.updated` with the saved record and
`previous_segment_ids` in the same transaction as the source edit. Unlinked notes
and annotations in other sessions are unaffected. Translations still refer to
exact source revisions and become stale after source changes.

Merging accepts 2–100 distinct, globally adjacent turns assigned to one speaker.
Adjacency follows `(start, id)`, including identical start times, independent of
request-array order or UI filters. Full source text/word arrays are loaded only
for requested turns; the ordered index supplies at most one extra adjacency ID.

A supplied translation `id` is a creation hint. Existing source-group/language
records keep their identity. If creating a new record would reuse another
record's ID, the write returns HTTP 409 `TRANSLATION_ID_CONFLICT`; it does not
reassign or reveal the other record. The whole batch rolls back. A repeated
idempotency key returns that same definite failure, not an uncertain pending
state. A corrected payload requires a new idempotency key. Omitting the creation
ID lets Olenji allocate one.

SRT/VTT start and end times are rounded to milliseconds. If rounding collapses a
cue, its rendered end is at least one millisecond after its rendered start.
Stored source timing, JSON, and JSONL remain unchanged. This enforces positive
subtitle duration; it does not improve recognition alignment or guarantee that a
very brief cue is comfortably readable.

## Coordinated distribution identity

A current stable release has one application version across PyPI, npm and direct downloads. The npm tarball embeds the identical Python wheel; the complete ZIP includes that wheel, the npm archive and canonical source. Direct download is a registry fallback with the same features. Prior immutable artifacts are recovery material, not a separate LTS or feature tier.

The private repository's maintainer coordinator is outside the installed application contract. It plans and resumes publication of a manifest-bound accepted set through existing exact-commit CI/OIDC guards, without rebuilding or overwriting mismatched published files. A verified registry upload does not establish installed acceptance or website delivery. Without an explicit reviewed site plan, website delivery remains pending. The Hetzner adapter must verify both registry identities, clean installed receipts, a fresh storage gate, protected host state and the expected prior release. See [RELEASING.md](/docs/1.4.0/source/#source-8936340d2e7844f9c8b7) for recovery and authority boundaries.
