Skip to main content

Native Codex live client

EXEPERT desktop has two independent Chat sources. EXEPERT continues to use the existing authenticated Supabase and 9Router path. Codex Experimental is a dual-server client for the official Codex App Server. It keeps the EXEPERT-owned local App Server and one optional external App Server connected at the same time. Both use authenticated JSON-RPC text frames over WebSocket. OpenAI documents this WebSocket transport as experimental and unsupported for production.

For the bounded paging/search implementation inventory and its validation evidence, see the Dashboard performance and Codex paging delivery.

Each server has independent state for up to eight controlled tasks and one read-only mirror. A task is identified by (server_id, thread_id), so an identical thread ID on LOCAL and REMOTE cannot collide. A mirror is persisted history, not interception of a private Codex Desktop connection, and does not receive another client's unsaved token deltas. Take Control and Fork become editable only after the matching App Server acknowledgement; while a human room is active, task writes additionally require a cooperative EXEPERT lease.

Supported topology

The App Server is the sole Codex transport. EXEPERT does not use MCP as an event bus, inspect private Codex Desktop pipes or rollout files, tail transcripts, install hooks, or activate the separate undocumented remoteControl/* pairing surface. MCP calls can still appear as normal activity inside a controlled task or persisted mirror page.

Source isolation

ChatSource::{Exepert, Codex} selects the visible surface inside the existing native Chat mode. The source boundary is stronger than a visual tab:

StateEXEPERT sourceCodex source
Transcript authorityEXEPERT local metadata plus Supabase responseCodex App Server live events for controlled tasks; persisted pages for a mirror
TransportAuthenticated HTTPSLOCAL authenticated loopback WS; REMOTE authenticated trusted WSS
Draft and attachmentsExisting GPUI/Supabase image pipelineSeparate GPUI state; local attachments only on LOCAL
Models and authVerified EXEPERT catalog and Supabase authApp Server model/account notifications
Feedback and affectExisting EXEPERT pipelinesNever used
Scroll, focus, popoversEXEPERT-ownedCodex-owned
PersistenceExisting EXEPERT contractServer-scoped non-content selections and UI preferences only; tokens live in Windows Credential Manager

Codex messages never enter NativeChatState, ChatPersistence, Supabase, or EXEPERT feedback and affect pipelines. Switching source or attached task keeps each draft, attachment list, scroll position, model, reasoning effort, pending request, and focus target independent. A mirror has no draft, attachment, approval, model-mutation, or settings-persistence path.

Connection sequence

The initialize request sets experimentalApi: true solely for persisted listing, paging, and search reads, and independently opts out of item/reasoning/textDelta. The committed stable bundle remains the controlled-client contract; the experimental bundle is an immutable, typed read-only extension. Initialization becomes Ready only after the response matches the pinned InitializeResponse fields, including an absolute codexHome; a timeout or malformed result terminates the connection and enters truthful recovery. Each supervisor owns one bounded WebSocket writer/reader bridge and one bounded protocol-reducer worker, which parses and normalizes text frames before correlating responses. Binary, oversized, malformed, or non-JSON frames fail safely. The local raw capability token is never passed on the command line: only its SHA-256 verifier is given to the child process.

Because LOCAL binds an ephemeral port, the controller setup panel displays the current ws://127.0.0.1:<port> value as TLS PROXY UPSTREAM and provides COPY PROXY UPSTREAM. Reconfigure the user-managed WSS proxy whenever the owned App Server restarts before copying a controller grant. EXEPERT does not alter proxy configuration or assume the public WSS route tracks a new port. TCP, TLS, and WebSocket handshakes have bounded timeouts so lifecycle commands cannot remain blocked indefinitely behind an unresponsive endpoint.

The deterministic state reducer routes normalized notifications by thread, turn, and item ID. GPUI drains its bounded event queue in batches, coalesces only adjacent deltas with an identical thread/turn/item/kind/summary-part key, and repaints once for the batch. Queue pressure blocks producers rather than dropping events; lifecycle, approval, completion, warning, and error events are lossless and never coalesced.

Protocol and version matrix

ComponentSupported in v1Policy
PlatformWindows x64Other platforms remain disconnected with truthful guidance
Codex CLIcodex-cli 0.149.1 exactlyAny other version is blocked before App Server launch
TransportAuthenticated ws://127.0.0.1:<ephemeral> plus one trusted wss:// profilePlaintext is loopback-only; no certificate bypass, shell, transcript tailing, or private GUI interception
API surfaceStable controlled methods plus experimental persisted readsExperimental methods pass a six-method read firewall
AuthenticationExisting Codex account plus per-server capability tokenRaw tokens live only in Windows Credential Manager or explicit copy/import grants
ReasoningReadable reasoning summariesRaw reasoning text and deltas are discarded at the reducer boundary
HistoryApp Server thread historyOne process-local mirror; no offline transcript cache in EXEPERT

EXEPERT never installs or upgrades Codex. Install the exact compatible external CLI yourself, then confirm the version before launching the native client:

npm install -g @openai/codex@0.149.1
codex --version

The version check must print codex-cli 0.149.1; EXEPERT blocks every other version before starting App Server.

REMOTE initialization additionally requires initialize.userAgent to be exactly Codex Desktop/0.149.1. Authentication, TLS trust, version, and schema failures stop reconnect attempts. Transient disconnects retry with bounded exponential backoff and jitter, discard pending writes, preserve persisted projections, and return previously controlled remote tasks to mirror mode. Switching LOCAL and REMOTE tabs does not disconnect either supervisor.

Remote new tasks require a manually entered absolute path meaningful to the remote host. EXEPERT cannot choose remote folders and disables controller-local attachments and open-file actions with explanatory copy. It never provisions certificates, TLS termination, DNS, tunnels, or public ports.

The historical 0.125.0 bundle remains unchanged. Exact 0.149.1 stable and experimental bundles live under desktop/exepert-desktop/schemas/codex-app-server/0.149.1/{stable,experimental}. Each adjacent manifest records the exact generator command, date, file count, generator executable digest, and main/v2 schema SHA-256 digests. Runtime DTOs cover only the supported stable surface and typed persisted pages instead of importing unpublished OpenAI Rust crates.

Event mapping

App Server messageNormalized behavior
item/startedCreate a provisional item keyed by thread, turn, and item
item/agentMessage/deltaAppend visible commentary or final text immediately
item/plan/deltaUpdate the matching expandable plan
item/reasoning/summaryTextDeltaUpdate readable reasoning summary only
item/reasoning/summaryPartAddedPreserve indexed readable-summary boundaries
item/reasoning/textDeltaDiscard before UI state or diagnostics
command output deltaAppend sanitized visible output within the item budget
item/completedReplace provisional data with the authoritative item
turn/startedCapture the accepted turn ID before the first streamed item
turn/completedSettle processing, interrupted, or failed state
thread/status/changedUpdate active, waiting-for-approval, and waiting-for-input badges
serverRequest/resolvedInvalidate the matching approval or question controls
unknown notificationStore a bounded, sanitized diagnostic
unknown server requestReply method-not-supported and show a blocking unsupported card

Late deltas after authoritative completion are ignored and counted. Hydrated history and live notifications reconcile by stable IDs rather than creating duplicate rows; identity includes both turn and item ID, so an item ID reused in a later turn cannot collide. A completion for an older turn cannot settle a newer active turn, and late deltas cannot reopen a settled task or create false unread state. Malformed known messages and responses that do not contain exactly one result or error become sanitized protocol errors instead of disappearing silently. Each attached task retains at most 500 normalized timeline items. Visible command or tool output is limited to 256 KiB per item, retaining the head and tail around an explicit truncation marker. Developer details keep only bounded allowlisted metadata and counts rather than a complete wire item. Diagnostics use a bounded process-local ring.

Discovery and read-only mirror

Stored-task discovery is a non-overlapping epoch. While Codex Chat is visible, the runtime polls every second; while it is backgrounded, every five seconds. Each automatic epoch requests only the first thread/list page with limit: 50, sortKey: "recency_at", descending order, and explicit source kinds cli, vscode, and appServer. It also sets useStateDbOnly: true, so automatic discovery reads the state database without scanning private rollout files to repair metadata. The fresh head is merged before already loaded rows by task ID. The runtime follows an opaque continuation cursor only after the user selects Load more tasks; repeated continuation requests coalesce.

An explicit Refresh starts a new epoch and replaces page one. A failed refresh or continuation retains the last good catalog, exposes a truthful retry state, and backs automatic reads off for 30 seconds. Discovery epochs are scoped to their App Server ID and connection generation. Recovery resets only the epoch counter—not the last complete rows—so a replacement process can reconcile without a stale-epoch gap.

The runtime hashes each ordered, normalized discovery result with process-local SHA-256 state. It does the same for mirror presentation data: metadata, ordered bounded items, cursor availability, omission flag, fidelity, and visible warning. The first result and every content change emit once; an identical later poll advances scheduling without emitting another snapshot. A transition back from STALE or UNAVAILABLE always emits recovery even if its content fingerprint matches the last healthy view. An identical Refresh result does not request a repaint.

The UI reducer rejects stale generation/epoch events and classifies accepted applications as Noop, metadata-only, or transcript-changing. The bridge notifies nothing for a no-op, refreshes the workbench for metadata, and reconciles the transcript child plus one root refresh when shared task/event metadata changes with the rows. It does not unconditionally notify the root once per poll. The one-second foreground and five-second background cadences, freshness timestamps, and privacy boundaries are unchanged.

Mirror creates one process-local selection per server outside that server's eight controlled attachments. It never calls thread/resume. Initial sync publishes one descending 50-item page without waiting for metadata or older history. Incremental sync re-reads the 25-item head overlap; (turnId, item.id) is the deduplication key. Load older history is the only path that follows the older opaque cursor. One validated page is prepended while the transcript restores its stable row bookmark. Selection and poll-cycle epochs reject late pages. The UI reports mirror freshness independently from the server's task status: SYNCING, MIRRORING, STALE, or UNAVAILABLE.

If more than 500 new items arrive between polls, the runtime abandons the incomplete incremental slice and reloads the newest contiguous window before advancing its anchor. The 500-item resident cap applies across explicit older pages. The Older history omitted notice and button state come from validated cursor availability and the resident cap, not merely from an exact item count. Persisted items must have non-empty turn, item, and type identity before a snapshot can be reported current.

Stored-task search uses thread/search with a 50-row page and an explicit Load more search results continuation. Search results are a separate view over the still-current catalog; clearing search restores the catalog without a new discovery scan. If server search is unavailable, the UI labels and uses a title-only search over the loaded catalog rather than claiming complete server coverage.

Mirror occurrence search uses thread/searchOccurrences, also in explicit 50-row pages. A loaded occurrence jumps to its stable transcript row. An unloaded occurrence is hydrated with one inclusive thread/turns/list request using its runtime-owned turnCursor, limit: 1, descending order, and full items. The returned thread, turn, and item IDs must match before a bounded focused transcript is presented. The latest mirror continues polling beneath that focused scope; Back to latest restores its bookmark. When occurrence search is unsupported, only the loaded mirror window is searched, capped at 50 results and labeled as incomplete server coverage.

Search-result controls are keyed by server, task, turn, and item rather than by their current list index. A completed background hydration creates one pending focus jump for that exact server/task and consumes it once, so revisiting a server cannot replay the jump or overwrite a restored bookmark. The editable mirror-search draft is also server/task-qualified.

Task, selection, and query epochs reject responses from another server, selection, or query. Cursors remain byte-exact and runtime-owned; the UI receives only typed continuation availability. Search strings, snippets, ranges, results, and focused context are process-local and are never saved to settings or diagnostics.

Some persisted stores do not implement every experimental page. The runtime falls back in this order:

  1. thread/items/list for full item fidelity;
  2. thread/turns/list with itemsView: "full";
  3. a five-second, 500-item-capped thread/read snapshot.

The latter two show Reduced fidelity. All paths sanitize strings, remove raw reasoning content and encrypted fields, cap memory, and never log raw pages. The bounded thread/read fallback must return the exact requested raw thread identity, and focused hydration rejects duplicate or mismatched item identity. Malformed continuation or refresh pages retain the last good rows and fidelity; refresh errors are exposed without replacing healthy content. Crossing the 500-row resident cap produces an explicit omission state. The read firewall contains only thread/list, thread/search, thread/searchOccurrences, thread/items/list, thread/turns/list, and thread/read. Resume, fork, unsubscribe, turns, approvals, model mutations, and interrupts cannot pass that gateway. The mirror control bar exposes Copy Visible Transcript, Take Control, Fork, and Stop Mirror, but it has no submission path of its own.

Only an explicitly unsupported or structurally incompatible pagination method causes a fidelity downgrade. Timeouts and other transient protocol failures mark the current mirror stale/unavailable and retry the preferred method on the next cycle. A turns page that reports summary or notLoaded instead of the requested itemsView: "full" is never presented as complete; it advances to the bounded thread/read fallback.

Take Over and Fork remain explicit user promotion actions outside automatic polling. The mirrored view remains present through queue rejection, timeout, or server rejection. Only a matching successful acknowledgement clears it and enters controlled streaming. Stop, task/source switch, exit, and recovery clear mirrored content from memory; a same-session recovery then resyncs the still requested selection. No mirror ID, cursor, page, content, or selection is saved to settings. Search state and focused context follow the same process-local boundary.

Mirror start/stop uses a process-local, last-write-wins lifecycle directive separate from the bounded mutation queue. Stop therefore clears the runtime selection even if that queue is saturated or the App Server is between recovery attempts; late pages no longer have a matching selection epoch and are discarded.

Transcript realization and native scrolling

The Codex timeline is owned by a child CodexTranscriptView entity. A top-aligned variable-height GPUI ListState maintains the existing 500-item logical bound while realizing only visible rows plus 512 px overdraw. Stable row keys cover server timeline items, local pending submissions, approvals, and the omitted-history notice. splice_focusable reconciles structure and remeasure_items invalidates changed heights without rebuilding every row; off-screen focused controls remain realized for keyboard continuity.

Each task retains a process-local bookmark containing its stable anchor key, fallback index, offset within the row, and tail-follow state. Prefix trimming, Activity filtering, task switching, and Markdown height changes preserve the anchor unless the reader was within 48 px of the bottom. Only that near-bottom state follows ordinary updates. Sending and explicit end actions remain immediate. Transcript scroll frames notify the child entity rather than explicitly notifying the root. A full-height flex wrapper gives the child a definite viewport; the cached entity boundary keeps wheel-frame redraws inside the transcript, reuses unchanged row paint, and leaves the surrounding workbench untouched.

The vendored GPUI List keeps its immediate wheel handler by default and adds an opt-out used only by this child. Pixel-precise touchpad input is applied directly. Discrete line-wheel input is interpolated with ease_out_quint over 140 ms, accumulates in one direction up to one viewport, cancels on reversal or explicit navigation, and clamps without bounce. Scrollbar input and GPUI reduced-motion mode are immediate. This is application-level GPUI interpolation, not PhysX, browser compositing, CSS smooth scrolling, or a platform blur/motion hook.

An unmodified middle click on the transcript toggles a separate process-local vertical auto-scroll session. Its visible anchor has a 12 px dead zone, linear speed beyond that zone, a 1,800 px/s cap, and a 50 ms elapsed-frame clamp. Contact with either boundary pauses frames without clearing the session; reversing pointer direction resumes it. A second middle click, left/right click, wheel input, Escape, stable-key jump, end jump, task change, or focused search scope change cancels the session. Starting it pauses tail follow. No pointer coordinates or motion state enter settings or diagnostics.

Loaded Codex UserMessage rows also project into a separate virtual checkpoint rail. Markers use stable server/task/turn/item-derived row identities, loaded chronological ordinals, Jump to user message N accessibility labels, and single-line descriptions capped at 160 Unicode characters. Click and keyboard activation call the existing jump_to_key path, which cancels motion and preserves a process-local bookmark. Prepending an explicitly requested older page can renumber labels without changing marker identity; the rail never pages.

Commands, tools, other developer activity, and authorized reasoning summaries use a hard disclosure boundary. Their collapsed branch mounts only a one-line Unicode-safe summary. Explicit expansion mounts the complete sanitized projected text and allowlisted structured details; collapsing unmounts them and remeasures the stable row while restoring the visible anchor. Warnings, user messages, commentary, and final answers remain fully visible, and raw reasoning continues to be discarded before UI state.

Each realized row owns 12 px left and right gutters while the list retains 16 px vertical padding and 12 px inter-message spacing. This keeps assistant copy away from the left edge and right-aligned user bubbles away from the scrollbar without changing the 760 px body cap. When the reader is outside the 48 px tail threshold, a 32 px opaque bottom-center arrow with a centered 16 px chevron floats over the transcript. The reduced-motion-aware 140 ms appearance grows from 28 px to 32 px. An offset accent dot records only new or revised content at or below the saved viewport anchor. Server-qualified task switches retain independent bookmarks; the active dot clears on a switch, an explicit jump, or a return to the tail.

When trustworthy whole-turn timing exists, one synthetic timing row is inserted immediately before the turn's first agent item, or at the active turn tail before output exists. The row reads Working for … while live and Worked for … when complete; an exact failed or cancelled duration reads Stopped after …. durationMs is authoritative when present, otherwise a valid startedAt/completedAt difference is used. A process-local monotonic anchor drives only the live display and is never persisted as server timing.

The per-turn timing map is qualified by server, task, and turn. Read-only mirrors retain timing returned with resident turns, and a bounded supplemental thread/turns/list request hydrates matching historical turns when the primary item page lacks those fields. Supplemental failure or malformed data does not weaken the existing thread/items/list to thread/turns/list to capped thread/read content fallback, replace last-good content, or fabricate a timer. Codex 0.149.1 does not expose a reasoning duration, so authorized ReasoningSummary content stays in its existing safe disclosure and no Thought duration is inferred.

Completed-response actions

Only authoritative completed Codex agent messages receive contextual actions. The native row offers Copy, Windows read aloud, explicit sharing to the human collaboration room, Markdown export, response details, Fork from here, and a local emoji-reaction picker. It does not expose edit, delete, or regenerate because the pinned App Server does not provide those history mutations.

Codex reactions are process-local annotations keyed by server-qualified task, turn, and item identity. At most 500 reacted targets are retained, missing authoritative items are pruned, and forgetting a REMOTE profile clears its entries. They survive task switching only for the current run and are never sent to the App Server, saved to Codex settings, included in diagnostics, or shared with collaboration peers. The composer uses the same bundled offline Emojibase 16.0.2 English/Malay catalog as Native Chat, including for writable REMOTE text-only tasks; mirrors remain read-only and expose no picker.

Fork from here sends thread/fork with the selected completed turn as lastTurnId; whole-task forks omit that field. The existing attachment-capacity, remote-room membership, cooperative lease, acknowledgement, and failure rules still apply before EXEPERT selects the fork. Rejection leaves the source task unchanged.

Codex response details identify the server, thread, turn, item, model, effort, status, authoritative state, and supplied turn timing provenance. Protocol token totals are labelled as task-level usage rather than response-level usage. Only source-provided timing facts are shown. Per-response throughput remains unavailable instead of being derived from whole-turn wall time. Speech receives only visible agent text and never raw reasoning, tool payloads, approvals, attachments, or protocol content.

Tasks, turns, and approvals

EXEPERT can attach up to eight task keys per server. Attached tasks remain subscribed while inactive, while EXEPERT Chat is selected, or while another desktop mode is visible. Detach calls thread/unsubscribe; it does not archive or delete Codex history. An active task requires an explicit Interrupt and detach action. Pending New Task, Fork, and Take Over operations reserve attachment capacity, and the runtime independently refuses and releases any successful promotion that would exceed eight. This prevents rapid clicks or reordered acknowledgements from creating a hidden ninth controlled task.

A new LOCAL task requires a workspace folder on first use, then calls thread/start with serviceName: "exepert_desktop". Because this pinned GPUI folder picker has no initial-directory option, a later New Task first offers the last still valid workspace as Use last workspace, with Choose another as the safe alternative. A REMOTE task uses its profile's manually entered absolute cwd. A stored task not already owned by the runtime uses the explicit Take Control flow. The user must confirm before thread/resume; forking is the safe alternative when the source task should remain available.

Takeover and detach are acknowledgement-driven. EXEPERT does not select or persist a takeover until thread/resume succeeds. An active detach first waits for turn/interrupt to reach a terminal turn or idle-status event, then sends thread/unsubscribe; the row remains attached until unsubscribe succeeds. An idle detach sends unsubscribe directly but follows the same acknowledgement rule. When a collaboration room is required, EXEPERT first acquires the task's lease, sends thread/resume, and exposes the editable composer only after its matching acknowledgement. Queue rejection, timeout, disconnect, invalid response, or App Server rejection releases the lease and keeps the task in read-only mirror mode. If another peer owns the lease, only waiting/mirroring or forking is offered; the existing lease is never revoked.

An idle composer starts a turn. During an active turn it steers with the current expectedTurnId; Stop sends turn/interrupt. Model, workspace, and permission changes are disabled while active. Model-specific reasoning efforts and image input are derived only from model/list; image submission is blocked when the selected model does not advertise the image modality. The independent Codex textarea uses the same 8/10/12-row responsive policy as EXEPERT Chat, freezing a populated draft's cap through resize. Optimistic user copy reconciles with the authoritative userMessage item and a failed or timed-out submission remains visible with process-local Retry and Discard actions.

Server-initiated command, file-change, permission, user-question, and supported MCP elicitation requests become typed pending cards. The card retains the raw JSON-RPC request ID for its response. Codex 0.149.1 does not advertise an availableDecisions list in its stable JSON Schema, so the client derives the allowed response strictly from that versioned request type: fixed command and file decisions; the requested permission profile or an explicit empty-profile decline; turn/session scope; typed fixed-choice question answers; or MCP accept/decline/cancel. Multi-question fixed-choice input is collected before one response is sent. Secret, free-form, custom-answer, and MCP form-mode input are shown with their sanitized scope but fail closed to decline/cancel until native typed fields exist; form acceptance is not exposed without form content. No request is auto-approved. acceptForSession has a separate confirmation. After an answer enters the bounded writer queue, the card remains visible and disabled until serverRequest/resolved confirms it, preventing a local queue success from being mistaken for server acceptance. If the writer queue rejects the response, the card is immediately re-enabled with a safe diagnostic.

Trust boundaries and redaction

Codex owns credentials, transcripts, account state, and task history. EXEPERT stores only the two Codex capability tokens in Windows Credential Manager: one stable local token and one optional remote-profile token. Settings version 2 contains stable non-secret server/profile IDs, labels, WSS endpoints, remote default cwd, selected server tab, server-qualified attached/active task keys, the executable path, last local workspace, active Chat source, and non-content UI preferences. Version 1 task IDs migrate deterministically into LOCAL state. Settings never contain transcript text, drafts, image bytes, tokens, API keys, approval grants, authorization headers, controller grants, or raw protocol payloads. Mirror cursors, freshness, and transcript content are also excluded.

Copy Controller Setup, Rotate Token, and Forget Remote require confirmation. The exported exepert-codex-v1: grant is the only explicit path that places a capability token on the clipboard; it is independent from a human room invitation. Local token rotation is allowed only when all LOCAL turns are idle and restarts only the owned child. Forgetting a profile closes EXEPERT's remote connection and deletes its credential, but never stops the remote App Server.

ANSI and unsafe control sequences are stripped before display. Diagnostics and runtime logs redact authorization headers, bearer tokens, environment secrets, credential-shaped keys, and credential-shaped values, including bare sk-, GitHub-token, and JWT-shaped values embedded in ordinary copy. Unknown protocol data is summarized by safe method and bounded metadata; arbitrary payload content is not copied into logs.

The context panel reports only sanitized operational data: CLI version, child state, request and event counts, last safe error, unknown-method counts, model, workspace, App Server-reported approval policy, permission profile, network state, writable roots, thread token usage, and rate limits. A value remains explicitly “not reported” until the pinned protocol supplies it.

Process ownership and recovery

The runtime state machine is:

NotStarted -> Locating -> Incompatible
-> Starting -> Initializing -> Ready
Ready -> Recovering -> Starting
Ready | Recovering | Failed -> Stopping -> NotStarted

The executable is located from an explicitly saved executable path, then as a direct codex.exe on PATH, then at the current npm native-package layout vendor/x86_64-pc-windows-msvc/bin/codex.exe (with the historical direct layout retained as a version-probed candidate). It is never launched through PowerShell, cmd, or another shell. The direct --version probe has a two-second deadline and kills an unresponsive candidate. On Windows the child console is hidden and starts suspended; EXEPERT assigns it to a kill-on-close Job Object before resuming its primary thread. This closes the descendant-process race that would exist if a running child could spawn before Job assignment. Bounded command, writer, raw frame, normalized-event, and GPUI event queues isolate blocking I/O without Tokio. Stderr content is withheld entirely; diagnostics retain only its bounded byte count. Dropping the GPUI runtime never joins a pipe or process thread on the UI path; the detached supervisor retains the Job Object and bounded shutdown.

An unexpected LOCAL exit retries after 1, 2, and 4 seconds, then requires manual Restart. REMOTE transient failures use bounded exponential backoff with jitter; authentication, TLS, schema, and version failures stop automatically. A successful restart initializes again, resumes and reads eligible attached tasks, reconciles history by server-qualified stable IDs, clears orphaned approvals, and marks interrupted work honestly. Remote disconnect discards pending writes and demotes controlled tasks to persisted read-only projections. Mirror tasks are never resumed, unsubscribed, approved, or interrupted by automatic recovery. A connection that reaches Ready resets its retry budget.

On confirmed application exit with active LOCAL or REMOTE turns, EXEPERT:

  1. interrupts active turns for which this EXEPERT peer holds control;
  2. releases collaboration leases and notifies human-room peers;
  3. closes both WebSocket connections and waits briefly for terminal events;
  4. stops only the owned App Server and closes/terminates its Job Object.

EXEPERT never sends a server-stop command to REMOTE. If its connection cannot be reached during shutdown, the UI warns that remote work may still be running and relies on the cooperative lease's 15-second expiry.

Cancelling the exit leaves the runtime and tasks running. The same confirmation path handles EXEPERT's Exit action, keyboard quit, and native window-close button, so window chrome cannot bypass active-turn interruption.

Failure behavior

Missing or incompatible Codex leaves the Experimental tab visible with exact installation, path, and required-version guidance. It never degrades or blocks EXEPERT Chat. A malformed or oversized WebSocket text frame becomes a safe protocol error. Binary frames are rejected. Unknown responses are counted and discarded; unresolved known requests time out into explicit errors. Unknown server requests are always answered with method-not-supported so an agent cannot wait forever on invisible UI.

Discovery failure retains the last atomic list and schedules the 30-second backoff. A mirror failure retains its last safe snapshot as STALE; when no snapshot was ever obtained it reports UNAVAILABLE. A failed promotion leaves the mirror read-only. These statuses make persisted-read fidelity distinct from the App Server's own idle, active, or notLoaded task status.

Device-code login displays the App Server verification URL and user code with copy and open actions. EXEPERT does not offer API-key entry or global logout. Network, filesystem, writable-root, or approval-policy changes that broaden access require explicit user confirmation.

Upgrade runbook

  1. Install the candidate Codex CLI without changing the runtime allowlist.
  2. Generate its stable JSON Schema bundle with codex app-server generate-json-schema --out <stable-directory> and its experimental bundle with --experimental --out <experimental-directory>.
  3. Record the exact codex --version, command, generation date, and bundle SHA-256 values in the manifest.
  4. Diff the new schema against 0.149.1, focusing on initialization, task and turn lifecycle, persisted list/item/turn pages, content items, approvals, account, models, and rate limits.
  5. Update only the affected typed DTOs and sanitized unknown-message handling.
  6. Replay deterministic fixtures for framing, request correlation, interleaved tasks, atomic discovery, mirror epochs/fallbacks, recovery, approvals, and raw-reasoning exclusion.
  7. Run the locked Rust, Clippy, Windows release, documentation, and diff gates.
  8. Perform the opt-in native App Server smoke without spending model tokens by default.
  9. Extend the exact version allowlist only after all evidence passes.

Limitations and non-goals

The feature is visible by default and labelled Experimental because App Server is not a production-supported public compatibility promise. V1 covers core local coding sessions, not Codex Desktop feature parity. Fixed-choice user questions are answerable; secret/free-form/custom-answer questions and MCP form content remain explicit fail-closed limitations. It excludes cloud tasks, managed remote hosting, scheduling, PR workflows, voice, plugin administration, dynamic tool hosting, archive/delete/rollback, background-terminal APIs, API-key storage, global logout, private GUI interception, forced cross-client ownership, raw reasoning, and offline transcript caching. Cooperative leases coordinate only participating EXEPERT peers; a CLI or another client with the same App Server capability token can bypass them. Read-only live mirror means bounded polling of persisted App Server history, not access to Codex Desktop's private transport or unsaved output.

Source-contract and replay tests prove deterministic code paths; they do not prove the native Windows UI Automation tree, window geometry, focus order, visual streaming, or account-specific behavior. Those remain separate manual release evidence.