Local Windows Codex thread relay MCP with synchronous and asynchronous dispatch support.
codex-thread-relay-mcp
codex-thread-relay-mcp lets one Codex App thread send work to another thread and get the result back, including across different projects or sessions.
This repository provides that thread-communication layer as an MCP server: trusted-project lookup, thread create/reuse, synchronous dispatch, asynchronous callback delivery, status queries, and recovery.
What It Talks To
- Windows Codex App state under
%USERPROFILE%\.codex - A separate Windows Codex CLI app-server process
- Projects that are already known and trusted by the Windows Codex App
Out of scope for this release:
- WSL Codex CLI
- cloud threads
- archived threads
The spawned Windows Codex CLI app-server uses service_tier="fast" so delegated turns keep working on Windows installs that reject the local flex path.
Prerequisites
- Windows
- Node.js 22
- npm
- Git
- PowerShell 7
- A working Windows Codex App installation
Installation
Install dependencies
cd <path-to-codex-thread-relay-mcp>
npm install
Register the MCP server
Add this MCP server entry to the Windows Codex config:
[mcp_servers.threadRelay]
type = "stdio"
command = "node"
args = ["<absolute-path-to-codex-thread-relay-mcp>\\src\\index.js"]
required = false
startup_timeout_sec = 30
tool_timeout_sec = 900
[mcp_servers.threadRelay.env]
THREAD_RELAY_CODEX_HOME = "<path-to-your-codex-home>"
Replace the example path values before using the config, then restart the Codex App.
Upgrade
- Pull the latest repository changes and rerun
npm install. - Restart the Codex App after updating the MCP server code or config.
- Rerun
npm run check,npm test, andnpm run smokebefore relying on the updated relay in live threads.
Troubleshooting
- If the relay tools do not appear in Codex, check the MCP config path and restart the Codex App.
- If
smokefails early, confirm the Codex App is running and the target project is already trusted by the Windows Codex App. - If async callback delivery stays
pending, check whether the source thread is busy, then userelay_dispatch_deliverorrelay_dispatch_recover. - If
relay_send_waitor synchronousrelay_dispatchtimes out on a long target turn, the timeout now includes arecoveryDispatchId. Userelay_dispatch_statusto inspect progress,relay_dispatch_recoverto resume waiting explicitly, and preferrelay_dispatch_asyncfor long-running autonomy loops.
Recent Validation (2026-04-14)
Live thread-relay regression run on a trusted project/thread:
dispatchId=<example-dispatch-a>:send_waittimed out,dispatch_statuscompleted, and returnedRELAY_TEST_MARKER=FRONTEND_RELAY_READONLY_OK.dispatchId=<example-dispatch-b>: another timeout, follow-updispatch_statuscompleted, returnedRELAY_FOLLOWUP_MARKER=POST_RECOVERY_THREAD_FREE_OK.- Immediate sync send after recovery returned within 45 seconds with
RELAY_DIRECT_SYNC_MARKER=OK. dispatchId=<example-dispatch-c>: forced 1-second timeout,relay_dispatch_recovercompleted withRELAY_RECOVER_PATH_MARKER=OK.
Outcome: timeout -> status, timeout -> recover, and post-recovery sends are stable. Remaining variability is target-thread runtime duration; short timeoutSec values may still time out, but recovery paths close cleanly.
Public Tools
relay_list_projects()relay_list_threads({ projectId, query? })relay_create_thread({ projectId, name? })relay_send_wait({ threadId, message, timeoutSec? })relay_dispatch({ projectId, message, threadId?, threadName?, query?, createIfMissing?, timeoutSec? })relay_dispatch_async({ projectId, message, threadId?, threadName?, query?, createIfMissing?, callbackThreadId?, timeoutSec? })relay_dispatch_status({ dispatchId })relay_dispatch_deliver({ dispatchId, callbackThreadId? })relay_dispatch_recover({ dispatchId?, projectId?, callbackThreadId?, limit? })
Dispatch resolution order is fixed:
threadId- exact
threadName - unique
querymatch - create a new thread when
createIfMissing=true
Error Model
Public relay error codes:
project_untrustedthread_not_foundtarget_ambiguousdispatch_not_foundcallback_target_invalidtarget_busyapp_server_unavailableturn_timeoutreply_missingtarget_turn_failed
MCP responses surface them through McpError.data.relayCode.
Durable State and Leases
Relay-owned state is stored outside CODEX_HOME:
- state:
%USERPROFILE%\.codex-relay\state.json - per-thread lease:
%USERPROFILE%\.codex-relay\locks\*.lease.json - per-dispatch lease:
%USERPROFILE%\.codex-relay\locks\dispatch-*.lease.json
Tracked records include:
- remembered thread metadata
- active thread leases
- async dispatch records
- callback status and retry state
- reply text, turn ids, timings, and failure metadata
Async dispatch state machine:
- dispatch:
queued -> running -> succeeded | failed | timed_out - callback:
not_requested | pending | delivered | failed
Recovery surfaces:
relay_dispatch_statusreads the durable record and can suggest recoveryrelay_dispatch_deliverretries callback delivery onlyrelay_dispatch_recoverresumes safe in-flight work, retries pending callbacks, or restarts a dispatch when that is the only safe option
Callback messages use the fixed envelope:
[Codex Relay Callback]Event-Type: codex.relay.dispatch.completed.v1BEGIN_CODEX_RELAY_CALLBACK_JSONEND_CODEX_RELAY_CALLBACK_JSON
Empty threads that were created but have not yet appeared in thread/list are still reachable through remembered-thread state.
Default shared Windows home:
CODEX_HOME=%USERPROFILE%\.codex
Optional environment variables:
THREAD_RELAY_CODEX_HOMETHREAD_RELAY_CODEX_COMMANDTHREAD_RELAY_CODEX_ARGSTHREAD_RELAY_HOMETHREAD_RELAY_REQUEST_TIMEOUT_MSTHREAD_RELAY_POLL_INTERVAL_MSTHREAD_RELAY_TURN_TIMEOUT_MSTHREAD_RELAY_DEBUG=1THREAD_RELAY_SOAK_CONCURRENCY
Verification
npm run check
npm test
npm run smoke
npm run soak
npm run audit:official
pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/verify.ps1
check: syntax checks for the runtime entrypoints and scriptstest:node:testcoverage for path normalization, leases, dispatch resolution, async delivery, and error propagationsmoke: live local smoke coverage for create/send/reuse and primary failure pathssoak: longer async callback and recovery pressure runsaudit:official: dependency audit against the official npm registry
Example Flow
- Call
relay_list_projects - Pick a trusted target project
- Call
relay_dispatchfor the shortest happy path - Fall back to
relay_list_threads/relay_create_thread/relay_send_waitwhen you need finer control - For async work, use
relay_dispatch_statusfor polling andrelay_dispatch_recoverwhen callback delivery or worker progress needs explicit recovery; omitdispatchIdto batch-sweep stale dispatches for one project
Scope Limits In This Version
- No WSL targets
- No cloud threads
- No archived threads
- No daemonized long-lived app-server pool
License
This repository is released under the MIT License. See LICENSE.