Your personal Codex control plane.
CodexNext turns codex app-server from a local CLI primitive into a
browser-first control plane for real coding work. The long-term goal is not a
chat wrapper or a model picker. It is a personal AI programming console where
you can sign in from one Web entry, pair your own machines, open projects and
sessions, control Codex turns, review approvals, recover history, and eventually
use the same control surface from mobile clients.
The project started with a deliberately small Phase 1 smoke test: prove that a TypeScript client can control the real Codex app-server over stdio. It has since grown into a relay-first product path with Web login, device pairing, replayed session events, fast history switching, deployment tooling, and CodexProvider runtime support for non-default providers and models.
CodexNext is a relay-first personal control plane for Codex app-server:
Browser / mobile Web / future mobile client
-> CodexNext Web login
-> Web HttpOnly cookie
-> Web server relay bootstrap
-> Control relay session
-> Paired outbound agent
-> Local Codex app-server on your machineThe normal product UX is relay-only. Users should not need an Agent URL, access
token, ?agent=, ?token=, or ?ownerToken= in the browser.
Codex CLI is powerful, but its native shape is local and terminal-centered. CodexNext builds the missing control surface around it:
- one browser entry for multiple paired machines
- real Codex sessions, history, turns, interrupts, steering, and approvals
- local Codex execution that keeps sandboxing and permissions inside Codex
- replay and recovery so refreshes, reconnects, and device changes do not lose the working context
- provider/model configuration that belongs to the current device and runtime, not a hard-coded global UI setting
- a shared relay-client boundary so Web and future mobile clients consume the same session, device, replay, and approval contracts
CodexNext has implemented the original foundation phases and is now in the product-hardening stage.
- Phase 1 local Codex app-server smoke test is implemented.
- Phase 2 local interactive Web Console is implemented.
- Phase 3 relay-first control plane is implemented.
- Phase 3C runtime reliability and diagnostics gate is completed.
- Shared Web/mobile relay-client boundaries are in progress.
- Device-aware CodexProvider catalog and runtime checks are implemented.
- In-session provider/model switching is implemented after the current turn completes, with active turns protected from unsafe runtime replacement.
- Web login with an HttpOnly session cookie.
- Relay-only browser bootstrap through the Web server.
- Device pairing, device registry, presence, revoke, and outbound agent connections.
- Remote control of local Codex app-server sessions from the Web UI.
- Session creation, message sending, turn steering, interrupt, approvals, and history loading.
- Replayable device events using
device:replayfor initial batches anddevice:eventfor live events. - Recent-first history loading and cached cold conversation switching.
- Normalized Codex turn/item rendering, instead of flattening Codex data into lossy chat text.
- CodexProvider runtime/catalog integration through the published provider package.
- Provider/model selection scoped to the active device.
- Same-session provider/model runtime updates after a turn completes.
- Linux
systemd, macOSlaunchd, and Windows WinSW deployment helpers. codexnext doctorchecks for local prerequisites, relay health, Web routing, agent health, Provider runtime/catalog, same-origin deployment, and public port exposure risks.
packages/protocolpackages/codex-clientpackages/relay-clientapps/agentapps/controlapps/web- relay login gate
- device pairing and revoke
- recent-first history loading
- shared recent-page cache for thread switching
- docs and ADRs
codexnext doctorcodexnext goal-smokecodexnext paircodexnext connect
- React Native / Expo mobile client
- OAuth / passkeys
- multi-user SaaS authorization
- non-Codex CLIs
- a rewritten Codex permission system
CodexNext intentionally does not replace Codex permissions. Permission mode, sandbox mode, approvals, and command execution enforcement remain inside the local Codex app-server.
- Node >= 24
- pnpm
- Codex CLI with
codex app-server - a valid Codex login/session on each machine that will run an agent
pnpm installpnpm typecheck
pnpm test
pnpm --filter @codexnext/agent dev -- doctor
pnpm --filter @codexnext/agent dev -- doctor --relay https://<your-relay-host>
pnpm --filter @codexnext/agent dev -- doctor \
--web https://<your-web-origin> \
--relay https://<your-relay-host> \
--require-same-origin \
--require-agent \
--require-provider \
--expect-closed <public-host>:3002 \
--expect-closed <public-host>:3922Think in service roles, not in one fixed machine layout:
control- device presence
- relay RPC
- event replay
- stale presence
- pairing / revoke
- audit log
web- login page
- HttpOnly cookie session
- relay session bootstrap
- browser/mobile UI
agent- one controllable Codex machine
- outbound connection to control
- local Codex execution
- approvals still enforced by Codex itself
Common topology:
- one server runs
control + web + agent - every additional machine runs
agent - browsers and phones open only the Web URL
Generate a password hash for the Web login gate:
node -e 'const {randomBytes,scryptSync}=require("node:crypto");const password=process.argv[1];const salt=randomBytes(16);const hash=scryptSync(password,salt,64);console.log(`scrypt$${salt.toString("base64url")}$${hash.toString("base64url")}`)' "your-password"Start the control server:
pnpm --filter @codexnext/control dev -- \
--owner-token "$CODEXNEXT_OWNER_TOKEN" \
--host 0.0.0.0 \
--port 3922 \
--production \
--allow-origin https://your-web-origin.exampleStart the Web app:
CODEXNEXT_RELAY_URL=http://127.0.0.1:3922 \
CODEXNEXT_OWNER_TOKEN="$CODEXNEXT_OWNER_TOKEN" \
CODEXNEXT_WEB_AUTH_PASSWORD_HASH="$CODEXNEXT_WEB_AUTH_PASSWORD_HASH" \
CODEXNEXT_WEB_SESSION_SECRET="$CODEXNEXT_WEB_SESSION_SECRET" \
CODEXNEXT_PUBLIC_ORIGIN=https://your-web-origin.example \
pnpm --filter @codexnext/web devPair a machine into the relay:
pnpm --filter @codexnext/agent dev -- pair --relay https://<your-relay-host>After pairing, the machine appears in the Web UI automatically.
For Linux systemd, macOS launchd, and service-role deployment examples, see
docs/RELAY_DEPLOYMENT.md.
Bundled helpers currently cover:
- Linux
systemd: any subset ofcontrol,web,agent - macOS
launchd: bundled helper currently targetsagent - Windows WinSW: XML templates for
control,web, andagent
Linux install examples:
./scripts/ops/install-linux-services.sh
./scripts/ops/install-linux-services.sh --roles agent
./scripts/ops/install-linux-services.sh --roles control,webmacOS agent install example:
./scripts/ops/install-macos-agent.shThe Linux systemd installer auto-detects a compatible Node >= 24 + pnpm
runtime path for the service user and writes it into the installed units. If
agent is among the selected roles, the detected runtime must also support
node:sqlite.
Startup helpers also self-heal PATH on launch. If the current environment does
not provide a compatible Node >= 24 + pnpm runtime, they probe common locations
such as PATH, ~/.local/share/pnpm, ~/.local/bin, ~/bin, and
~/.nvm/versions/node/*/bin. The agent helper also auto-discovers a usable
codex binary from common locations.
Use doctor before and after deployment:
pnpm --filter @codexnext/agent dev -- doctor
pnpm --filter @codexnext/agent dev -- doctor --relay https://<your-relay-host>
pnpm --filter @codexnext/agent dev -- doctor \
--web https://<your-web-origin> \
--relay https://<your-relay-host> \
--require-same-origin \
--require-agent \
--require-provider \
--expect-closed <public-host>:3002 \
--expect-closed <public-host>:3922Set CODEXNEXT_OWNER_TOKEN only in a trusted shell when using --require-agent
or --require-provider; doctor exchanges it for a short relay session and never
prints the raw token.
Doctor checks Node, pnpm, Codex CLI, device identity file permissions, relay health, Web/control env presence, production origin risks, Web auth status, relay session bootstrap routing, Socket.IO routing, Agent health, Provider runtime/catalog, expected-closed public ports, and hidden direct-mode env state. It reports secret presence and risk without printing raw token values.
The current roadmap keeps the original arc intact:
- Finish the shared relay-client boundary for Web and mobile consumers.
- Decide and scaffold the first mobile client path.
- Make device, machine, session, thread, workspace, and browser-client identity rules explicit across multiple clients.
- Harden conflict handling when several browser/mobile clients control the same device or session.
- Continue Provider runtime work so configured providers and models are discoverable, device-aware, and safe to switch.
- Expand slash-command workflows such as fast mode, MCP setup, personality, code review, initialization, and other CodexNext-specific commands.
See docs/ROADMAP.md for phase details.
- Public relay Web requires login.
ownerTokenis server-only.- Relay session tokens are issued after login and should not be persisted client-side.
- Relay full-access follows Codex by default; set
CODEXNEXT_DISABLE_RELAY_FULL_ACCESS=1on the control server only if you intentionally want an extra relay-only safety gate. - Relay reconnect uses
device:replayinitial batches anddevice:eventlive events. - Approvals and sandbox enforcement remain Codex-native.
- Shared relay client helpers define the Web/mobile replay auth boundary without storing owner or device tokens client-side.
Hidden Dev-Only Direct Mode
Direct mode is no longer part of the normal product path.
A hidden local troubleshooting path still exists for development only:
CODEXNEXT_ENABLE_DEV_DIRECT=1 pnpm --filter @codexnext/agent dev -- dev-serve --host 127.0.0.1 --port 17361This command is intentionally hidden from normal UX and does not print tokenized Web URLs.
These guardrails protect the shipped chat path and relay product surface. Keep them intact when changing chat state, history, provider runtime, sidebar, rendering, deployment, or diagnostics.
Conversation state guardrails
- The chat canvas reads from the normalized conversation store as the only live
source of truth. Derived history, sidebar rows, or legacy
chatItemsmirrors may reconcile into that store, but must not replace live/pending/streaming state wholesale. - Conversation identity is canonicalized by
conversationKey = threadId ?? sessionId ?? pendingClientId. RPC acknowledgements may remap aliases from an optimistic key to a real session/thread key, but the UI must not fork a second independent conversation. - Sending a message must write a client message id, optimistic user message,
and thinking placeholder immediately. The outbox state machine is
pending -> sent -> streaming -> complete/failed; an RPC ack only means the backend accepted the turn, not that streaming is complete. - Socket replay/live events may append or reconcile entities by sequence, message id, turn id, and client message id. They must not silently switch the user's selected conversation, reorder the active thread, or overwrite a live pending turn.
- History hydration is per-turn reconciliation. It may confirm completed turns and fill gaps, but it must preserve current live, pending, and streaming items unless the matching client message id, turn id, or message id proves replacement is correct.
- Development tracing must cover submit intent, queued state, RPC start, ack, socket receive, reducer apply, stream seen, selected conversation render, and reconciliation or failure. Render traces must stay summarized; never log the full visible message list on every render.
Codex app-server semantic guardrails
CodexNext must preserve Codex app-server turn/item semantics. Do not collapse
official app-server ThreadItem data into flat chat text as the integration
boundary.
- Status: completed - Protocol schemas use official turn fields:
itemsView,status,error,startedAt,completedAt, anddurationMsare required on app-server turns. - Status: completed - Protocol schemas require app-server item
idandtype, and expose item render classification for user, assistant, process, and metadata items. - Status: completed - Agent event adaptation records app-server item lifecycle, reasoning deltas, MCP progress, and process output as structured local events.
- Status: completed - Historical
thread/read/thread/turns/listdata and realtime app-server notifications enter the same normalized turn store before chat rendering. Refresh, cold switching, replay, and live streaming must project from that store instead of maintaining separate history/live UI paths. - Status: completed - Submit reconciliation uses historical
CodexThreadTurn.itemsto confirm submitted user input and later assistant or command responses. It must not decide delivery state from legacy flatLocalCodexHistoryMessage[]pages. - Status: completed - Web chat state no longer exposes flat message
hydration helpers. History hydration and older-page prepending must use
hydrateSessionFromTurns/prependSessionHistoryTurnswith app-server turn data. - Status: completed - History hydration guards read normalized page state
(
historyPages[sourceKey]) through chat-state selectors. They must not scanChatItemprojection ids such ashistory-${sessionId}-.... - Status: completed - History turn hydration and prepend reconciliation
preserve local live items from the selected conversation projection only. They
must not fall back to global
workspace.chatItems. - Status: completed - Local submit state, thinking feedback, ack binding,
agent errors, legacy assistant/command/diff deltas, and outbox recovery are
represented as turn/items first.
ChatItemis a projection for the current renderer, not a business-state write target for those flows. - Status: completed -
TurnGroupis a read-only projection derived from normalized turns. It classifiesuserMessageas user input, process item types as process,agentMessageas answer, and carries status/timing without writing back to the store. - Status: completed - The chat canvas receives
TurnGroupprojections as the primary render input. LegacyChatItem[]is only a projection fallback at the renderer boundary and must not become a second live source of truth. - Status: completed - Chat rendering uses
ChatRenderItemas the renderer DTO.TurnGroupItemmust not carrychatItem; render items are derived at the UI boundary and must never be written back to the turn store. - Status: completed - Chat header titles and the summary sheet read the
selected conversation's
TurnGroupprojection. They must not acceptChatItem[]fallback inputs. - Status: completed - Sidebar session rows receive precomputed titles from
per-session
TurnGroupprojections. Sidebar grouping must not inspect globalchatItems. - Status: completed - Development render traces and turn completion checks
read
TurnGroup/ normalized turn selectors. They must stay summary-only and must not scan globalworkspace.chatItems. - Status: completed - Completed turns with process items render a
turn-level process summary such as
已处理 5m 58s; running or failed turns keep process, approval, and error rows visible. Assistant answer items remain expanded and are not hidden inside the process summary. - Status: completed - High-content blocks use a shared thin
CollapsibleBlockwrapper. Markdown, code highlighting, diff parsing, and virtualization continue to use the installed render stack (react-markdown,remark-gfm,rehype-highlight, and@tanstack/react-virtual) instead of a custom renderer or a new dependency. - Status: completed - Model selection remains part of the start/resume/turn path. Schema or adapter work must not drop the selected model when switching or sending.
Conversation performance guardrails
Cold conversation switching is a product-critical path. The implemented architecture is local-first: clicking a conversation commits selection immediately, renders normalized in-memory or persisted cache first, and lets network history refresh run in the background.
- Selection should commit in under 50 ms. Do not block the click path on
getCodexHistoryTurns,listSessions, event replay, or history hydration. - Show the best local state first: in-memory conversation, persisted conversation cache, or a lightweight thread skeleton using sidebar title and preview metadata. Avoid empty waits for unopened conversations.
- Persist a bounded recent normalized turn cache per conversation in IndexedDB,
separate from the outbox and legacy
ChatItemprojections. The outbox is for unsent/in-flight recovery; it is not enough for fast cold thread switching. - Revalidate stale conversations in the background and merge by message id, turn id, and client message id. Users should not experience "loading the whole history" as the primary interaction.
- Prefetch visible sidebar threads, pinned threads, and recent threads during idle time with a small concurrency budget.
- Large histories must use virtualized rendering or an equivalent windowing strategy. The chat canvas must not render hundreds of Markdown/code-highlighted messages in one synchronous pass.
- Development render logs must remain summary-only: selected key, message count, latest sequence, status counts, and latest item metadata. Do not log every visible message on each render.
All seven cold conversation switching requirements are implemented:
- Status: completed - Conversation selection is local-first and must not
await
getCodexHistoryTurns,listSessions, replay, or history hydration. - Status: completed - The chat surface renders the best local state first: normalized in-memory conversation, persisted conversation cache, or a lightweight thread skeleton.
- Status: completed - Recent conversation bodies are persisted in bounded
IndexedDB cache as normalized
turnOrder/turnsdata, separate from the outbox recovery layer.ChatItem[]is only a renderer fallback projection. - Status: completed - Network history refresh runs as background stale-while-revalidate and reconciles by message id, turn id, and client message id.
- Status: completed - Sidebar history prefetch runs during idle time for visible, pinned, and recent threads with bounded concurrency.
- Status: completed - Large chat histories use virtualized rendering so the UI does not synchronously render hundreds of Markdown/code-highlighted messages.
- Status: completed - Development render logs are summary-only and must not write the full visible message list per render.
UX regression guardrails
- Development-only Next.js route indicators must not cover mobile composer controls. Keep dev overlays away from the bottom-left composer action area.
- Mobile chat states must prioritize the conversation viewport. Loading and empty states should be lightweight; they must not take over the screen like a desktop card.
- Sidebar thread titles must be readable summaries. Terminal output, build logs, stack traces, and prompt noise should be collapsed into the user command or the most useful diagnostic line.
- Fresh browsers with no localStorage must recover relay devices after Web session bootstrap. They should not strand the user on "connect device" when the relay already has online devices.
- Sidebar action controls must remain discoverable: thread rows expose pin and
archive with concise
aria-label/titletext, and mobile keeps the actions accessible without hover. - These UX rules are additive to the conversation state/performance guardrails above. Do not fix visual polish by weakening normalized conversation state, optimistic outbox, reconciliation, cache-first switching, virtualization, or summary-only dev traces.