Codex 系统手册

exec 模式和脚本化会话启动

stage-9.22 个文件

这一阶段是给“非聊天窗口”的用法准备和跑完整个会话,主要服务于自动化脚本。用户执行 codex exec 时,exec/src/lib.rs 像总调度员:读命令参数和配置,检查是否登录,启动或连接本地服务,拿到用户提示词,设置 JSONL 这类方便机器读取的输出格式,最后根据运行结果给出退出码。若用户要接着旧会话,tui/src/session_resume.rs 会先把旧会话的线程编号、原工作目录和模型找回来,像替你翻出上次的病历,避免接错地方、用错工具。两者配合,把一次性会话从准备、执行到收尾交代清楚。

本阶段的文件2

会话恢复解析

在启动前确定所需的已保存会话元数据,使 exec 能用正确的 cwd 和模型恢复或派生正确线程。

tui/src/session_resume.rs源码 ↗
orchestration恢复会话或分叉会话前

恢复会话时,程序需要知道几件事:这是哪个旧对话、当时在哪个文件夹里工作、用的是什么模型。正常情况下,这些信息由 app-server,也就是负责会话生命周期的后台服务保存。但有些时候,前端界面还没真正恢复线程,就得先做决定,比如是否提醒用户“当前文件夹”和“旧会话文件夹”不一样。这个文件就是中间协调员:先尽量问状态数据库;如果问不到,就去读本地 rollout 文件。rollout 文件可以理解成一本按行记录的流水账,里面有会话创建信息和每轮对话的上下文。文件还会比较两个路径是否其实指向同一个地方,必要时弹出 TUI(终端图形界面)提示,让用户选择继续用当前目录、旧目录,或者退出。

函数细节11
resolve_session_thread_id54–65 ↗
async fn resolve_session_thread_id(
    path: &Path,
    id_str_if_uuid: Option<&str>,
) -> Option<ThreadId>

作用:这个函数用来确定要恢复的是哪一个对话线程。用户如果直接给了一个看起来像 UUID 的编号,它就用这个编号;否则它会从保存的 rollout 文件里找。

数据流:输入是一条文件路径,以及一个可能存在的线程编号字符串。函数先看有没有现成编号;有的话就尝试把字符串转成 ThreadId,也就是会话线程的唯一身份证。没有的话,它读取 rollout 文件里的恢复状态,从里面取出 thread_id。最后返回找到的线程编号,找不到或格式不对就返回空。

调用关系:它会在 handle_key 处理用户按键时被用到,通常发生在用户选择恢复某个会话的时候。它自己只负责找线程编号;如果需要从文件兜底,就把读取 rollout 的脏活交给 read_rollout_resume_state。

调用图:调用 2 个内部函数(from_string, read_rollout_resume_state);被 1 处调用(handle_key)。

read_session_model67–84 ↗
async fn read_session_model(
    state_db_ctx: Option<&StateRuntime>,
    thread_id: ThreadId,
    path: Option<&Path>,
) -> Option<String>

作用:这个函数用来找旧会话当时使用的模型名。这样恢复或展示会话时,程序就不会误以为旧会话用了别的模型。

数据流:输入包括可选的状态数据库、线程编号,以及可选的 rollout 文件路径。它先问状态数据库有没有这个线程的元数据;如果里面有 model,就直接返回。数据库没有、没连上,或者没有模型信息时,它再读 rollout 文件,从最近的上下文记录里拿 model。最后返回模型名,实在找不到就返回空。

调用关系:它被 infer_session_for_thread_notification 和 session_state_for_thread_read 使用,用在需要推断或展示某个线程状态的时候。它的工作顺序很明确:优先相信后台状态库,只有不够用时才调用 read_rollout_resume_state 去查本地流水账。

调用图:调用 1 个内部函数(read_rollout_resume_state);被 2 处调用(infer_session_for_thread_notification, session_state_for_thread_read)。

