Codex 产品形态全景
用户在不同场景中会看到 codex、codex exec、codex app-server、 codex exec-server、@openai/codex-sdk 和 openai-codex 等名称。它们不是多套 相互独立的 Agent 引擎,也不能因为都能“运行 Codex”就当成同一个入口。
要看清它们的关系,需要先分开四个维度:
| 维度 | 回答的问题 | 典型对象 |
|---|---|---|
| 交付载体 | 二进制如何到达用户机器 | 安装脚本、npm package、Homebrew、Python wheel |
| 产品入口 | 谁接收用户或程序的请求 | TUI、Exec、App Server、SDK |
| 通信边界 | 请求是函数调用、进程内消息还是跨进程协议 | bounded channel、JSONL、JSON-RPC、WebSocket |
| 执行平面 | 命令、文件和网络副作用在哪里发生 | 本地 runtime、Exec Server、远程 environment |
同一个 Rust runtime 可以被多个入口复用;同一个入口也可以通过不同交付载体安装。 这是理解后面所有进程关系的前提。
阅读前建议先看 Codex 项目概览,理解 Thread/Turn 和本地 runtime。 本文只比较交付、连接寿命、协议与执行平面,不展开每个入口内部的 Session/Task 实现;同一张拓扑图中的 两个箭头,也不代表它们拥有相同的失败和资源回收语义。
1. 安装包与二进制
1.1 npm包Launcher
@openai/codex 的 package.json 只把 codex 命令指向 bin/codex.js。发布脚本 则为 Linux、macOS 和 Windows 的 x64/arm64 组合生成平台包,再把它们写成主包的 optionalDependencies。
Node launcher 根据 process.platform 和 process.arch 选择目标三元组,从对应平台包的 vendor/<target>/bin/ 目录定位原生程序:
源码位置:codex-cli/bin/codex.js :: findCodexExecutable
// JS launcher 只解析平台包和原生 binary,不实现 agent runtime。
function findCodexExecutable() {
let vendorRoot;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
} catch {
vendorRoot = path.join(__dirname, "..", "vendor");
}
const codexExecutable = path.join(
vendorRoot,
targetTriple,
"bin",
process.platform === "win32" ? "codex.exe" : "codex",
);
if (existsSync(codexExecutable)) {
return codexExecutable;
}
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
? "bun install -g @openai/codex@latest"
: packageManager === "pnpm"
? "pnpm add -g @openai/codex@latest"
: "npm install -g @openai/codex@latest";
throw new Error(
`Missing optional dependency ${platformPackage}. Reinstall Codex: ${updateCommand}`,
);
}定位成功后,launcher 把原参数不变地传给原生二进制,继承终端的 stdin/stdout/stderr:
源码位置:codex-cli/bin/codex.js :: child spawn
// stdio 继承让原生进程直接成为用户可交互的前台程序。
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: "inherit",
env,
});launcher 还转发 SIGINT、SIGTERM 和 SIGHUP,并镜像子进程的退出码或信号。 因此,通过 npm 运行 codex exec 时,Node 只负责定位和托管子进程;命令解析、 会话、模型请求和工具执行都在 Rust 程序中。
1.2 统一安装入口
仓库根 README 同时提供独立安装脚本、npm、Homebrew 和 GitHub Release 压缩包。 这些是不同的交付通道,并不会选择不同的 Agent 引擎。
发布工程还可单独生成以 codex-app-server 为 entrypoint 的原生包。它与 codex app-server 共用 codex-app-server crate,但交付形式不同:前者是独立二进制, 后者是 codex multitool 的子命令。
2. codex 多入口
2.1 顶层命令
codex-rs/cli 只定义一个默认二进制 codex。MultitoolCli 把 TUI 参数作为 默认分支,同时通过 Subcommand 选择 Exec、App Server、MCP Server、Exec Server 等入口。
| 命令 | 直接调用对象 | 主要输入输出 | 典型调用方 |
|---|---|---|---|
codex | codex_tui | 终端交互 UI 与持续事件流 | 人类用户 |
codex exec | codex_exec::run_main | 单次非交互任务,文本或 JSONL | shell 脚本、CI、TypeScript SDK |
codex review | 复用 codex_exec | 非交互审查事件 | 审查自动化 |
codex app-server | codex_app_server | 双向 JSON-RPC | IDE、Python SDK、长连接客户端 |
codex app-server daemon ... | codex_app_server_daemon | 机器可读的生命周期结果 | SSH 和远程控制工具 |
codex mcp-server | codex_mcp_server | stdio MCP 服务 | MCP client |
codex exec-server | codex_exec_server | 进程、文件、HTTP 的 JSON-RPC | 本地或远程执行环境 |
codex app | app_cmd | 启动或安装桌面 App | macOS/Windows 用户 |
codex app 只在 macOS 和 Windows 编译。app-server、remote-control 和 exec-server 在这个版本的 CLI 帮助中仍被标记为 experimental,不应把它们的当前参数当成永久不变的稳定接口。
2.2 TUI与Exec入口
顶层分派图只表明“进入哪个前端”,不表示它们各自实现了一套会话协议。 当前 TUI 和 Exec 都使用 codex-app-server-client 的类型化请求、通知和事件模型。
这个分层带来一个很重要的区别:复用 App Server 协议模型,不等于一定启动 App Server 子进程。进程内客户端通过有界 channel 与嵌入式 runtime 通信,仍保留 JSON-RPC 的 request/notification/event 形状,但不经过 stdio 或 socket。
3. TUI与Exec
3.1 TUI服务目标
TUI 用 AppServerTarget 区分嵌入式 runtime、本地 daemon 和显式远程 endpoint。 选择规则很具体:显式远程 endpoint 优先;否则,只有当本次启动配置可以被现有 daemon 复用,且默认 Unix Socket 可连接时,才使用本地 daemon;其余情况回退到嵌入式 runtime。
源码位置:codex-rs/tui/src/lib.rs :: app_server_target_for_launch
// 显式 endpoint 优先;只有满足复用条件才选择隐式本地 daemon。
fn app_server_target_for_launch(
explicit_remote_endpoint: Option<RemoteAppServerEndpoint>,
default_daemon_socket: Option<AbsolutePathBuf>,
can_reuse_implicit_local_daemon: bool,
) -> AppServerTarget {
match explicit_remote_endpoint {
Some(endpoint) => AppServerTarget::Remote { endpoint },
None if can_reuse_implicit_local_daemon => {
default_daemon_socket.map_or(AppServerTarget::Embedded, |socket_path| {
AppServerTarget::LocalDaemon {
endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
}
})
}
None => AppServerTarget::Embedded,
}
}“可以复用”并不只是检查 socket。如果本次带有 CLI 配置覆盖、非默认 loader、 strict-config 或无法重放的启动覆盖,TUI 会使用嵌入式 runtime,避免把本次参数 错配给一个早已启动的 daemon。
3.2 Exec 客户端
codex exec 创建 InProcessClientStartArgs,把 SessionSource::Exec、客户端名称、 配置、状态库和 environment manager 交给 InProcessAppServerClient::start。然后它使用 thread/start、thread/resume、turn/start 和 turn/interrupt 等类型化 App Server 请求驱动任务。
源码位置:codex-rs/exec/src/lib.rs :: run_exec_session
// Exec 复用 in-process App Server client,而不是另建一条 Core 控制协议。
let mut request_ids = RequestIdSequencer::new();
let mut client = InProcessAppServerClient::start(in_process_start_args)
.await
.map_err(|err| {
anyhow::anyhow!("failed to initialize in-process app-server client: {err}")
})?;Exec 的特殊之处是产品行为:它从 stdin/参数读取任务,以人类文本或 JSONL 输出事件, 遇到未恢复的失败时使用非零退出码。它的 runtime 仍在当前 codex 进程中,不会因为 名字包含 exec 就自动连接一个 exec-server 进程。
4. App Server协议
4.1 Transport适配
App Server 面向需要长会话、双向请求和细粒度事件的客户端。仓库 README 明确说明 VS Code 扩展使用该界面;Python SDK 也直接作为它的 typed JSON-RPC client。
协议采用 JSON-RPC 2.0 的消息模型,但 wire 上省略 "jsonrpc":"2.0" 字段。每条连接 必须先发送一次 initialize,再发送 initialized 通知;然后才能通过 thread/*、 turn/* 和其他方法工作。
源码位置:codex-rs/app-server-transport/src/transport/mod.rs :: AppServerTransport
// transport 只描述连接承载方式,request/notification 语义保持一致。
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum AppServerTransport {
Stdio,
UnixSocket { socket_path: AbsolutePathBuf },
WebSocket { bind_address: SocketAddr },
Off,
}| Transport | 连接模型 | 当前用途 | 重要限制 |
|---|---|---|---|
stdio:// | 每行一个 JSON-RPC 消息 | 父进程直接托管,默认值 | 服务生命周期通常跟随父客户端 |
unix:// | Unix Stream 上的 WebSocket Upgrade | 本机 daemon 控制面 | 依赖 Unix Socket |
ws://IP:PORT | WebSocket text frame | 实验性网络客户端 | 文档明确标记 unsupported,不应用于生产 |
off | 不对外监听 | 仅需内部功能的启动形态 | 没有本地 client transport |
App Server 在传输入口、请求处理和输出之间使用有界队列。输入过载时返回可重试的 JSON-RPC -32001 错误,而不是无限缓存新请求。长连接 client 因此必须同时处理响应、 服务器主动请求、通知、背压和断连。
4.2 Daemon 管理
codex app-server daemon 是生命周期控制层,不是第二套 App Server 实现。它在 CODEX_HOME/app-server-daemon/ 中保存 settings、PID 记录和操作锁,然后用受管理的 codex 二进制启动 app-server --listen unix://。
start 是幂等的;restart 会停止旧进程并启动新进程;bootstrap 还可启动 独立更新循环。但这一生命周期实现当前只支持 Unix,Windows 上会直接返回 “daemon lifecycle is only supported on Unix platforms”。daemon 也不会接管一个已经使用同一 socket、但不受它管理的 App Server 进程。
5. SDK运行拓扑
5.1 TypeScript模式
@openai/codex-sdk 的 Thread 保存 thread id,但不保持一个长期运行的 Rust 子进程。 每次 run() 或 runStreamed() 都调用 CodexExec.run(),后者构造 exec --experimental-json;如果 thread id 已存在,再追加 resume <thread-id>。
源码位置:sdk/typescript/src/exec.ts :: CodexExec::run commandArgs
// TypeScript SDK 通过稳定 CLI 子命令启动一次非交互 Turn。
const commandArgs: string[] = ["exec", "--experimental-json"];恢复已有 thread 不会切换到另一套 client,只是在同一组命令参数后追加 resume 与 thread id:
源码位置:sdk/typescript/src/exec.ts :: CodexExec::run resume args
// resume 只是参数分支,仍沿用同一个子进程执行器。
if (args.threadId) {
commandArgs.push("resume", args.threadId);
}参数组装完成后,SDK 才为这一次 Turn 创建子进程:
源码位置:sdk/typescript/src/exec.ts :: CodexExec::run spawn
// AbortSignal 直接绑定子进程,取消不会创建额外常驻服务。
const child = spawn(this.executablePath, commandArgs, {
env,
signal: args.signal,
});SDK 逐行解析子进程 stdout 的 JSONL;thread.started 事件到达后保存 id,下一次 Turn 再由新的 codex exec resume 进程恢复。AbortSignal 会传给 Node spawn,非零退出码 或信号则转换为 SDK 错误。
发布 TypeScript SDK 时,staging 脚本会把同版本 @openai/codex 加入 dependencies, 所以默认 binary resolution 与 CLI npm 包使用相同的平台原生包布局。
5.2 Python子进程
Python openai-codex 选择了另一个边界。CodexClient.start() 从精确依赖的 openai-codex-cli-bin wheel 定位原生二进制,启动 app-server --listen stdio://, 并为 stdout 消息路由和 stderr 排空分别启动线程。
源码位置:sdk/python/src/openai_codex/client.py :: CodexClient.start
# Python SDK 维持一个 stdio App Server 子进程,并在重复 start 时保持幂等。
def start(self) -> None:
if self._proc is not None:
return
path_dirs: tuple[Path, ...] = ()
if self.config.launch_args_override is not None:
args = list(self.config.launch_args_override)
else:
codex_bin = _resolve_codex_bin(self.config)
if self.config.codex_bin is None:
path_dirs = _installed_codex_path_dirs()
args = [str(codex_bin)]
for kv in self.config.config_overrides:
args.extend(["--config", kv])
args.extend(["app-server", "--listen", "stdio://"])
env = os.environ.copy()
if self.config.env:
env.update(self.config.env)
_prepend_path_dirs(env, path_dirs)
self._proc = subprocess.Popen(
args,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
cwd=self.config.cwd,
env=env,
bufsize=1,
)Codex() 构造期间就会完成 start、initialize 和 initialized handshake;同一个 client 上的多个 thread/turn 复用这个 App Server 进程。关闭时先 terminate,两秒内未退出 则 kill,因此调用方应使用 context manager 或显式 close()。
两种 SDK 都是本地 Rust runtime 的高层封装,但它们的并发、资源回收和协议错误模型 不同,不能把 TypeScript 的“Thread 对象”理解为一条始终存活的 App Server 连接。
两套 SDK 都叫“Codex client”,但它们持有的运行对象不同。下面的类图用真实类名展示 TypeScript 的 per-Turn Exec 关系,以及 Python 的长寿命 App Server client 关系。
TypeScript 的 Thread 只是参数与 ID handle,真正执行由 CodexExec 每次拉起;Python CodexClient 则拥有可复用的 stdio 进程。两者不能按同一套进程生命周期处理。
5.3 Python运行时版本
Rust workspace 的版本是 0.147.0,但 Python SDK 的 pyproject.toml 此时精确依赖 openai-codex-cli-bin==0.144.4。Python SDK 使用独立的 python-v* beta 发布流程,该流程从 pyproject 读取这个 pin,并用对应 runtime 生成协议类型。
所以,对 Python 应用判定能力边界时,应查看实际安装的 SDK 和 runtime wheel,不能仅凭 Rust 仓库 tag 推导运行时版本。调用方可以用 CodexConfig.codex_bin 显式覆盖二进制, 但这也意味着需要自行承担 SDK 生成类型与 runtime 协议不匹配的风险。
6. Exec Server
codex exec-server 与 codex exec 只是名字相似。Exec 是一个会创建 Thread/Turn、 请求模型并消费 Agent 事件的无 UI 客户端;Exec Server 则把进程、文件系统、环境信息和 HTTP 请求暴露成另一套 JSON-RPC 执行协议。
源码位置:codex-rs/exec-server-protocol/src/protocol.rs :: executor method constants
// 方法名是 executor JSON-RPC 的稳定边界,不是 Core 的 Op/Event 协议。
pub const INITIALIZE_METHOD: &str = "initialize";
pub const INITIALIZED_METHOD: &str = "initialized";
pub const EXEC_METHOD: &str = "process/start";
pub const EXEC_READ_METHOD: &str = "process/read";
pub const EXEC_WRITE_METHOD: &str = "process/write";
pub const EXEC_SIGNAL_METHOD: &str = "process/signal";
pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
pub const ENVIRONMENT_INFO_METHOD: &str = "environment/info";
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
pub const HTTP_REQUEST_METHOD: &str = "http/request";
pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";Exec Server 可以在 stdio 上服务单连接,也可以监听 WebSocket;另一种模式是通过 --remote <URL> --environment-id <ID> 向远程环境注册。它本身不是 TUI、SDK 或模型 client, 而是 Codex runtime 可以选择的副作用落点。
这也解释了为什么 TUI、Exec 和 App Server 都可以通过同一个 EnvironmentManager 选择本地或 远程执行环境:产品入口和工作区所在地是两个独立的选择。
7. 选择入口
| 需求 | 适合的入口 | 主要原因 |
|---|---|---|
| 人在终端中持续交互 | codex TUI | 会话选择、审批、流式输出和本地终端体验 |
| shell/CI 执行一次任务 | codex exec | 非交互退出语义,支持 JSONL |
| Node 应用以 Turn 为单位调用 | TypeScript SDK | 封装 codex exec,自动解析事件和恢复 thread |
| Python 应用维护多个 Thread/Turn | Python SDK | 长生命周期 App Server 客户端与生成类型 |
| IDE 或自定义富客户端 | App Server | 双向 JSON-RPC、审批请求和细粒度事件 |
| SSH 机器上持续运行 App Server | App Server Daemon | Unix Socket、幂等生命周期和可选更新循环 |
| 把命令、文件和 HTTP 落在其他环境 | Exec Server | 为 runtime 提供远程执行平面 |
最终的判断标准不是“哪个名字看起来更强”,而是调用方需要的连接寿命、协议粒度、 输出形式和执行环境。这四个问题一旦确定,Codex 的产品入口就不再是一组混乱的命令名, 而是一组共用 runtime、却有明确边界的 client 与 service。
8. 入口与握手测试
产品拓扑需要由状态变化反向验证:TUI 何时回退 Embedded,WebSocket connection 何时获得调用资格, 远程连接在什么情况下连 socket 都不能尝试,以及 in-process client 的 shutdown 是否等 worker 完成。
8.1 TUI配置
app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable 提供了默认 daemon socket,却把 can_reuse_implicit_local_daemon 设为 false,最终必须选择 Embedded。测试证明“socket 存在”不是复用 daemon 的充分条件。
源码位置:codex-rs/tui/src/lib.rs
// :: app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable
#[test]
fn app_server_target_for_launch_skips_local_daemon_when_launch_config_is_not_replayable()
-> color_eyre::Result<()> {
let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?;
let target = app_server_target_for_launch(
/*explicit_remote_endpoint*/ None,
Some(socket_path),
// 当前启动参数不能由既有daemon重放,因此禁止隐式复用。
/*can_reuse_implicit_local_daemon*/ false,
);
assert_eq!(target, AppServerTarget::Embedded);
Ok(())
}这条 fallback 不代表启动失败被吞掉:它发生在目标选择阶段,Embedded runtime 随后的 initialize 或 Session 构造仍可能返回错误。
8.2 Initialize资格
websocket_transport_routes_per_connection_handshake_and_responses 同时连接两个 client。ws1 完成 initialize 不会让 ws2 获得资格;ws2 在 initialize 前调用 config/read 必须得到 Not initialized。两条连接之后 即使复用相同 request id,响应也必须分别路由。
源码位置:codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs
// :: websocket_transport_routes_per_connection_handshake_and_responses(关键路径)
send_initialize_request(&mut ws1, /*id*/ 1, "ws_client_one").await?;
let first_init = read_response_for_id(&mut ws1, /*id*/ 1).await?;
assert_eq!(first_init.id, RequestId::Integer(1));
// ws1的initialize response不能泄漏到尚未初始化的ws2。
assert_no_message(&mut ws2, Duration::from_millis(250)).await?;
send_config_read_request(&mut ws2, /*id*/ 2).await?;
let not_initialized = read_error_for_id(&mut ws2, /*id*/ 2).await?;
assert_eq!(not_initialized.error.message, "Not initialized");
send_initialize_request(&mut ws2, /*id*/ 3, "ws_client_two").await?;
let second_init = read_response_for_id(&mut ws2, /*id*/ 3).await?;
assert_eq!(second_init.id, RequestId::Integer(3));
// request id只在connection内唯一,不是App Server全局主键。
send_config_read_request(&mut ws1, /*id*/ 77).await?;
send_config_read_request(&mut ws2, /*id*/ 77).await?;
let ws1_config = read_response_for_id(&mut ws1, /*id*/ 77).await?;
let ws2_config = read_response_for_id(&mut ws2, /*id*/ 77).await?;
assert_eq!(ws1_config.id, RequestId::Integer(77));
assert_eq!(ws2_config.id, RequestId::Integer(77));服务端的 ConnectionSessionState 用 OnceLock 保存初始化结果;非 initialize request 在该状态为空时由 dispatch_initialized_client_request() 返回 invalid request。这是连接级协议 gate,不是 Thread 是否存在 的运行时 gate。
8.3 明文WS边界
remote_connect_rejects_non_loopback_ws_when_auth_configured 使用 ws://example.com 和 bearer token。 客户端必须在建立网络连接前返回 InvalidInput,避免把凭据放进非加密的远端 WebSocket handshake; loopback ws:// 与远端 wss:// 才是允许组合。
源码位置:codex-rs/app-server-client/src/lib.rs
// :: remote_connect_rejects_non_loopback_ws_when_auth_configured(完整断言)
// URL与token组合在connect内部先通过transport policy,再尝试网络握手。
let result = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
endpoint: RemoteAppServerEndpoint::WebSocket {
websocket_url: "ws://example.com:4500".to_string(),
auth_token: Some("remote-bearer-token".to_string()),
},
client_name: "codex-app-server-client-test".to_string(),
client_version: "0.0.0-test".to_string(),
experimental_api: true,
mcp_server_openai_form_elicitation: false,
opt_out_notification_methods: Vec::new(),
channel_capacity: 8,
})
.await;
let err = match result {
Ok(_) => panic!("non-loopback ws should be rejected before connect"),
Err(err) => err,
};
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert!(
err.to_string()
.contains("remote auth tokens require `wss://` or loopback `ws://` URLs")
);这组测试验证 remote client 的 transport policy,不代表文档把实验性网络 WebSocket提升为生产承诺。
8.4 进程内关闭
shutdown_waits_for_in_process_drain 使用暂停时间的 Tokio test:worker 收到 Shutdown 后故意等待 30 秒, 再设置 completion flag 并响应。client.shutdown().await 返回时 flag 必须已经为 true,证明 shutdown 不是 只把 command 放入 channel 就提前结束。
源码位置:codex-rs/app-server-client/src/lib.rs
// :: shutdown_waits_for_in_process_drain(完整核心路径)
let (command_tx, mut command_rx) = mpsc::channel(1);
let (_event_tx, event_rx) = mpsc::channel(1);
let completed = Arc::new(AtomicBool::new(false));
let worker_completed = Arc::clone(&completed);
let worker_handle = tokio::spawn(async move {
let response_tx = match command_rx.recv().await {
Some(ClientCommand::Shutdown { response_tx }) => response_tx,
_ => panic!("expected shutdown command"),
};
tokio::time::sleep(Duration::from_secs(30)).await;
worker_completed.store(true, Ordering::Release);
let _ = response_tx.send(Ok(()));
});
let client = InProcessAppServerClient {
command_tx,
event_rx,
worker_handle,
};
// start_paused让测试虚拟推进30秒,不把真实CI拖慢。
client.shutdown().await.expect("shutdown should complete");
assert!(completed.load(Ordering::Acquire));Python SDK 的 CodexClient.close() 还实现了 stdin close → terminate → 2 秒 wait → exception 时 kill,并各 给 reader/stderr thread 0.5 秒 join。当前测试没有直接模拟 wait(timeout=2) 失败并断言 kill() 被 调用的 Python 场景,因此这条升级关闭路径有源码控制流,但仍缺直接回归测试。
8.5 连接生命周期
# 找出哪些入口持有长期进程,哪些只持有一次Turn子进程。
rg -n "subprocess.Popen|spawn\(|InProcessAppServerClient::start|RemoteAppServerClient::connect" \
sdk codex-rs/tui codex-rs/exec
# 找出initialize gate和不同transport的关闭入口。
rg -n "Not initialized|ClientNotification::Initialized|shutdown_waits_for" \
codex-rs/app-server codex-rs/app-server-client读者应能回答:
- 为什么 TUI 复用 App Server protocol 不等于一定启动 App Server 子进程?
- 为什么 ws1 initialize 成功不能使 ws2 调用
config/read? - TypeScript SDK 与 Python SDK 的进程 owner 和关闭时机有什么差异?
codex exec与codex exec-server为什么不能因为名称相似而混用?
接下来可阅读 Core运行时架构总览 进入共享 runtime,或用 CodexThread背压 深入不同 transport 的队列语义。
