# Codex App Server feasibility — 0.2.2

Date: **2026-10-03 (Asia/Shanghai)**. Installed CLI and existing managed daemon: **0.154.0**. Core spike PASS; **CodexAdapter added**.

Authority order: installed `codex --version`, `codex app-server --help`, and `codex app-server generate-ts --experimental` → real protocol requests → [0.154.0 daemon client source](https://github.com/openai/codex/blob/rust-v0.154.0/codex-rs/app-server-daemon/src/client.rs), [current official repository](https://github.com/openai/codex/tree/main/codex-rs/app-server), [official App Server documentation](https://developers.openai.com/codex/app-server). Generated schemas were inspected locally; the adapter does not parse Codex session JSONL.

## Seven answers

| Question | Result and evidence |
|---|---|
| Static discovery | PASS. An independent `codex app-server --listen stdio://` initialized and listed existing stored threads. An initial limit-5 request returned 5. |
| Exact cwd filter | PASS. `thread/list` filtered the requested ESP32 project cwd, returned 5 threads, and every row matched exactly. Gateway uses pagination until `nextCursor` is absent. |
| Dormant resume | PASS. Created a dedicated test thread, completed `SESSION_READY`, closed the independent server completely, started a new server and resumed the identical ID without a TUI. |
| Send input | PASS. `turn/start` accepted text, then streamed `SESSION_RESUMED`. This is the adapter's input method. `thread/queue/add` is present in the installed schema (`threadId`, `input`, `clientUserMessageId`); queue management is not implemented and was not needed for the acceptance proof. A currently RUNNING Codex turn returns 409 rather than silently queuing/steering. |
| Streaming | PASS. Observed `item/agentMessage/delta`, `item/started`, `item/completed`, `turn/started`, `turn/completed`, `thread/status/changed`. A separate read-only `pwd` turn emitted real `commandExecution` activity and ended with `CODEX_TOOL_OK`. These map to unified text/tool/state activity. |
| Approval | Protocol confirmed in installed generated `ServerRequest` and decision types. Requests are `item/commandExecution/requestApproval` and `item/fileChange/requestApproval`. Both accept `accept`, `acceptForSession`, `decline`, `cancel`; response keeps the incoming request ID and returns `result.decision`. See [approval design](approval-design.md). No interactive approval round-trip or UI is claimed. |
| Model/settings | PASS. Actual `thread/settings/update` changed/confirmed `model=gpt-6-astra`, `effort=low`, `approvalPolicy=on-request` on the smoke thread and returned `{}`. Schema exposes all three. No Settings UI/API is added. |
| Restart recovery | PASS. Independent server process stopped/restarted; `thread/list` rediscovered the persisted smoke thread; `thread/resume` preserved its ID; input and assistant output succeeded. `thread/read`, `thread/turns/list` (2 turns) and `thread/items/list` also succeeded. |

The table splits static discovery and cwd filtering for clarity; it covers all seven requested capability areas.

## Existing daemon reuse and concurrency

`codex app-server daemon version` reported an already running managed daemon, same version 0.154.0. Its official control socket is `$CODEX_HOME/app-server-control/app-server-control.sock`. Gateway connects via **WebSocket over this Unix socket**, never through Tunnel or a TCP listener. This is stricter than loopback-only networking. No second daemon/service was deployed and the existing daemon was not restarted.

The `codex app-server proxy` command forwards raw transport bytes; it is not a JSON-lines endpoint. A JSON-lines initialize to it timed out during the investigation. The actual daemon protocol uses a WebSocket upgrade, as confirmed against version-tagged source. Python websockets must use `compression=None` with this daemon; offering permessage-deflate made its handshake close. The final adapter uses this verified transport.

Gateway is one persistent client of the shared daemon. The daemon owns thread runtime identity; resuming a loaded thread rejoins it, and Gateway serializes resume+input setup. Existing Desktop activity remained available during the smoke. We only mutated the dedicated smoke thread. This verifies coexistence through the shared daemon, **not** arbitrary concurrent writers of the same thread in independent legacy/in-process servers. Avoid handing one thread to a second independent runtime. Independent spike servers were stopped before the daemon resumed their test thread.

Disconnect clears Gateway's live overlay and fails outstanding inputs with an unknown-outcome error. The next request reconnects and initializes; input is never automatically replayed. Gateway shutdown closes its client connection and does not kill/restart the user's daemon. Daemon lifecycle/restart recovery remains owned by the existing Codex manager; there is no duplicate `codex-app-server.service`.

## Real identities and integration proof

Test thread: `01a0ff2c-30b1-7f80-8863-9cb15616a118` in a dedicated temporary project.

1. Independent server: `SESSION_READY` → process closed → new process → same ID → `SESSION_RESUMED`.
2. A separate Gateway connected to the existing daemon: catalog `DORMANT`, POST input returned HTTP 202 with `resumed:true` in **0.256 seconds**; activity showed **GATEWAY_CODEX_RESUMED**, state DONE, identical ID.
3. Existing live thread input: read-only shell `pwd`; observed `commandExecution` and **CODEX_TOOL_OK**, state DONE.

Reproduce the independent spike explicitly (uses real model calls and a fresh test project):

```bash
server/.venv/bin/python scripts/spike-codex-control.py /absolute/path/to/registered/project
```

Enable integration only after a successful spike on the installed version: `GATEWAY_CODEX_ENABLED=1`. Set `GATEWAY_CODEX_SOCKET` only when the daemon uses a non-default path. Approval-required commands are declined with a visible reason until a later approval UI exists. READY in 0.2.2 means static discovery, same-ID resume, input submission, and text/tool/state activity, not Approval or Voice support.