resolve_cwd_for_resume_or_fork86–112 ↗
async fn resolve_cwd_for_resume_or_fork(
    tui: &mut Tui,
    state_db_ctx: Option<&StateRuntime>,
    current_cwd: &Path,
    thread_id: ThreadId,
    path: Option<&Path>,
    action: CwdPromptActi

作用:这个函数决定恢复或分叉旧会话时应该在哪个工作目录里运行。工作目录就是程序当前“站在哪个文件夹里办事”,选错了可能会读错项目文件。

数据流:输入有 TUI 界面、可选状态数据库、当前目录、线程编号、可选 rollout 路径、提示动作,以及是否允许弹窗询问。它先用 read_session_cwd 找旧会话的目录;如果找不到,就告诉调用方继续但不指定目录。如果找到了,并且允许提示,同时当前目录和旧目录不一样,它会打开一个选择提示,让用户选当前目录、旧会话目录,或者退出。最后输出 Continue 并带上选好的目录,或输出 Exit 表示用户取消。

调用关系:它被 resume_target_session 和 run_ratatui_app 调用,是恢复/分叉流程里决定目录的关键关卡。它先找数据,再用 cwds_differ 判断需不需要提醒,必要时把交互交给 cwd_prompt::run_cwd_selection_prompt。

调用图:调用 3 个内部函数(run_cwd_selection_prompt, cwds_differ, read_session_cwd);被 2 处调用(resume_target_session, run_ratatui_app);外部调用 2 个(to_path_buf, Continue)。

read_session_cwd114–138 ↗
async fn read_session_cwd(
    state_db_ctx: Option<&StateRuntime>,
    thread_id: ThreadId,
    path: Option<&Path>,
) -> Option<PathBuf>

作用:这个函数专门读取旧会话的工作目录。它让上层不用关心目录到底来自状态数据库,还是来自本地 rollout 文件。

数据流:输入是可选状态数据库、线程编号和可选 rollout 路径。它先查状态数据库里的线程元数据;查到就返回里面的 cwd,也就是工作目录。查不到时,如果有 rollout 路径,就读取 rollout 恢复状态并取其中的 cwd。如果 rollout 读取失败,它会写一条警告日志,然后返回空。

调用关系:它只被 resolve_cwd_for_resume_or_fork 调用,属于目录选择流程的底层取数步骤。它失败时不会中断整个程序,只会记录 warn 日志,让上层决定是否继续。

调用图:调用 1 个内部函数(read_rollout_resume_state);被 1 处调用(resolve_cwd_for_resume_or_fork);外部调用 1 个(warn!)。

cwds_differ140–142 ↗
fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool

作用:这个函数判断两个工作目录是否真的不同。它不是简单比较字符串,而是先做路径规范化,避免同一个目录因为写法不同被误判。

数据流:输入是当前目录和旧会话目录。它调用路径工具 paths_match_after_normalization,把类似多余斜杠、相对路径等差异尽量抹平后再比较。比较结果相同就返回 false,不同就返回 true。

调用关系:它被 resolve_cwd_for_resume_or_fork 用来决定要不要弹出目录选择提示,也被 rebuild_config_for_resume_or_fallback 用来判断恢复配置时目录是否变化。它是一个小而重要的安全检查,防止用户被无意义地打扰,也防止真正换了目录却没有提示。

调用图:被 2 处调用(rebuild_config_for_resume_or_fallback, resolve_cwd_for_resume_or_fork);外部调用 1 个(paths_match_after_normalization)。

read_rollout_resume_state144–188 ↗
async fn read_rollout_resume_state(path: &Path) -> io::Result<RolloutResumeState>

作用:这个函数读取 rollout 文件,从里面拼出恢复会话所需的关键信息:线程编号、工作目录和模型。rollout 文件像一份逐行追加的历史流水账,它是状态数据库不可用时的备用来源。

数据流:输入是一条 rollout 文件路径。函数打开这个文件,逐行读取;空行会跳过,坏掉的 JSON 行也会跳过。它识别两类记录:session_meta 提供最早的线程编号和初始目录;turn_context 提供每轮对话时的目录和模型,并且后面的 turn_context 会覆盖前面的目录和模型。读完后,如果至少见过一条合法记录,就返回整理好的 RolloutResumeState;如果文件等于空流水账,就返回错误。

调用关系:它是本文件的兜底读取核心,被 resolve_session_thread_id、read_session_model 和 read_session_cwd 调用,也被三个测试直接验证。它依赖 open_rollout_line_reader 打开按行读取器,然后把 JSON 内容转换成几个小结构体。

调用图:被 6 处调用(read_session_cwd, read_session_model, resolve_session_thread_id, rollout_resume_state_falls_back_to_session_meta, rollout_resume_state_prefers_latest_turn_context, rollout_resume_state_skips_malformed_lines);外部调用 4 个(other, open_rollout_line_reader, format!, default)。

tests::rollout_line196–206 ↗
fn rollout_line(
        timestamp: &str,
        item_type: &str,
        payload: serde_json::Value,
    ) -> serde_json::Value

作用:这是测试用的小工具,用来快速造一行假的 rollout 记录。它让测试代码不用每次手写完整 JSON。

数据流:输入是时间戳、记录类型和 payload 内容。函数把它们包装成一个 JSON 对象,字段包括 timestamp、type 和 payload。输出就是一条可以写进测试 rollout 文件的 JSON 值。

调用关系:它服务于本文件里的测试用例,被测试函数拿来生成 session_meta 和 turn_context 记录。它不参与真实运行,只是让测试数据更清楚、更少重复。

调用图:外部调用 1 个(json!)。

tests::write_rollout_lines208–215 ↗
fn write_rollout_lines(path: &Path, lines: &[serde_json::Value]) -> std::io::Result<()>

作用:这是测试用的写文件工具,把多条假的 rollout 记录写成真正的按行 JSON 文件。这样测试就能像真实程序一样读取文件。

数据流:输入是目标文件路径和一组 JSON 行。函数把每个 JSON 值转成字符串,每条后面加一个换行符,最后一次性写入磁盘。输出是写文件是否成功的结果;成功时磁盘上会多出一个测试 rollout 文件。

调用关系:它被多个测试调用,用来准备 read_rollout_resume_state 要读取的输入文件。真实代码不会调用它,它只负责搭建测试现场。

调用图:外部调用 3 个(new, to_string, write)。

tests::rollout_resume_state_prefers_latest_turn_context218–256 ↗
async fn rollout_resume_state_prefers_latest_turn_context() -> std::io::Result<()>

作用:这个测试确认:如果 rollout 里有多轮 turn_context,恢复时应该采用最新一轮的工作目录和模型。这样会话恢复才贴近最后一次真实对话状态。

数据流:测试先创建临时目录和一个线程编号,再写入一条 session_meta 和两条 turn_context,其中最后一条代表最新状态。然后调用 read_rollout_resume_state 读取文件。最后断言结果里的 thread_id 来自 session_meta,cwd 和 model 来自最新的 turn_context。

调用关系:它直接验证 read_rollout_resume_state 的覆盖规则。测试数据由 tests::rollout_line 和 tests::write_rollout_lines 准备,检查结果用 assert_eq! 完成。

调用图:调用 2 个内部函数(new, read_rollout_resume_state);外部调用 5 个(new, assert_eq!, json!, rollout_line, write_rollout_lines)。

tests::rollout_resume_state_falls_back_to_session_meta259–284 ↗
async fn rollout_resume_state_falls_back_to_session_meta() -> std::io::Result<()>

作用:这个测试确认:如果 rollout 里只有 session_meta,没有后续 turn_context,恢复逻辑也能用最初记录的工作目录。这样旧格式或很短的会话仍然能恢复。

数据流:测试创建临时文件,只写入一条 session_meta,里面有线程编号和目录。然后调用 read_rollout_resume_state。最后检查返回的线程编号和目录都正确,并确认模型为空,因为文件里确实没有模型信息。

调用关系:它直接覆盖 read_rollout_resume_state 的兜底行为。它用 tests::rollout_line 生成记录,用 tests::write_rollout_lines 写入文件,然后断言读取结果。

调用图:调用 2 个内部函数(new, read_rollout_resume_state);外部调用 5 个(new, assert_eq!, json!, rollout_line, write_rollout_lines)。

tests::rollout_resume_state_skips_malformed_lines287–310 ↗
async fn rollout_resume_state_skips_malformed_lines() -> std::io::Result<()>

作用:这个测试确认:rollout 文件里混进坏掉的行时,读取函数不会因为一行脏数据就整个失败。只要还有合法记录,就应该尽量恢复出有用信息。

数据流:测试先写入一条合法的 session_meta,再故意追加一段不完整的 JSON。然后调用 read_rollout_resume_state。最后检查它仍然读出了正确的线程编号和工作目录,说明坏行被跳过了。

调用关系:它验证 read_rollout_resume_state 的容错能力。这个行为对真实用户很重要,因为日志式文件一旦尾部损坏,程序仍应尽量救回会话,而不是直接放弃。

调用图:调用 2 个内部函数(new, read_rollout_resume_state);外部调用 7 个(new, assert_eq!, format!, json!, to_string, write, rollout_line)。

exec 运行时编排

运行非交互式 exec 流程,从 CLI 派生的设置到 app-server 启动、提示构建、事件循环执行,以及最终输出生成。

exec/src/lib.rs源码 ↗
entrypointstartup, main loop, request handling, teardown

这个文件像一个“无人值守的前台调度员”。用户在终端里输入 codex exec 后,它先读参数和配置,检查登录、权限、沙盒(限制程序能动哪些文件和网络的安全笼子)、本地模型设置等。然后它在同一个进程里启动 Codex 的 app-server(可以理解成后台服务),创建或恢复一个会话,把用户的文字、图片或代码评审请求发进去。运行时它只让最终答案或 JSON 事件走标准输出,警告和日志走标准错误,避免把自动化工具读结果时弄乱。它还处理 Ctrl-C 中断、服务端事件过滤、缺失事件补读、审批请求拒绝,以及最后根据是否出错决定退出码。没有它,exec 模式就会变成一堆零件:配置不知道怎么合并,请求不知道怎么发,输出也可能混在一起,脚本很难安全使用。

函数细节46
RequestIdSequencer::new192–194 ↗
fn new() -> Self

作用:创建一个请求编号生成器。每次 exec 要向后台服务发请求时,都需要一个编号,方便把请求和响应对上。

数据流:进去没有参数 → 它把下一个编号设成 1 → 出来一个可以连续发号的小对象。

调用关系:在 run_exec_session 开始和后台服务通信前创建,后面所有请求都从它这里拿编号。

调用图:被 1 处调用(run_exec_session)。

RequestIdSequencer::next196–200 ↗
fn next(&mut self) -> RequestId

作用:取出下一个请求编号,并让内部计数往后走一步。这样每个请求都有不同的“排队号码”。

数据流:进去是生成器当前保存的数字 → 它把这个数字包装成协议里的请求 ID,再把内部数字加 1 → 出来一个可发送给后台服务的请求 ID,同时生成器状态被更新。

调用关系:被 request_shutdownmaybe_backfill_turn_completed_items 等函数使用,也在主会话流程里给各种后台请求发号。

调用图:被 2 处调用(maybe_backfill_turn_completed_items, request_shutdown);外部调用 1 个(Integer)。

exec_root_span221–228 ↗
fn exec_root_span() -> tracing::Span

作用:创建一次 codex exec 运行的追踪范围。追踪范围可以理解成给日志和性能记录套上的一个文件夹,方便之后知道哪些记录属于同一次运行。

数据流:进去没有业务数据 → 它创建带有线程 ID 和回合 ID 空位的追踪 span → 出来一个之后可继续填信息的 span。

调用关系run_main 在启动早期调用它,后续 run_exec_session 会在这个 span 里运行,并把 thread 和 turn 信息填进去。

调用图:被 1 处调用(run_main);外部调用 1 个(info_span!)。

exec_stderr_env_filter230–236 ↗
fn exec_stderr_env_filter() -> EnvFilter

作用:决定哪些日志可以写到标准错误。它默认只放出错误级别日志,避免把命令的正式输出污染掉。

数据流:进去读取环境变量里的日志设置 → 如果用户设置了就用用户的,否则用内置的保守规则 → 出来一个日志过滤器。

调用关系run_main 用它配置日志层,保证普通模式和 JSON 模式的标准输出都保持干净。

调用图:被 1 处调用(run_main);外部调用 1 个(try_from_default_env)。

run_main238–580 ↗
async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()>

作用:这是 codex exec 的主启动函数。它负责从命令行参数一路准备到真正开始执行会话。

数据流:进去是解析好的 CLI 参数和可执行文件路径 → 它处理颜色、配置覆盖、工作目录、云配置、本地模型、权限、登录、遥测、状态数据库和内置服务启动参数 → 最后把整理好的 ExecRunArgs 交给 run_exec_session,成功时返回空结果,失败时返回错误或直接退出。

调用关系:它是本文件最上层的调度中心。它调用 load_bootstrap_config_or_exit 先读基础配置,调用 build_exec_config 生成最终配置,然后把运行阶段交给 run_exec_session

调用图:调用 18 个内部函数(default, find_codex_home, resolve_oss_provider, install_sqlite_telemetry, record_process_start, from_codex_home, from_env, from_optional_paths, build_exec_config, exec_root_span (+8 more));外部调用 22 个(default, new, anyhow!, removed_full_auto_warning, cloud_config_bundle_loader_for_storage, check_execpolicy_for_warnings, resolve_bootstrap_auth_keyring_backend_kind, init_state_db, enforce_login_restrictions, set_parent_from_context (+12 more))。

build_exec_config582–615 ↗
async fn build_exec_config(
    overrides: ConfigOverrides,
    preserve_headless_approval_policy: bool,
    build_config: BuildConfig,
) -> std::io::Result<Config>

作用:构建 exec 模式最终使用的配置,并特殊处理自动评审审批策略。它的作用是避免无头模式默认“永不询问审批”时,挡住本来应该由自动评审处理的流程。

数据流:进去是配置覆盖项、是否保留无头审批策略的标记,以及一个真正构建配置的函数 → 它先按无头默认策略构建,必要时再去掉这个策略重试 → 出来最终 Config,或保留原始错误。

调用关系run_main 在真正启动会话前调用它。它不自己读文件,而是调用传入的构建函数,专心决定是否需要重建一次配置。

调用图:被 1 处调用(run_main);外部调用 1 个(clone)。

load_bootstrap_config_or_exit618–655 ↗
async fn load_bootstrap_config_or_exit(
    codex_home: &Path,
    cwd: Option<&AbsolutePathBuf>,
    cli_kv_overrides: Vec<(String, codex_config::TomlValue)>,
    loader_overrides: LoaderOverrides,

作用:读取启动阶段必须知道的配置。读不到或配置写错时,它会打印清楚的错误并退出。

数据流:进去是 Codex 主目录、当前目录、命令行配置覆盖、加载选项和云配置加载器 → 它调用配置加载器读取并合并配置层 → 出来配置加载结果;如果失败,就把错误格式化后写到标准错误并结束进程。

调用关系run_main 在早期调用它,有时还会为了 OSS 本地模型默认提供商再调用一次。

调用图:调用 1 个内部函数(load_config_toml_with_layer_stack);被 1 处调用(run_main);外部调用 2 个(eprintln!, exit)。

run_exec_session657–1039 ↗
async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()>

作用:真正执行一次 exec 会话:启动后台服务、发送用户请求、接收事件、打印结果,并决定最后是否报错退出。

数据流:进去是一整包运行参数,包括配置、提示词、命令、图片、输出模式和服务启动参数 → 它创建事件输出器,必要时检查本地模型,构造用户请求或评审请求,启动或恢复线程,发送 turn/start 或 review/start,然后循环读取服务事件 → 出来成功结果;过程中会写输出、处理中断、拒绝不支持的交互请求、关闭服务,若看到致命错误会用非零退出码结束。

调用关系run_main 把准备好的所有东西交给它。它内部调用大量小函数来生成线程参数、解析提示词、寻找恢复会话、发送请求、过滤事件和处理服务端请求。

调用图:调用 20 个内部函数(start, new, build_review_request, create_with_ansi, new, handle_server_request, lagged_event_warning_message, load_output_schema, maybe_backfill_turn_completed_items, request_shutdown (+10 more));被 1 处调用(run_main);外部调用 18 个(new, TurnStarted, new, anyhow!, system_bwrap_warning, user_facing_hint, get_git_repo_root, ensure_oss_provider_ready, eprintln!, error! (+8 more))。

thread_start_params_from_config1041–1063 ↗
fn thread_start_params_from_config(config: &Config) -> ThreadStartParams

作用:把内部配置翻译成“新建线程”请求需要的参数。后台服务不直接吃完整配置,所以要整理成协议字段。

数据流:进去是 Config → 它提取模型、工作目录、工作区、审批策略、权限配置、沙盒模式和少量配置覆盖 → 出来 ThreadStartParams

调用关系run_exec_session 在新建会话线程时调用它。它会请 permissions_selection_from_configapprovals_reviewer_override_from_configthread_config_overrides_from_config 帮忙取对应字段。

调用图:调用 3 个内部函数(approvals_reviewer_override_from_config, permissions_selection_from_config, thread_config_overrides_from_config);被 1 处调用(run_exec_session);外部调用 1 个(default)。

thread_resume_params_from_config1065–1086 ↗
fn thread_resume_params_from_config(config: &Config, thread_id: String) -> ThreadResumeParams

作用:把内部配置翻译成“恢复已有线程”请求需要的参数。它和新建线程类似,但多带一个要恢复的线程 ID。

数据流:进去是 Config 和线程 ID → 它提取模型、目录、权限、审批、沙盒和覆盖项,并放入线程 ID → 出来 ThreadResumeParams

调用关系run_exec_session 找到可恢复线程后调用它,然后通过 send_request_with_response 发给后台服务。

调用图:调用 3 个内部函数(approvals_reviewer_override_from_config, permissions_selection_from_config, thread_config_overrides_from_config);被 1 处调用(run_exec_session);外部调用 1 个(default)。

thread_config_overrides_from_config1088–1092 ↗
fn thread_config_overrides_from_config(config: &Config) -> Option<HashMap<String, Value>>

作用:提取需要随线程请求传给后台服务的额外配置覆盖。目前主要是“绕过 hook 信任检查”这个开关。

数据流:进去是 Config → 如果开启了 bypass_hook_trust,就生成一个包含该布尔值的键值表;否则返回空 → 出来可选的 JSON 配置覆盖。

调用关系:被新建线程和恢复线程参数构造函数调用,作为请求里的补充配置。

调用图:被 2 处调用(thread_resume_params_from_config, thread_start_params_from_config)。

permissions_selection_from_config1094–1099 ↗
fn permissions_selection_from_config(config: &Config) -> Option<String>

作用:从配置里找出当前激活的权限方案 ID。权限方案就是一套规定 Codex 能做什么、不能做什么的规则。

数据流:进去是 Config → 它查看权限设置里是否有激活方案 → 有的话取出方案 ID,没有就返回空。

调用关系:被 thread_start_params_from_configthread_resume_params_from_config 调用,用来决定请求里是传权限方案,还是退回到沙盒模式。

调用图:被 2 处调用(thread_resume_params_from_config, thread_start_params_from_config)。

permission_profile_id_from_active_profile1101–1103 ↗
fn permission_profile_id_from_active_profile(active: ActivePermissionProfile) -> String

作用:从一个已激活的权限方案对象里取出它的 ID。这个 ID 是后台服务识别权限方案的名字牌。

数据流:进去是激活权限方案 → 它读取其中的 id 字段 → 出来这个字符串 ID。

调用关系:作为 permissions_selection_from_config 的小帮手使用,专门做字段提取。

sandbox_mode_from_permission_profile1105–1128 ↗
fn sandbox_mode_from_permission_profile(
    permission_profile: &PermissionProfile,
    cwd: &Path,
) -> Option<codex_app_server_protocol::SandboxMode>

作用:根据权限方案推断旧式沙盒模式。沙盒模式可以理解成给 Codex 套的安全边界,比如只能读、能写当前工作区、或完全放开。

数据流:进去是权限方案和当前工作目录 → 它检查文件系统和网络限制:禁用权限时给完全访问,外部管理时不返回沙盒,托管权限时按是否能写磁盘或当前目录来判断 → 出来协议里的沙盒模式或空。

调用关系:在构造线程启动或恢复参数时,如果没有直接传权限方案,就用它把新权限体系转换成后台能理解的沙盒字段。

调用图:调用 2 个内部函数(file_system_sandbox_policy, network_sandbox_policy)。

approvals_reviewer_override_from_config1130–1134 ↗
fn approvals_reviewer_override_from_config(
    config: &Config,
) -> Option<codex_app_server_protocol::ApprovalsReviewer>

作用:把配置里的审批评审者设置转换成后台协议使用的格式。审批评审者决定遇到敏感操作时由谁判断。

数据流:进去是 Config → 它取出 approvals_reviewer 并转换协议类型 → 出来一个可选的审批评审者字段。

调用关系:被新建线程和恢复线程参数构造函数调用,确保后台服务按同一套审批规则运行。

调用图:被 2 处调用(thread_resume_params_from_config, thread_start_params_from_config)。

send_request_with_response1136–1151 ↗
async fn send_request_with_response(
    client: &InProcessAppServerClient,
    request: ClientRequest,
    method: &str,
) -> Result<T, String>

作用:向内置后台服务发送一个请求,并等待指定类型的响应。它还会把错误信息加上方法名,方便人看懂哪里失败。

数据流:进去是客户端、请求对象和方法名 → 它调用客户端发送 typed request,并尝试把响应解成调用方要的类型 → 出来响应对象;失败时出来带方法名前缀的错误字符串。

调用关系run_exec_sessionresolve_resume_thread_id、补读和关闭流程都靠它发请求,是本文件和 app-server 通信的统一小通道。

调用图:调用 1 个内部函数(request_typed);被 2 处调用(resolve_resume_thread_id, run_exec_session)。

session_configured_from_thread_start_response1153–1174 ↗
fn session_configured_from_thread_start_response(
    response: &ThreadStartResponse,
    config: &Config,
) -> Result<SessionConfiguredEvent, String>

作用:把“新建线程”的后台响应转换成核心协议里的会话配置事件。这个事件用于告诉输出器:当前会话到底用的什么模型、目录和权限。

数据流:进去是 ThreadStartResponse 和当前配置 → 它取出线程、模型、审批、权限、目录等字段 → 交给通用转换函数生成 SessionConfiguredEvent

调用关系run_exec_session 新建线程成功后调用它。它把具体的 start 响应交给 session_configured_from_thread_response 统一处理。

调用图:调用 1 个内部函数(session_configured_from_thread_response);被 1 处调用(run_exec_session)。

session_configured_from_thread_resume_response1176–1197 ↗
fn session_configured_from_thread_resume_response(
    response: &ThreadResumeResponse,
    config: &Config,
) -> Result<SessionConfiguredEvent, String>

作用:把“恢复线程”的后台响应转换成核心协议里的会话配置事件。这样恢复旧会话时,后续输出和新会话走同一套格式。

数据流:进去是 ThreadResumeResponse 和当前配置 → 它抽出线程 ID、会话 ID、模型、权限、目录等信息 → 交给通用转换函数生成 SessionConfiguredEvent

调用关系run_exec_session 恢复线程成功后调用它,和新建线程转换函数共用 session_configured_from_thread_response

调用图:调用 1 个内部函数(session_configured_from_thread_response);被 1 处调用(run_exec_session)。

review_target_to_api1199–1206 ↗
fn review_target_to_api(target: ReviewTarget) -> ApiReviewTarget

作用:把内部的代码评审目标转换成 app-server API 要的评审目标格式。目标可能是未提交改动、某个分支、某个提交,或自定义说明。

数据流:进去是内部 ReviewTarget → 它按不同枚举分支逐一搬运字段 → 出来 API 层的 ApiReviewTarget

调用关系run_exec_session 在发送 review/start 前调用它,保证评审请求符合后台服务协议。

调用图:被 1 处调用(run_exec_session)。

session_configured_from_thread_response1212–1258 ↗
fn session_configured_from_thread_response(
    session_id: &str,
    thread_id: &str,
    parent_thread_id: Option<&str>,
    thread_source: Option<codex_protocol::protocol::ThreadSource>,
    thread

作用:统一生成“会话已配置”事件,并检查线程 ID、会话 ID 这些关键 ID 是否合法。

数据流:进去是一组明确字段,包括会话 ID、线程 ID、父线程、模型、权限、目录和推理强度等 → 它把字符串 ID 解析成强类型 ID,失败就返回可读错误 → 成功时出来一个完整的 SessionConfiguredEvent

调用关系:被新建线程和恢复线程的转换函数共用,避免两条路径生成的会话信息不一致。

调用图:调用 2 个内部函数(from_string, from_string);被 2 处调用(session_configured_from_thread_resume_response, session_configured_from_thread_start_response)。

lagged_event_warning_message1260–1262 ↗
fn lagged_event_warning_message(skipped: usize) -> String

作用:生成“事件流落后并丢了若干事件”的警告文字。事件流落后通常表示后台发得太快,接收端没来得及处理。

数据流:进去是被跳过的事件数量 → 它拼成一条说明文字 → 出来警告字符串。

调用关系run_exec_session 收到 Lagged 事件时调用它,然后写日志并交给输出器展示警告。

调用图:被 1 处调用(run_exec_session);外部调用 1 个(format!)。

should_process_notification1264–1322 ↗
fn should_process_notification(
    notification: &ServerNotification,
    thread_id: &str,
    turn_id: &str,
) -> bool

作用:判断某条后台通知是不是当前这次 exec 运行该处理的。这样可以过滤掉别的线程或别的回合的消息。

数据流:进去是通知、当前线程 ID 和当前 turn ID → 它按通知类型检查是否匹配,配置警告等全局消息会放行,具体任务消息必须 ID 对上 → 出来 true 或 false。

调用关系run_exec_session 的事件循环每收到服务端通知都会调用它,只有通过的通知才交给事件输出器。

调用图:被 1 处调用(run_exec_session)。

maybe_backfill_turn_completed_items1324–1365 ↗
async fn maybe_backfill_turn_completed_items(
    thread_ephemeral: bool,
    client: &InProcessAppServerClient,
    request_ids: &mut RequestIdSequencer,
    notification: &mut ServerNotification,
)

作用:在最终完成事件里缺少消息条目时,尝试从历史记录里补回来。这样即使中间事件因为压力被丢了,最终输出也尽量完整。

数据流:进去是线程是否临时、客户端、请求编号器和一条可修改的通知 → 它先判断是否需要补读;需要时发 thread/read,找到对应 turn 的 items 后填回完成通知 → 出来没有单独返回值,但可能修改通知内容。

调用关系run_exec_session 在处理每条通知前调用它。它会用 should_backfill_turn_completed_items 判断条件,用 turn_items_for_thread 从读取结果里取 items。

调用图:调用 3 个内部函数(next, should_backfill_turn_completed_items, turn_items_for_thread);被 1 处调用(run_exec_session);外部调用 1 个(warn!)。

should_backfill_turn_completed_items1369–1378 ↗
fn should_backfill_turn_completed_items(
    thread_ephemeral: bool,
    notification: &ServerNotification,
) -> bool

作用:判断某个完成通知是否适合补读历史条目。临时线程没有可靠历史,所以不能补。

数据流:进去是线程是否临时和通知 → 如果通知不是 turn completed、线程是临时的、或完成事件已经带 items,就返回 false;否则返回 true。

调用关系:只被 maybe_backfill_turn_completed_items 调用,用来保护补读逻辑不要乱读或重复读。

调用图:被 1 处调用(maybe_backfill_turn_completed_items)。

turn_items_for_thread1380–1389 ↗
fn turn_items_for_thread(
    thread: &AppServerThread,
    turn_id: &str,
) -> Option<Vec<AppServerThreadItem>>

作用:从一个线程的历史回合里找到指定 turn 的条目列表。条目就是模型消息、工具调用、文件改动等过程记录。

数据流:进去是线程对象和 turn ID → 它遍历线程里的 turns,找到 ID 相同的那一个 → 出来该 turn 的 items 副本;找不到就返回空。

调用关系maybe_backfill_turn_completed_items 读回线程历史后调用它,把缺失的完成事件内容补齐。

调用图:被 1 处调用(maybe_backfill_turn_completed_items)。

all_thread_source_kinds1391–1404 ↗
fn all_thread_source_kinds() -> Vec<ThreadSourceKind>

作用:列出恢复会话时愿意搜索的所有线程来源。来源表示线程最初来自 CLI、VS Code、exec、子代理等地方。

数据流:进去没有参数 → 它创建一个包含所有已知来源类型的列表 → 出来这个列表。

调用关系resolve_resume_thread_id 查询线程列表时使用它,确保 --resume 能尽量找到历史线程,而不只找某一种来源。

调用图:被 1 处调用(resolve_resume_thread_id);外部调用 1 个(vec!)。

latest_thread_cwd1406–1413 ↗
async fn latest_thread_cwd(thread: &AppServerThread) -> PathBuf

作用:找出一个历史线程最近实际使用的工作目录。工作目录会影响恢复会话时是否算“同一个项目”。

数据流:进去是线程对象 → 如果线程有历史文件路径,它尝试从最近的 turn context 里解析 cwd;解析不到就用线程自带 cwd → 出来一个路径。

调用关系resolve_resume_thread_id 在筛选候选线程时调用它。它会把读取历史文件的工作交给 parse_latest_turn_context_cwd

调用图:调用 1 个内部函数(parse_latest_turn_context_cwd);被 1 处调用(resolve_resume_thread_id)。

parse_latest_turn_context_cwd1415–1430 ↗
async fn parse_latest_turn_context_cwd(path: &Path) -> Option<PathBuf>

作用:从会话历史文件里倒着找最近一次记录的工作目录。倒着找是因为越靠后的记录越新。

数据流:进去是历史文件路径 → 它读取文本,按行从后往前看,跳过空行和解析不了的 JSON 行,遇到 turn context 就取出 cwd → 出来路径;读不到则返回空。

调用关系:被 latest_thread_cwd 调用,用来让恢复逻辑更准确地判断历史会话最后在哪个目录运行。

调用图:被 1 处调用(latest_thread_cwd);外部调用 1 个(read_to_string)。

cwds_match1432–1434 ↗
fn cwds_match(current_cwd: &Path, session_cwd: &Path) -> bool

作用:判断两个工作目录是否其实是同一个位置。它会做路径规范化,避免因为写法不同导致误判。

数据流:进去是当前目录和历史会话目录 → 它调用路径工具做标准化比较 → 出来 true 或 false。

调用关系resolve_resume_thread_id 用它筛选恢复目标,默认只恢复当前项目目录里的会话,除非用户指定搜索全部。

调用图:被 1 处调用(resolve_resume_thread_id);外部调用 1 个(paths_match_after_normalization)。

resolve_resume_thread_id1436–1549 ↗
async fn resolve_resume_thread_id(
    client: &InProcessAppServerClient,
    config: &Config,
    state_db: Option<&StateDbHandle>,
    args: &crate::cli::ResumeArgs,
) -> anyhow::Result<Option<Strin

作用:根据 resume 参数找出应该恢复哪个线程。用户可能给的是“最近一次”、UUID、标题,甚至只给了一个名字。

数据流:进去是后台客户端、配置、可选状态数据库和 resume 参数 → 它先决定模型提供商过滤条件;如果是 last,就分页查最近线程并按目录筛选;如果给了 ID,先认 UUID,再查状态数据库和旧元数据,最后用后台 thread/list 按搜索词查 → 出来找到的线程 ID,找不到则返回空。

调用关系run_exec_session 在处理 Resume 子命令时调用它。它会调用 send_request_with_response 查后台,调用 latest_thread_cwdcwds_match 判断目录。

调用图:调用 5 个内部函数(all_thread_source_kinds, cwds_match, latest_thread_cwd, resume_lookup_model_providers, send_request_with_response);被 1 处调用(run_exec_session);外部调用 3 个(parse_str, Integer, find_thread_meta_by_name_str)。

resume_lookup_model_providers1551–1560 ↗
fn resume_lookup_model_providers(
    config: &Config,
    args: &crate::cli::ResumeArgs,
) -> Option<Vec<String>>

作用:决定恢复会话时是否按当前模型提供商过滤。恢复“最近一次”时需要过滤,避免拿到别的提供商的线程。

数据流:进去是配置和 resume 参数 → 如果用户要 last,就返回当前模型提供商 ID 列表;否则返回空,表示不限制 → 出来可选过滤列表。

调用关系resolve_resume_thread_id 在查询线程列表前调用它,用于构造 thread/list 请求。

调用图:被 1 处调用(resolve_resume_thread_id);外部调用 1 个(vec!)。

canceled_mcp_server_elicitation_response1562–1569 ↗
fn canceled_mcp_server_elicitation_response() -> Result<Value, String>

作用:生成一个“取消 MCP 询问”的响应。MCP 可以理解成外部工具协议,elicitation 是工具向用户追问信息;但 exec 是无人值守模式,不做交互。

数据流:进去没有参数 → 它构造 action 为 Cancel 的响应对象,并转成 JSON 值 → 出来 JSON;如果编码失败则返回错误字符串。

调用关系handle_server_request 遇到 MCP 询问时调用它,然后用 resolve_server_request 把取消答复发回后台。

调用图:被 1 处调用(handle_server_request);外部调用 1 个(to_value)。

request_shutdown1571–1585 ↗
async fn request_shutdown(
    client: &InProcessAppServerClient,
    request_ids: &mut RequestIdSequencer,
    thread_id: &str,
) -> Result<(), String>

作用:请求后台停止向当前线程推送事件。它相当于告诉服务端:这个 exec 客户端要收工了。

数据流:进去是客户端、请求编号器和线程 ID → 它生成 thread/unsubscribe 请求并发送 → 成功时出来空结果,失败时出来错误字符串。

调用关系run_exec_session 在事件处理器要求关闭时调用它,通常发生在最终输出已经足够、可以结束监听时。

调用图:调用 1 个内部函数(next);被 1 处调用(run_exec_session)。

resolve_server_request1587–1597 ↗
async fn resolve_server_request(
    client: &InProcessAppServerClient,
    request_id: RequestId,
    value: serde_json::Value,
    method: &str,
) -> Result<(), String>

作用:向后台服务回复:某个服务端请求已经被正常处理,并附上结果值。

数据流:进去是客户端、请求 ID、JSON 结果和方法名 → 它调用客户端的 resolve 接口 → 成功返回空;失败时返回带方法名的错误说明。

调用关系handle_server_request 在自动取消 MCP 询问时使用它,把取消响应交回后台。

调用图:调用 1 个内部函数(resolve_server_request);被 1 处调用(handle_server_request)。

reject_server_request1599–1616 ↗
async fn reject_server_request(
    client: &InProcessAppServerClient,
    request_id: RequestId,
    method: &str,
    reason: String,
) -> Result<(), String>

作用:向后台服务回复:某个服务端请求不能被 exec 模式处理。比如审批、用户输入、动态工具调用都不适合无人值守命令。

数据流:进去是客户端、请求 ID、方法名和拒绝原因 → 它构造 JSON-RPC 错误对象并发回后台 → 成功返回空;失败返回错误说明。

调用关系handle_server_request 遇到不支持的交互请求时统一调用它。

调用图:调用 1 个内部函数(reject_server_request);被 1 处调用(handle_server_request)。

server_request_method_name1618–1628 ↗
fn server_request_method_name(request: &ServerRequest) -> String

作用:从服务端请求里尽量提取方法名,用于错误信息。提取不到时就叫 unknown

数据流:进去是服务端请求 → 它先转成 JSON,再读 method 字段 → 出来方法名字符串,或默认的 unknown。

调用关系handle_server_request 一开始调用它,后续拒绝请求时把方法名写进日志和错误里。

调用图:被 1 处调用(handle_server_request);外部调用 1 个(to_value)。

handle_server_request1630–1762 ↗
async fn handle_server_request(
    client: &InProcessAppServerClient,
    request: ServerRequest,
    error_seen: &mut bool,
)

作用:处理后台服务主动发来的“需要客户端回应”的请求。因为 exec 不能弹窗问用户,所以大多数交互请求都会被明确拒绝。

数据流:进去是客户端、服务端请求和错误标记 → 它先取方法名,然后按请求类型决定:MCP 询问自动取消,其它审批、用户输入、动态工具、刷新令牌、证明生成等都返回拒绝 → 如果处理失败,会把 error_seen 设为 true 并写警告。

调用关系run_exec_session 的事件循环收到 ServerRequest 时调用它。它会调用 canceled_mcp_server_elicitation_responseresolve_server_requestreject_server_request

调用图:调用 4 个内部函数(canceled_mcp_server_elicitation_response, reject_server_request, resolve_server_request, server_request_method_name);被 1 处调用(run_exec_session);外部调用 2 个(format!, warn!)。

load_output_schema1764–1788 ↗
fn load_output_schema(path: Option<PathBuf>) -> Option<Value>

作用:读取用户指定的输出 JSON 结构要求。这个 schema 用来告诉模型最终答案应该符合什么格式。

数据流:进去是可选文件路径 → 没有路径就返回空;有路径则读取文件并解析成 JSON → 成功返回 JSON 值;读文件或解析失败时打印错误并退出。

调用关系run_exec_session 在构造用户 turn 前调用它,把 schema 放进 turn/start 请求。

调用图:被 1 处调用(run_exec_session);外部调用 3 个(eprintln!, read_to_string, exit)。

PromptDecodeError::fmt1798–1813 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把提示词解码错误变成人能看懂的提示。比如告诉用户输入不是 UTF-8,应该先转换编码。

数据流:进去是具体错误类型和格式化器 → 它按 UTF-8、UTF-16 或不支持 BOM 的情况写出不同说明 → 出来格式化后的文字。

调用关系read_prompt_from_stdin 解码失败后会打印这个错误文字,让用户知道怎么修正 stdin 输入。

调用图:外部调用 1 个(write!)。

decode_prompt_bytes1816–1844 ↗
fn decode_prompt_bytes(input: &[u8]) -> Result<String, PromptDecodeError>

作用:把从标准输入读到的原始字节转成字符串,并识别常见文本编码标记。它主要保护用户不要把非 UTF-8 文件误当提示词送进去。

数据流:进去是一段字节 → 它去掉 UTF-8 BOM,拒绝 UTF-32,按 BOM 识别 UTF-16 小端或大端,否则按 UTF-8 解码 → 出来字符串,或具体的解码错误。

调用关系read_prompt_from_stdin 读完字节后调用它。遇到 UTF-16 时,它把实际转换交给 decode_utf16

调用图:调用 1 个内部函数(decode_utf16);被 1 处调用(read_prompt_from_stdin);外部调用 1 个(from_utf8)。

decode_utf161846–1861 ↗
fn decode_utf16(
    input: &[u8],
    encoding: &'static str,
    decode_unit: fn([u8; 2]) -> u16,
) -> Result<String, PromptDecodeError>

作用:把 UTF-16 编码的字节转换成普通字符串。UTF-16 是一种两个字节为基本单位的文字编码。

数据流:进去是字节、编码名称和把两个字节变成数字的函数 → 它先检查字节数是否成对,再逐对转成 u16,最后转成字符串 → 出来字符串;长度不对或内容非法时返回 UTF-16 错误。

调用关系:只被 decode_prompt_bytes 调用,用来处理带 UTF-16 BOM 的 stdin 输入。

调用图:被 1 处调用(decode_prompt_bytes);外部调用 1 个(from_utf16)。

read_prompt_from_stdin1863–1908 ↗
fn read_prompt_from_stdin(behavior: StdinPromptBehavior) -> Option<String>

作用:从标准输入读取提示词。它会根据调用场景决定是必须读、强制读,还是只在有管道输入时追加读。

数据流:进去是 stdin 行为策略 → 它检查 stdin 是终端还是管道,必要时提示用户;然后读完所有字节,用 decode_prompt_bytes 转字符串,并检查是否为空 → 出来可选提示词;必须有内容但没有时会打印错误并退出。

调用关系resolve_promptresolve_root_prompt 调用它,负责支持管道输入、- 表示从 stdin 读、以及“命令行提示词 + stdin 附加上下文”的模式。

调用图:调用 1 个内部函数(decode_prompt_bytes);被 2 处调用(resolve_prompt, resolve_root_prompt);外部调用 4 个(new, eprintln!, stdin, exit)。

prompt_with_stdin_context1910–1917 ↗
fn prompt_with_stdin_context(prompt: &str, stdin_text: &str) -> String

作用:把命令行里的提示词和 stdin 里的附加内容拼在一起,并用 <stdin>...</stdin> 标出来。

数据流:进去是原提示词和 stdin 文本 → 它在两者之间加空行和标签,必要时补一个换行 → 出来组合后的提示词字符串。

调用关系resolve_root_prompt 在用户既给了位置参数提示词、又通过管道传入额外内容时调用它。

调用图:被 1 处调用(resolve_root_prompt);外部调用 1 个(format!)。

resolve_prompt1919–1934 ↗
fn resolve_prompt(prompt_arg: Option<String>) -> String

作用:确定一个必须存在的提示词到底来自哪里。普通字符串直接用,- 或缺省时从 stdin 读。

数据流:进去是可选提示词参数 → 如果是普通文本就返回;如果是 - 就强制读 stdin;如果没有参数就要求在管道里读 stdin → 出来最终提示词字符串,读不到时退出。

调用关系run_exec_session 的 resume 路径、resolve_root_promptbuild_review_request 都会调用它,统一处理 stdin 作为提示词的规则。

调用图:调用 1 个内部函数(read_prompt_from_stdin);被 3 处调用(build_review_request, resolve_root_prompt, run_exec_session);外部调用 2 个(matches!, unreachable!)。

resolve_root_prompt1936–1947 ↗
fn resolve_root_prompt(prompt_arg: Option<String>) -> String

作用:解析普通 codex exec 根命令的提示词。它比 resolve_prompt 多一个能力:如果已经有命令行提示词,还能把管道内容当附加上下文。

数据流:进去是可选提示词参数 → 如果是普通文本,它尝试用可选追加模式读 stdin;读到就拼接,读不到就只用原文本;如果是 - 或没有文本,就交给 resolve_prompt → 出来最终提示词。

调用关系run_exec_session 在没有子命令、准备启动用户 turn 时调用它。它会用 read_prompt_from_stdinprompt_with_stdin_context

调用图:调用 3 个内部函数(prompt_with_stdin_context, read_prompt_from_stdin, resolve_prompt);被 1 处调用(run_exec_session)。

build_review_request1949–1977 ↗
fn build_review_request(args: &ReviewArgs) -> anyhow::Result<ReviewRequest>

作用:根据 review 子命令参数构造代码评审请求。它决定要评审未提交改动、某个基础分支、某个提交,还是用户自定义说明。

数据流:进去是 ReviewArgs → 它按优先级检查 --uncommitted--base--commit 和自定义提示词;自定义提示词会通过 resolve_prompt 读取并去掉首尾空白 → 出来 ReviewRequest;如果没有指定目标或自定义说明为空,就返回错误。

调用关系run_exec_session 发现用户运行 review 子命令时调用它,然后把生成的目标经 review_target_to_api 发给后台服务。

调用图:调用 1 个内部函数(resolve_prompt);被 1 处调用(run_exec_session);外部调用 1 个(bail!)。