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:
| State | EXEPERT source | Codex source |
|---|---|---|
| Transcript authority | EXEPERT local metadata plus Supabase response | Codex App Server live events for controlled tasks; persisted pages for a mirror |
| Transport | Authenticated HTTPS | LOCAL authenticated loopback WS; REMOTE authenticated trusted WSS |
| Draft and attachments | Existing GPUI/Supabase image pipeline | Separate GPUI state; local attachments only on LOCAL |
| Models and auth | Verified EXEPERT catalog and Supabase auth | App Server model/account notifications |
| Feedback and affect | Existing EXEPERT pipelines | Never used |
| Scroll, focus, popovers | EXEPERT-owned | Codex-owned |
| Persistence | Existing EXEPERT contract | Server-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
| Component | Supported in v1 | Policy |
|---|---|---|
| Platform | Windows x64 | Other platforms remain disconnected with truthful guidance |
| Codex CLI | codex-cli 0.149.1 exactly | Any other version is blocked before App Server launch |
| Transport | Authenticated ws://127.0.0.1:<ephemeral> plus one trusted wss:// profile | Plaintext is loopback-only; no certificate bypass, shell, transcript tailing, or private GUI interception |
| API surface | Stable controlled methods plus experimental persisted reads | Experimental methods pass a six-method read firewall |
| Authentication | Existing Codex account plus per-server capability token | Raw tokens live only in Windows Credential Manager or explicit copy/import grants |
| Reasoning | Readable reasoning summaries | Raw reasoning text and deltas are discarded at the reducer boundary |
| History | App Server thread history | One 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 message | Normalized behavior |
|---|---|
item/started | Create a provisional item keyed by thread, turn, and item |
item/agentMessage/delta | Append visible commentary or final text immediately |
item/plan/delta | Update the matching expandable plan |
item/reasoning/summaryTextDelta | Update readable reasoning summary only |
item/reasoning/summaryPartAdded | Preserve indexed readable-summary boundaries |
item/reasoning/textDelta | Discard before UI state or diagnostics |
| command output delta | Append sanitized visible output within the item budget |
item/completed | Replace provisional data with the authoritative item |
turn/started | Capture the accepted turn ID before the first streamed item |
turn/completed | Settle processing, interrupted, or failed state |
thread/status/changed | Update active, waiting-for-approval, and waiting-for-input badges |
serverRequest/resolved | Invalidate the matching approval or question controls |
| unknown notification | Store a bounded, sanitized diagnostic |
| unknown server request | Reply 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.
Persisted search
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:
thread/items/listfor full item fidelity;thread/turns/listwithitemsView: "full";- a five-second, 500-item-capped
thread/readsnapshot.
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:
- interrupts active turns for which this EXEPERT peer holds control;
- releases collaboration leases and notifies human-room peers;
- closes both WebSocket connections and waits briefly for terminal events;
- 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
- Install the candidate Codex CLI without changing the runtime allowlist.
- 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>. - Record the exact
codex --version, command, generation date, and bundle SHA-256 values in the manifest. - 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. - Update only the affected typed DTOs and sanitized unknown-message handling.
- Replay deterministic fixtures for framing, request correlation, interleaved tasks, atomic discovery, mirror epochs/fallbacks, recovery, approvals, and raw-reasoning exclusion.
- Run the locked Rust, Clippy, Windows release, documentation, and diff gates.
- Perform the opt-in native App Server smoke without spending model tokens by default.
- 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.