Codex 系统手册

线程和会话编排

stage-1144 个文件

这一阶段像客服中心的排班台,处在真正执行每轮回答之前和过程中:先决定任务挂到哪条对话线。入口文件把能力统一摆出来;线程管理器负责新建、恢复、复制、关闭、切换线程,并接上本地记录。环境选择、会话外壳和服务工具箱准备模型、目录、权限、插件。输入队列、注入器、任务和状态记事本保证一轮一轮不串线。存储、服务器和终端界面负责保存、重连、筛选、显示;目标、技能、旁路对话等扩展再插进来一起跑。

本阶段涉及的状态24
  • reg-effective-config系统最终采用的那份配置,把文件、云端规则、项目设置、命令行参数和临时覆盖合在一起,后面各模块都按它办事。
  • reg-feature-flags当前打开或关闭的实验功能和能力开关,决定界面、模型、工具、插件等功能能不能用。
  • reg-local-state-runtime本地 SQLite 和 StateRuntime 打开的那套持久化句柄,负责让线程、日志、目标、记忆等数据能长期保存。
  • reg-thread-session-store当前有哪些线程和会话、哪个正在用、哪些能恢复或关闭的共享会话账本。
  • reg-conversation-history每条线程里的用户消息、模型回复、工具调用、文件改动和审批记录等对话历史。
  • reg-agent-graph多智能体和 fork 线程之间谁带起谁、父子关系是否还活着的关系图。
  • reg-model-catalog系统当前知道有哪些模型、各模型有什么能力、来自哪个提供方以及能不能选用的模型清单。
  • reg-plugin-registry已安装、可用、来自市场或本地的插件清单,以及每个插件的能力和加载结果。
  • reg-mcp-registryMCP 服务器、连接、工具和资源的登记表,模型和前端都靠它知道能调用哪些外部能力。
  • reg-skills-catalog当前能用的技能目录,包括技能来自哪里、说明文件在哪里、是否允许自动调用。
  • reg-memory-store系统保存和检索长期记忆的地方,包含记忆文件、目录、搜索结果和后台整理状态。
  • reg-transport-rpc-connectionsstdio、本机 socket、WebSocket、JSON-RPC、relay 等通道上的连接、请求编号和收发队列。
  • reg-ui-interaction-stateTUI 或前端当前显示哪个线程、输入框内容、弹窗、快捷键、通知和终端接管等界面状态。
  • reg-turn-execution-state一轮对话正在排队、运行、取消、追加输入、压缩上下文或等待工具结果的临时但跨模块共享状态。
  • reg-rollout-trace-logrollout 记录下来的可回放流水账和会话状态,用来恢复、排错、重连和事后分析。
  • reg-goal-store系统当前保存和检索的目标、任务意图和目标相关上下文,用于在线程运行、提示词组装和结果更新时持续引用。
  • reg-agent-orchestration-runtime多智能体运行时的活跃代理登记、并发闸门、等待区、代理间消息和等待结果状态。
  • reg-realtime-session-state线程级实时对话的会话参数、音频/文本输入缓冲、实时 WebSocket 状态和流式通知进度。
  • reg-workspace-trust-state当前工作区和目录的信任状态、授权根目录及同步到前端和沙箱的工作区权限信息。
  • reg-runtime-environment-registry本地、云端或远程执行环境的可用清单、默认选择和当前线程选中的运行目标。
  • reg-external-import-state外部聊天记录或会话导入时的来源登记、去重映射、转换进度和已导入标记。
  • reg-active-model-selection当前线程或会话实际选中的模型、提供方和推理参数状态,区别于仅列出可用模型的模型目录。
  • reg-active-workspace-context当前会话选中的工作目录、项目根和能力根等工作区上下文,用于提示词、工具执行、权限判断和结果展示。
  • reg-extension-lifecycle-state已加载扩展在账户、线程、回合和工具生命周期中注册的贡献、回调上下文和运行中结果状态。

本阶段的文件44

核心线程运行时

这些文件定义核心库表面、线程管理器、实时线程门面、环境解析,以及用于构建和运行长生命周期对话线程的会话/任务机制。

core/src/lib.rs源码 ↗
othercross-cutting

这个文件本身不写具体功能,而是把整个核心库的零件装到一起。可以把它想成一个大型工具箱的标签和分区:哪些抽屉存在,哪些工具能被外面的人拿到,都在这里说明。它声明了很多内部模块,比如会话、线程、配置、沙箱、插件、技能、网络代理、执行命令等;同时用 pub use 把一些重要类型和函数重新导出,外部代码就不用知道它们藏在哪个小文件里。文件开头的 #![deny(clippy::print_stdout, clippy::print_stderr)] 是一条编译期禁令,意思是核心库里不允许直接 print 到标准输出或错误输出。这样能避免后台库代码突然把字打到用户界面上,造成界面混乱;所有给用户看的内容都要经过专门的界面层或日志系统。这里还有一些旧名字的兼容别名,标了 deprecated,意思是还能用,但建议换成新名字。

core-api/src/lib.rs源码 ↗
othercross-cutting

可以把这个文件想成一个服务台。项目内部有很多柜台:配置、登录、线程管理、模型列表、协议消息、执行服务器等等。外部程序如果直接去找这些柜台,就要记住一大堆包名和路径,很容易用错,也会被内部结构变化影响。这个文件做的事很简单:用 pub use 把这些重要入口统一摆出来,形成一个稳定、好用的公共 API。比如外部想启动或管理 Codex 线程,可以从这里拿到 ThreadManagerNewThreadCodexThread;想读配置,可以拿到 Config 和各种配置类型;想处理协议事件,也能拿到 OpEventMsg 等。文件顶部的 deny 规则会让编译器拒绝一些不该暴露或永远用不到的公开接口,避免公共门面变脏。它自己不实现业务逻辑,但决定了外部世界“看见”这个系统的方式。

core/src/thread_manager.rs源码 ↗
orchestrationstartup, request handling, main loop, teardown, cross-cutting

可以把这个文件想成酒店前台。每个 CodexThread 就像一间正在使用的房间,ThreadManager 负责开房、续住、退房、查房号,还要把钥匙、住客历史、可用工具、运行环境、插件、认证信息都配齐。它同时照顾“热”的线程,也就是已经在内存里运行的线程;也能从磁盘或数据库里的记录恢复“冷”的线程。它还支持 fork,也就是从旧对话的某个历史快照复制出一条新对话,像从一本书的某一页开始写平行版本。文件里有不少测试专用入口,用临时目录和假认证来模拟真实运行。重要的是,它会尽量保证线程创建后的第一个事件一定是“会话已配置”,这样外部拿到线程时,基础信息已经可靠准备好了。

函数细节69
set_thread_manager_test_mode_for_tests94–96 ↗
fn set_thread_manager_test_mode_for_tests(enabled: bool)

作用:打开或关闭线程管理器的测试模式。测试模式会启用一些只给自动化测试用的行为,比如记录提交过的操作。

数据流:进去一个布尔值,表示是否启用测试模式 → 它把这个值写进一个全局开关 → 之后新建 ThreadManager 时会读到这个开关,并决定是否启用测试辅助功能。

调用关系:它被测试辅助代码和测试构造函数调用;正式业务流程不应该依赖它。

调用图:被 3 处调用(set_thread_manager_test_mode, with_models_provider_for_tests, with_models_provider_home_and_state_for_tests)。

should_use_test_thread_manager_behavior98–100 ↗
fn should_use_test_thread_manager_behavior() -> bool

作用:读取当前是否处于线程管理器测试模式。它是测试开关的查询口。

数据流:不需要输入 → 从全局原子布尔值里读取当前状态 → 返回 true 或 false,告诉调用者要不要启用测试行为。

调用关系:ThreadManager::new 和测试构造函数会用它来决定是否创建操作日志。

调用图:被 2 处调用(new, with_models_provider_home_and_state_for_tests)。

TempCodexHomeGuard::drop107–109 ↗
fn drop(&mut self)

作用:测试结束时自动删除临时的 Codex 主目录。它像临时工走之前把房间打扫干净。

数据流:对象被释放时带着一个目录路径 → 调用系统删除整个目录树 → 不返回结果,删除失败也被忽略,避免测试清理影响主流程。

调用关系:它是 Rust 的 Drop 钩子,在 TempCodexHomeGuard 生命周期结束时自动触发;测试构造函数会把它挂在线程管理器上。

调用图:外部调用 1 个(remove_dir_all)。

ForkSnapshot::from154–156 ↗
fn from(value: usize) -> Self

作用:把旧代码传入的数字转换成新的 fork 快照方式。这样老调用点不用立刻全部改写。

数据流:进去一个 usize 数字 → 把它解释成“在第 n 条用户消息前截断” → 输出 ForkSnapshot::TruncateBeforeNthUserMessage。

调用关系:ThreadManager::fork_thread 和 fork_thread_from_history 接受 Into<ForkSnapshot>,所以旧的数字参数会通过这里自动转换。

调用图:外部调用 1 个(TruncateBeforeNthUserMessage)。

build_models_manager225–234 ↗
fn build_models_manager(
    config: &Config,
    auth_manager: Arc<AuthManager>,
) -> SharedModelsManager

作用:根据配置和登录信息创建模型列表管理器。它让线程管理器知道有哪些 AI 模型可用、如何刷新这些模型信息。

数据流:进去配置和认证管理器 → 先按配置创建模型服务提供方,再用 Codex 主目录和模型目录配置生成模型管理器 → 返回一个共享的模型管理器。

调用关系:ThreadManager::new 在启动时调用它,把模型能力放进共享状态里,之后 list_models 等接口会使用这个管理器。

调用图:被 1 处调用(new);外部调用 1 个(create_model_provider)。

thread_store_from_config236–255 ↗
fn thread_store_from_config(
    config: &Config,
    state_db: Option<StateDbHandle>,
) -> Arc<dyn ThreadStore>

作用:根据配置决定线程历史存在哪里:本地持久化,或只存在内存里。没有这个选择,恢复旧对话和测试隔离都会很麻烦。

数据流:进去项目配置和可选状态数据库 → 如果配置是本地存储,就创建本地线程库,必要时启动压缩后台任务;如果是内存存储,就按 id 拿一个内存库 → 返回统一的 ThreadStore 接口。

调用关系:上层启动或测试准备线程管理器时会调用它;后续所有读写历史、更新元数据都会通过返回的存储对象完成。

调用图:调用 3 个内部函数(for_id, new, from_config);被 2 处调用(build_prompt_input, tool_handlers_cascade_close_and_resume_and_keep_explicitly_closed_subtrees_closed);外部调用 2 个(new, spawn_rollout_compression_worker)。

ThreadManager::new259–312 ↗
fn new(
        config: &Config,
        auth_manager: Arc<AuthManager>,
        session_source: SessionSource,
        environment_manager: Arc<EnvironmentManager>,
        extensions: Arc<ExtensionR

作用:创建正式运行用的 ThreadManager。它把认证、模型、环境、插件、技能、线程存储等零件装到同一个管理器里。

数据流:进去配置、认证管理器、环境管理器、扩展、存储、统计客户端等依赖 → 创建线程表、广播通道、插件管理器、MCP 管理器、技能管理器和模型管理器 → 返回一个可用的 ThreadManager。

调用关系:这是生产代码最主要的构造入口;后续 start、resume、fork、shutdown 等操作都依赖这里组装好的 ThreadManagerState。

调用图:调用 5 个内部函数(new_with_options, new_with_restriction_product, new_with_extensions, build_models_manager, should_use_test_thread_manager_behavior);被 17 处调用(build_prompt_input, explicit_installation_id_skips_codex_home_file, interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_history, interrupted_fork_snapshot_preserves_explicit_turn_id, interrupted_fork_snapshot_uses_persisted_mid_turn_history_without_live_source, new_uses_active_provider_for_model_refresh, resume_active_thread_from_rollout_returns_running_thread, resume_and_fork_do_not_restore_thread_environments_from_rollout, resume_stopped_thread_from_rollout_preserves_thread_source, resume_stopped_thread_from_rollout_spawns_new_thread (+7 more));外部调用 7 个(clone, new, new, new, bundled_skills_enabled, restriction_product, channel)。

ThreadManager::with_models_provider_for_tests316–335 ↗
fn with_models_provider_for_tests(
        auth: CodexAuth,
        provider: ModelProviderInfo,
    ) -> Self

作用:为集成测试创建一个带假模型提供方的 ThreadManager。它会自动使用临时目录,避免污染用户真实数据。

数据流:进去测试认证信息和模型提供方信息 → 开启测试模式,创建临时 Codex 主目录和默认测试环境管理器 → 返回一个带清理守卫的 ThreadManager。

调用关系:测试辅助函数会调用它;它继续委托给 with_models_provider_and_home_for_tests 完成具体组装。

调用图:调用 2 个内部函数(set_thread_manager_test_mode_for_tests, default_for_tests);被 2 处调用(thread_manager_with_models_provider, thread_manager);外部调用 5 个(new, with_models_provider_and_home_for_tests, format!, temp_dir, create_dir_all)。

ThreadManager::with_models_provider_and_home_for_tests339–352 ↗
fn with_models_provider_and_home_for_tests(
        auth: CodexAuth,
        provider: ModelProviderInfo,
        codex_home: PathBuf,
        environment_manager: Arc<EnvironmentManager>,
    ) -> Se

作用:为测试创建 ThreadManager,但允许指定 Codex 主目录。这样测试可以检查某个目录下的存储行为。

数据流:进去测试认证、模型提供方、目录路径和环境管理器 → 默认不传状态数据库 → 调用更底层的测试构造函数并返回结果。

调用关系:多个测试场景使用它;它只是薄包装,真正工作交给 with_models_provider_home_and_state_for_tests。

调用图:被 10 处调用(interrupted_v2_agent_is_lost_after_residency_eviction, residency_slot_reservation_unloads_oldest_idle_v2_agent, resume_agent_releases_slot_after_resume_failure, resume_agent_respects_max_threads_limit, spawn_agent_limit_shared_across_clones, spawn_agent_releases_slot_after_shutdown, spawn_agent_respects_max_threads_limit, thread_manager_with_models_provider_and_home, shutdown_all_threads_bounded_submits_shutdown_to_every_thread, start_thread_keeps_internal_threads_hidden_from_normal_lookups);外部调用 1 个(with_models_provider_home_and_state_for_tests)。

ThreadManager::with_models_provider_home_and_state_for_tests354–417 ↗
fn with_models_provider_home_and_state_for_tests(
        auth: CodexAuth,
        provider: ModelProviderInfo,
        codex_home: PathBuf,
        environment_manager: Arc<EnvironmentManager>,

作用:测试版 ThreadManager 的完整构造函数,还能带状态数据库。它用于需要模拟线程树、恢复、限制等复杂场景的测试。

数据流:进去测试认证、模型提供方、目录、环境管理器和可选状态库 → 创建假 AuthManager、本地线程存储、模型管理器、插件、MCP、技能等 → 返回测试用 ThreadManager。

调用关系:更简单的测试构造函数会调用它;它与 ThreadManager::new 类似,但所有依赖都偏向测试安全和可控。

调用图:调用 8 个内部函数(new_with_options, new_with_restriction_product, new, set_thread_manager_test_mode_for_tests, should_use_test_thread_manager_behavior, from_auth_for_testing, new, from_absolute_path_checked);被 6 处调用(new_with_config, list_agent_subtree_thread_ids_finds_live_descendants_of_unloaded_root, resume_agent_from_rollout_does_not_reopen_v2_descendants, resume_thread_subagent_restores_stored_nickname_and_role, thread_manager_with_models_provider_home_and_state, multi_agent_v2_interrupt_agent_accepts_unloaded_task_name_target);外部调用 10 个(clone, new, new, clone, new, channel, empty_extension_registry, create_model_provider, panic!, new_v4)。

ThreadManager::session_source419–421 ↗
fn session_source(&self) -> SessionSource

作用:返回这个管理器默认的会话来源。会话来源用来区分是普通执行、子代理,还是其他入口创建的线程。

数据流:不需要输入 → 从共享状态里复制 session_source → 返回给调用者。

调用关系:外部需要了解当前管理器创建线程的默认身份时会用它。

ThreadManager::auth_manager423–425 ↗
fn auth_manager(&self) -> Arc<AuthManager>

作用:拿到认证管理器。认证管理器保存登录状态和 API 认证方式。

数据流:不需要输入 → 克隆共享指针 Arc,也就是多处可安全共享的引用 → 返回认证管理器给调用者。

调用关系:其他模块需要登录信息或认证模式时会通过这个 getter 取走。

ThreadManager::skills_manager427–429 ↗
fn skills_manager(&self) -> Arc<SkillsManager>

作用:拿到技能管理器。技能可以理解成系统内置或用户提供的一组能力包。

数据流:不需要输入 → 从状态里克隆技能管理器指针 → 返回给调用者。

调用关系:register_thread_config 等配置注册流程会使用它,把技能能力接到线程配置里。

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

ThreadManager::plugins_manager431–433 ↗
fn plugins_manager(&self) -> Arc<PluginsManager>

作用:拿到插件管理器。插件管理器负责让外部插件参与 Codex 的能力扩展。

数据流:不需要输入 → 从状态里克隆插件管理器指针 → 返回给需要注册或查询插件的代码。

调用关系:register_thread_config 会调用它;线程启动时也会把同一个插件管理器传给 Codex。

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

ThreadManager::mcp_manager435–437 ↗
fn mcp_manager(&self) -> Arc<McpManager>

作用:拿到 MCP 管理器。MCP 是一种让外部工具或服务接入模型的协议,这里负责它们的统一入口。

数据流:不需要输入 → 克隆状态里的 MCP 管理器 → 返回共享引用。

调用关系:需要访问 MCP 工具链的上层代码会用它;线程启动时也会把它交给 Codex。

ThreadManager::environment_manager439–441 ↗
fn environment_manager(&self) -> Arc<EnvironmentManager>

作用:拿到环境管理器。环境管理器知道线程可以在哪些运行环境里执行命令或工具。

数据流:不需要输入 → 从状态中克隆环境管理器指针 → 返回给调用者。

调用关系:register_thread_config 等流程会用它;默认环境选择和环境校验也依赖它。

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

ThreadManager::default_environment_selections443–448 ↗
fn default_environment_selections(
        &self,
        cwd: &AbsolutePathBuf,
    ) -> Vec<TurnEnvironmentSelection>

作用:根据当前工作目录生成默认运行环境选择。这样新线程不必每次都手动指定环境。

数据流:进去一个当前目录 cwd → 调用环境选择工具,结合环境管理器算出默认列表 → 返回一组 TurnEnvironmentSelection。

调用关系:这是对 default_thread_environment_selections 的简单封装,给外部提供统一入口。

调用图:调用 1 个内部函数(default_thread_environment_selections)。

ThreadManager::validate_environment_selections450–473 ↗
fn validate_environment_selections(
        &self,
        environments: &[TurnEnvironmentSelection],
    ) -> CodexResult<()>

作用:检查用户指定的运行环境是否合理。它会防止同一个环境重复出现,也会拦下不存在的环境 id。

数据流:进去一组环境选择 → 用集合记录已见过的 id,逐个检查重复和是否能在环境管理器里找到 → 成功返回空结果,失败返回 InvalidRequest 错误。

调用关系:resolve_turn_environment_selections 会调用它,在真正启动线程前挡住错误请求。

调用图:被 1 处调用(resolve_turn_environment_selections);外部调用 4 个(with_capacity, format!, InvalidRequest, len)。

ThreadManager::get_models_manager475–477 ↗
fn get_models_manager(&self) -> SharedModelsManager

作用:返回模型管理器,供外部直接做更细的模型相关操作。

数据流:不需要输入 → 克隆共享模型管理器指针 → 返回它。

调用关系:它是一个访问口;ThreadManager::list_models 和 list_collaboration_modes 则是更常用的简化接口。

ThreadManager::list_models479–484 ↗
async fn list_models(&self, refresh_strategy: RefreshStrategy) -> Vec<ModelPreset>

作用:列出当前可用的模型预设。调用者可以选择是否刷新模型列表。

数据流:进去刷新策略 → 交给模型管理器异步获取模型列表 → 返回 ModelPreset 列表。

调用关系:前端或命令行需要展示可选模型时会调用它;实际数据来自 build_models_manager 创建的模型管理器。

ThreadManager::list_collaboration_modes486–488 ↗
fn list_collaboration_modes(&self) -> Vec<CollaborationModeMask>

作用:列出当前模型支持的协作模式。协作模式描述模型是否支持某些多代理或协同能力。

数据流:不需要输入 → 查询模型管理器保存的协作模式信息 → 返回模式列表。

调用关系:它直接依赖模型管理器,通常用于配置界面或能力判断。

ThreadManager::list_thread_ids490–492 ↗
async fn list_thread_ids(&self) -> Vec<ThreadId>

作用:列出当前内存里正在跟踪的普通线程 id。内部线程会被过滤掉,不展示给用户。

数据流:不需要输入 → 委托给 ThreadManagerState::list_thread_ids → 返回线程 id 列表。

调用关系:外部想知道当前有哪些活跃线程时会用它;真正读取线程表的逻辑在状态对象里。

ThreadManager::subscribe_thread_created494–496 ↗
fn subscribe_thread_created(&self) -> broadcast::Receiver<ThreadId>

作用:订阅“新线程创建”通知。之后有线程创建时,订阅者能收到线程 id。

数据流:不需要输入 → 从广播通道创建一个接收器 → 返回给调用者。

调用关系:监听型组件会用它;ThreadManagerState::notify_thread_created 会往这个通道发消息。

ThreadManager::get_thread498–500 ↗
async fn get_thread(&self, thread_id: ThreadId) -> CodexResult<Arc<CodexThread>>

作用:按线程 id 找到正在内存里运行的线程。找不到或是内部线程时会报错。

数据流:进去线程 id → 委托状态对象查线程表 → 返回 CodexThread 的共享引用,或 ThreadNotFound 错误。

调用关系:spawn_subagent、update_thread_metadata 和测试断言会调用它;底层逻辑在 ThreadManagerState::get_thread。

调用图:被 3 处调用(assert_thread_not_loaded, spawn_subagent, update_thread_metadata)。

ThreadManager::update_thread_metadata507–538 ↗
async fn update_thread_metadata(
        &self,
        thread_id: ThreadId,
        patch: ThreadMetadataPatch,
        include_archived: bool,
    ) -> CodexResult<StoredThread>

作用:更新线程的元数据,比如标题、归档状态之类的信息。它兼顾正在运行的线程和已经卸载的冷线程。

数据流:进去线程 id、补丁内容和是否包含已归档线程 → 如果线程正在运行,就通过 CodexThread 更新,保证和实时历史写入顺序一致;否则直接更新线程存储 → 返回更新后的 StoredThread 或错误。

调用关系:这是外部更新元数据的统一入口;它先试 get_thread,失败后才走 thread_store。

调用图:调用 1 个内部函数(get_thread);外部调用 2 个(format!, InvalidRequest)。

ThreadManager::list_agent_subtree_thread_ids541–580 ↗
async fn list_agent_subtree_thread_ids(
        &self,
        thread_id: ThreadId,
    ) -> CodexResult<Vec<ThreadId>>

作用:列出某个线程以及它创建出来的所有子代理线程。它能同时看数据库里的历史关系和内存里的活关系。

数据流:进去根线程 id → 先把根 id 放入结果,再从状态数据库读取已记录的子孙线程,再从 AgentControl 查询当前活着的子树 → 去重后返回完整列表。

调用关系:需要关闭、展示或操作一个代理树时会用它;它调用 agent_control,把实时代理关系也纳入结果。

调用图:调用 1 个内部函数(agent_control);外部调用 2 个(new, new)。

ThreadManager::start_thread582–586 ↗
async fn start_thread(&self, config: Config) -> CodexResult<NewThread>

作用:用最简单的方式开启一个全新线程。调用者只需要给配置,不需要关心工具和历史。

数据流:进去 Config → 包装成没有动态工具的启动请求 → 调用 start_thread_with_tools → 返回 NewThread。

调用关系:这是普通新建对话的便捷入口;它把工作交给更通用的 start_thread_with_tools。

调用图:调用 1 个内部函数(start_thread_with_tools);被 1 处调用(start_thread);外部调用 2 个(pin, new)。

ThreadManager::start_thread_with_tools588–609 ↗
async fn start_thread_with_tools(
        &self,
        config: Config,
        dynamic_tools: Vec<codex_protocol::dynamic_tools::DynamicToolSpec>,
    ) -> CodexResult<NewThread>

作用:开启一个新线程,并额外带上一批动态工具。动态工具是运行时临时提供给模型使用的工具说明。

数据流:进去 Config 和工具列表 → 根据 cwd 计算默认环境,组装 StartThreadOptions,初始历史设为 New → 调用 start_thread_with_options。

调用关系:ThreadManager::start_thread 调用它;它继续把复杂启动流程交给 start_thread_with_options。

调用图:调用 2 个内部函数(default_thread_environment_selections, start_thread_with_options);被 1 处调用(start_thread);外部调用 2 个(pin, default)。

ThreadManager::start_thread_with_options611–617 ↗
async fn start_thread_with_options(
        &self,
        options: StartThreadOptions,
    ) -> CodexResult<NewThread>

作用:按完整选项启动线程。它允许调用者指定历史、来源、工具、环境、追踪信息等。

数据流:进去 StartThreadOptions → 默认不是 fork 来源 → 调用 start_thread_with_options_and_fork_source → 返回新线程。

调用关系:start_thread_with_tools 使用它;需要更细控制线程创建的代码也可以走这里。

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

ThreadManager::start_thread_with_options_and_fork_source619–649 ↗
async fn start_thread_with_options_and_fork_source(
        &self,
        options: StartThreadOptions,
        forked_from_thread_id: Option<ThreadId>,
    ) -> CodexResult<NewThread>

作用:启动线程时额外记录它是否来自某个被 fork 的线程。fork 是从旧对话复制历史开新分支。

数据流:进去启动选项和可选 fork 来源 id → 从历史里推断恢复来源,补齐 session_source 和 thread_source → 调用状态对象的 spawn_thread_with_source 创建线程。

调用关系:start_thread_with_options 和 spawn_subagent 都会调用它;它是公开启动选项和底层 spawn 之间的桥。

调用图:调用 1 个内部函数(agent_control);被 2 处调用(spawn_subagent, start_thread_with_options);外部调用 2 个(clone, pin)。

ThreadManager::spawn_subagent653–686 ↗
async fn spawn_subagent(
        &self,
        forked_from_thread_id: ThreadId,
        mut options: StartThreadOptions,
    ) -> CodexResult<NewThread>

作用:从已有线程的历史里创建一个子代理线程。子代理像主任务派出去的分身,会继承一段上下文再继续工作。

数据流:进去被 fork 的线程 id 和启动选项 → 找到源线程,确保历史已落盘并刷完,读取完整历史 → 把历史转成可恢复格式,再按“被中断快照”生成 fork 历史 → 启动新线程并记录来源。

调用关系:它调用 get_thread、stored_thread_to_initial_history、fork_history_from_snapshot,最后回到 start_thread_with_options_and_fork_source。

调用图:调用 5 个内部函数(from_config_and_version, get_thread, start_thread_with_options_and_fork_source, fork_history_from_snapshot, stored_thread_to_initial_history)。

ThreadManager::resume_thread_from_rollout688–703 ↗
async fn resume_thread_from_rollout(
        &self,
        config: Config,
        rollout_path: PathBuf,
        auth_manager: Arc<AuthManager>,
        parent_trace: Option<W3cTraceContext>,
    )

作用:从 rollout 文件路径恢复一个旧线程。rollout 可以理解成对话过程的持久化记录。

数据流:进去配置、rollout 路径、认证管理器和追踪信息 → 先从路径读取初始历史 → 调用 resume_thread_with_history 真正恢复线程 → 返回 NewThread。

调用关系:resume_conversation 会调用它;读取历史的细节在 initial_history_from_rollout_path。

调用图:调用 2 个内部函数(initial_history_from_rollout_path, resume_thread_with_history);被 1 处调用(resume_conversation);外部调用 1 个(pin)。

ThreadManager::resume_thread_with_history706–739 ↗
async fn resume_thread_with_history(
        &self,
        config: Config,
        initial_history: InitialHistory,
        auth_manager: Arc<AuthManager>,
        parent_trace: Option<W3cTraceContex

作用:用已经准备好的历史恢复线程。它适合历史不一定来自路径、而是调用者已经拿到的情况。

数据流:进去配置、初始历史、认证管理器和追踪信息 → 计算默认环境,从历史里推断会话来源和线程来源 → 调用状态对象 spawn_thread_with_source → 返回恢复后的线程。

调用关系:resume_thread_from_rollout 会调用它;它和新建线程共用同一套底层 spawn 流程。

调用图:调用 3 个内部函数(default_thread_environment_selections, agent_control, get_resumed_session_sources);被 1 处调用(resume_thread_from_rollout);外部调用 3 个(pin, new, default)。

ThreadManager::start_thread_with_user_shell_override_for_tests741–766 ↗
async fn start_thread_with_user_shell_override_for_tests(
        &self,
        config: Config,
        user_shell_override: crate::shell::Shell,
    ) -> CodexResult<NewThread>

作用:测试专用:启动新线程时强行指定用户 shell。shell 是执行命令用的外壳程序,比如 bash 或 zsh。

数据流:进去配置和测试指定的 shell → 计算默认环境,组装新线程启动参数,并把 shell override 传到底层 → 返回新线程。

调用关系:测试辅助入口 start_thread_with_user_shell_override 会调用它;正式业务不应使用。

调用图:调用 2 个内部函数(default_thread_environment_selections, agent_control);被 1 处调用(start_thread_with_user_shell_override);外部调用 4 个(clone, pin, new, default)。

ThreadManager::resume_thread_from_rollout_with_user_shell_override_for_tests768–802 ↗
async fn resume_thread_from_rollout_with_user_shell_override_for_tests(
        &self,
        config: Config,
        rollout_path: PathBuf,
        auth_manager: Arc<AuthManager>,
        user_shell

作用:测试专用:从 rollout 恢复线程,同时强行指定 shell。它用来验证不同 shell 下恢复逻辑是否正确。

数据流:进去配置、rollout 路径、认证管理器和 shell → 读取历史、计算默认环境、推断来源 → 调用底层 spawn_thread_with_source,并传入 shell override。

调用关系:测试辅助入口会调用它;它复用了 initial_history_from_rollout_path 和底层恢复流程。

调用图:调用 3 个内部函数(default_thread_environment_selections, agent_control, initial_history_from_rollout_path);被 1 处调用(resume_thread_from_rollout_with_user_shell_override);外部调用 3 个(pin, new, default)。

ThreadManager::remove_thread807–809 ↗
async fn remove_thread(&self, thread_id: &ThreadId) -> Option<Arc<CodexThread>>

作用:把某个线程从管理器的内存表里移除。注意这不一定立刻销毁线程,因为别处可能还持有共享引用。

数据流:进去线程 id 引用 → 获取线程表写锁并删除对应项 → 返回被删除的线程引用,或 None。

调用关系:外部或测试需要手动卸载线程时会用它;状态对象里也有一个类似的 remove_thread。

ThreadManager::shutdown_all_threads_bounded814–860 ↗
async fn shutdown_all_threads_bounded(&self, timeout: Duration) -> ThreadShutdownReport

作用:在限定时间内并发关闭所有正在跟踪的线程。它能告诉调用者哪些关好了、哪些提交关闭失败、哪些超时。

数据流:进去超时时长 → 拿到当前线程快照,对每个线程并发调用 shutdown_and_wait,并套上超时 → 汇总报告,移除成功关闭的线程,排序后返回 ThreadShutdownReport。

调用关系:程序退出或测试清理时会调用它;没有按时关闭的线程会留在表里,方便之后重试或排查。

调用图:外部调用 1 个(default)。

ThreadManager::fork_thread866–881 ↗
async fn fork_thread(
        &self,
        snapshot: S,
        config: Config,
        path: PathBuf,
        thread_source: Option<ThreadSource>,
        parent_trace: Option<W3cTraceContext>,

作用:从某个 rollout 路径读取历史,并按指定快照方式复制出新线程。它是从磁盘历史 fork 的入口。

数据流:进去快照规则、配置、路径、线程来源和追踪信息 → 读取路径对应的 InitialHistory → 调用 fork_thread_from_history → 返回新线程。

调用关系:外部 fork_thread API 会调用它;它把文件读取和实际 fork 分开处理。

调用图:调用 2 个内部函数(fork_thread_from_history, initial_history_from_rollout_path);被 1 处调用(fork_thread);外部调用 1 个(into)。

ThreadManager::initial_history_from_rollout_path883–899 ↗
async fn initial_history_from_rollout_path(
        &self,
        rollout_path: PathBuf,
    ) -> CodexResult<InitialHistory>

作用:把一个 rollout 路径转成线程启动能用的初始历史。它负责从线程存储里把历史记录读出来。

数据流:进去 rollout 路径 → 通过 thread_store 按路径读取线程,并要求包含历史 → 调用 stored_thread_to_initial_history 转换格式 → 返回 InitialHistory。

调用关系:resume_thread_from_rollout、fork_thread 和测试恢复入口都会调用它。

调用图:调用 1 个内部函数(stored_thread_to_initial_history);被 3 处调用(fork_thread, resume_thread_from_rollout, resume_thread_from_rollout_with_user_shell_override_for_tests);外部调用 1 个(clone)。

ThreadManager::fork_thread_from_history902–921 ↗
async fn fork_thread_from_history(
        &self,
        snapshot: S,
        config: Config,
        history: InitialHistory,
        thread_source: Option<ThreadSource>,
        parent_trace: Optio

作用:从已经加载好的历史 fork 出一个新线程。调用者不必再提供 rollout 路径。

数据流:进去快照规则、配置、历史、线程来源和追踪信息 → 把快照参数转成 ForkSnapshot → 调用 fork_thread_with_initial_history → 返回新线程。

调用关系:fork_thread 会在读完历史后调用它;它是历史已在手时的 fork 入口。

调用图:调用 1 个内部函数(fork_thread_with_initial_history);被 1 处调用(fork_thread);外部调用 1 个(into)。

ThreadManager::fork_thread_with_initial_history923–971 ↗
async fn fork_thread_with_initial_history(
        &self,
        snapshot: ForkSnapshot,
        config: Config,
        history: InitialHistory,
        thread_source: Option<ThreadSource>,

作用:执行真正的 fork 准备工作:判断来源、处理多代理版本、裁剪历史,再启动新线程。

数据流:进去快照规则、配置、历史、来源和追踪信息 → 推断 forked_from_thread_id,决定多代理版本,生成中断标记,按快照规则改写历史,计算默认环境 → 调用状态对象 spawn_thread 创建新线程。

调用关系:fork_thread_from_history 调用它;它使用 fork_history_from_snapshot 来处理历史切分。

调用图:调用 5 个内部函数(default_thread_environment_selections, from_config_and_version, agent_control, fork_history_from_snapshot, forked_from_id);被 1 处调用(fork_thread_from_history);外部调用 4 个(clone, pin, new, default)。

ThreadManager::agent_control973–975 ↗
fn agent_control(&self) -> AgentControl

作用:创建一个 AgentControl 控制柄。它让代理相关代码能通过弱引用回到线程管理器状态,而不强行延长生命周期。

数据流:不需要输入 → 把 ThreadManagerState 的 Arc 降级成 weak 弱引用 → 创建并返回 AgentControl。

调用关系:启动线程、恢复线程、fork 和列子树时都会用它,把代理控制能力交给 Codex 或子代理流程。

调用图:调用 1 个内部函数(new);被 6 处调用(fork_thread_with_initial_history, list_agent_subtree_thread_ids, resume_thread_from_rollout_with_user_shell_override_for_tests, resume_thread_with_history, start_thread_with_options_and_fork_source, start_thread_with_user_shell_override_for_tests);外部调用 1 个(downgrade)。

ThreadManager::captured_ops978–984 ↗
fn captured_ops(&self) -> Vec<(ThreadId, Op)>

作用:测试专用:取出测试模式下记录过的线程操作。用于断言系统有没有向线程提交预期命令。

数据流:不需要输入 → 如果存在操作日志并能加锁,就复制日志内容;否则返回空列表 → 不改变状态。

调用关系:只有测试编译时存在;记录动作发生在 ThreadManagerState::send_op。

ThreadManagerState::state_db988–990 ↗
fn state_db(&self) -> Option<StateDbHandle>

作用:返回可选的状态数据库句柄。状态数据库保存线程生成关系等跨会话状态。

数据流:不需要输入 → 克隆 state_db 里的句柄 → 返回 Some 或 None。

调用关系:ThreadManager::list_agent_subtree_thread_ids 会用它读取已持久化的子孙线程关系。

ThreadManagerState::list_thread_ids992–1001 ↗
async fn list_thread_ids(&self) -> Vec<ThreadId>

作用:列出当前加载在内存里的用户可见线程 id。内部线程不会出现在结果里。

数据流:不需要输入 → 读线程表,过滤掉 session_source 标记为内部的线程 → 收集并返回 id 列表。

调用关系:ThreadManager::list_thread_ids 委托它完成实际查询。

ThreadManagerState::list_live_thread_spawn_edges1004–1022 ↗
async fn list_live_thread_spawn_edges(&self) -> Vec<(ThreadId, ThreadId)>

作用:列出当前内存中活着的父子线程关系。它只看已加载线程,不读取数据库。

数据流:不需要输入 → 扫描线程表,跳过内部线程,找出 SubAgent::ThreadSpawn 类型的线程 → 返回父线程 id 到子线程 id 的边列表。

调用关系:代理控制或诊断代码可以用它了解当前活跃的线程树。

ThreadManagerState::get_thread1025–1031 ↗
async fn get_thread(&self, thread_id: ThreadId) -> CodexResult<Arc<CodexThread>>

作用:在线程表里按 id 找线程,并屏蔽内部线程。它是状态层最基础的查找函数。

数据流:进去线程 id → 读锁查看 HashMap → 如果找到且不是内部线程,返回共享线程;否则返回 ThreadNotFound。

调用关系:send_op、user_instructions_for_spawn、initial_multi_agent_version_for_spawn 和 parent_rollout_thread_trace_for_source 都依赖它。

调用图:被 4 处调用(initial_multi_agent_version_for_spawn, parent_rollout_thread_trace_for_source, send_op, user_instructions_for_spawn);外部调用 1 个(ThreadNotFound)。

ThreadManagerState::read_stored_thread1033–1056 ↗
async fn read_stored_thread(
        &self,
        params: ReadThreadParams,
    ) -> CodexResult<StoredThread>

作用:从线程存储里读取某个持久化线程,并把存储层错误翻译成 Codex 层错误。

数据流:进去 ReadThreadParams → 调用 thread_store.read_thread → 如果成功返回 StoredThread;如果是未找到或非法请求,转换成更适合上层的 CodexErr。

调用关系:这是状态层读取冷线程的统一工具,避免上层直接处理 ThreadStoreError 的细节。

ThreadManagerState::send_op1059–1067 ↗
async fn send_op(&self, thread_id: ThreadId, op: Op) -> CodexResult<String>

作用:向指定线程提交一个操作。操作 Op 可以理解成发给对话线程的一条命令或输入。

数据流:进去线程 id 和 Op → 先找到线程;如果测试日志开启,就记录一份;再调用 thread.submit → 返回提交 id 或错误。

调用关系:AgentControl 或其他调度代码会通过它给线程发命令;captured_ops 会读取它在测试模式下留下的日志。

调用图:调用 1 个内部函数(get_thread);外部调用 1 个(clone)。

ThreadManagerState::remove_thread1070–1072 ↗
async fn remove_thread(&self, thread_id: &ThreadId) -> Option<Arc<CodexThread>>

作用:从状态对象的线程表里删除线程。它是底层版本的移除入口。

数据流:进去线程 id 引用 → 获取写锁,从 HashMap 删除对应线程 → 返回被删线程或 None。

调用关系:代理控制或内部清理代码可使用它;公开层也有 ThreadManager::remove_thread。

ThreadManagerState::effective_multi_agent_version_for_spawn1074–1090 ↗
async fn effective_multi_agent_version_for_spawn(
        &self,
        initial_history: &InitialHistory,
        session_source: Option<&SessionSource>,
        parent_thread_id: Option<ThreadId>,

作用:决定新线程应该使用哪个多代理版本。多代理版本会影响子代理和中断标记的格式。

数据流:进去历史、来源、父线程 id、fork 来源 id 和配置 → 先尝试从历史或父线程继承版本;如果没有,就用配置里的特性开关算默认版本 → 返回最终版本。

调用关系:fork_thread_with_initial_history 会调用它;它内部依赖 initial_multi_agent_version_for_spawn。

调用图:调用 1 个内部函数(initial_multi_agent_version_for_spawn)。

ThreadManagerState::initial_multi_agent_version_for_spawn1092–1118 ↗
async fn initial_multi_agent_version_for_spawn(
        &self,
        initial_history: &InitialHistory,
        session_source: Option<&SessionSource>,
        parent_thread_id: Option<ThreadId>,

作用:尝试从已有上下文推断新线程要继承的多代理版本。它优先尊重父线程、恢复历史或 fork 来源。

数据流:进去初始历史、可选会话来源、父线程 id、fork 来源 id → 找出可能继承的线程 id,查找该线程的版本 → 调用 resolve_multi_agent_version 合并历史和继承信息 → 返回可选版本。

调用关系:effective_multi_agent_version_for_spawn 和 spawn_thread_with_source 都会调用它,保证启动时版本选择一致。

调用图:调用 2 个内部函数(resolve_multi_agent_version, get_thread);被 2 处调用(effective_multi_agent_version_for_spawn, spawn_thread_with_source)。

ThreadManagerState::user_instructions_for_spawn1135–1168 ↗
async fn user_instructions_for_spawn(
        &self,
        session_source: &SessionSource,
        parent_thread_id: Option<ThreadId>,
        forked_from_thread_id: Option<ThreadId>,
    ) -> Loade

作用:决定新线程启动时使用哪些用户指令。用户指令是用户预先写好的偏好或规则。

数据流:进去会话来源、父线程 id 和 fork 来源 id → 如果是根线程,就从 provider 重新加载用户指令;如果是子代理,就尽量从父线程的实时会话继承 → 返回指令和警告列表。

调用关系:spawn_thread_with_source 在真正创建 Codex 前调用它;这样子代理不会随便重新加载一套不同指令。

调用图:调用 1 个内部函数(get_thread);被 1 处调用(spawn_thread_with_source);外部调用 2 个(new, is_non_root_agent)。

ThreadManagerState::spawn_new_thread1171–1189 ↗
async fn spawn_new_thread(
        &self,
        config: Config,
        agent_control: AgentControl,
    ) -> CodexResult<NewThread>

作用:状态层的新线程便捷入口,使用默认会话来源且没有历史。

数据流:进去配置和 AgentControl → 补齐默认来源、无父线程、无 fork 来源等参数 → 调用 spawn_new_thread_with_source → 返回 NewThread。

调用关系:内部代理或控制流程可以用它快速启动根线程;更复杂情况走 spawn_new_thread_with_source。

调用图:调用 1 个内部函数(spawn_new_thread_with_source);外部调用 2 个(pin, clone)。

ThreadManagerState::spawn_new_thread_with_source1192–1227 ↗
async fn spawn_new_thread_with_source(
        &self,
        config: Config,
        agent_control: AgentControl,
        session_source: SessionSource,
        parent_thread_id: Option<ThreadId>,

作用:状态层的新线程启动入口,允许指定会话来源、父线程、继承环境和执行策略。

数据流:进去配置、控制柄、来源、父子关系、指标名、继承环境等 → 如果没传环境,就计算默认环境 → 调用 spawn_thread_with_source,以 InitialHistory::New 启动。

调用关系:spawn_new_thread 调用它;它把“新线程”这种情况转换成通用 spawn。

调用图:调用 1 个内部函数(spawn_thread_with_source);被 1 处调用(spawn_new_thread);外部调用 4 个(clone, pin, new, default)。

ThreadManagerState::resume_thread_with_history_with_source1229–1264 ↗
async fn resume_thread_with_history_with_source(
        &self,
        options: ResumeThreadWithHistoryOptions,
    ) -> CodexResult<NewThread>

作用:状态层的恢复线程入口,允许指定来源和继承信息。它常用于代理恢复场景。

数据流:进去 ResumeThreadWithHistoryOptions → 拆出配置、历史、控制柄、来源、父线程和继承信息 → 计算默认环境和 thread_source → 调用 spawn_thread_with_source。

调用关系:代理控制相关流程可调用它;它和公开的 resume_thread_with_history 类似,但参数更适合内部使用。

调用图:调用 2 个内部函数(default_thread_environment_selections, spawn_thread_with_source);外部调用 4 个(clone, pin, new, default)。

ThreadManagerState::fork_thread_with_source1267–1302 ↗
async fn fork_thread_with_source(
        &self,
        config: Config,
        initial_history: InitialHistory,
        agent_control: AgentControl,
        session_source: SessionSource,
        th

作用:状态层的 fork 入口,允许指定会话来源、父线程、fork 来源和继承资源。

数据流:进去配置、初始历史、控制柄、来源、父子关系、继承环境等 → 补齐默认环境 → 调用 spawn_thread_with_source 创建线程。

调用关系:内部代理或多代理流程需要 fork 时会用它;真正创建逻辑仍集中在 spawn_thread_with_source。

调用图:调用 1 个内部函数(spawn_thread_with_source);外部调用 4 个(clone, pin, new, default)。

ThreadManagerState::spawn_thread1306–1341 ↗
async fn spawn_thread(
        &self,
        config: Config,
        initial_history: InitialHistory,
        auth_manager: Arc<AuthManager>,
        agent_control: AgentControl,
        parent_threa

作用:以管理器默认会话来源启动一个线程。它是通用 spawn 的一层简化包装。

数据流:进去配置、历史、认证、控制柄、父线程、fork 来源、工具、追踪、环境等 → 填入默认 session_source 和无继承环境策略 → 调用 spawn_thread_with_source。

调用关系:ThreadManager 的 fork 和测试启动入口会调用它;它把参数转给最底层的 spawn_thread_with_source。

调用图:调用 1 个内部函数(spawn_thread_with_source);外部调用 2 个(pin, clone)。

ThreadManagerState::spawn_thread_with_source1344–1441 ↗
async fn spawn_thread_with_source(
        &self,
        config: Config,
        initial_history: InitialHistory,
        auth_manager: Arc<AuthManager>,
        agent_control: AgentControl,

作用:这是创建或恢复线程的核心函数。它负责处理重复恢复、用户指令、版本继承、追踪上下文,然后真正启动 Codex。

数据流:进去完整启动参数 → 如果是恢复已有运行线程,就直接返回现有线程;否则加载或继承用户指令,准备父 trace 和多代理版本,调用 Codex::spawn → 再调用 finalize_thread_spawn 注册线程并返回 NewThread。

调用关系:几乎所有 start、resume、fork、subagent 流程最终都会走到这里;它把准备工作交给 user_instructions_for_spawn、initial_multi_agent_version_for_spawn、parent_rollout_thread_trace_for_source 和 finalize_thread_spawn。

调用图:调用 5 个内部函数(spawn, finalize_thread_spawn, initial_multi_agent_version_for_spawn, parent_rollout_thread_trace_for_source, user_instructions_for_spawn);被 4 处调用(fork_thread_with_source, resume_thread_with_history_with_source, spawn_new_thread_with_source, spawn_thread);外部调用 6 个(clone, pin, clone, format!, matches!, InvalidRequest)。

ThreadManagerState::finalize_thread_spawn1443–1484 ↗
async fn finalize_thread_spawn(
        &self,
        codex: Codex,
        thread_id: ThreadId,
        session_source: SessionSource,
    ) -> CodexResult<NewThread>

作用:完成线程启动后的最后验收和登记。它要求 Codex 发出的第一个事件必须是 SessionConfigured。

数据流:进去刚启动的 Codex、线程 id 和会话来源 → 读取第一个事件,确认是正确的配置事件 → 创建 CodexThread 并插入线程表;如果 id 已存在,就关闭重复 Codex 并返回错误。

调用关系:spawn_thread_with_source 在 Codex::spawn 成功后调用它;它保证外部拿到的线程已经有可靠的 session_configured 信息。

调用图:调用 3 个内部函数(new, next_event, shutdown_and_wait);被 1 处调用(spawn_thread_with_source);外部调用 4 个(new, format!, InvalidRequest, warn!)。

ThreadManagerState::notify_thread_created1486–1488 ↗
fn notify_thread_created(&self, thread_id: ThreadId)

作用:向订阅者广播某个线程已经创建。广播失败不会影响主流程。

数据流:进去线程 id → 发送到 thread_created_tx 广播通道 → 不关心是否有接收者或发送是否成功。

调用关系:创建通知的生产者会调用它;subscribe_thread_created 返回的接收器会收到这里发出的 id。

调用图:外部调用 1 个(send)。

ThreadManagerState::parent_rollout_thread_trace_for_source1490–1518 ↗
async fn parent_rollout_thread_trace_for_source(
        &self,
        session_source: &SessionSource,
        initial_history: &InitialHistory,
    ) -> codex_rollout_trace::ThreadTraceContext

作用:为新建子代理准备父线程的 rollout 追踪上下文。追踪上下文用于把父子线程的历史记录串成一棵树。

数据流:进去会话来源和初始历史 → 如果不是新建的 ThreadSpawn 子代理,或是恢复历史,就返回禁用追踪;否则查找父线程并复制它的 rollout trace,查不到也返回禁用 → 输出 ThreadTraceContext。

调用关系:spawn_thread_with_source 在创建 Codex 前调用它;它依赖 get_thread,但失败不会阻止子线程创建。

调用图:调用 2 个内部函数(get_thread, disabled);被 1 处调用(spawn_thread_with_source);外部调用 1 个(matches!)。

stored_thread_to_initial_history1521–1536 ↗
fn stored_thread_to_initial_history(
    stored_thread: StoredThread,
    rollout_path: Option<PathBuf>,
) -> CodexResult<InitialHistory>

作用:把存储层读出来的 StoredThread 转成启动线程用的 InitialHistory。它要求存储结果必须带历史内容。

数据流:进去 StoredThread 和可选 rollout 路径 → 检查 history 是否存在;不存在就报 Fatal → 把线程 id、历史条目和 rollout 路径包装成 InitialHistory::Resumed。

调用关系:initial_history_from_rollout_path 和 spawn_subagent 都会调用它,把持久化数据接到启动流程。

调用图:被 2 处调用(initial_history_from_rollout_path, spawn_subagent);外部调用 1 个(Resumed)。

thread_store_rollout_read_error1538–1544 ↗
fn thread_store_rollout_read_error(err: ThreadStoreError) -> CodexErr

作用:把按 rollout 路径读取线程时的存储错误翻译成 Codex 错误。这样上层不用理解存储库的内部错误类型。

数据流:进去 ThreadStoreError → 未找到转成 ThreadNotFound,非法请求转成 InvalidRequest,其他错误转成 Fatal → 返回 CodexErr。

调用关系:initial_history_from_rollout_path 在读取失败时使用它。

调用图:外部调用 4 个(format!, Fatal, InvalidRequest, ThreadNotFound)。

thread_store_metadata_update_error1546–1557 ↗
fn thread_store_metadata_update_error(thread_id: ThreadId, err: ThreadStoreError) -> CodexErr

作用:把更新线程元数据时的存储错误翻译成 Codex 错误。它会特别处理“不支持更新”这种情况。

数据流:进去线程 id 和 ThreadStoreError → 根据错误类别转换成 ThreadNotFound、InvalidRequest、UnsupportedOperation 或 Fatal → 返回 CodexErr。

调用关系:ThreadManager::update_thread_metadata 在运行线程或冷线程更新失败时都会用它包装错误。

调用图:外部调用 5 个(format!, Fatal, InvalidRequest, ThreadNotFound, UnsupportedOperation)。

truncate_before_nth_user_message1565–1590 ↗
fn truncate_before_nth_user_message(
    history: InitialHistory,
    n: usize,
    snapshot_state: &SnapshotTurnState,
) -> InitialHistory

作用:生成一个在第 n 条用户消息之前截断的 fork 历史。它用于从旧对话的某个点开新分支。

数据流:进去历史、用户消息序号 n 和当前快照是否处于半回合状态 → 找出所有用户消息位置;如果 n 越界且历史还在半回合,就切掉未完成回合;否则按第 n 条用户消息前截断 → 空历史返回 New,非空返回 Forked。

调用关系:fork_history_from_snapshot 在 TruncateBeforeNthUserMessage 模式下调用它;它依赖 truncation 工具找用户消息位置。

调用图:调用 1 个内部函数(get_rollout_items);被 1 处调用(fork_history_from_snapshot);外部调用 3 个(Forked, truncate_rollout_before_nth_user_message_from_start, user_message_positions_in_rollout)。

snapshot_turn_state1599–1650 ↗
fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState

作用:判断一段历史是不是停在“半个回合”中。半个回合是用户发话后,系统还没完整结束或中断的状态。

数据流:进去 InitialHistory → 取出 rollout 条目,用 ThreadHistoryBuilder 重建回合状态 → 如果有明确活跃回合且还在进行中,记录 turn_id 和起点;否则用最后一条用户消息后是否有完成或中断事件来判断 → 返回 SnapshotTurnState。

调用关系:fork_history_from_snapshot 先调用它,再决定 fork 时是直接截断还是补一个中断边界。

调用图:调用 2 个内部函数(new, get_rollout_items);被 1 处调用(fork_history_from_snapshot);外部调用 1 个(user_message_positions_in_rollout)。

fork_history_from_snapshot1652–1680 ↗
fn fork_history_from_snapshot(
    snapshot: ForkSnapshot,
    history: InitialHistory,
    interrupted_marker: InterruptedTurnHistoryMarker,
) -> InitialHistory

作用:按指定快照规则把原历史改造成 fork 用历史。它处理“截断到某条用户消息前”和“像现在被中断一样复制”两种模式。

数据流:进去 ForkSnapshot、原始历史和中断标记配置 → 先判断历史是否半回合;截断模式交给 truncate_before_nth_user_message;中断模式会把恢复历史转成 fork 历史,并在半回合时追加中断边界 → 返回新的 InitialHistory。

调用关系:fork_thread_with_initial_history 和 spawn_subagent 都调用它;追加中断事件的细节交给 append_interrupted_boundary。

调用图:调用 3 个内部函数(append_interrupted_boundary, snapshot_turn_state, truncate_before_nth_user_message);被 2 处调用(fork_thread_with_initial_history, spawn_subagent);外部调用 1 个(Forked)。

append_interrupted_boundary1685–1721 ↗
fn append_interrupted_boundary(
    history: InitialHistory,
    turn_id: Option<String>,
    interrupted_marker: InterruptedTurnHistoryMarker,
) -> InitialHistory

作用:给 fork 历史末尾补上和真实中断一样的记录。这样新分支不会误以为上一个回合还在继续。

数据流:进去历史、可选 turn_id 和中断标记配置 → 创建 TurnAborted 事件,必要时先追加一个响应标记 → 把这些条目加入历史,最终返回 InitialHistory::Forked。

调用关系:fork_history_from_snapshot 在 Interrupted 模式且发现历史半回合时调用它;它复用 interrupted_turn_history_marker,保持和真实中断路径一致。

调用图:调用 1 个内部函数(interrupted_turn_history_marker);被 1 处调用(fork_history_from_snapshot);外部调用 6 个(new, TurnAborted, Forked, push, EventMsg, ResponseItem)。

core/src/environment_selection.rs源码 ↗
orchestrationsession setup / environment selection updates / request handling

可以把它想成给每一轮任务安排“工位”。用户可能选本机,也可能选远程机器,还会指定工作目录。这个文件先根据环境管理器给出默认选择,再把这些选择解析成真正能运行命令、读写文件的 TurnEnvironment。解析可能比较慢,比如远程环境要连过去询问信息,所以 ThreadEnvironments 用异步任务在后台准备快照;别人要用时调用 snapshot 等它完成即可。它还会避免重复环境编号,只保留第一次出现的那个;解析失败的环境会被跳过并写警告,不让整个流程崩掉。TurnEnvironmentSnapshot 则像一张“当前工位清单”,能取第一个主环境、找本地环境、拿主环境的文件系统,或在只有一个本地环境时给老代码返回本地路径。文件底部的测试覆盖了默认选择、重复项、失败跳过、更新竞争等重要边界。

函数细节26
default_thread_environment_selections20–32 ↗
fn default_thread_environment_selections(
    environment_manager: &EnvironmentManager,
    cwd: &AbsolutePathBuf,
) -> Vec<TurnEnvironmentSelection>

作用:生成一轮对话默认要使用的环境列表。它把环境管理器里的默认环境编号,配上当前工作目录,变成后面能解析的选择项。

数据流:输入是环境管理器和当前目录 → 它读取管理器给出的默认环境编号,并把当前目录转成路径 URI(一种可表示本地或远程路径的格式)→ 输出一组 TurnEnvironmentSelection,每个选择都包含环境编号和工作目录。

调用关系:它通常在新建、恢复或派生线程时先被调用,用来给 ThreadEnvironments 提供初始选择。它自己只问 default_environment_ids 要编号,不真正连接环境。

调用图:调用 1 个内部函数(default_environment_ids);被 7 处调用(default_environment_selections, fork_thread_with_initial_history, resume_thread_from_rollout_with_user_shell_override_for_tests, resume_thread_with_history, start_thread_with_tools, start_thread_with_user_shell_override_for_tests, resume_thread_with_history_with_source)。

ThreadEnvironments::new44–56 ↗
fn new(
        environment_manager: Arc<EnvironmentManager>,
        local_shell: Shell,
        shell_snapshot: ShellSnapshot,
        current: TurnEnvironmentSnapshot,
    ) -> Self

作用:创建一个保存“当前环境快照”的容器。它把环境管理器、本地 shell、shell 快照工具和已有快照装起来,供后续更新和读取。

数据流:输入是环境管理器、本地命令行外壳、shell 快照器和当前快照 → 它把当前快照包装成一个已经完成的异步任务 → 输出 ThreadEnvironments 实例,里面已经有一份可读取的初始快照。

调用关系:会在会话或测试搭建时调用。之后 update_selections 会替换里面的快照任务,snapshot 会读取这个任务的结果。

调用图:被 7 处调用(latest_environment_update_wins_while_previous_resolution_is_pending, local_environment_uses_configured_shell, resolve_turn_environments, new, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, resolved_environments_for_configuration);外部调用 2 个(from_pointee, ready)。

ThreadEnvironments::update_selections58–83 ↗
fn update_selections(&self, environments: &[TurnEnvironmentSelection])

作用:把新的环境选择应用到当前线程。它不会卡住调用者,而是启动后台任务去解析新选择,并让“最新一次更新”成为之后读取的目标。

数据流:输入是一组新的 TurnEnvironmentSelection → 它先尽量拿到上一份已完成快照,以便复用已有环境;然后复制需要的信息,启动异步解析任务 → 它更新内部的 snapshot_task,之后 snapshot 会等这次新任务完成。

调用关系:外部在用户切换环境或线程初始化时调用它。它把真正解析工作交给 resolve_snapshot,并通过 tokio 后台任务运行;这样即使旧解析还卡着,新的选择也能覆盖旧的读取入口。

调用图:外部调用 9 个(clone, new, load, store, resolve_snapshot, clone, clone, to_vec, spawn)。

ThreadEnvironments::resolve_snapshot85–124 ↗
async fn resolve_snapshot(
        environment_manager: Arc<EnvironmentManager>,
        local_shell: Shell,
        shell_snapshot: ShellSnapshot,
        current: TurnEnvironmentSnapshot,
        en

作用:把一批环境选择解析成一份完整快照。它负责去重、复用旧结果,并跳过无法解析的环境。

数据流:输入是环境管理器、本地 shell、shell 快照器、上一份快照和新的选择列表 → 它按顺序处理选择,重复的环境编号只保留第一次;如果旧快照里已有同编号同目录的环境就直接复用,否则调用 resolve_selection 新建 → 输出 TurnEnvironmentSnapshot,里面只有成功解析的环境。

调用关系:它由 update_selections 启动的后台任务调用。遇到单个环境失败时只记录警告,不中断整个快照生成。

调用图:外部调用 4 个(with_capacity, resolve_selection, with_capacity, warn!)。

ThreadEnvironments::resolve_selection126–171 ↗
async fn resolve_selection(
        environment_manager: &EnvironmentManager,
        local_shell: &Shell,
        shell_snapshot: &ShellSnapshot,
        selected_environment: &TurnEnvironmentSelecti

作用:把一个具体的环境选择变成真正可用的 TurnEnvironment。它会找到环境对象,确定应该用哪个 shell,并启动 shell 快照构建。

数据流:输入是环境管理器、本地 shell、shell 快照器和一个选择项 → 它用环境编号查找环境;远程环境会询问远端信息来推断 shell,本地环境直接使用配置好的本地 shell;然后创建 TurnEnvironment,并在后台准备 shell 快照 → 成功时输出 TurnEnvironment,失败时返回错误。

调用关系:resolve_snapshot 在需要新建环境时调用它。它会调用环境管理器的 get_environment,也会使用 Shell::from_environment_shell_info 解释远程返回的 shell 信息,并把 shell 快照构建任务交给 tokio 执行。

调用图:调用 3 个内部函数(new, from_environment_shell_info, get_environment);外部调用 4 个(clone, clone, spawn, warn!)。

ThreadEnvironments::snapshot173–175 ↗
async fn snapshot(&self) -> TurnEnvironmentSnapshot

作用:读取当前这份环境快照。如果后台解析还没完成,它会等待完成后再返回。

数据流:输入是 ThreadEnvironments 自身 → 它取出当前保存的异步快照任务并等待结果 → 输出 TurnEnvironmentSnapshot,不直接改变环境选择。

调用关系:后续需要运行命令、找主环境或拿文件系统的地方会调用它。它读取的是 update_selections 最近一次存进去的任务。

调用图:外部调用 1 个(load_full)。

ThreadEnvironments::environment_manager177–179 ↗
fn environment_manager(&self) -> Arc<EnvironmentManager>

作用:拿到内部保存的环境管理器。调用者可以继续用它查询或操作可用环境。

数据流:输入是 ThreadEnvironments 自身 → 它克隆 Arc(共享引用计数指针,可以安全共享同一个对象)→ 输出同一个 EnvironmentManager 的共享句柄。

调用关系:这是一个简单的访问口。它不解析环境,只把 ThreadEnvironments 里持有的管理器交给需要的人。

调用图:外部调用 1 个(clone)。

TurnEnvironmentSnapshot::primary188–190 ↗
fn primary(&self) -> Option<&TurnEnvironment>

作用:取出快照里的第一个环境,也就是默认的主环境。很多地方会把第一个环境当作最重要、最先使用的那个。

数据流:输入是一份环境快照 → 它查看环境列表的第一个元素 → 如果有环境就返回引用,没有就返回空。

调用关系:primary_environment 和 primary_filesystem 都通过它找主环境。它体现了“选择列表顺序很重要”的规则。

调用图:被 2 处调用(primary_environment, primary_filesystem)。

TurnEnvironmentSnapshot::local192–196 ↗
fn local(&self) -> Option<&TurnEnvironment>

作用:在快照里找一个本地环境。需要明确在本机执行或读取本机信息时会用到它。

数据流:输入是一份环境快照 → 它从前到后检查每个环境是否不是远程环境 → 找到第一个本地环境就返回,否则返回空。

调用关系:这是快照上的便捷查询函数。它不创建环境,只在已有清单里筛选。

TurnEnvironmentSnapshot::primary_environment199–202 ↗
fn primary_environment(&self) -> Option<Arc<codex_exec_server::Environment>>

作用:测试用的辅助函数,用来拿到主环境背后的 Environment 对象。它方便测试比较是不是同一个环境实例。

数据流:输入是一份快照 → 它先调用 primary 找主环境,再克隆里面的共享 Environment 指针 → 输出主环境对象的共享句柄,或空。

调用关系:只在测试编译时存在。它依赖 primary,所以主环境的定义仍然是列表第一个。

调用图:调用 1 个内部函数(primary)。

TurnEnvironmentSnapshot::to_selections204–209 ↗
fn to_selections(&self) -> Vec<TurnEnvironmentSelection>

作用:把已经解析好的环境快照重新变回选择列表。测试或保存状态时,可以用它检查当前快照对应哪些环境编号和目录。

数据流:输入是一份快照 → 它遍历每个 TurnEnvironment,并调用它自己的 selection 方法 → 输出一组 TurnEnvironmentSelection。

调用关系:测试经常用它确认解析结果是否符合预期。它是“解析后的环境”到“可记录的选择项”的反向转换。

TurnEnvironmentSnapshot::primary_filesystem211–214 ↗
fn primary_filesystem(&self) -> Option<Arc<dyn ExecutorFileSystem>>

作用:拿到主环境的文件系统接口。后续如果要预热插件或读取主环境里的文件,就需要这个入口。

数据流:输入是一份快照 → 它先调用 primary 找主环境,再从主环境对象里取文件系统 → 输出文件系统的共享接口,或空。

调用关系:warm_plugins_and_skills_for_session_init 会用它在会话初始化时准备插件和技能。它把调用者从“先找主环境再取文件系统”的细节里解放出来。

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

TurnEnvironmentSnapshot::single_local_environment216–222 ↗
fn single_local_environment(&self) -> Option<&TurnEnvironment>

作用:确认快照里“只有一个环境,而且它是本地环境”。只有满足这个严格条件时才返回它。

数据流:输入是一份快照 → 它检查列表是否刚好一个元素,再检查这个环境不是远程 → 条件都满足就返回该环境,否则返回空。

调用关系:single_local_environment_cwd 会调用它。这个严格判断是为了避免在多环境或远程环境场景下误把路径当成本机路径。

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

TurnEnvironmentSnapshot::single_local_environment_cwd224–228 ↗
fn single_local_environment_cwd(&self) -> Option<AbsolutePathBuf>

作用:在兼容老代码时使用:如果当前只有一个本地环境,就把它的工作目录转成本机绝对路径。

数据流:输入是一份快照 → 它先调用 single_local_environment 确认只有一个本地环境;再把 PathUri 转成 AbsolutePathBuf(本机绝对路径)→ 成功则输出路径,不满足条件或转换失败就返回空。

调用关系:这是过渡用的兼容入口。注释里也说明,未来更多地方改用 PathUri 后,这种转换就可以删掉。

调用图:调用 1 个内部函数(single_local_environment)。

tests::resolve_turn_environments244–257 ↗
async fn resolve_turn_environments(
        environment_manager: Arc<EnvironmentManager>,
        selections: &[TurnEnvironmentSelection],
    ) -> Arc<ThreadEnvironments>

作用:测试用的小工具:给一组选择,快速建好 ThreadEnvironments 并等它解析完成。

数据流:输入是环境管理器和选择列表 → 它创建 ThreadEnvironments,使用默认用户 shell 和禁用的 shell 快照器;然后调用 update_selections 并等待 snapshot 完成 → 输出可继续检查的 ThreadEnvironments。

调用关系:多个测试复用它,避免每个测试都重复搭建异步环境解析流程。它覆盖的是正式的 new、update_selections 和 snapshot 路线。

调用图:调用 3 个内部函数(new, default_user_shell, disabled);外部调用 2 个(new, default)。

tests::test_runtime_paths259–265 ↗
fn test_runtime_paths() -> ExecServerRuntimePaths

作用:测试用的小工具:构造执行服务器需要的运行时路径。它让测试环境知道当前可执行文件在哪里。

数据流:输入为空 → 它读取当前测试进程的可执行文件路径,并传给 ExecServerRuntimePaths::new → 输出测试可用的运行时路径配置。

调用关系:涉及远程环境或执行服务器的测试会调用它。它只是准备测试依赖,不参与正式运行逻辑。

调用图:调用 1 个内部函数(new);外部调用 1 个(current_exe)。

tests::default_thread_environment_selections_use_manager_default_id268–284 ↗
async fn default_thread_environment_selections_use_manager_default_id()

作用:验证默认环境选择会使用环境管理器给出的默认编号。这里的场景是测试管理器默认指向远程环境。

数据流:输入是测试创建出的当前目录和环境管理器 → 它调用 default_thread_environment_selections → 断言输出正好是远程环境编号加当前目录 URI。

调用关系:它直接测试 default_thread_environment_selections,确保线程启动时默认选项不会乱用别的环境。

调用图:调用 3 个内部函数(create_for_tests, current_dir, from_abs_path);外部调用 2 个(assert_eq!, test_runtime_paths)。

tests::toml_default_thread_environment_selections_include_local_and_remote287–318 ↗
async fn toml_default_thread_environment_selections_include_local_and_remote()

作用:验证从 environments.toml 配置读环境时,默认选择会同时包含本地和远程环境。

数据流:输入是临时目录里的 environments.toml 和当前目录 → 它从配置目录创建 EnvironmentManager,再生成默认选择 → 断言结果先有本地环境,再有远程环境,并且目录相同。

调用关系:它覆盖配置文件影响默认环境列表的情况。这样可以防止配置加载后默认环境顺序或内容被改坏。

调用图:调用 3 个内部函数(from_codex_home, current_dir, from_abs_path);外部调用 4 个(assert_eq!, test_runtime_paths, write, tempdir)。

tests::default_thread_environment_selections_empty_when_default_disabled321–329 ↗
async fn default_thread_environment_selections_empty_when_default_disabled()

作用:验证如果环境功能被关闭,默认选择就是空列表。这样不会偷偷启用任何执行环境。

数据流:输入是一个没有环境的 EnvironmentManager → 它调用 default_thread_environment_selections → 断言输出为空。

调用关系:它测试 default_thread_environment_selections 的禁用场景,保证没有默认环境时线程不会凭空创建环境。

调用图:调用 2 个内部函数(without_environments, current_dir);外部调用 1 个(assert_eq!)。

tests::local_environment_uses_configured_shell332–357 ↗
async fn local_environment_uses_configured_shell()

作用:验证本地环境会使用外部配置好的 shell,而不是随便重新猜一个。shell 就是运行命令的程序,比如 zsh 或 bash。

数据流:输入是一个指定为 zsh 的本地 shell 和本地环境选择 → 它创建 ThreadEnvironments,更新选择并读取快照 → 断言主环境里保存的 shell 就是配置的那个。

调用关系:它测试 resolve_selection 处理本地环境的分支。这个测试保证用户配置的命令行外壳会被尊重。

调用图:调用 5 个内部函数(new, disabled, default_for_tests, current_dir, from_abs_path);外部调用 4 个(new, assert_eq!, default, from)。

tests::resolve_environment_selections_keeps_first_duplicate_id360–382 ↗
async fn resolve_environment_selections_keeps_first_duplicate_id()

作用:验证同一个环境编号出现多次时,只保留第一次。这样同一环境不会因为重复选择而被加入两遍。

数据流:输入是两个同编号但目录不同的本地环境选择 → 它解析这些选择 → 断言最终快照只保留第一个选择。

调用关系:它测试 resolve_snapshot 的去重规则。这个规则防止调用者传入重复项时造成混乱。

调用图:调用 3 个内部函数(default_for_tests, current_dir, from_abs_path);外部调用 3 个(new, assert_eq!, resolve_turn_environments)。

tests::resolved_environment_selections_use_first_selection_as_primary385–423 ↗
async fn resolved_environment_selections_use_first_selection_as_primary()

作用:验证解析后的第一个选择会成为主环境。主环境会被很多后续功能优先使用,所以顺序必须可靠。

数据流:输入是一个本地环境选择和指定工作目录 → 它解析后读取快照 → 断言 primary 的环境编号、工作目录和 shell 都符合预期。

调用关系:它测试 resolve_snapshot 保持选择顺序,以及 primary 的语义。它还间接确认 shell 信息能正确解析出来。

调用图:调用 3 个内部函数(default_for_tests, current_dir, from_abs_path);外部调用 4 个(clone, new, assert_eq!, resolve_turn_environments)。

tests::unresolved_environment_selections_are_skipped426–448 ↗
async fn unresolved_environment_selections_are_skipped()

作用:验证不存在的环境编号会被跳过,而不会让所有环境解析失败。

数据流:输入是一个缺失环境选择和一个正常本地环境选择 → 它解析选择列表 → 断言最终只剩正常本地环境。

调用关系:它测试 resolve_snapshot 对 resolve_selection 错误的处理方式。这个行为能让部分配置坏掉时系统仍尽量可用。

调用图:调用 3 个内部函数(default_for_tests, current_dir, from_abs_path);外部调用 3 个(new, assert_eq!, resolve_turn_environments)。

tests::latest_environment_update_wins_while_previous_resolution_is_pending451–495 ↗
async fn latest_environment_update_wins_while_previous_resolution_is_pending()

作用:验证如果旧的远程解析还卡着,后来的本地环境更新仍然能赢。也就是说,用户最新选择不会被慢吞吞的旧任务覆盖。

数据流:输入是一个会挂起连接的远程环境选择,然后再输入本地环境选择 → 它先启动远程解析并等连接发生,再调用 update_selections 改成本地 → 最终 snapshot 返回本地选择。

调用关系:它测试 update_selections 的并发更新设计。ArcSwap 保存的是最新任务,所以 snapshot 会跟随最后一次更新。

调用图:调用 6 个内部函数(new, default_user_shell, disabled, create_for_tests_with_local, current_dir, from_abs_path);外部调用 9 个(new, assert_eq!, default, test_runtime_paths, format!, from_ref, from_secs, bind, timeout)。

tests::matching_environment_id_and_cwd_reuse_resolved_environment498–549 ↗
async fn matching_environment_id_and_cwd_reuse_resolved_environment()

作用:验证环境编号和工作目录都没变时,会复用已经解析好的环境;目录变了才重新解析。

数据流:输入是一个远程环境选择 → 它先解析一次,再修改管理器里的远程地址;随后用相同选择更新,检查仍复用旧 Environment;再改工作目录更新,检查这次变成新 Environment → 输出是断言结果。

调用关系:它测试 resolve_snapshot 的复用逻辑。复用可以减少不必要的远程连接,但目录变化必须触发重新创建,避免用错工作目录。

调用图:调用 3 个内部函数(create_for_tests, current_dir, from_abs_path);外部调用 6 个(clone, new, assert!, resolve_turn_environments, test_runtime_paths, from_ref)。

tests::single_local_environment_cwd_requires_exactly_one_local_environment552–592 ↗
async fn single_local_environment_cwd_requires_exactly_one_local_environment()

作用:验证 single_local_environment_cwd 只有在“刚好一个本地环境”时才返回路径。远程环境或多个环境时都不能返回本机路径。

数据流:输入分别是单一本地快照、单一远程快照、以及本地加远程的多环境快照 → 它调用 single_local_environment_cwd → 断言只有单一本地场景返回当前目录,另外两种都返回空。

调用关系:它测试 single_local_environment 和 single_local_environment_cwd 的安全边界。这个边界避免多环境时代码误以为某个 URI 一定是本机路径。

调用图:调用 4 个内部函数(create_for_tests, default_for_tests, current_dir, from_abs_path);外部调用 5 个(clone, new, assert_eq!, resolve_turn_environments, vec!)。

core/src/codex_thread.rs源码 ↗
orchestrationcross-cutting:线程创建后到关闭前一直活跃,提交用户输入、读取事件、改配置、恢复会话、插件空闲回调和关闭时都会用到

可以把 CodexThread 想成一家餐厅前台:真正做饭的是底层的 Codex 和 Session,但客人、服务员、插件都先来前台登记需求。这个文件定义了线程的配置快照 ThreadConfigSnapshot、启动空闲自动任务失败时的错误 TryStartTurnIfIdleError,以及最主要的 CodexThread。它解决的问题是:线程里有很多敏感动作,比如启动一轮模型回复、注入消息、改权限、读历史、暂停会话、调用 MCP 工具(外部工具和资源接口),如果大家都直接操作底层对象,很容易打乱顺序或绕过检查。这里统一做入口,必要时先检查容量、权限、是否空闲,再把活儿转交给底层 Session 或 Codex。它还保留一些线程级信息,比如 rollout 文件路径、会话配置事件、离线询问计数等,用来保证恢复、记录、插件生命周期和前端状态更新都能对上。

函数细节53
TryStartTurnIfIdleError::new101–103 ↗
fn new(reason: TryStartTurnIfIdleRejectionReason, input: Vec<ResponseItem>) -> Self

作用:创建一个“空闲自动启动失败”的错误对象。它不仅说明为什么失败,还把原始输入原封不动带回去,方便调用方稍后重试或记录。

数据流:进去的是失败原因和本来要交给模型看的输入项;函数把它们装进 TryStartTurnIfIdleError;出来的是一个包含原因和原输入的错误值,不改动外部状态。

调用关系:它由 try_start_turn_if_idle 这类空闲启动流程在拒绝自动任务时使用,是把底层拒绝原因包装成稳定错误的收口。

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

TryStartTurnIfIdleError::reason106–108 ↗
fn reason(&self) -> TryStartTurnIfIdleRejectionReason

作用:读取这次空闲自动启动为什么被拒绝。调用方可以据此决定是等会儿再试、直接丢弃,还是提示用户。

数据流:进去的是这个错误对象本身;函数只读取里面保存的 reason;出来的是一个固定的拒绝原因,不消耗错误对象,也不改任何状态。

调用关系:它通常在拿到 TryStartTurnIfIdleError 后被上层代码调用,用来理解 try_start_turn_if_idle 为什么没能启动。

TryStartTurnIfIdleError::into_input112–114 ↗
fn into_input(self) -> Vec<ResponseItem>

作用:把失败时没能提交的原始输入取回来。这样失败不会让模型输入丢失。

数据流:进去的是整个错误对象;函数消耗这个错误对象,取出其中保存的输入列表;出来的是原来的 ResponseItem 列表,不做修改。

调用关系:它接在 try_start_turn_if_idle 失败之后使用,让插件或调用方可以重试、丢弃或写日志,而不是把输入困在错误里。

ThreadConfigSnapshot::cwd118–120 ↗
fn cwd(&self) -> &AbsolutePathBuf

作用:返回这个线程当前认为的工作目录,也就是命令和文件操作默认从哪里开始。工作目录像“当前所在文件夹”,很多安全和路径判断都靠它。

数据流:进去的是配置快照;函数读取 environments 里的 legacy_fallback_cwd;出来的是这个目录的引用,不复制也不修改配置。

调用关系:它会被恢复线程、比较配置差异、从快照生成设置,以及 sandbox_policy 使用,属于很多配置判断的基础小入口。

调用图:被 4 处调用(build_thread_from_snapshot, collect_resume_override_mismatches, thread_settings_from_config_snapshot, sandbox_policy)。

ThreadConfigSnapshot::environment_selections122–124 ↗
fn environment_selections(&self) -> &[TurnEnvironmentSelection]

作用:返回这个线程可用的运行环境选择列表。外部代码可以看出这一轮可能在哪些环境里执行。

数据流:进去的是配置快照;函数读取 environments.environments;出来的是环境选择列表的只读切片,不改任何内容。

调用关系:它是读取 ThreadConfigSnapshot 的便捷方法,供需要展示或检查运行环境的代码使用。

ThreadConfigSnapshot::sandbox_policy126–131 ↗
fn sandbox_policy(&self) -> SandboxPolicy

作用:根据线程的权限配置和当前工作目录,算出实际的沙盒策略。沙盒就是给程序活动范围加围栏,防止它随便读写或执行危险操作。

数据流:进去的是配置快照;函数先通过 cwd 取工作目录,再把权限配置和目录交给沙盒兼容函数计算;出来的是 SandboxPolicy,不保存到快照里。

调用关系:它在比较恢复时的配置差异时会被调用,用来判断当前权限配置会对应怎样的实际执行限制。

调用图:调用 1 个内部函数(cwd);被 1 处调用(collect_resume_override_mismatches);外部调用 1 个(compatibility_sandbox_policy_for_permission_profile)。

CodexThread::new173–186 ↗
fn new(
        codex: Codex,
        session_configured: SessionConfiguredEvent,
        rollout_path: Option<PathBuf>,
        session_source: SessionSource,
    ) -> Self

作用:创建一个新的 CodexThread,把底层 Codex、会话配置、记录路径和来源信息包在一起。没有这一步,外部就没有统一的线程操作入口。

数据流:进去的是底层 Codex、SessionConfiguredEvent、可选 rollout 路径和会话来源;函数把它们存进结构体,并把离线询问计数初始化为 0;出来的是新的 CodexThread。

调用关系:它由 finalize_thread_spawn 在线程真正生成完后调用,是线程从底层会话变成对外对象的最后装配步骤。

调用图:被 1 处调用(finalize_thread_spawn);外部调用 1 个(new)。

CodexThread::submit188–190 ↗
async fn submit(&self, op: Op) -> CodexResult<String>

作用:向线程提交一个操作,比如用户输入、控制命令或其他协议动作。它是很多上层流程最常用的投递入口。

数据流:进去的是一个 Op 操作;函数原样交给底层 Codex.submit;出来的是提交编号或错误,底层队列会因此收到新工作。

调用关系:提交线程设置、实时会话开关、请求捕获、用户输入、队列消息和运行一轮等流程都会用它;它自己主要把活交给 Codex。

调用图:调用 1 个内部函数(submit);被 8 处调用(submit_thread_settings, close_realtime_conversation, start_realtime_conversation, capture_from_requests, submit_user_input, submit_queue_only_agent_mail, submit_user_input, run_turn)。

CodexThread::session_telemetry193–195 ↗
fn session_telemetry(&self) -> SessionTelemetry

作用:取出这个线程的遥测句柄。遥测就是运行时统计和日志,用来观察系统表现、排查问题。

数据流:进去的是线程对象;函数读取底层 session services 里的 session_telemetry 并克隆一份句柄;出来的是可继续使用的 SessionTelemetry。

调用关系:它给需要记录线程级生产数据的外部路径使用,不让调用方直接进入更大的 session 内部。

CodexThread::shutdown_and_wait197–199 ↗
async fn shutdown_and_wait(&self) -> CodexResult<()>

作用:请求线程关闭,并等待它真的收尾结束。用于保证后台任务、队列和资源不是半途被硬切。

数据流:进去的是线程对象;函数把关闭请求交给 Codex.shutdown_and_wait;出来的是成功或 Codex 错误,底层会话状态会走向终止。

调用关系:它是关闭流程的对外入口,真正的关闭细节由底层 Codex 执行。

调用图:调用 1 个内部函数(shutdown_and_wait)。

CodexThread::wait_until_terminated202–204 ↗
async fn wait_until_terminated(&self)

作用:只等待底层会话循环已经结束,不主动发起关闭。适合已经触发关闭后,另一个地方想等它完全停下。

数据流:进去的是线程对象;函数等待 session_loop_termination 这个异步完成信号;出来时没有返回值,只表示底层循环已经终止。

调用关系:loop_agent 会用它等待代理线程结束,避免外层循环在线程还没停稳时继续推进。

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

CodexThread::emit_thread_resume_lifecycle206–221 ↗
async fn emit_thread_resume_lifecycle(&self)

作用:在线程恢复时通知所有插件式扩展:这个线程又回来了。这样扩展可以恢复自己存在会话或线程里的附加数据。

数据流:进去的是线程对象;函数遍历扩展提供的线程生命周期贡献者,把 session_store 和 thread_store 传给它们的 on_thread_resume;出来没有返回值,但扩展可能更新自己的数据。

调用关系:它处在线程恢复流程中,像广播通知一样逐个叫醒扩展;每个扩展收到通知后自行处理恢复工作。

CodexThread::emit_thread_idle_lifecycle_if_idle223–228 ↗
async fn emit_thread_idle_lifecycle_if_idle(&self)

作用:如果线程现在真的空闲,就触发“线程空闲”的生命周期通知。插件可以借这个时机做不打扰用户的后台工作。

数据流:进去的是线程对象;函数把判断和触发动作交给 session.emit_thread_idle_lifecycle_if_idle;出来没有直接结果,可能引发扩展回调。

调用关系:emit_resume_goal_snapshot_and_continue 会调用它,让恢复后的线程在合适时机继续空闲相关工作。

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

CodexThread::ensure_rollout_materialized231–233 ↗
async fn ensure_rollout_materialized(&self)

作用:确保线程的 rollout 记录已经落成实际可用的存储。rollout 可以理解为这段会话的执行记录。

数据流:进去的是线程对象;函数调用底层 session.ensure_rollout_materialized;出来没有返回值,但底层可能创建或准备好记录文件。

调用关系:这是隐藏的内部辅助入口,给需要确保记录存在的流程使用,实际工作由 Session 完成。

CodexThread::flush_rollout236–238 ↗
async fn flush_rollout(&self) -> std::io::Result<()>

作用:把当前 rollout 记录刷到磁盘或持久层,减少崩溃时丢记录的风险。

数据流:进去的是线程对象;函数调用 session.flush_rollout;出来是 I/O 成功或失败结果,持久化记录可能被更新。

调用关系:它是线程层面的保存按钮,底层写入细节由 Session 管。

CodexThread::submit_with_trace240–246 ↗
async fn submit_with_trace(
        &self,
        op: Op,
        trace: Option<W3cTraceContext>,
    ) -> CodexResult<String>

作用:提交一个操作,同时带上追踪信息。追踪信息像快递单号,可以把一次请求经过的不同模块串起来排查。

数据流:进去的是 Op 和可选 W3cTraceContext;函数交给 Codex.submit_with_trace;出来的是提交编号或错误,底层收到带追踪上下文的新操作。

调用关系:submit_core_op 会用它提交核心操作,让请求链路的追踪信息能继续传到 Codex 内部。

调用图:调用 1 个内部函数(submit_with_trace);被 2 处调用(submit_core_op, submit_core_op)。

CodexThread::submit_user_input_with_client_user_message_id248–263 ↗
async fn submit_user_input_with_client_user_message_id(
        &self,
        op: Op,
        trace: Option<W3cTraceContext>,
        client_user_message_id: Option<String>,
    ) -> CodexResult<Stri

作用:提交用户输入,并保留客户端给这条用户消息的编号。提交前还会先确认当前线程有执行容量,避免把任务塞进已经不该执行的状态。

数据流:进去的是操作、可选追踪信息和可选客户端消息编号;函数先让 agent_control 检查这个操作能不能执行,再交给底层提交;出来的是提交编号或错误,队列中会出现一条带客户端标识的用户输入。

调用关系:它是更严格的用户输入入口,比普通 submit 多了执行容量检查,随后把实际投递交给 Codex。

调用图:调用 1 个内部函数(submit_user_input_with_client_user_message_id)。

CodexThread::set_thread_memory_mode266–268 ↗
async fn set_thread_memory_mode(&self, mode: ThreadMemoryMode) -> anyhow::Result<()>

作用:保存这个线程以后是否允许生成记忆。记忆模式会影响系统以后能不能从这条线程里提炼长期信息。

数据流:进去的是 ThreadMemoryMode;函数交给 Codex.set_thread_memory_mode;出来是成功或错误,底层持久状态可能被更新。

调用关系:它是线程记忆资格设置的公开入口,具体保存逻辑在 Codex 里。

调用图:调用 1 个内部函数(set_thread_memory_mode)。

CodexThread::steer_input270–287 ↗
async fn steer_input(
        &self,
        input: Vec<UserInput>,
        additional_context: BTreeMap<String, AdditionalContextEntry>,
        expected_turn_id: Option<&str>,
        client_user_me

作用:把额外用户输入送进正在进行的对话轮次,用来“转向”当前回复。比如模型还在工作时,用户补充一句要求。

数据流:进去的是用户输入列表、额外上下文、期望的 turn 编号、客户端消息编号和元数据;函数交给 Codex.steer_input;出来是成功编号或 SteerInputError,当前活跃轮次可能收到新信息。

调用关系:steer_user_input 会调用它;它本身不做转向细节,而是把信息交给底层 Codex 判断能不能注入当前轮次。

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

CodexThread::inject_if_running294–299 ↗
async fn inject_if_running(
        &self,
        items: Vec<ResponseItem>,
    ) -> Result<(), Vec<ResponseItem>>

作用:如果当前线程正有一轮模型在跑,就把一些模型可见内容塞进去;如果没有在跑,就把原内容退回。

数据流:进去的是 ResponseItem 列表;函数调用 session.inject_if_running;成功时这些内容被注入当前轮次,失败时原列表作为 Err 返回且不被改动。

调用关系:这是只拿到 CodexThread 的调用方通向 Session 注入能力的桥,常用于扩展或后台事件想补充当前模型上下文。

CodexThread::try_start_turn_if_idle314–319 ↗
async fn try_start_turn_if_idle(
        &self,
        items: Vec<ResponseItem>,
    ) -> Result<(), TryStartTurnIfIdleError>

作用:只在线程真正空闲、没有用户任务排队、也不是计划模式时,才启动一个自动模型轮次。它防止插件在不合适的时候抢先开新回复。

数据流:进去的是模型可见的 ResponseItem 列表;函数交给 session.try_start_turn_if_idle 做空闲检查和启动;成功时新轮次开始,失败时返回带原因和原输入的 TryStartTurnIfIdleError。

调用关系:这是扩展在 on_thread_idle 里想自动发起工作时必须走的入口,保证用户触发的任务永远优先。

CodexThread::set_app_server_client_info321–334 ↗
async fn set_app_server_client_info(
        &self,
        app_server_client_name: Option<String>,
        app_server_client_version: Option<String>,
        mcp_elicitations_auto_deny: bool,
    ) -

作用:记录连接到这个线程的 app-server 客户端信息,以及 MCP 询问是否自动拒绝。这样底层可以按客户端能力和策略运行。

数据流:进去的是客户端名称、版本和是否自动拒绝 MCP elicitation 的开关;函数交给 Codex.set_app_server_client_info;出来是约束检查结果,底层客户端状态可能更新。

调用关系:同名上层接口会调用它;它是 app-server 把客户端身份和行为偏好传进线程的入口。

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

CodexThread::preview_thread_settings_overrides337–343 ↗
async fn preview_thread_settings_overrides(
        &self,
        overrides: CodexThreadSettingsOverrides,
    ) -> ConstraintResult<ThreadConfigSnapshot>

作用:预览一组线程设置改动会变成什么样,但不真正提交。像改配置前先点“预览”,看是否符合限制。

数据流:进去的是 CodexThreadSettingsOverrides;函数先调用 thread_settings_update 把覆盖项整理成 SessionSettingsUpdate,再交给 session.preview_settings;出来是配置快照或约束错误,线程实际设置不被改变。

调用关系:build_thread_settings_overrides 会用它检查用户想改的线程设置;它把整理覆盖项的工作交给 thread_settings_update。

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

CodexThread::thread_settings_update345–392 ↗
async fn thread_settings_update(
        &self,
        overrides: CodexThreadSettingsOverrides,
    ) -> SessionSettingsUpdate

作用:把外部传来的零散设置覆盖项,整理成底层 Session 能理解的一包设置更新。

数据流:进去的是 CodexThreadSettingsOverrides;函数拆开每个可选字段,如果没明确给 collaboration_mode,就从当前会话模式结合模型和推理力度变化算一个;出来是 SessionSettingsUpdate。

调用关系:它只被 preview_thread_settings_overrides 调用,是预览配置前的格式转换和默认值补齐步骤。

调用图:被 1 处调用(preview_thread_settings_overrides);外部调用 1 个(default)。

CodexThread::submit_with_id395–397 ↗
async fn submit_with_id(&self, sub: Submission) -> CodexResult<()>

作用:提交一个已经带有编号的 Submission。注释说明这是临时入口,未来打算移除。

数据流:进去的是 Submission;函数交给 Codex.submit_with_id;出来是成功或 Codex 错误,底层队列按这个既有编号接收提交。

调用关系:它是少数需要自带提交编号的内部路径使用的桥,实际投递仍由 Codex 完成。

调用图:调用 1 个内部函数(submit_with_id)。

CodexThread::next_event399–401 ↗
async fn next_event(&self) -> CodexResult<Event>

作用:读取线程产生的下一个事件。事件可以是模型输出、状态变化、工具结果等,是外部观察线程进展的主要方式。

数据流:进去的是线程对象;函数等待 Codex.next_event;出来是一个 Event 或错误,底层事件流被向前消费一步。

调用关系:提交线程设置、等待事件、等待 MCP 服务和运行一轮等流程都会调用它,用来从线程里取下一条消息。

调用图:调用 1 个内部函数(next_event);被 4 处调用(submit_thread_settings, wait_for_event_with_timeout, wait_for_mcp_server, run_turn)。

CodexThread::agent_status403–405 ↗
async fn agent_status(&self) -> AgentStatus

作用:查询当前代理状态,比如是否空闲、忙碌或处于其他状态。外部循环用它判断接下来该不该继续等或提交任务。

数据流:进去的是线程对象;函数调用 Codex.agent_status;出来是 AgentStatus,不改变线程。

调用关系:loop_agent 会调用它观察代理状态;真正状态来源在底层 Codex。

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

CodexThread::list_background_terminals407–409 ↗
async fn list_background_terminals(&self) -> Vec<BackgroundTerminalInfo>

作用:列出线程里还在后台运行的终端进程。这样前端或控制层能看到哪些命令还没结束。

数据流:进去的是线程对象;函数读取 session.list_background_terminals;出来是 BackgroundTerminalInfo 列表,包含条目编号、进程编号、命令和目录。

调用关系:它是查看后台终端的线程级入口,实际进程清单由 Session 维护。

CodexThread::terminate_background_terminal411–416 ↗
async fn terminate_background_terminal(&self, process_id: i32) -> bool

作用:按进程编号终止一个后台终端。用于用户或系统想停掉还在跑的命令。

数据流:进去的是 process_id;函数交给 session.terminate_background_terminal;出来是布尔值,表示是否找到并尝试终止了对应进程。

调用关系:它和 list_background_terminals 配套,一个负责看,一个负责停;具体杀进程逻辑在 Session 内部。

CodexThread::subscribe_status418–420 ↗
fn subscribe_status(&self) -> watch::Receiver<AgentStatus>

作用:订阅代理状态变化。订阅就像打开一个状态通知频道,以后状态变了可以收到更新。

数据流:进去的是线程对象;函数克隆 agent_status 的 watch 接收器;出来是 watch::Receiver,调用方可继续监听状态变化。

调用关系:它给内部需要实时观察状态的组件使用,不需要反复主动调用 agent_status。

CodexThread::token_usage_info429–431 ↗
async fn token_usage_info(&self) -> Option<TokenUsageInfo>

作用:读取这个线程当前缓存的完整 token 用量。token 可以粗略理解为模型读写文字时计费和限额用的小单位。

数据流:进去的是线程对象;函数读取 session.token_usage_info;出来是可选 TokenUsageInfo,不修改状态。

调用关系:send_thread_token_usage_update_to_connection 会调用它,把恢复或分叉后的完整用量发给连接方,避免只发总量而漏掉上一轮明细。

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

CodexThread::inject_user_message_without_turn434–446 ↗
async fn inject_user_message_without_turn(&self, message: String)

作用:记录一条用户身份的前缀消息,但不开始新的用户回合。适合补充上下文,而不是让系统以为用户又发起了一轮对话。

数据流:进去的是一段文本;函数把它包装成 role 为 user 的 ResponseItem::Message,再调用 session.inject_no_new_turn;出来没有返回值,线程记录里多了这条消息但没有新 turn 边界。

调用关系:它是内部辅助入口,用于需要补写用户消息但不能触发新轮次的场景。

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

CodexThread::inject_response_items449–469 ↗
async fn inject_response_items(&self, items: Vec<ResponseItem>) -> CodexResult<()>

作用:把原始 Responses API 项目写进线程记录,但不启动新轮次。它用于导入或补录模型可见内容。

数据流:进去的是 ResponseItem 列表;如果为空,直接返回 InvalidRequest;否则创建默认 turn 上下文,必要时记录并设置参考上下文项,再注入这些项目并刷新 rollout;出来是成功或错误,记录文件可能被写入。

调用关系:它比普通注入更完整,会确保上下文和持久记录同步;具体注入和刷盘由 Session 完成。

调用图:外部调用 1 个(InvalidRequest)。

CodexThread::rollout_path471–473 ↗
fn rollout_path(&self) -> Option<PathBuf>

作用:返回这个线程 rollout 记录文件的位置。调用方可以据此找到会话记录存在哪里。

数据流:进去的是线程对象;函数克隆保存的可选 PathBuf;出来是路径副本或 None,不改状态。

调用关系:build_thread_from_loaded_snapshot 会用它在从快照重建线程视图时拿到记录路径。

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

CodexThread::session_configured475–477 ↗
fn session_configured(&self) -> SessionConfiguredEvent

作用:返回线程创建时的会话配置事件副本。这个事件包含线程编号等基础配置信息。

数据流:进去的是线程对象;函数克隆 session_configured;出来是 SessionConfiguredEvent 副本,不影响原值。

调用关系:加载恢复来源和从快照构建线程时会调用它,用来取得线程的固定配置事实。

调用图:被 2 处调用(load_thread_from_resume_source_or_send_internal, build_thread_from_loaded_snapshot);外部调用 1 个(clone)。

CodexThread::is_running479–481 ↗
fn is_running(&self) -> bool

作用:判断这个线程底层的提交通道是否还开着。通道关了,通常表示线程不能再正常接收新操作。

数据流:进去的是线程对象;函数检查 codex.tx_sub 是否关闭;出来是 true 或 false,不改任何状态。

调用关系:它是一个轻量健康检查,供需要避免往已关闭线程投递工作的内部代码使用。

CodexThread::guardian_trunk_rollout_path483–489 ↗
async fn guardian_trunk_rollout_path(&self) -> Option<PathBuf>

作用:读取 guardian review 相关主干 rollout 路径。可以理解为审查流程对应的原始记录位置。

数据流:进去的是线程对象;函数询问 session.guardian_review_session.trunk_rollout_path;出来是可选路径。

调用关系:它把审查会话里的路径信息通过 CodexThread 暴露出来,避免外部直接进入 guardian_review_session。

CodexThread::load_history491–503 ↗
async fn load_history(
        &self,
        include_archived: bool,
    ) -> ThreadStoreResult<StoredThreadHistory>

作用:从线程存储里读取历史记录。include_archived 决定是否连已归档内容一起读。

数据流:进去的是 include_archived;函数先从 session 取得可持久化的 live_thread,失败就转成 ThreadStoreError::Internal,再调用 load_history;出来是 StoredThreadHistory 或存储错误。

调用关系:apply_thread_read_store_fields 会调用它补齐线程历史字段;它负责把当前运行线程连接到持久化存储。

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

CodexThread::read_thread505–520 ↗
async fn read_thread(
        &self,
        include_archived: bool,
        include_history: bool,
    ) -> ThreadStoreResult<StoredThread>

作用:读取线程的存储版整体信息,可以选择是否包含归档内容和历史。用于把运行中的线程变成可展示或可返回的数据。

数据流:进去的是 include_archived 和 include_history 两个开关;函数先拿 live_thread,失败转内部存储错误,再调用 read_thread;出来是 StoredThread 或错误。

调用关系:它是读取线程存储视图的入口,底层具体怎么读由 live_thread 对象完成。

CodexThread::update_thread_metadata522–535 ↗
async fn update_thread_metadata(
        &self,
        patch: ThreadMetadataPatch,
        include_archived: bool,
    ) -> ThreadStoreResult<StoredThread>

作用:更新线程的元数据,比如标题、标记或其他非正文信息。patch 表示只改其中一部分。

数据流:进去的是 ThreadMetadataPatch 和 include_archived;函数先取得 live_thread,失败转成存储内部错误,再调用 update_metadata;出来是更新后的 StoredThread 或错误。

调用关系:它把外部的元数据修改请求接到线程存储层,保证修改的是当前这条 live thread。

CodexThread::state_db537–539 ↗
fn state_db(&self) -> Option<StateDbHandle>

作用:返回这个线程关联的状态数据库句柄,如果有的话。状态数据库用于保存恢复目标、线程关系等运行状态。

数据流:进去的是线程对象;函数调用 Codex.state_db;出来是可选 StateDbHandle,不修改状态。

调用关系:pending_resume_goal_state 和 persist_thread_spawn_edge_for_source 会用它读取或记录恢复、派生线程相关状态。

调用图:调用 1 个内部函数(state_db);被 2 处调用(pending_resume_goal_state, persist_thread_spawn_edge_for_source)。

CodexThread::config_snapshot541–543 ↗
async fn config_snapshot(&self) -> ThreadConfigSnapshot

作用:获取当前线程配置的快照。快照就是把当前设置拍一张照片,之后可以用来展示、比较或生成覆盖项。

数据流:进去的是线程对象;函数调用 Codex.thread_config_snapshot;出来是 ThreadConfigSnapshot。

调用关系:加载实时线程视图、恢复线程、构造环境覆盖和构造设置覆盖时都会调用它,是很多配置相关流程的基础。

调用图:调用 1 个内部函数(thread_config_snapshot);被 4 处调用(load_live_thread_view, load_thread_from_resume_source_or_send_internal, build_environment_override, build_thread_settings_overrides)。

CodexThread::instruction_sources546–548 ↗
async fn instruction_sources(&self) -> Vec<AbsolutePathBuf>

作用:返回这条线程加载模型指令时用到的文件路径。这样可以解释模型行为是受哪些说明文件影响的。

数据流:进去的是线程对象;函数调用 Codex.instruction_sources;出来是路径列表。

调用关系:它给需要展示或审计指令来源的调用方使用,底层来源信息由 Codex 保存。

调用图:调用 1 个内部函数(instruction_sources)。

CodexThread::config550–552 ↗
async fn config(&self) -> Arc<crate::config::Config>

作用:读取当前线程使用的完整配置对象。返回的是共享引用计数指针 Arc,可以多人安全持有同一份配置。

数据流:进去的是线程对象;函数等待 session.get_config;出来是 Arc<Config>,不直接修改配置。

调用关系:build_refresh_config 会调用它,以当前配置为基础构造新的运行时配置。

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

CodexThread::runtime_mcp_config555–557 ↗
async fn runtime_mcp_config(&self, config: &crate::config::Config) -> codex_mcp::McpConfig

作用:根据当前线程的扩展数据,解析出运行时 MCP 配置。MCP 是让模型访问外部工具和资源的一套接口。

数据流:进去的是一个 Config 引用;函数交给 session.runtime_mcp_config;出来是 McpConfig,其中可能包含线程级扩展后的工具配置。

调用关系:build_refresh_config 会调用它,确保刷新配置时 MCP 运行设置也按当前线程数据重新计算。

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

CodexThread::multi_agent_version559–561 ↗
fn multi_agent_version(&self) -> Option<MultiAgentVersion>

作用:返回当前线程使用的多代理协议版本,如果有的话。多代理指多个代理协同工作的模式。

数据流:进去的是线程对象;函数读取 session.multi_agent_version;出来是可选 MultiAgentVersion。

调用关系:ensure_direct_input_allowed 和 is_resident_candidate 会用它判断当前线程是否允许某些输入方式或是否适合作为常驻线程。

调用图:被 2 处调用(ensure_direct_input_allowed, is_resident_candidate)。

CodexThread::refresh_runtime_config566–568 ↗
async fn refresh_runtime_config(&self, next_config: crate::config::Config)

作用:用调用方给的新配置刷新线程的运行时用户配置层。线程自己的层级设置和会话启动时固定的设置不会被重置。

数据流:进去的是新的 Config;函数交给 session.refresh_runtime_config;出来没有返回值,底层运行时配置状态会更新。

调用关系:它是配置热刷新入口,适合外部配置变化后让已存在的线程跟上新配置。

CodexThread::environment_selections570–572 ↗
async fn environment_selections(&self) -> Vec<TurnEnvironmentSelection>

作用:读取线程当前的环境选择列表。调用方可以知道后续执行可能选哪些环境。

数据流:进去的是线程对象;函数调用 Codex.thread_environment_selections;出来是 TurnEnvironmentSelection 列表。

调用关系:它是线程级环境读取入口,和 ThreadConfigSnapshot::environment_selections 类似,但直接从活线程读取最新值。

调用图:调用 1 个内部函数(thread_environment_selections)。

CodexThread::read_mcp_resource574–586 ↗
async fn read_mcp_resource(
        &self,
        server: &str,
        uri: &str,
    ) -> anyhow::Result<serde_json::Value>

作用:从指定 MCP 服务器读取一个资源,并把结果转成通用 JSON。JSON 是常见的数据格式,方便前端或接口直接传输。

数据流:进去的是服务器名和资源 uri;函数创建 ReadResourceRequestParams,调用 session.read_resource,再用 serde_json 转成 Value;出来是 JSON 值或错误。

调用关系:它是线程对外读取 MCP 资源的桥,具体资源访问由 Session 和对应 MCP 服务器完成。

调用图:外部调用 2 个(new, to_value)。

CodexThread::call_mcp_tool588–599 ↗
async fn call_mcp_tool(
        &self,
        server: &str,
        tool: &str,
        arguments: Option<serde_json::Value>,
        meta: Option<serde_json::Value>,
    ) -> anyhow::Result<CallTool

作用:调用指定 MCP 服务器上的某个工具。工具可以是搜索、读文件、查数据库等外部能力。

数据流:进去的是服务器名、工具名、可选参数和可选元信息;函数交给 session.call_tool;出来是 CallToolResult 或错误,外部工具可能被实际执行。

调用关系:它是线程层面的 MCP 工具调用入口,负责把调用请求转给当前会话的 MCP 运行环境。

CodexThread::enabled601–603 ↗
fn enabled(&self, feature: Feature) -> bool

作用:检查某个功能开关是否启用。功能开关用于控制新能力是否对当前线程开放。

数据流:进去的是 Feature;函数调用 Codex.enabled;出来是 true 或 false,不修改状态。

调用关系:它让上层代码在走某条新流程前先问线程是否支持该功能,判断逻辑由 Codex 提供。

调用图:调用 1 个内部函数(enabled)。

CodexThread::increment_out_of_band_elicitation_count605–619 ↗
async fn increment_out_of_band_elicitation_count(&self) -> CodexResult<u64>

作用:把“带外询问”的计数加一,并在从 0 变成 1 时暂停相关会话状态。带外询问可以理解为不在普通对话轮次里的额外提问。

数据流:进去的是线程对象;函数先锁住计数器,防止两个异步任务同时改同一个数字;如果加一溢出就返回 Fatal 错误;成功后返回新计数,若原来是 0 还会把会话的带外询问暂停状态设为 true。

调用关系:它和 decrement_out_of_band_elicitation_count 配套使用,保证只要还有带外询问没结束,线程就保持暂停状态。

CodexThread::decrement_out_of_band_elicitation_count621–638 ↗
async fn decrement_out_of_band_elicitation_count(&self) -> CodexResult<u64>

作用:把“带外询问”的计数减一,并在减到 0 时解除暂停。它防止询问结束后线程一直停着不动。

数据流:进去的是线程对象;函数锁住计数器,如果已经是 0 就返回 InvalidRequest;否则减一并返回新计数,若现在变成 0,就把会话的带外询问暂停状态设为 false。

调用关系:它必须和 increment_out_of_band_elicitation_count 成对出现,是带外询问生命周期的收尾动作。

调用图:外部调用 1 个(InvalidRequest)。

core/src/session/mod.rs源码 ↗
orchestrationstartup, main loop, request handling, teardown, cross-cutting

从这段代码看,core/src/session/mod.rs 像一个客服中心的总机。外部先调用 Codex::spawn 开一个新线程,它会整理配置、模型、权限、历史记录和工具,然后启动后台循环。之后外部用 submit 或 steer_input 把用户输入、关机命令等投递进去,再用 next_event 一条条收回模型回复、警告、错误和状态变化。Session 这层则保存更细的“现场资料”:当前配置、历史、token 用量、线程持久化文件、网络访问规则、子代理完成通知等。它还会把事件写入 rollout(可理解为对话录像,方便恢复和分叉),必要时同步给实时语音/文本通道,或者通知父代理。整体作用是把很多容易乱套的事情——启动、通信、恢复、配置热更新、权限变更、事件分发——集中收口,保证一次对话线程能从开始到结束被可靠追踪和控制。 从这段代码看,Session 像一个对话现场的总调度台。它会把危险操作先挂起,发事件去问用户或审核系统,等回复回来再继续;也会把每轮对话写进历史和持久化记录,方便恢复、回放和控制上下文长度。它还会生成模型每轮开始前需要看的“背景资料”,比如权限规则、环境信息、插件能力和用户说明。另一个重点是安全和资源控制:它会记录临时或整场会话授予的权限,统计 token(可以粗略理解为模型读写文字的计量单位)用量,必要时开启新上下文窗口,避免对话太长。整体上,这个文件不是做某一个小算法,而是在运行过程中把很多零件协调起来,让一次会话安全、可追踪、可中断、可恢复。

函数细节149
SteerInputError::to_error_event251–278 ↗
fn to_error_event(&self) -> ErrorEvent

作用:把“追加引导输入失败”的内部错误,翻译成外部客户端能看懂的错误事件。这样调用方不会只看到一个程序内部枚举,而是能得到明确的人话提示。

数据流:进去的是一个 SteerInputError,例如没有活跃回合、回合编号不匹配、当前回合不能被引导、输入为空;函数按类型生成 ErrorEvent,里面放错误消息和错误分类;出来的是可发送给客户端的错误事件,不改动其他状态。

调用关系:它位于 steer input 的失败出口附近。当会话拒绝新的引导输入时,外层可以用它把失败原因包装成协议事件;内部只用格式化文本,不再把活儿交给复杂流程。

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

resolve_multi_agent_version441–457 ↗
fn resolve_multi_agent_version(
    conversation_history: &InitialHistory,
    inherited_multi_agent_version: Option<MultiAgentVersion>,
) -> Option<MultiAgentVersion>

作用:决定这个线程要使用哪个多代理版本。多代理就是一个主代理可以派生子代理协作;版本决定它们能用的沟通方式。

数据流:进去的是初始对话历史和从父线程继承来的版本;函数先尊重“禁用”设置,再看历史里是否记录过版本,再看继承值,最后对老的恢复/分叉线程默认给 V1;出来的是一个可选版本。

调用关系:它在 Codex::spawn_internal 创建会话时被调用,也用于计算派生线程的初始版本。它替启动流程解决兼容老线程的问题。

调用图:调用 1 个内部函数(get_multi_agent_version);被 2 处调用(spawn_internal, initial_multi_agent_version_for_spawn)。

Codex::spawn466–488 ↗
async fn spawn(args: CodexSpawnArgs) -> CodexResult<CodexSpawnOk>

作用:这是外部启动一个 Codex 线程的入口。它先处理追踪信息,再交给真正的内部启动流程。

数据流:进去的是 CodexSpawnArgs,里面有配置、历史、各种管理器和可选父追踪;函数检查父追踪是否合法,创建一个 tracing span(一次操作的追踪标签),把父子追踪接起来;出来的是 CodexSpawnOk,里面有可用的 Codex 对象和线程 id。

调用关系:交互运行、线程派生和测试都会调用它。它不亲自组装会话细节,而是把整理后的参数交给 Codex::spawn_internal,并把整个启动过程包进可观测的追踪里。

调用图:被 3 处调用(run_codex_thread_interactive, guardian_subagent_does_not_inherit_parent_exec_policy_rules, spawn_thread_with_source);外部调用 5 个(spawn_internal, context_from_w3c_trace_context, set_parent_from_w3c_trace_context, info_span!, warn!)。

Codex::spawn_internal490–691 ↗
async fn spawn_internal(args: CodexSpawnArgs) -> CodexResult<CodexSpawnOk>

作用:真正把一个 Codex 会话搭起来。它像开店前准备:摆好收银台、员工、规则、库存、监控和后门通道,然后启动后台工作循环。

数据流:进去的是完整启动参数;函数建立提交通道和事件通道,加载或继承执行策略,选择模型和基础提示词,决定多代理版本,组装 SessionConfiguration,创建 Session,再启动 submission_loop 后台任务;出来的是包含 Codex 句柄和 thread_id 的启动结果。

调用关系:Codex::spawn 调它。它会调用 resolve_multi_agent_version、get_service_tier、session_permission_profile_state_from_config、Session::new 和 submission_loop,是整个文件里启动阶段的核心编排点。

调用图:调用 9 个内部函数(default, load, get_service_tier, submission_loop, resolve_multi_agent_version, new, session_loop_termination_from_handle, session_permission_profile_state_from_config, new);外部调用 11 个(clone, new, pin, bounded, unbounded, is_guardian_reviewer_source, info_span!, matches!, from_config, spawn (+1 more))。

Codex::submit694–696 ↗
async fn submit(&self, op: Op) -> CodexResult<String>

作用:给会话提交一个操作,比如用户输入、批准命令或关闭。它是最常用的简化入口。

数据流:进去的是 Op 操作;函数不给它附加外部追踪,直接转给 submit_with_trace;出来的是这次提交的唯一 id。

调用关系:很多处理函数和关闭流程都会用它。它把普通提交统一导向 Codex::submit_with_trace。

调用图:调用 1 个内部函数(submit_with_trace);被 8 处调用(handle_exec_approval, handle_patch_approval, handle_request_permissions, handle_request_user_input, shutdown_delegate, submit, interrupt_and_drain_turn, shutdown_and_wait)。

Codex::submit_with_trace698–712 ↗
async fn submit_with_trace(
        &self,
        op: Op,
        trace: Option<W3cTraceContext>,
    ) -> CodexResult<String>

作用:提交一个操作,同时可以带上调用链追踪信息。追踪信息方便之后排查“这件事从哪里发起”。

数据流:进去的是 Op 和可选 W3C trace context(跨系统追踪用的一串标识);函数生成 UUID 作为提交 id,打包成 Submission;出来的是 id,并把提交送入内部队列。

调用关系:Codex::submit 会调用它;它再把实际发送交给 Codex::submit_with_id。

调用图:调用 1 个内部函数(submit_with_id);被 2 处调用(submit_with_trace, submit);外部调用 1 个(now_v7)。

Codex::submit_user_input_with_client_user_message_id714–730 ↗
async fn submit_user_input_with_client_user_message_id(
        &self,
        op: Op,
        trace: Option<W3cTraceContext>,
        client_user_message_id: Option<String>,
    ) -> CodexResult<Stri

作用:专门提交用户输入,并保留客户端自己的消息 id。这样外部界面可以把自己发出的消息和 Codex 内部处理结果对上号。

数据流:进去的是用户输入类 Op、追踪信息和客户端消息 id;函数确认操作确实是用户输入,生成内部提交 id,组成 Submission;出来的是内部 id,并把提交放进队列。

调用关系:它用于需要消息对账的用户输入路径。真正发送仍交给 Codex::submit_with_id。

调用图:调用 1 个内部函数(submit_with_id);被 1 处调用(submit_user_input_with_client_user_message_id);外部调用 2 个(now_v7, debug_assert!)。

Codex::submit_with_id734–743 ↗
async fn submit_with_id(&self, mut sub: Submission) -> CodexResult<()>

作用:把已经打包好的提交真正塞进会话的输入队列。它是提交链路的最后一站。

数据流:进去的是 Submission;如果里面没有追踪信息,就从当前执行环境补一份;然后发送到 tx_sub 通道;成功没有返回内容,失败说明内部代理已经死了。

调用关系:submit_with_trace 和 submit_user_input_with_client_user_message_id 都依赖它。后台 submission_loop 会从这个通道取活儿干。

调用图:被 3 处调用(submit_with_id, submit_user_input_with_client_user_message_id, submit_with_trace);外部调用 2 个(send, current_span_w3c_trace_context)。

Codex::set_thread_memory_mode749–754 ↗
async fn set_thread_memory_mode(
        &self,
        mode: codex_protocol::protocol::ThreadMemoryMode,
    ) -> anyhow::Result<()>

作用:修改当前线程的记忆模式,并把这个选择保存下来。记忆模式决定线程以后如何保留或使用记忆。

数据流:进去的是 ThreadMemoryMode;函数把模式和当前 session 交给持久化处理函数;出来是成功或失败结果,成功时线程记录被更新。

调用关系:外部设置线程记忆时调用它。它不自己写存储,而是交给 handlers::persist_thread_memory_mode_update。

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

Codex::shutdown_and_wait756–765 ↗
async fn shutdown_and_wait(&self) -> CodexResult<()>

作用:请求会话关闭,并等后台循环真的停下来。它避免外部以为关了、内部其实还在跑。

数据流:进去没有额外数据;函数提交 Op::Shutdown,如果内部已经死了也当作可接受,然后等待 session_loop_termination;出来是关闭成功或其他错误。

调用关系:shutdown、线程派生收尾等流程会用它。它通过 Codex::submit 发关机命令,再等待 spawn_internal 创建的后台任务结束。

调用图:调用 1 个内部函数(submit);被 3 处调用(shutdown_and_wait, shutdown, finalize_thread_spawn);外部调用 1 个(clone)。

Codex::next_event767–774 ↗
async fn next_event(&self) -> CodexResult<Event>

作用:从会话里取下一条事件。外部界面靠它知道模型说了什么、工具执行到哪了、有没有错误。

数据流:进去没有参数;函数从 rx_event 事件通道等待一条 Event;出来是事件,若通道关闭则报内部代理死亡。

调用关系:事件消费循环、关闭代理和派生线程收尾会调用它。事件由 Session::send_event_raw 或 deliver_event_raw 写入通道。

调用图:调用 1 个内部函数(recv);被 4 处调用(shutdown_delegate, next_event, interrupt_and_drain_turn, finalize_thread_spawn)。

Codex::steer_input776–793 ↗
async fn steer_input(
        &self,
        input: Vec<UserInput>,
        additional_context: BTreeMap<String, AdditionalContextEntry>,
        expected_turn_id: Option<&str>,
        client_user_me

作用:在一个正在进行的回合里追加“引导输入”。这像用户在助手还没结束时补充一句“等等,按这个方向来”。

数据流:进去的是用户输入、额外上下文、期望回合 id、客户端消息 id 和元数据;函数原样转给 Session::steer_input;出来是提交 id,或 SteerInputError。

调用关系:外部 steer_input 接口会用它。它只是 Codex 外壳到 Session 内核的转接层。

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

Codex::set_app_server_client_info795–811 ↗
async fn set_app_server_client_info(
        &self,
        app_server_client_name: Option<String>,
        app_server_client_version: Option<String>,
        mcp_elicitations_auto_deny: bool,
    ) -

作用:记录连接到本线程的应用服务器客户端信息,并设置 MCP 询问是否自动拒绝。MCP 可理解为外部工具/服务接入协议。

数据流:进去的是客户端名称、版本和是否自动拒绝 MCP elicitation(外部服务向用户追问);函数更新会话设置,再调整 MCP 连接管理器;出来是约束检查结果。

调用关系:上层应用服务器接入线程后会调用它。它内部使用 Session::update_settings,并直接通知 MCP 管理器。

调用图:被 1 处调用(set_app_server_client_info);外部调用 1 个(default)。

Codex::agent_status813–815 ↗
async fn agent_status(&self) -> AgentStatus

作用:读取当前代理的最新状态,比如初始化中、运行中、完成或出错。

数据流:进去无参数;函数从 watch 通道里借出当前值并克隆;出来是 AgentStatus,不改变状态。

调用关系:外部状态查询接口会调用它。状态由事件投递流程和子代理完成流程更新。

调用图:被 1 处调用(agent_status);外部调用 1 个(borrow)。

Codex::thread_config_snapshot817–820 ↗
async fn thread_config_snapshot(&self) -> ThreadConfigSnapshot

作用:拿到当前线程配置的一份快照。快照适合给 UI 展示或给外部检查,而不是直接修改内部配置。

数据流:进去无参数;函数锁住 session 状态,读取 session_configuration 并生成 ThreadConfigSnapshot;出来是快照。

调用关系:config_snapshot 接口会调用它。它只读 Session 状态,不触发配置变更。

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

Codex::instruction_sources822–831 ↗
async fn instruction_sources(&self) -> Vec<AbsolutePathBuf>

作用:列出本线程加载过哪些指令文件来源。这样用户能知道助手遵循的额外说明来自哪里。

数据流:进去无参数;函数读取 loaded_agents_md,如果有就取出 sources;出来是路径列表,没有则为空列表。

调用关系:instruction_sources 查询接口会用它。它从 Session 配置状态中取资料。

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

Codex::thread_environment_selections833–839 ↗
async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection>

作用:查询线程当前选了哪些运行环境。运行环境可以理解为代码在哪个工作区或沙箱里执行。

数据流:进去无参数;函数读取 session_configuration 的环境选择并复制成列表;出来是 TurnEnvironmentSelection 列表。

调用关系:environment_selections 接口会调用它。它只读当前 Session 配置。

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

Codex::state_db841–843 ↗
fn state_db(&self) -> Option<state_db::StateDbHandle>

作用:取得会话状态数据库的句柄,如果这个线程启用了状态数据库的话。句柄就是后续读写数据库用的入口。

数据流:进去无参数;函数转问 Session::state_db;出来是可选 StateDbHandle。

调用关系:state_db 查询接口会调用它。它把 Codex 外壳请求转给 Session。

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

Codex::enabled845–847 ↗
fn enabled(&self, feature: Feature) -> bool

作用:判断某个功能开关是否在当前线程启用。功能开关用于灰度或实验功能。

数据流:进去的是 Feature;函数询问 session;出来是 true 或 false。

调用关系:enabled 查询接口会调用它。具体判断逻辑在 Session 的 feature 状态里。

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

get_service_tier850–862 ↗
fn get_service_tier(
    configured_service_tier: Option<String>,
    fast_mode_enabled: bool,
    model_info: &ModelInfo,
) -> Option<String>

作用:决定是否把服务档位传给模型请求。服务档位可以理解为请求模型时选择的服务等级。

数据流:进去的是配置里的档位、快速模式是否开启、模型信息;如果没开快速模式就返回空,否则只保留默认档位或模型支持的档位;出来是可选字符串。

调用关系:Codex::spawn_internal 在组装 SessionConfiguration 时调用它,避免把模型不支持的档位发出去。

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

session_permission_profile_state_from_config864–868 ↗
fn session_permission_profile_state_from_config(
    config: &Config,
) -> CodexResult<PermissionProfileState>

作用:从配置里取出线程启动时的权限档案状态。权限档案决定哪些操作需要批准、沙箱如何限制等。

数据流:进去的是 Config;函数读取 config.permissions.permission_profile_state 并克隆;出来是 PermissionProfileState。

调用关系:Codex::spawn_internal 用它把配置中的权限信息放进 SessionConfiguration。

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

completed_session_loop_termination871–873 ↗
fn completed_session_loop_termination() -> SessionLoopTermination

作用:制造一个“已经结束”的会话循环结束信号。主要用于测试或不需要真实后台任务的场景。

数据流:进去无参数;函数创建一个立即完成的 future,并包装成可共享对象;出来是 SessionLoopTermination。

调用关系:测试 review session 会用它。它不等待真实任务,只提供符合接口的假结束句柄。

调用图:被 1 处调用(test_review_session);外部调用 1 个(ready)。

session_loop_termination_from_handle875–883 ↗
fn session_loop_termination_from_handle(
    handle: JoinHandle<()>,
) -> SessionLoopTermination

作用:把后台任务句柄包装成一个可等待的结束信号。外部只关心“什么时候停”,不用直接碰 tokio 任务句柄。

数据流:进去的是 JoinHandle;函数创建异步块等待这个任务结束,并转成共享 future;出来是 SessionLoopTermination。

调用关系:Codex::spawn_internal 启动 submission_loop 后调用它;Codex::shutdown_and_wait 后面会等待这个信号。

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

thread_title_from_thread_store885–912 ↗
async fn thread_title_from_thread_store(
    live_thread: Option<&LiveThread>,
    thread_store: &Arc<dyn ThreadStore>,
    conversation_id: ThreadId,
) -> Option<String>

作用:从线程存储里读取一个适合展示的线程标题。它会避免把空标题或和预览文字重复的标题拿出来。

数据流:进去的是可选 live_thread、线程存储和 conversation_id;函数优先从 live_thread 读,否则从 ThreadStore 读;取 name,去掉空白,并确认它不等于 preview;出来是可选标题。

调用关系:它是线程展示信息的辅助函数。本段没有显示调用方,但它连接了实时线程对象和持久线程仓库。

Session::app_server_client_metadata915–924 ↗
async fn app_server_client_metadata(&self) -> AppServerClientMetadata

作用:读取当前应用服务器客户端的名称和版本。用于事件、请求或诊断时标明是谁在连接这个会话。

数据流:进去无参数;函数锁住状态,复制 app_server_client_name 和 app_server_client_version;出来是 AppServerClientMetadata。

调用关系:它给需要客户端元数据的内部流程使用,数据来源通常由 Codex::set_app_server_client_info 写入。

Session::managed_network_proxy_active_for_permission_profile926–930 ↗
fn managed_network_proxy_active_for_permission_profile(
        permission_profile: &PermissionProfile,
    ) -> bool

作用:判断某个权限档案下是否应该启用托管网络代理。网络代理像一道门卫,能拦截或允许联网请求。

数据流:进去的是 PermissionProfile;函数只要看到不是 Disabled 就返回 true;出来是布尔值。

调用关系:它是网络代理相关判断的基础小工具,供会话启动或权限变化时使用。

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

Session::build_model_client_beta_features_header937–958 ↗
fn build_model_client_beta_features_header(config: &Config) -> Option<String>

作用:把启用的实验功能整理成发给模型客户端的请求头。请求头可以理解为随请求附带的一行说明。

数据流:进去的是 Config;函数遍历功能表,挑出需要告诉模型客户端且已启用的功能 key,用逗号拼起来;如果没有则返回 None,否则返回字符串。

调用关系:模型客户端创建请求时会用这种信息。它把本地功能开关转换成模型服务能识别的标记。

Session::start_managed_network_proxy960–996 ↗
async fn start_managed_network_proxy(
        spec: &crate::config::NetworkProxySpec,
        exec_policy: &codex_execpolicy::Policy,
        permission_profile: &PermissionProfile,
        network_po

作用:启动托管网络代理,并把运行时地址记录下来。它让会话里的网络请求能经过统一的允许/拒绝规则。

数据流:进去的是代理配置、执行策略、权限档案、可选决策器和观察器、需求开关及审计信息;函数先把执行策略中的网络规则合并进代理配置,再启动代理,读取 HTTP 和 SOCKS 地址;出来是已启动代理和会话可用的代理运行信息。

调用关系:权限变化刷新流程会调用它。它把 NetworkProxySpec 的规则加工后交给 start_proxy。

调用图:调用 2 个内部函数(start_proxy, with_exec_policy_network_rules)。

Session::refresh_managed_network_proxy_for_current_permission_profile998–1068 ↗
async fn refresh_managed_network_proxy_for_current_permission_profile(&self)

作用:当权限档案变化时,刷新或启动网络代理。这样新权限会立刻影响后续联网,而不是等下次重启。

数据流:进去无参数;函数拿刷新锁,读取当前配置和权限,重算代理规则,合并执行策略网络规则;如果代理已存在就在线更新,否则启动新代理并保存;出来无显式结果,失败只记警告。

调用关系:Session::update_settings 发现权限档案变化后调用它。它会调用 Session::start_managed_network_proxy,并更新 services.network_proxy。

调用图:被 1 处调用(update_settings);外部调用 4 个(new, start_managed_network_proxy, error!, warn!)。

Session::codex_home1071–1074 ↗
async fn codex_home(&self) -> AbsolutePathBuf

作用:读取当前会话的 Codex 主目录。这个目录通常放配置、策略和持久化资料。

数据流:进去无参数;函数锁住状态,克隆 codex_home 路径;出来是 AbsolutePathBuf。

调用关系:需要找本地配置或写入策略文件的流程会用它。

Session::subscribe_out_of_band_elicitation_pause_state1076–1078 ↗
fn subscribe_out_of_band_elicitation_pause_state(&self) -> watch::Receiver<bool>

作用:订阅“带外询问是否暂停”的状态。带外询问指不是普通对话流里的额外追问。

数据流:进去无参数;函数从 watch 通道创建一个接收者;出来是能持续收到 bool 状态变化的 Receiver。

调用关系:需要监听暂停/恢复的组件会调用它。状态由 set_out_of_band_elicitation_pause_state 更新。

Session::set_out_of_band_elicitation_pause_state1080–1082 ↗
fn set_out_of_band_elicitation_pause_state(&self, paused: bool)

作用:设置带外询问是否暂停,并通知所有订阅者。

数据流:进去的是 paused 布尔值;函数替换 watch 通道里的当前值;出来无返回,订阅者会收到新状态。

调用关系:控制带外询问流程的地方会调用它;订阅端通过 subscribe_out_of_band_elicitation_pause_state 得到变化。

Session::get_tx_event1084–1086 ↗
fn get_tx_event(&self) -> Sender<Event>

作用:拿到事件发送通道的副本。这样内部其他组件也能向外部事件流发消息。

数据流:进去无参数;函数克隆 tx_event;出来是 Sender<Event>。

调用关系:需要直接发送事件的内部模块会用它。正常事件路径更多通过 send_event 或 send_event_raw。

Session::state_db1088–1090 ↗
fn state_db(&self) -> Option<state_db::StateDbHandle>

作用:返回当前会话的状态数据库句柄,如果有启用。数据库句柄是后续读写状态的入口。

数据流:进去无参数;函数克隆 services.state_db;出来是可选 StateDbHandle。

调用关系:Codex::state_db 会转调它,外部查询可以从 Codex 层拿到这个句柄。

Session::live_thread_for_persistence1092–1098 ↗
fn live_thread_for_persistence(
        &self,
        operation: &str,
    ) -> anyhow::Result<&LiveThread>

作用:取得用于持久化的 live thread;如果持久化没启用,就给出明确错误。live thread 可理解为正在写入的对话录像对象。

数据流:进去的是操作名称;函数调用 live_thread,如果没有就生成“无法执行该操作”的错误;出来是 LiveThread 引用或错误。

调用关系:需要强制写入线程持久化的流程会用它。它依赖 Session::live_thread。

调用图:调用 1 个内部函数(live_thread)。

Session::live_thread1100–1102 ↗
fn live_thread(&self) -> Option<&LiveThread>

作用:查看当前会话有没有连接到实时持久化线程对象。

数据流:进去无参数;函数返回 services.live_thread 的引用;出来是可选 LiveThread。

调用关系:flush_rollout、persist_rollout_items、try_ensure_rollout_materialized 等持久化流程都会用它。

调用图:被 5 处调用(current_rollout_path, flush_rollout, live_thread_for_persistence, persist_rollout_items, try_ensure_rollout_materialized)。

Session::flush_rollout1106–1112 ↗
async fn flush_rollout(&self) -> std::io::Result<()>

作用:把已写入的对话录像刷到存储里。它像保存文档,避免缓冲区里的内容还没落盘。

数据流:进去无参数;如果有 live_thread 就调用 flush 并把错误转成 io::Error,没有则什么都不做;出来是 IO 结果。

调用关系:record_initial_history 在恢复或分叉后会调用它,确保初始化历史尽快稳定保存。

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

Session::try_ensure_rollout_materialized1114–1119 ↗
async fn try_ensure_rollout_materialized(&self) -> std::io::Result<()>

作用:尽力确保对话录像真正形成文件或持久记录。materialized 可以理解为“从内存里的临时东西变成可保存的实体”。

数据流:进去无参数;如果有 live_thread 就调用 persist;出来是 IO 结果,没有 live_thread 时成功返回。

调用关系:ensure_rollout_materialized 包装它,后者负责吞掉错误并记录警告。

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

Session::ensure_rollout_materialized1121–1125 ↗
async fn ensure_rollout_materialized(&self)

作用:保证对话录像被持久化,但不让失败打断主流程。它适合在记录历史时做“尽量保存”。

数据流:进去无参数;函数调用 try_ensure_rollout_materialized,失败就写警告;出来无返回。

调用关系:hook_transcript_path、record_initial_history 和记录用户提示时会调用它。

调用图:调用 1 个内部函数(try_ensure_rollout_materialized);被 3 处调用(hook_transcript_path, record_initial_history, record_user_prompt_and_emit_turn_item);外部调用 1 个(warn!)。

Session::next_internal_sub_id1127–1132 ↗
fn next_internal_sub_id(&self) -> String

作用:生成一个内部自动提交 id,当前用于自动压缩这类系统发起的输入。

数据流:进去无参数;函数用原子计数器加一,拼成 auto-compact-数字;出来是字符串 id。

调用关系:route_realtime_text_input 调用它,为实时文本输入造一个内部回合编号。

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

Session::route_realtime_text_input1134–1151 ↗
async fn route_realtime_text_input(self: &Arc<Self>, text: String)

作用:把实时通道来的文本当作普通用户输入送进会话。实时通道可能来自语音或流式界面。

数据流:进去的是文本;函数生成内部 id,把文本包成 Op::UserInput,并调用 user_input_or_turn_inner;出来无显式结果,输入会进入正常处理流程。

调用关系:实时输入层会调用它。它把实时世界接到普通用户输入处理器。

调用图:调用 2 个内部函数(next_internal_sub_id, user_input_or_turn_inner);外部调用 2 个(default, vec!)。

Session::get_total_token_usage1153–1156 ↗
async fn get_total_token_usage(&self) -> i64

作用:读取当前累计 token 用量。token 可以粗略理解为模型处理文本时计费和计量的小片段。

数据流:进去无参数;函数锁状态,根据是否包含服务器推理用量计算总数;出来是 i64 数字。

调用关系:用量展示或压缩判断会用它。它从 SessionState 里读历史统计。

Session::auto_compact_window_snapshot1158–1161 ↗
async fn auto_compact_window_snapshot(&self) -> AutoCompactWindowSnapshot

作用:获取自动压缩窗口的当前快照。自动压缩用于在对话太长时缩短上下文。

数据流:进去无参数;函数锁状态并调用 auto_compact_window_snapshot;出来是 AutoCompactWindowSnapshot。

调用关系:需要展示或判断压缩状态的流程会调用它。

Session::estimated_tokens_after_last_model_generated_item1163–1168 ↗
async fn estimated_tokens_after_last_model_generated_item(&self) -> i64

作用:估算最近一次模型生成内容之后又新增了多少 token。用于判断上下文增长情况。

数据流:进去无参数;函数读取历史对象的估算值;出来是 i64。

调用关系:压缩和用量监控相关流程会用它。

Session::total_token_usage1170–1173 ↗
async fn total_token_usage(&self) -> Option<TokenUsage>

作用:返回完整的 token 使用统计,如果当前还没有统计则返回空。

数据流:进去无参数;函数从状态里取 token_info,并提取 total_token_usage;出来是可选 TokenUsage。

调用关系:UI 或报告模块可用它显示总用量。

Session::token_usage_info1181–1184 ↗
async fn token_usage_info(&self) -> Option<TokenUsageInfo>

作用:读取更完整的 token 用量信息,不只总数。它保留模型请求统计所需的结构化数据。

数据流:进去无参数;函数锁状态并返回 token_info;出来是可选 TokenUsageInfo。

调用关系:统计展示、恢复会话后的用量同步等流程会用它。

Session::get_estimated_token_count1186–1192 ↗
async fn get_estimated_token_count(
        &self,
        turn_context: &TurnContext,
    ) -> Option<i64>

作用:按某个回合上下文估算当前历史会消耗多少 token。

数据流:进去的是 TurnContext;函数锁状态,让 history 根据该上下文估算 token;出来是可选 i64。

调用关系:模型调用前、压缩判断前可能会调用它。

Session::get_base_instructions1194–1199 ↗
async fn get_base_instructions(&self) -> BaseInstructions

作用:读取当前会话的基础指令。基础指令就是每次给模型的底层说明,决定助手的大方向。

数据流:进去无参数;函数锁状态并复制 base_instructions 文本;出来是 BaseInstructions。

调用关系:apply_rollout_reconstruction 和 recompute_token_usage 会调用它,用来重建或估算上下文。

调用图:被 2 处调用(apply_rollout_reconstruction, recompute_token_usage)。

Session::merge_connector_selection1202–1208 ↗
async fn merge_connector_selection(
        &self,
        connector_ids: HashSet<String>,
    ) -> HashSet<String>

作用:把新的连接器选择合并进当前选择。连接器可以理解为可用外部数据源或服务。

数据流:进去是一组 connector id;函数锁住状态并合并;出来是合并后的 HashSet。

调用关系:连接器选择更新流程会用它,实际状态改动由 SessionState 完成。

Session::get_connector_selection1211–1214 ↗
async fn get_connector_selection(&self) -> HashSet<String>

作用:读取当前选中的连接器集合。

数据流:进去无参数;函数锁状态并取出当前集合;出来是 HashSet<String>。

调用关系:需要展示或使用连接器选择的流程会调用它。

Session::clear_connector_selection1217–1220 ↗
async fn clear_connector_selection(&self)

作用:清空当前连接器选择。

数据流:进去无参数;函数锁住状态并调用 clear_connector_selection;出来无返回,内部集合被清空。

调用关系:重置连接器选择或开始新上下文时会用它。

Session::record_initial_history1222–1308 ↗
async fn record_initial_history(&self, conversation_history: InitialHistory)

作用:在线程启动时把已有历史接回当前会话。它负责处理新线程、清空线程、恢复线程和分叉线程的不同情况。

数据流:进去的是 InitialHistory;函数判断是否已有用户回合,设置“下一回合是否第一回合”,对恢复/分叉历史重建上下文、填充 token 信息、保存或刷写 rollout,并在模型变更时发警告;出来无返回,但会修改状态和持久化内容。

调用关系:Session 初始化流程会调用它。它会调用 apply_rollout_reconstruction、flush_rollout、persist_rollout_items、ensure_rollout_materialized 和 send_event。

调用图:调用 7 个内部函数(apply_rollout_reconstruction, ensure_rollout_materialized, flush_rollout, persist_rollout_items, send_event, set_previous_turn_settings, initial_history_has_prior_user_turns);外部调用 4 个(last_token_info_from_rollout, format!, Warning, warn!)。

Session::apply_rollout_reconstruction1318–1359 ↗
async fn apply_rollout_reconstruction(
        &self,
        turn_context: &TurnContext,
        rollout_items: &[RolloutItem],
    ) -> Option<PreviousTurnSettings>

作用:把保存过的对话录像重建成当前内存里的历史。这样恢复或分叉线程时,模型能接上以前的上下文。

数据流:进去的是 TurnContext 和 rollout_items;函数重建 history、上次回合设置、参考上下文项和窗口 id,必要时处理旧图片,然后替换状态里的历史,并按配置估算压缩窗口前缀 token;出来是 previous_turn_settings。

调用关系:record_initial_history 在恢复和分叉时调用它。它还会调用 get_base_instructions、clone_history 和 set_auto_compact_window_estimated_prefill_for_scope。

调用图:调用 4 个内部函数(prepare_response_items, clone_history, get_base_instructions, set_auto_compact_window_estimated_prefill_for_scope);被 1 处调用(record_initial_history);外部调用 1 个(matches!)。

Session::set_auto_compact_window_estimated_prefill_for_scope1361–1375 ↗
async fn set_auto_compact_window_estimated_prefill_for_scope(
        &self,
        turn_context: &TurnContext,
        tokens: i64,
    )

作用:在特定压缩计数模式下,记录自动压缩窗口前面已经占用的 token 数。

数据流:进去的是 TurnContext 和 token 数;函数先检查配置是否是 BodyAfterPrefix,不是就退出;是的话锁状态写入估算前缀;出来无返回。

调用关系:apply_rollout_reconstruction 和 recompute_token_usage 会调用它,用于保持自动压缩计算准确。

调用图:被 2 处调用(apply_rollout_reconstruction, recompute_token_usage);外部调用 1 个(matches!)。

Session::last_token_info_from_rollout1377–1382 ↗
fn last_token_info_from_rollout(rollout_items: &[RolloutItem]) -> Option<TokenUsageInfo>

作用:从对话录像里找出最后一次记录的 token 用量信息。恢复线程时可立刻显示旧用量。

数据流:进去的是 rollout_items;函数从后往前找 TokenCount 事件并克隆其中 info;出来是可选 TokenUsageInfo。

调用关系:record_initial_history 在恢复或分叉时调用它,把历史用量塞回当前状态。

调用图:外部调用 1 个(iter)。

Session::previous_turn_settings1384–1387 ↗
async fn previous_turn_settings(&self) -> Option<PreviousTurnSettings>

作用:读取上一个回合使用的设置。它用于判断本回合是否需要告诉模型“设置变了”。

数据流:进去无参数;函数锁状态并取 previous_turn_settings;出来是可选 PreviousTurnSettings。

调用关系:构造上下文更新时会用到这类信息;本段还提供 set_previous_turn_settings 来写入。

Session::set_previous_turn_settings1389–1395 ↗
async fn set_previous_turn_settings(
        &self,
        previous_turn_settings: Option<PreviousTurnSettings>,
    )

作用:保存上一个回合的设置,供下个回合比较。

数据流:进去的是可选 PreviousTurnSettings;函数锁状态并写入;出来无返回。

调用关系:record_initial_history 会调用它;后续 build_settings_update_items 会读取类似状态。

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

Session::update_settings1397–1435 ↗
async fn update_settings(
        &self,
        updates: SessionSettingsUpdate,
    ) -> ConstraintResult<()>

作用:在线程运行中更新会话设置,比如客户端信息、环境选择或权限。它会检查约束,不能随便改坏配置。

数据流:进去的是 SessionSettingsUpdate;函数尝试应用到 session_configuration,记录新旧配置,必要时更新环境选择,通知扩展配置变化;如果权限档案变了,还刷新网络代理;出来是成功或约束错误。

调用关系:Codex::set_app_server_client_info 等运行时设置入口会调用它。它会调用 emit_config_changed_contributors 和 refresh_managed_network_proxy_for_current_permission_profile。

调用图:调用 2 个内部函数(emit_config_changed_contributors, refresh_managed_network_proxy_for_current_permission_profile);外部调用 1 个(warn!)。

Session::preview_settings1437–1446 ↗
async fn preview_settings(
        &self,
        updates: &SessionSettingsUpdate,
    ) -> ConstraintResult<ThreadConfigSnapshot>

作用:预览一组设置改动会得到什么线程配置快照,但不真正写入。适合 UI 在用户确认前展示结果。

数据流:进去的是设置更新引用;函数在当前配置上试应用,成功则转成 ThreadConfigSnapshot;出来是快照或约束错误,不改变状态。

调用关系:配置预览接口会调用它。它和 update_settings 使用同一套 apply 校验,但不落地。

Session::set_session_startup_prewarm1448–1454 ↗
async fn set_session_startup_prewarm(
        &self,
        startup_prewarm: SessionStartupPrewarmHandle,
    )

作用:保存启动预热句柄。预热通常是提前准备模型或资源,减少第一次响应延迟。

数据流:进去的是 SessionStartupPrewarmHandle;函数锁状态并保存;出来无返回。

调用关系:启动过程或预热管理流程会调用它;take_session_startup_prewarm 会把它取走。

Session::take_session_startup_prewarm1456–1459 ↗
async fn take_session_startup_prewarm(&self) -> Option<SessionStartupPrewarmHandle>

作用:取出并清掉启动预热句柄,表示这个预热资源要被消费了。

数据流:进去无参数;函数锁状态并 take;出来是可选 SessionStartupPrewarmHandle,状态里对应值被清空。

调用关系:真正开始使用预热资源的流程会调用它。

Session::get_config1461–1467 ↗
async fn get_config(&self) -> std::sync::Arc<Config>

作用:读取会话启动时保留的原始配置对象。名字里提示不要随便用,是因为线程里还有更适合的运行时配置视图。

数据流:进去无参数;函数锁状态并克隆 Arc<Config>;出来是共享配置指针。

调用关系:需要访问底层配置的内部流程会调用它,尤其是还没拆到更细接口的旧代码。

Session::user_instructions1469–1477 ↗
async fn user_instructions(&self) -> Option<codex_extension_api::UserInstructions>

作用:读取从 AGENTS.md 等指令文件中加载到的用户指令。

数据流:进去无参数;函数锁状态,查看 loaded_agents_md,并提取 user_instructions;出来是可选 UserInstructions。

调用关系:需要把用户指令交给模型或扩展的流程会调用它。

Session::provider1479–1482 ↗
async fn provider(&self) -> ModelProviderInfo

作用:读取当前模型提供商信息。提供商就是模型服务来自哪里、该怎么连接。

数据流:进去无参数;函数锁状态并克隆 provider;出来是 ModelProviderInfo。

调用关系:模型请求构造和诊断信息会用它。

Session::refresh_runtime_config1484–1525 ↗
async fn refresh_runtime_config(&self, next_config: Config)

作用:在会话不中断的情况下刷新运行时配置。它主要更新用户配置层,同时保留线程启动时已有的局部覆盖。

数据流:进去的是新的 Config 快照;函数把用户层替换进当前配置,重算工具建议配置,通知扩展配置变化,清空技能和插件缓存,重建 hooks(钩子,某些时机自动执行的小程序),最后确认配置没被更新覆盖后发布 hooks;出来无返回,但运行时配置和缓存会变。

调用关系:reload_user_config_layer 会调用它;有外部已准备好配置快照时也应优先用它。它会调用 emit_config_changed_contributors 和 build_hooks_for_config。

调用图:调用 3 个内部函数(resolve_tool_suggest_config_from_layer_stack, emit_config_changed_contributors, build_hooks_for_config);被 1 处调用(reload_user_config_layer);外部调用 3 个(clone, new, ptr_eq)。

Session::emit_config_changed_contributors1527–1546 ↗
fn emit_config_changed_contributors(
        &self,
        previous_config: Option<&Config>,
        new_config: Option<&Config>,
    )

作用:通知扩展:会话配置变了。扩展可以据此更新自己依赖的行为。

数据流:进去的是可选旧配置和新配置;如果任一为空或两者相同就退出;否则遍历 config_contributors,调用 on_config_changed;出来无返回。

调用关系:update_settings 和 refresh_runtime_config 都会调用它。它是配置变化向扩展传播的统一出口。

调用图:被 2 处调用(refresh_runtime_config, update_settings)。

Session::reload_user_config_layer1548–1616 ↗
async fn reload_user_config_layer(&self)

作用:从本地配置文件重新加载用户配置层,并刷新当前会话。它是旧式本地热重载路径。

数据流:进去无参数;函数找出用户配置 toml 文件路径,逐个读取和解析,文件不存在就当空配置;然后把这些用户配置层合并进当前配置,重算工具建议配置,最后调用 refresh_runtime_config;出来无返回,读取或解析失败会记录警告并停止。

调用关系:本地配置重载命令会调用它。它把文件 IO 转换成 Config 后交给 refresh_runtime_config。

调用图:调用 2 个内部函数(resolve_tool_suggest_config_from_layer_stack, refresh_runtime_config);外部调用 6 个(default, with_capacity, read_to_string, Table, vec!, warn!)。

Session::build_settings_update_items1618–1641 ↗
async fn build_settings_update_items(
        &self,
        reference_context_item: Option<&TurnContextItem>,
        current_context: &TurnContext,
    ) -> Vec<ResponseItem>

作用:生成要写进模型上下文的“设置变化说明”。当 shell、权限或人格等变了,模型需要知道。

数据流:进去的是参考上下文项和当前 TurnContext;函数读取上回合设置、当前 shell 和执行策略,再调用 context_manager 的构造函数;出来是一组 ResponseItem。

调用关系:record_context_updates_and_set_reference_context_item 会调用它。它把 Session 状态整理后交给 context_manager::updates::build_settings_update_items。

调用图:调用 2 个内部函数(build_settings_update_items, user_shell);被 1 处调用(record_context_updates_and_set_reference_context_item)。

Session::track_turn_codex_error1644–1652 ↗
fn track_turn_codex_error(&self, turn_context: &TurnContext, error: &CodexErr)

作用:把某个回合里的 Codex 错误记录到分析系统。这样后续可以统计和排查错误。

数据流:进去的是 TurnContext 和 CodexErr;函数把线程 id、回合 id 和错误转成 TurnCodexErrorFact,交给 analytics_events_client;出来无返回。

调用关系:回合失败处理时会调用它。它不改变会话状态,只上报分析事件。

调用图:调用 1 个内部函数(from_codex_err)。

Session::send_event1655–1695 ↗
async fn send_event(&self, turn_context: &TurnContext, msg: EventMsg)

作用:发送一条面向外部的会话事件,并顺带完成记录、状态更新、实时同步和兼容旧事件等工作。它是事件出口的主入口。

数据流:进去的是 TurnContext 和 EventMsg;函数记录终止错误、写追踪,把消息包装成 Event 发出,必要时通知父代理、同步实时文本、结束实时交接,并生成旧协议事件继续发送;出来无返回,但事件会被持久化并送到 rx_event。

调用关系:很多地方用它发警告、工具项开始/完成、模型校验和审批请求。它内部调用 send_event_raw、maybe_notify_parent_of_terminal_turn、maybe_mirror_event_text_to_realtime 等。

调用图:调用 5 个内部函数(maybe_clear_realtime_handoff_for_event, maybe_mirror_event_text_to_realtime, maybe_notify_parent_of_terminal_turn, send_event_raw, show_raw_agent_reasoning);被 13 处调用(emit_model_verification, emit_turn_item_completed, emit_turn_item_started, emit_turn_moderation_metadata, maybe_warn_on_server_model_mismatch, notify_stream_error, record_initial_history, request_command_approval, request_patch_approval, request_permissions_for_environment (+3 more));外部调用 1 个(clone)。

Session::maybe_notify_parent_of_terminal_turn1698–1744 ↗
async fn maybe_notify_parent_of_terminal_turn(
        &self,
        turn_context: &TurnContext,
        msg: &EventMsg,
    )

作用:当 V2 多代理里的子代理回合结束时,通知父代理结果。这样父代理不用轮询,也能知道孩子干完了或失败了。

数据流:进去的是 TurnContext 和事件消息;函数只处理 V2、子代理、TurnComplete 或 TurnAborted;根据终止错误或事件得出 AgentStatus,若是最终状态就转发给父线程;出来无返回。

调用关系:send_event 每次发事件后会调用它。它会在合适时调用 forward_child_completion_to_parent。

调用图:调用 2 个内部函数(is_final, forward_child_completion_to_parent);被 1 处调用(send_event);外部调用 3 个(agent_status_from_event, matches!, Errored)。

Session::forward_child_completion_to_parent1747–1805 ↗
async fn forward_child_completion_to_parent(
        &self,
        turn_context: &TurnContext,
        parent_thread_id: ThreadId,
        child_agent_path: &codex_protocol::AgentPath,
        status

作用:把子代理完成消息真正发给父线程,并在追踪开启时记录这次代理间通信。

数据流:进去的是回合上下文、父线程 id、子代理路径和状态;函数从子路径推导父代理路径,格式化完成消息,构造 InterAgentCommunication,发给 agent_control;成功后可写入 rollout trace;出来无返回,失败只打 debug 日志。

调用关系:maybe_notify_parent_of_terminal_turn 调它。它把 Session 的子代理状态交给 agent_control 发送到父线程。

调用图:调用 3 个内部函数(format_inter_agent_completion_message, as_str, new);被 1 处调用(maybe_notify_parent_of_terminal_turn);外部调用 3 个(new, debug!, clone)。

Session::maybe_mirror_event_text_to_realtime1807–1817 ↗
async fn maybe_mirror_event_text_to_realtime(&self, msg: &EventMsg)

作用:把某些事件里的文字同步到实时对话通道。这样实时界面能听到或看到普通事件流里的文本。

数据流:进去的是 EventMsg;函数先提取可同步文本,确认实时会话正在运行,再调用 handoff_out;出来无返回,失败只记 debug。

调用关系:send_event 会调用它。它连接普通事件系统和 conversation 实时通道。

调用图:调用 1 个内部函数(realtime_text_for_event);被 1 处调用(send_event);外部调用 1 个(debug!)。

Session::maybe_clear_realtime_handoff_for_event1819–1827 ↗
async fn maybe_clear_realtime_handoff_for_event(&self, msg: &EventMsg)

作用:当一个回合完成时,结束实时通道的交接状态。避免下一段实时输出接到旧回合后面。

数据流:进去的是 EventMsg;如果不是 TurnComplete 就返回;否则调用 handoff_complete,并清掉 active handoff;出来无返回。

调用关系:send_event 每次发事件后会调用它。它和 maybe_mirror_event_text_to_realtime 一起维护实时输出生命周期。

调用图:被 1 处调用(send_event);外部调用 2 个(debug!, matches!)。

Session::send_event_raw1829–1837 ↗
async fn send_event_raw(&self, event: Event)

作用:发送一条已经包装好的 Event,并把它写入对话录像。raw 表示它不再做 send_event 那些额外包装判断。

数据流:进去的是 Event;函数把事件消息变成 RolloutItem 持久化,记录协议追踪,再交给 deliver_event_raw;出来无返回。

调用关系:send_event 和 build_initial_context 会调用它。它把事件持久化和实际投递串起来。

调用图:调用 2 个内部函数(deliver_event_raw, persist_rollout_items);被 2 处调用(build_initial_context, send_event);外部调用 1 个(vec!)。

Session::deliver_event_raw1839–1847 ↗
async fn deliver_event_raw(&self, event: Event)

作用:把事件真正送进事件通道,并更新最后已知代理状态。

数据流:进去的是 Event;函数尝试从消息推导 AgentStatus 并写入 watch 通道,然后发送到 tx_event;如果接收端已关,只记录 debug;出来无返回。

调用关系:send_event_raw 调用它。Codex::next_event 的接收端会从同一通道拿到这些事件。

调用图:被 1 处调用(send_event_raw);外部调用 2 个(agent_status_from_event, debug!)。

Session::emit_turn_item_started1849–1860 ↗
async fn emit_turn_item_started(&self, turn_context: &TurnContext, item: &TurnItem)

作用:发出“某个回合项目开始了”的事件。回合项目可能是用户消息、模型消息或工具调用等。

数据流:进去的是 TurnContext 和 TurnItem;函数复制 item,补上线程 id、回合 id 和开始时间,包装成 ItemStartedEvent 并发送;出来无返回。

调用关系:记录响应项或用户提示时会调用它。它通过 send_event 进入统一事件流程。

调用图:调用 2 个内部函数(send_event, now_unix_timestamp_ms);被 2 处调用(record_response_item_and_emit_turn_item, record_user_prompt_and_emit_turn_item);外部调用 2 个(clone, ItemStarted)。

Session::emit_turn_item_completed1862–1878 ↗
async fn emit_turn_item_completed(
        &self,
        turn_context: &TurnContext,
        item: TurnItem,
    )

作用:发出“某个回合项目完成了”的事件,并记录首个模型输出耗时等指标。

数据流:进去的是 TurnContext 和 TurnItem;函数先记录 TTFM 指标,再补上完成时间并发送 ItemCompletedEvent;出来无返回。

调用关系:记录响应项或用户提示完成时调用它。它通过 send_event 分发事件。

调用图:调用 3 个内部函数(send_event, now_unix_timestamp_ms, record_turn_ttfm_metric);被 2 处调用(record_response_item_and_emit_turn_item, record_user_prompt_and_emit_turn_item);外部调用 1 个(ItemCompleted)。

Session::persist_execpolicy_amendment1882–1900 ↗
async fn persist_execpolicy_amendment(
        &self,
        amendment: &ExecPolicyAmendment,
    ) -> Result<(), ExecPolicyUpdateError>

作用:把执行策略的新增批准规则保存下来,并更新当前执行策略。执行策略就是决定哪些命令能跑、哪些要问用户的规则。

数据流:进去的是 ExecPolicyAmendment;函数读取 codex_home,然后调用 exec_policy.append_amendment_and_update;出来是成功或 ExecPolicyUpdateError。

调用关系:用户批准某类命令后会调用它。后续命令检查会使用更新后的执行策略。

Session::turn_context_for_sub_id1902–1909 ↗
async fn turn_context_for_sub_id(&self, sub_id: &str) -> Option<Arc<TurnContext>>

作用:根据回合 id 找到当前活跃回合的上下文。上下文里有模型、配置、状态等本回合资料。

数据流:进去的是 sub_id;函数查看 active_turn,如果正在运行的任务 id 匹配,就克隆 TurnContext;出来是可选 Arc<TurnContext>。

调用关系:记录执行策略或网络策略变更消息时会调用它,以便把说明注入到正确回合。

调用图:被 2 处调用(record_execpolicy_amendment_message, record_network_policy_amendment_message)。

Session::active_turn_context_and_cancellation_token1911–1920 ↗
async fn active_turn_context_and_cancellation_token(
        &self,
    ) -> Option<(Arc<TurnContext>, CancellationToken)>

作用:取得当前活跃回合上下文和一个可取消它的令牌。取消令牌像一个遥控开关,可以通知任务停下。

数据流:进去无参数;函数锁 active_turn,若有任务就克隆 TurnContext,并从任务取消令牌派生子令牌;出来是可选二元组。

调用关系:需要中断或附加控制当前回合的内部流程会用它。

调用图:外部调用 1 个(clone)。

Session::record_execpolicy_amendment_message1922–1936 ↗
async fn record_execpolicy_amendment_message(
        &self,
        sub_id: &str,
        amendment: &ExecPolicyAmendment,
    )

作用:把“已保存命令批准规则”这件事写进当前对话上下文。这样模型以后知道用户已经允许了某类命令。

数据流:进去的是 sub_id 和 ExecPolicyAmendment;函数把命令格式化成允许前缀,构造 ApprovedCommandPrefixSaved 消息,找到对应回合上下文,然后 inject_no_new_turn 注入;出来无返回。

调用关系:执行策略规则保存后会调用它。它依赖 turn_context_for_sub_id,并把消息交给 inject_no_new_turn。

调用图:调用 4 个内部函数(into, new, turn_context_for_sub_id, format_allow_prefixes);外部调用 2 个(vec!, warn!)。

Session::persist_network_policy_amendment1938–1989 ↗
async fn persist_network_policy_amendment(
        &self,
        amendment: &NetworkPolicyAmendment,
        network_approval_context: &NetworkApprovalContext,
    ) -> anyhow::Result<()>

作用:保存用户对某个网络域名的允许或拒绝规则,并尽量立刻更新运行中的网络代理。

数据流:进去的是 NetworkPolicyAmendment 和审批上下文;函数拿刷新锁,校验域名和被批准域名一致,生成执行策略网络规则;若代理已运行就更新允许/拒绝列表,再把规则写入 exec_policy;出来是成功或错误。

调用关系:网络访问审批通过后会调用它。它会调用 validated_network_policy_amendment_host,并更新 services.network_proxy 和 exec_policy。

调用图:调用 1 个内部函数(execpolicy_network_rule_amendment);外部调用 1 个(validated_network_policy_amendment_host)。

Session::validated_network_policy_amendment_host1991–2005 ↗
fn validated_network_policy_amendment_host(
        amendment: &NetworkPolicyAmendment,
        network_approval_context: &NetworkApprovalContext,
    ) -> anyhow::Result<String>

作用:确认要保存的网络规则域名,确实就是用户刚批准的域名。它防止把 A 域名的批准偷偷写成 B 域名。

数据流:进去的是 amendment 和审批上下文;函数标准化两个 host 后比较,不一致就报错,一致就返回标准化 host;出来是 host 字符串或错误。

调用关系:persist_network_policy_amendment 在写入任何规则前调用它,是网络权限安全检查的一道门。

调用图:外部调用 2 个(anyhow!, normalize_host)。

Session::record_network_policy_amendment_message2007–2016 ↗
async fn record_network_policy_amendment_message(
        &self,
        sub_id: &str,
        amendment: &NetworkPolicyAmendment,
    )

作用:把“已保存网络规则”这件事注入当前对话。这样模型知道某个域名之后已被允许或拒绝。

数据流:进去的是 sub_id 和 NetworkPolicyAmendment;函数构造 NetworkRuleSaved 消息,找到当前回合上下文,并用 inject_no_new_turn 注入;出来无返回。

调用关系:网络策略保存后会调用它。它和 record_execpolicy_amendment_message 类似,都是把外部批准结果写回模型可见上下文。

调用图:调用 3 个内部函数(into, new, turn_context_for_sub_id);外部调用 1 个(vec!)。

Session::request_command_approval2033–2103 ↗
async fn request_command_approval(
        &self,
        turn_context: &TurnContext,
        call_id: String,
        approval_id: Option<String>,
        command: Vec<String>,
        cwd: AbsoluteP

作用:在命令真正执行前,向用户或审核界面请求批准。它用于拦住可能有风险的命令,等到明确同意、拒绝或中止后再继续。

数据流:输入是一条命令、工作目录、原因、网络或执行策略相关补充信息,以及本轮上下文 → 它先把一个“等待审批的回执通道”登记到当前轮状态里,再整理命令展示信息和可选审批按钮,最后发出 ExecApprovalRequest 事件 → 输出是一个 ReviewDecision,也就是批准、拒绝、终止等决定;如果等不到回复,就当作 Abort。

调用关系:它先通过 parse_command 把命令变成更适合展示的形式,再用 send_event 把审批请求送出去。之后 notify_approval 会根据 approval_id 找到这里登记的等待项,把审批结果送回来。

调用图:调用 3 个内部函数(send_event, now_unix_timestamp_ms, parse_command);外部调用 3 个(channel, ExecApprovalRequest, warn!)。

Session::request_patch_approval2109–2144 ↗
async fn request_patch_approval(
        &self,
        turn_context: &TurnContext,
        call_id: String,
        changes: HashMap<PathBuf, FileChange>,
        reason: Option<String>,
        gran

作用:在应用文件补丁前,请用户批准这些文件改动。它让系统能先展示“哪些文件会变、怎么变”,再决定是否动手。

数据流:输入是补丁调用编号、文件改动集合、原因和可能授予的根目录 → 它登记一个等待审批的通道,然后发送 ApplyPatchApprovalRequest 事件 → 输出不是立刻的决定,而是一个接收器,调用方之后可以从里面等到 ReviewDecision。

调用关系:它和 request_command_approval 的模式类似:先登记等待项,再发事件。后续用户回应会通过 notify_approval 回到同一个 pending approval 表里。

调用图:调用 2 个内部函数(send_event, now_unix_timestamp_ms);外部调用 3 个(channel, ApplyPatchApprovalRequest, warn!)。

Session::request_permissions_for_environment2150–2316 ↗
async fn request_permissions_for_environment(
        self: &Arc<Self>,
        turn_context: &Arc<TurnContext>,
        call_id: String,
        args: RequestPermissionsArgs,
        environment: Tur

作用:当模型或工具想临时获得更多权限时,这个函数负责发起权限申请。它会根据当前审批策略判断是否需要问人、问 Guardian 审核器,还是直接给默认空权限。

数据流:输入是申请编号、申请的权限、目标环境、取消信号和本轮上下文 → 它先检查审批策略;如果不允许申请或目录不是本机路径,就返回空权限;如果走 Guardian,就创建审核请求并等待结果;否则登记等待响应并发 RequestPermissions 事件 → 输出是标准化后的 RequestPermissionsResponse,或者取消时输出 None;如果批准,还会把权限记录到本轮或整场会话状态里。

调用关系:request_permissions_for_cwd 会先把当前目录包装成环境选择,再调用它。它内部可能把活交给 Guardian 审核流程,也会调用 normalize_request_permissions_response 防止返回超出申请范围的权限,并调用 record_granted_request_permissions_for_turn 保存结果。

调用图:调用 3 个内部函数(record_granted_request_permissions_for_turn, send_event, now_unix_timestamp_ms);被 1 处调用(request_permissions_for_cwd);外部调用 12 个(clone, clone, normalize_request_permissions_response, default, new_guardian_review_id, routes_approval_to_guardian, spawn_approval_request_review, channel, RequestPermissions, clone (+2 more))。

Session::request_permissions_for_cwd2318–2351 ↗
async fn request_permissions_for_cwd(
        self: &Arc<Self>,
        turn_context: &Arc<TurnContext>,
        call_id: String,
        args: RequestPermissionsArgs,
        cwd: AbsolutePathBuf,

作用:这是按当前工作目录申请权限的便捷入口。调用方只给一个 cwd,它会找到对应环境,再交给真正的权限申请流程。

数据流:输入是权限申请参数、工作目录、调用编号和取消信号 → 它从本轮环境列表里找指定环境,没有指定就用主环境,并把 cwd 写进环境选择 → 输出来自 request_permissions_for_environment;如果找不到环境,就返回空权限响应。

调用关系:它是 request_permissions_for_environment 的前置包装器,主要负责把“一个目录”转换成“一个可审批的运行环境”。

调用图:调用 2 个内部函数(request_permissions_for_environment, from_abs_path);外部调用 1 个(default)。

Session::request_user_input2357–2391 ↗
async fn request_user_input(
        &self,
        turn_context: &TurnContext,
        call_id: String,
        args: RequestUserInputArgs,
    ) -> Option<RequestUserInputResponse>

作用:在模型运行到一半需要用户补充回答时,向前端发出提问。它让系统可以暂停等待用户,而不是随便猜。

数据流:输入是调用编号、本轮上下文和要问的问题 → 它把等待用户回答的通道登记到当前轮状态,标记本轮请求过用户输入,然后发送 RequestUserInput 事件 → 输出是用户回答;如果通道断了,就返回 None。

调用关系:它发出的请求会由前端展示给用户。用户回答回来后,notify_user_input_response 会找到这里登记的通道并把答案送回来。

调用图:调用 1 个内部函数(send_event);外部调用 3 个(channel, RequestUserInput, warn!)。

Session::notify_user_input_response2397–2420 ↗
async fn notify_user_input_response(
        &self,
        sub_id: &str,
        response: RequestUserInputResponse,
    )

作用:把用户对中途提问的回答送回正在等待的流程。它相当于把前端交回来的答卷塞回原来的等待点。

数据流:输入是 turn 的 sub_id 和用户回答 → 它从当前轮状态里移除对应的等待项 → 如果找到了,就把回答发送给 request_user_input;如果没找到,只记录警告,不改变主流程。

调用关系:它是 request_user_input 的配对回调。前者登记等待,后者在用户实际回复时解除等待。

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

Session::notify_request_permissions_response2426–2477 ↗
async fn notify_request_permissions_response(
        &self,
        call_id: &str,
        response: RequestPermissionsResponse,
    )

作用:接收前端或外部审核给出的权限申请结果,并把结果送回等待中的申请。它还会做安全收口,避免审批结果给出比申请更多的权限。

数据流:输入是权限申请 call_id 和审批响应 → 它找到并移除对应的 pending request,按环境目录标准化权限;如果目录不适合本机处理,则降级为空权限 → 它记录被授予的权限,然后把响应发回等待者;找不到等待项时只警告。

调用关系:它对应 request_permissions_for_environment 中非 Guardian 路径登记的等待项。它会调用 normalize_request_permissions_response 和 record_granted_request_permissions_for_turn,保证权限既安全又被状态记住。

调用图:调用 1 个内部函数(record_granted_request_permissions_for_turn);外部调用 3 个(normalize_request_permissions_response, default, warn!)。

Session::normalize_request_permissions_response2479–2506 ↗
fn normalize_request_permissions_response(
        requested_permissions: RequestPermissionProfile,
        response: RequestPermissionsResponse,
        cwd: &Path,
    ) -> RequestPermissionsRespons

作用:清洗权限审批结果,防止返回不合理或超范围的权限。它是权限系统里的“安全闸门”。

数据流:输入是原本申请的权限、审批返回的权限和当前目录 → 如果响应要求“严格自动审查”但范围却是整场会话,就直接降为空权限;如果没有授予权限就原样返回;否则把申请权限和响应权限取交集 → 输出一份不会超过原申请范围的响应。

调用关系:request_permissions_for_environment 和 notify_request_permissions_response 都会在拿到审批结果后调用它,再决定是否记录权限。

调用图:调用 1 个内部函数(intersect_permission_profiles);外部调用 3 个(default, into, matches!)。

Session::record_granted_request_permissions_for_turn2508–2537 ↗
async fn record_granted_request_permissions_for_turn(
        &self,
        response: &RequestPermissionsResponse,
        environment_id: &str,
        originating_turn_state: Option<&Arc<Mutex<crat

作用:把已经批准的权限记下来,方便后续同一轮或同一会话复用。它区分“只本轮有效”和“整场会话有效”。

数据流:输入是权限响应、环境编号和原始 turn 状态 → 如果没有权限就直接结束;如果范围是 Turn,就写入本轮状态,并可能开启严格自动审查;如果范围是 Session,就写入会话全局状态 → 输出为空,但状态被更新。

调用关系:它被 request_permissions_for_environment 和 notify_request_permissions_response 调用,作为审批完成后的落账步骤。

调用图:被 2 处调用(notify_request_permissions_response, request_permissions_for_environment)。

Session::granted_turn_permissions2543–2551 ↗
async fn granted_turn_permissions(
        &self,
        environment_id: &str,
    ) -> Option<AdditionalPermissionProfile>

作用:查询某个环境在当前轮已经拿到的临时权限。调用者可用它判断是否还需要再次申请。

数据流:输入是环境编号 → 它读取当前活跃 turn 的状态 → 输出该环境的本轮附加权限;如果没有活跃轮或没记录,就返回 None。

调用关系:它不主动触发审批,只是给执行或权限判断逻辑查看当前轮已批准的结果。

Session::strict_auto_review_enabled_for_turn2557–2564 ↗
async fn strict_auto_review_enabled_for_turn(&self) -> bool

作用:查询当前轮是否开启了严格自动审查。这个标志通常来自权限审批结果,用来让后续行为更谨慎。

数据流:没有业务输入 → 它读取当前活跃 turn 的状态 → 输出 true 或 false;没有活跃轮时返回 false。

调用关系:它读取 record_granted_request_permissions_for_turn 可能写入的 turn 状态,供后续检查逻辑使用。

Session::granted_session_permissions2566–2572 ↗
async fn granted_session_permissions(
        &self,
        environment_id: &str,
    ) -> Option<AdditionalPermissionProfile>

作用:查询某个环境在整场会话里已经拿到的权限。它用于跨多轮复用已批准的会话级权限。

数据流:输入是环境编号 → 它读取会话全局状态 → 输出该环境的会话级附加权限;没有则返回 None。

调用关系:它和 granted_turn_permissions 相对应,一个看全局会话,一个看当前轮。

Session::notify_dynamic_tool_response2578–2597 ↗
async fn notify_dynamic_tool_response(&self, call_id: &str, response: DynamicToolResponse)

作用:把动态工具调用的结果送回等待中的调用点。动态工具可以理解为运行时才决定使用的外部能力。

数据流:输入是工具调用 call_id 和工具响应 → 它从当前轮状态里移除对应等待项 → 找到就发送响应,找不到就记录警告。

调用关系:它配合之前登记的 pending dynamic tool 使用,职责和 notify_approval、notify_user_input_response 类似,都是把异步回复送回原等待处。

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

Session::notify_approval2603–2622 ↗
async fn notify_approval(&self, approval_id: &str, decision: ReviewDecision)

作用:接收用户或审核器对某个审批请求的决定,并唤醒等待中的命令或补丁流程。

数据流:输入是 approval_id 和 ReviewDecision → 它从当前轮状态中移除 pending approval → 找到就把决定发给等待者;没找到就警告。

调用关系:它是 request_command_approval 和 request_patch_approval 的回传入口。审批请求先在那里登记,最终在这里完成闭环。

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

Session::prepare_conversation_items_for_history2626–2638 ↗
fn prepare_conversation_items_for_history(
        &self,
        turn_context: &TurnContext,
        items: &'a [ResponseItem],
    ) -> Cow<'a, [ResponseItem]>

作用:在把对话项写入历史前,按功能开关决定是否先处理图片。主要是为了让历史里的图片尺寸适合后续使用。

数据流:输入是本轮上下文和一组 ResponseItem → 如果没有开启 ResizeAllImages,就借用原数据;如果开启,就复制一份并调用 prepare_response_items 处理 → 输出可能是原切片,也可能是处理后的新列表。

调用关系:record_conversation_items 和 record_inter_agent_communication 在写历史前会调用它,保证进入历史的数据格式符合当前功能设置。

调用图:调用 1 个内部函数(prepare_response_items);被 2 处调用(record_conversation_items, record_inter_agent_communication);外部调用 3 个(Borrowed, Owned, to_vec)。

Session::response_item_from_user_input2640–2654 ↗
fn response_item_from_user_input(
        &self,
        turn_context: &TurnContext,
        input: Vec<UserInput>,
    ) -> ResponseItem

作用:把用户输入转换成模型历史能保存的 ResponseItem。它还会根据图片处理功能决定图片现在处理还是稍后处理。

数据流:输入是本轮上下文和用户输入列表 → 它检查 ResizeAllImages 功能,选择图片处理方式,然后用 ResponseInputItem::from_user_input 包装 → 输出一个 ResponseItem。

调用关系:record_user_prompt_and_emit_turn_item 会用它把用户消息写入对话历史,同时另外保留 UI 需要的展示细节。

调用图:调用 2 个内部函数(from_user_input, from);被 1 处调用(record_user_prompt_and_emit_turn_item)。

Session::record_conversation_items2656–2669 ↗
async fn record_conversation_items(
        &self,
        turn_context: &TurnContext,
        items: &[ResponseItem],
    )

作用:把一批对话内容正式写进会话历史、持久化记录,并通知外部。它是“这段对话真的发生了”的记账入口。

数据流:输入是本轮上下文和 ResponseItem 列表 → 它先准备图片等内容,再写入内存历史,然后持久化为 rollout 记录,最后逐条发送 RawResponseItem 事件 → 输出为空,但历史、磁盘记录和外部事件都更新。

调用关系:record_response_item_and_emit_turn_item、record_user_prompt_and_emit_turn_item、record_context_updates_and_set_reference_context_item 都会调用它。它内部再调用 persist_rollout_response_items 和 send_raw_response_items。

调用图:调用 3 个内部函数(persist_rollout_response_items, prepare_conversation_items_for_history, send_raw_response_items);被 3 处调用(record_context_updates_and_set_reference_context_item, record_response_item_and_emit_turn_item, record_user_prompt_and_emit_turn_item);外部调用 2 个(as_ref, iter)。

Session::record_inter_agent_communication2671–2689 ↗
async fn record_inter_agent_communication(
        &self,
        turn_context: &TurnContext,
        communication: InterAgentCommunication,
    )

作用:记录代理之间的通信内容,让多代理协作过程也进入历史和持久化日志。这样恢复会话时不会丢掉代理互相说过什么。

数据流:输入是一条 InterAgentCommunication 和本轮上下文 → 它把通信转成模型输入项,按历史规则处理,写入状态,持久化为 InterAgentCommunication rollout 项,并发送原始响应项事件 → 输出为空,但历史和日志被更新。

调用关系:它复用 prepare_conversation_items_for_history 和 send_raw_response_items,但持久化时使用专门的 RolloutItem::InterAgentCommunication。

调用图:调用 4 个内部函数(persist_rollout_items, prepare_conversation_items_for_history, send_raw_response_items, to_model_input_item);外部调用 2 个(InterAgentCommunication, from_ref)。

Session::maybe_warn_on_server_model_mismatch2691–2728 ↗
async fn maybe_warn_on_server_model_mismatch(
        self: &Arc<Self>,
        turn_context: &Arc<TurnContext>,
        server_model: String,
    ) -> bool

作用:检查服务器实际使用的模型是否和用户请求的模型一致。如果不一致,它会向用户发出明确警告。

数据流:输入是服务器报告的模型名和本轮上下文 → 它把请求模型和服务器模型都转成小写比较;一致则返回 false;不一致则记录日志,发送 ModelReroute 事件和 Warning 事件 → 输出 true 表示发生了模型改道。

调用关系:它用 send_event 通知前端模型被重新路由。这个函数把底层服务器事实转成用户能看到的提示。

调用图:调用 1 个内部函数(send_event);外部调用 5 个(format!, info!, ModelReroute, Warning, warn!)。

Session::emit_model_verification2730–2740 ↗
async fn emit_model_verification(
        self: &Arc<Self>,
        turn_context: &Arc<TurnContext>,
        verifications: Vec<ModelVerification>,
    )

作用:发送模型验证结果事件。它让外部界面或客户端知道这轮请求涉及哪些模型验证信息。

数据流:输入是一组 ModelVerification 和本轮上下文 → 它包装成 ModelVerificationEvent → 通过 send_event 发出,状态本身不改变。

调用关系:它是一个事件出口,专门把模型验证信息从会话内部送到外部订阅者。

调用图:调用 1 个内部函数(send_event);外部调用 1 个(ModelVerification)。

Session::emit_turn_moderation_metadata2742–2749 ↗
async fn emit_turn_moderation_metadata(
        self: &Arc<Self>,
        turn_context: &Arc<TurnContext>,
        metadata: TurnModerationMetadataEvent,
    )

作用:发送本轮的审核或安全元数据。元数据可以理解为给这轮对话贴上的安全相关标签或说明。

数据流:输入是 TurnModerationMetadataEvent 和本轮上下文 → 它包装进 EventMsg::TurnModerationMetadata → 通过 send_event 发出。

调用关系:它和 emit_model_verification 类似,是把内部安全信息传给外部的事件出口。

调用图:调用 1 个内部函数(send_event);外部调用 1 个(TurnModerationMetadata)。

Session::replace_history2752–2759 ↗
async fn replace_history(
        &self,
        items: Vec<ResponseItem>,
        reference_context_item: Option<TurnContextItem>,
    )

作用:用一套新的对话历史替换当前历史。它通常用于恢复、压缩或重建上下文后的状态切换。

数据流:输入是新的 ResponseItem 列表和可选的参考上下文项 → 它锁住会话状态并调用 replace_history → 输出为空,但内存中的历史被整体替换。

调用关系:它只改内存状态,不负责持久化;需要持久化压缩结果时会用 replace_compacted_history。

Session::replace_compacted_history2761–2782 ↗
async fn replace_compacted_history(
        &self,
        items: Vec<ResponseItem>,
        reference_context_item: Option<TurnContextItem>,
        compacted_item: CompactedItem,
    )

作用:用压缩后的历史替换当前历史,并把这次压缩写进持久化记录。它用于对话太长时“打包整理”上下文。

数据流:输入是替换后的历史、参考上下文项和压缩记录 → 它先替换内存历史,再持久化 Compacted 项;如果有参考上下文,也一起持久化;最后标记下一次会话开始来源是 Compact → 输出为空,但历史和 rollout 日志都更新。

调用关系:它调用 persist_rollout_items 写入压缩痕迹,比 replace_history 多了持久化和后续 hook 来源标记。

调用图:调用 1 个内部函数(persist_rollout_items);外部调用 2 个(Compacted, TurnContext)。

Session::persist_rollout_response_items2784–2791 ↗
async fn persist_rollout_response_items(&self, items: &[ResponseItem])

作用:把普通 ResponseItem 转成可持久化的 rollout 项并写入。rollout 可以理解为会话过程的流水账。

数据流:输入是一组 ResponseItem → 它逐个克隆并包装成 RolloutItem::ResponseItem → 交给 persist_rollout_items 写入存储。

调用关系:record_conversation_items 会调用它。它自己不接触磁盘细节,而是把写入交给 persist_rollout_items。

调用图:调用 1 个内部函数(persist_rollout_items);被 1 处调用(record_conversation_items);外部调用 1 个(iter)。

Session::enabled2793–2795 ↗
fn enabled(&self, feature: Feature) -> bool

作用:快速查询某个功能开关是否打开。调用方用它决定某些新能力或实验功能要不要生效。

数据流:输入是 Feature 枚举值 → 它读取 Session 持有的 features → 输出 true 或 false。

调用关系:这是一个简单的功能开关查询入口,给会话内部或附近模块使用。

Session::features2797–2799 ↗
fn features(&self) -> ManagedFeatures

作用:返回当前会话的功能开关集合副本。调用者可以自己进一步查询多个开关。

数据流:没有业务输入 → 它克隆 ManagedFeatures → 输出这份功能开关对象。

调用关系:它是 Session 对功能配置的访问器,不做额外判断。

Session::collaboration_mode2801–2804 ↗
async fn collaboration_mode(&self) -> CollaborationMode

作用:读取当前会话的协作模式。协作模式会影响提示词和多代理协作方式。

数据流:没有业务输入 → 它读取会话状态里的 session_configuration.collaboration_mode → 输出其克隆值。

调用关系:build_initial_context 会把协作模式转成模型可见的说明;这个函数提供运行时查询。

Session::multi_agent_version2806–2808 ↗
fn multi_agent_version(&self) -> Option<MultiAgentVersion>

作用:查看会话已经确定的多代理版本。多代理版本就是多代理协作协议或实现方案的版本。

数据流:没有业务输入 → 它读取一次性初始化的 multi_agent_version → 输出 Some 版本或 None。

调用关系:resolve_multi_agent_version_for_model 会先调用它,如果已经定过版本就不再重新选择。

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

Session::set_multi_agent_version_if_unset2810–2815 ↗
fn set_multi_agent_version_if_unset(
        &self,
        multi_agent_version: MultiAgentVersion,
    ) -> MultiAgentVersion

作用:如果多代理版本还没定,就设置它;如果已经定了,就保持原值。这样同一会话不会中途换协议。

数据流:输入是候选 MultiAgentVersion → 它通过 get_or_init 只在未设置时写入 → 输出最终使用的版本。

调用关系:resolve_multi_agent_version_for_model 在选出默认版本后调用它,保证选择只发生一次。

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

Session::resolve_multi_agent_version_for_model2817–2831 ↗
fn resolve_multi_agent_version_for_model(
        &self,
        model_info: &ModelInfo,
        config: &Config,
    ) -> MultiAgentVersion

作用:根据模型信息和配置决定本会话使用哪个多代理版本。它避免每轮都重新猜版本。

数据流:输入是 ModelInfo 和 Config → 它先看会话是否已有版本;没有则优先用模型指定版本,否则用配置按功能推导出的版本 → 输出最终版本,并在会话中固定下来。

调用关系:它调用 multi_agent_version 和 set_multi_agent_version_if_unset,承担“选一次并缓存”的角色。

调用图:调用 2 个内部函数(multi_agent_version, set_multi_agent_version_if_unset)。

Session::send_raw_response_items2833–2841 ↗
async fn send_raw_response_items(&self, turn_context: &TurnContext, items: &[ResponseItem])

作用:把原始对话项逐条发给外部监听者。外部界面可据此同步显示或记录模型看到的内容。

数据流:输入是本轮上下文和 ResponseItem 列表 → 它遍历每一项,包装成 RawResponseItemEvent → 逐条 send_event。

调用关系:record_conversation_items 和 record_inter_agent_communication 在写完历史后调用它,让外部也知道新内容。

调用图:调用 1 个内部函数(send_event);被 2 处调用(record_conversation_items, record_inter_agent_communication);外部调用 1 个(RawResponseItem)。

Session::build_initial_context2843–3077 ↗
async fn build_initial_context(
        &self,
        turn_context: &TurnContext,
    ) -> Vec<ResponseItem>

作用:生成一轮对话开始时模型需要看的初始背景。它把权限规则、环境、插件、技能、协作模式、用户说明等整理成模型消息。

数据流:输入是本轮 TurnContext → 它读取会话状态中的参考上下文、上一轮设置、基础指令和压缩窗口;再按配置和功能开关收集权限说明、开发者说明、应用连接器、技能、插件、扩展贡献内容、环境信息和 token 预算等 → 输出一组 ResponseItem,作为要注入模型上下文的初始消息。

调用关系:maybe_start_new_context_window 和 record_context_updates_and_set_reference_context_item 会调用它。它是上下文拼装中心,会向连接器、插件、扩展、技能系统拿材料,然后拼成模型可读的消息。

调用图:调用 19 个内部函数(list_accessible_and_enabled_connectors_from_manager, from_connectors, from_plugins, from, from_collaboration_mode, from_turn_context, new, new, build_contextual_user_message, build_developer_update_item (+9 more));被 2 处调用(maybe_start_new_context_window, record_context_updates_and_set_reference_context_item);外部调用 9 个(new, new, with_capacity, with_capacity, build_available_skills, default_skill_metadata_budget, is_guardian_reviewer_source, Warning, vec!)。

Session::persist_rollout_items3079–3085 ↗
async fn persist_rollout_items(&self, items: &[RolloutItem])

作用:把会话流水账项写入当前 live thread。它是很多记录操作最终落盘的公共出口。

数据流:输入是一组 RolloutItem → 它先找当前 live_thread;如果存在,就调用 append_items 写入;失败则记录错误 → 输出为空,成功时持久化存储增加新记录。

调用关系:record_conversation_items、record_inter_agent_communication、replace_compacted_history、maybe_start_new_context_window、record_context_updates_and_set_reference_context_item 等都会间接或直接调用它。

调用图:调用 1 个内部函数(live_thread);被 7 处调用(maybe_start_new_context_window, persist_rollout_response_items, record_context_updates_and_set_reference_context_item, record_initial_history, record_inter_agent_communication, replace_compacted_history, send_event_raw);外部调用 1 个(error!)。

Session::clone_history3087–3090 ↗
async fn clone_history(&self) -> ContextManager

作用:复制当前对话历史管理器。它让调用方可以在不直接改原状态的情况下查看或计算历史。

数据流:没有业务输入 → 它锁住状态并克隆历史 → 输出 ContextManager 副本。

调用关系:apply_rollout_reconstruction 和 recompute_token_usage 会用它,前者用于重建,后者用于重新估算 token 用量。

调用图:被 2 处调用(apply_rollout_reconstruction, recompute_token_usage)。

Session::current_window_id3092–3097 ↗
async fn current_window_id(&self) -> String

作用:生成当前上下文窗口的唯一标识。它把线程编号和自动压缩窗口编号拼在一起。

数据流:没有业务输入 → 它读取当前 auto_compact_window_id 和 thread_id → 输出形如“thread:window”的字符串。

调用关系:它为日志、追踪或上下文窗口识别提供一个稳定名字。

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

Session::advance_auto_compact_window_id3099–3102 ↗
async fn advance_auto_compact_window_id(&self) -> u64

作用:推进自动压缩窗口编号。每次开启新窗口时需要新的编号,避免混淆不同阶段的上下文。

数据流:没有业务输入 → 它锁住状态并调用 advance_auto_compact_window_id → 输出新的窗口编号。

调用关系:它属于上下文窗口生命周期控制的一部分,和 maybe_start_new_context_window 的窗口切换概念配套。

Session::request_new_context_window3104–3107 ↗
async fn request_new_context_window(&self)

作用:标记“接下来要开一个新的上下文窗口”。这像先按下一个开关,真正切换会在稍后的检查点发生。

数据流:没有业务输入 → 它锁住状态并设置新窗口请求标记 → 输出为空。

调用关系:maybe_start_new_context_window 会读取这个请求标记,如果确实有请求,就真正替换历史并持久化。

Session::maybe_start_new_context_window3109–3140 ↗
async fn maybe_start_new_context_window(
        &self,
        turn_context: &TurnContext,
    ) -> Option<u64>

作用:如果之前请求过新上下文窗口,就真正开始一个新窗口。它会用初始上下文替换历史,并记录一次压缩事件。

数据流:输入是本轮上下文 → 它先尝试从状态里领取一个新窗口编号;没有请求则返回 None;有请求则构建初始上下文,替换历史,持久化 Compacted 和 TurnContext 项,标记会话开始来源为 Compact,重新计算 token → 输出 Some(window_id)。

调用关系:它调用 build_initial_context、persist_rollout_items 和 recompute_token_usage,是上下文窗口切换的执行者。

调用图:调用 4 个内部函数(build_initial_context, persist_rollout_items, recompute_token_usage, to_turn_context_item);外部调用 3 个(new, Compacted, TurnContext)。

Session::reference_context_item3142–3145 ↗
async fn reference_context_item(&self) -> Option<TurnContextItem>

作用:读取当前用于计算上下文差异的参考项。它帮助系统知道上一轮的基线是什么。

数据流:没有业务输入 → 它读取状态里的 reference_context_item → 输出可选 TurnContextItem。

调用关系:record_context_updates_and_set_reference_context_item 会更新这个基线;这个函数提供只读访问。

Session::record_context_updates_and_set_reference_context_item3161–3191 ↗
async fn record_context_updates_and_set_reference_context_item(
        &self,
        turn_context: &TurnContext,
    )

作用:在每轮开始时记录该给模型看的上下文更新,并刷新差异计算基线。它避免每轮都重复塞完整背景,节省 token。

数据流:输入是本轮上下文 → 如果还没有参考上下文,就构建完整初始上下文;否则只构建设置差异项;有内容就写入对话历史;无论有没有内容,都持久化新的 TurnContextItem,并把它设为新的参考基线 → 输出为空。

调用关系:它会调用 build_initial_context 或 build_settings_update_items,再用 record_conversation_items 和 persist_rollout_items 记录结果。

调用图:调用 5 个内部函数(build_initial_context, build_settings_update_items, persist_rollout_items, record_conversation_items, to_turn_context_item);外部调用 1 个(TurnContext)。

Session::update_token_usage_info3193–3201 ↗
async fn update_token_usage_info(
        &self,
        turn_context: &TurnContext,
        token_usage: Option<&TokenUsage>,
    )

作用:更新 token 用量信息,并通知外部当前 token 状态。token 是模型上下文长度和计费/限额相关的重要计量。

数据流:输入是本轮上下文和可选 TokenUsage → 它先记录用量,再发送 TokenCount 事件 → 输出为空,状态和外部显示都更新。

调用关系:它是 token 用量更新的高层入口,内部依次调用 record_token_usage_info 和 send_token_count_event。

调用图:调用 2 个内部函数(record_token_usage_info, send_token_count_event)。

Session::record_token_usage_info3203–3234 ↗
async fn record_token_usage_info(
        &self,
        turn_context: &TurnContext,
        token_usage: Option<&TokenUsage>,
    )

作用:把服务器返回的 token 用量写入会话状态,并通知扩展。扩展可以基于用量做自己的统计或提示。

数据流:输入是本轮上下文和可选 TokenUsage → 有用量时,它更新状态里的 token_info;若配置要求,还会记录自动压缩所需的服务器预填信息;随后把 token_info 发给所有 token_usage_contributors → 输出为空。

调用关系:它被 update_token_usage_info 调用,只负责记录和扩展回调,不负责发 TokenCount 事件。

调用图:调用 1 个内部函数(model_context_window);被 1 处调用(update_token_usage_info);外部调用 1 个(matches!)。

Session::recompute_token_usage3236–3272 ↗
async fn recompute_token_usage(&self, turn_context: &TurnContext)

作用:在历史被替换或压缩后,重新估算当前上下文占用了多少 token。这样界面和自动压缩判断不会用旧数字。

数据流:输入是本轮上下文 → 它复制历史,读取基础指令,估算总 token;估算失败则退出;成功后更新状态里的 token_info,设置自动压缩相关预填估算,并发送 TokenCount 事件 → 输出为空。

调用关系:maybe_start_new_context_window 在切换新窗口后会调用它。它还调用 clone_history、get_base_instructions、set_auto_compact_window_estimated_prefill_for_scope 和 send_token_count_event。

调用图:调用 5 个内部函数(clone_history, get_base_instructions, send_token_count_event, set_auto_compact_window_estimated_prefill_for_scope, model_context_window);被 1 处调用(maybe_start_new_context_window);外部调用 1 个(default)。

Session::update_rate_limits3274–3281 ↗
async fn update_rate_limits(
        &self,
        turn_context: &TurnContext,
        new_rate_limits: RateLimitSnapshot,
    )

作用:更新当前速率限制信息,并让外部同步看到。速率限制可以理解为服务端允许你在一段时间内用多少资源。

数据流:输入是本轮上下文和新的 RateLimitSnapshot → 它写入速率限制状态,然后发送 TokenCount 事件,因为该事件同时携带 token 和限额信息 → 输出为空。

调用关系:它是速率限制更新入口,内部调用 record_rate_limits_info 和 send_token_count_event。

调用图:调用 2 个内部函数(record_rate_limits_info, send_token_count_event)。

Session::record_rate_limits_info3283–3288 ↗
async fn record_rate_limits_info(&self, new_rate_limits: RateLimitSnapshot)

作用:把新的速率限制快照写进会话状态。它只记账,不负责通知外部。

数据流:输入是 RateLimitSnapshot → 它锁住状态并 set_rate_limits → 输出为空,状态被替换为新限额。

调用关系:update_rate_limits 会先调用它,再发事件通知外部。

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

Session::mcp_dependency_prompted3290–3293 ↗
async fn mcp_dependency_prompted(&self) -> HashSet<String>

作用:读取已经提示过用户的 MCP 依赖名称集合。MCP 可以理解为外部工具/服务连接协议,这里避免重复提示同一依赖。

数据流:没有业务输入 → 它读取状态里的 mcp_dependency_prompted 集合 → 输出 HashSet<String>。

调用关系:它和 record_mcp_dependency_prompted 配套,一个读已提示列表,一个写入新提示记录。

Session::record_mcp_dependency_prompted3295–3301 ↗
async fn record_mcp_dependency_prompted(&self, names: I)

作用:记录哪些 MCP 依赖已经提示过用户。这样后续不会一遍遍打扰用户。

数据流:输入是一批依赖名称 → 它锁住状态并合并记录 → 输出为空,已提示集合变大。

调用关系:它是 mcp_dependency_prompted 的写入端,供依赖检查或启动流程在提示后调用。

Session::set_server_reasoning_included3303–3306 ↗
async fn set_server_reasoning_included(&self, included: bool)

作用:记录服务器响应里是否包含推理内容。这个标志会影响后续显示或处理服务端推理信息的方式。

数据流:输入是布尔值 included → 它写入会话状态 → 输出为空。

调用关系:它提供一个简单状态开关,供收到服务器能力或响应特征后更新。

Session::send_token_count_event3308–3315 ↗
async fn send_token_count_event(&self, turn_context: &TurnContext)

作用:向外部发送当前 token 用量和速率限制信息。前端可用它更新进度、剩余上下文或限额显示。

数据流:输入是本轮上下文 → 它读取状态中的 token_info 和 rate_limits,包装成 TokenCountEvent → 通过 send_event 发出。

调用关系:recompute_token_usage、set_total_tokens_full、update_rate_limits、update_token_usage_info 都会调用它,是 token/限额信息的统一通知出口。

调用图:调用 1 个内部函数(send_event);被 4 处调用(recompute_token_usage, set_total_tokens_full, update_rate_limits, update_token_usage_info);外部调用 1 个(TokenCount)。

Session::set_total_tokens_full3317–3323 ↗
async fn set_total_tokens_full(&self, turn_context: &TurnContext)

作用:把 token 使用状态标记为已经占满上下文窗口,并立即通知外部。它用于明确告诉系统“空间满了”。

数据流:输入是本轮上下文 → 如果能取得模型上下文窗口,就把状态设置为满额;然后发送 TokenCount 事件 → 输出为空。

调用关系:它调用 send_token_count_event,和正常 token 更新共用同一个外部通知通道。

调用图:调用 2 个内部函数(send_token_count_event, model_context_window)。

Session::record_response_item_and_emit_turn_item3325–3339 ↗
async fn record_response_item_and_emit_turn_item(
        &self,
        turn_context: &TurnContext,
        response_item: ResponseItem,
    )

作用:记录一个模型或系统响应项,并在能解析成 turn item 时发出开始和完成事件。turn item 可以理解为前端展示的一小块对话进度。

数据流:输入是本轮上下文和一个 ResponseItem → 它先写入历史和持久化记录;再尝试解析成 TurnItem;如果成功,就依次发 started 和 completed 事件 → 输出为空。

调用关系:它调用 record_conversation_items 完成记账,再调用 emit_turn_item_started 和 emit_turn_item_completed 通知 UI 生命周期。

调用图:调用 3 个内部函数(emit_turn_item_completed, emit_turn_item_started, record_conversation_items);外部调用 2 个(parse_turn_item, from_ref)。

Session::record_user_prompt_and_emit_turn_item3341–3359 ↗
async fn record_user_prompt_and_emit_turn_item(
        &self,
        turn_context: &TurnContext,
        input: &[UserInput],
        client_id: Option<String>,
    )

作用:记录用户发来的提示词,并发出前端可展示的用户消息事件。它特别保留 UI 需要的富文本片段,不让它们在转换成历史项时丢掉。

数据流:输入是用户输入列表、本轮上下文和可选客户端消息 id → 它把输入转成 ResponseItem 写入历史;再直接从原始 UserInput 构造 UserMessageItem,附上 client_id,发 started/completed 事件;最后确保 rollout 已经 materialized → 输出为空。

调用关系:它调用 response_item_from_user_input、record_conversation_items、emit_turn_item_started、emit_turn_item_completed 和 ensure_rollout_materialized,是用户消息进入系统的关键记录点。

调用图:调用 6 个内部函数(emit_turn_item_completed, emit_turn_item_started, ensure_rollout_materialized, record_conversation_items, response_item_from_user_input, new);外部调用 3 个(to_vec, UserMessage, from_ref)。

Session::notify_stream_error3361–3377 ↗
async fn notify_stream_error(
        &self,
        turn_context: &TurnContext,
        message: impl Into<String>,
        codex_error: CodexErr,
    )

作用:当响应流断开或出错时,向外部发送清楚的错误事件。这样用户不会只看到沉默或卡住。

数据流:输入是本轮上下文、面向用户的错误消息和 CodexErr → 它提取详细错误文本和 HTTP 状态码,包装成 StreamErrorEvent → 通过 send_event 发出。

调用关系:它把底层网络/响应流错误转换成会话事件,供前端或调用方展示和处理。

调用图:调用 2 个内部函数(send_event, http_status_code_value);外部调用 3 个(into, to_string, StreamError)。

Session::steer_input3386–3459 ↗
async fn steer_input(
        &self,
        input: Vec<UserInput>,
        additional_context: BTreeMap<String, AdditionalContextEntry>,
        expected_turn_id: Option<&str>,
        client_user_me

作用:在一个普通回合还在运行时,追加用户输入来“ steering(引导)”当前任务。比如模型正在工作,用户又补一句要求,它会把这句放进当前轮输入队列。

数据流:输入是用户输入、附加上下文、期望的 turn_id、客户端消息 id 和可选响应 API 元数据 → 它确认当前有活跃普通任务,检查 turn_id 是否匹配,拒绝空输入;然后合并附加上下文,写入元数据,把附加上下文和用户输入放进 pending input 队列 → 输出当前活跃 turn_id;各种不满足条件会返回 SteerInputError。

调用关系:它直接操作 active_turn、state.additional_context 和 input_queue。它不会自己记录历史,而是把新输入排队给当前活跃 turn 后续消费。

调用图:外部调用 1 个(NoActiveTurn)。

Session::record_memory_citation_for_turn3461–3470 ↗
async fn record_memory_citation_for_turn(&self, sub_id: &str)

作用:标记某一轮出现过 memory citation,也就是引用过记忆内容。这个标记可用于后续展示或统计。

数据流:输入是 sub_id → 它通过 input_queue 找到对应 turn_state;找不到就结束;找到后把 has_memory_citation 设为 true → 输出为空。

调用关系:它依赖 input_queue 根据 sub_id 找到当前或相关 turn 状态,是一个小的状态标记函数。

Session::interrupt_task3472–3479 ↗
async fn interrupt_task(self: &Arc<Self>)

作用:处理中断请求,停止当前正在运行的任务。比如用户按了停止按钮时,就需要走这里。

数据流:没有业务输入 → 它记录日志,检查是否有活跃 turn,然后调用 abort_all_tasks 以 Interrupted 原因终止任务;如果本来没有活跃 turn,还会取消 MCP 启动 → 输出为空。

调用关系:它是会话中断入口,会把实际停止工作交给 abort_all_tasks,并在空闲但 MCP 正启动时调用 cancel_mcp_startup。

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

Session::hooks3481–3483 ↗
fn hooks(&self) -> Arc<Hooks>

作用:读取当前 hooks 配置对象。hooks 可以理解为某些时机自动运行的外部脚本或插件回调。

数据流:没有业务输入 → 它从 services 中加载 hooks 的 Arc 指针 → 输出 Hooks 的共享引用。

调用关系:它是会话访问 hook 系统的简单入口;build_hooks_for_config 负责构造 Hooks。

Session::user_shell3485–3487 ↗
fn user_shell(&self) -> Arc<shell::Shell>

作用:返回用户 shell 配置。shell 就是运行命令时使用的命令解释器,比如 bash、zsh 或类似环境。

数据流:没有业务输入 → 它克隆 services.user_shell 的 Arc → 输出共享的 Shell 对象。

调用关系:build_initial_context 和 build_settings_update_items 会用它把环境信息写进模型上下文。

调用图:被 2 处调用(build_initial_context, build_settings_update_items);外部调用 1 个(clone)。

Session::current_rollout_path3489–3494 ↗
async fn current_rollout_path(&self) -> anyhow::Result<Option<PathBuf>>

作用:查询当前会话流水账文件在本地的路径。这个路径可给 hook 或外部工具读取 transcript。

数据流:没有业务输入 → 它先找 live_thread;没有则返回 Ok(None);有则询问 local_rollout_path 并把错误转成 anyhow 错误 → 输出可选 PathBuf。

调用关系:hook_transcript_path 会调用它,并在需要前先确保 rollout 已经落地。

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

Session::hook_transcript_path3496–3505 ↗
async fn hook_transcript_path(&self) -> Option<PathBuf>

作用:给 hook 系统准备 transcript 文件路径。它先确保会话记录已经实际生成,再返回路径。

数据流:没有业务输入 → 它调用 ensure_rollout_materialized,然后调用 current_rollout_path;成功就返回路径,失败就警告并返回 None → 输出可选 PathBuf。

调用关系:它是 hooks 获取会话记录位置的安全包装,避免 hook 去读一个还没生成的文件。

调用图:调用 2 个内部函数(current_rollout_path, ensure_rollout_materialized);外部调用 1 个(warn!)。

Session::take_pending_session_start_source3507–3512 ↗
async fn take_pending_session_start_source(
        &self,
    ) -> Option<codex_hooks::SessionStartSource>

作用:取走并清空待处理的会话开始来源。来源可说明这次启动是普通开始、压缩后继续等。

数据流:没有业务输入 → 它锁住状态并调用 take_pending_session_start_source → 输出可选 SessionStartSource,同时状态中的待处理值被消费掉。

调用关系:replace_compacted_history 和 maybe_start_new_context_window 会设置 Compact 来源;这个函数在后续需要触发 hook 或记录来源时取出来。

Session::show_raw_agent_reasoning3514–3516 ↗
fn show_raw_agent_reasoning(&self) -> bool

作用:查询是否允许显示原始代理推理内容。原始推理可能比较敏感,所以由配置控制。

数据流:没有业务输入 → 它读取 services.show_raw_agent_reasoning → 输出布尔值。

调用关系:send_event 会调用它,决定事件发送时是否包含或展示原始推理信息。

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

emit_subagent_session_started3519–3555 ↗
fn emit_subagent_session_started(
    analytics_events_client: &AnalyticsEventsClient,
    client_metadata: AppServerClientMetadata,
    session_id: SessionId,
    thread_id: ThreadId,
    parent_thre

作用:记录一个子代理线程启动的分析事件。子代理可以理解为主任务派出去帮忙的另一个小助手。

数据流:输入是分析客户端、客户端元数据、会话和线程 id、父线程 id、线程配置和子代理来源 → 它先确认客户端名称和版本都存在;缺少就跳过并警告;存在则生成当前时间,把各种 id 和配置转成字符串,调用 track_subagent_thread_started 上报 → 输出为空。

调用关系:run_codex_thread_interactive 会在启动子代理线程时调用它。它不参与会话业务决策,只负责分析埋点。

调用图:调用 1 个内部函数(track_subagent_thread_started);被 1 处调用(run_codex_thread_interactive);外部调用 4 个(now, to_string, to_string, warn!)。

build_hooks_for_config3558–3586 ↗
async fn build_hooks_for_config(
    config: &Config,
    plugins_manager: &PluginsManager,
    environment: Option<&TurnEnvironment>,
) -> Hooks

作用:根据配置、插件和当前环境构造 Hooks 对象。它把旧的 notify 命令、插件 hook、shell 参数和信任设置合成一套可执行的 hook 配置。

数据流:输入是 Config、PluginsManager 和可选 TurnEnvironment → 它从环境 shell 推导 hook 执行程序和参数,读取插件配置并加载插件结果,取出插件 hook 来源和警告,然后创建 HooksConfig 并构造 Hooks → 输出 Hooks。

调用关系:refresh_runtime_config 会调用它,在运行时配置刷新时重建 hook 系统。它会向 plugins_manager 查询插件,再把结果交给 Hooks::new。

调用图:调用 2 个内部函数(plugins_for_config, new);被 1 处调用(refresh_runtime_config);外部调用 1 个(plugins_config_input)。

core/src/session/session.rs源码 ↗
orchestrationstartup, config update, cross-cutting

可以把这个文件理解成“开一间工作室”的总开关。Session 是已经开起来的工作室,里面有当前线程编号、事件出口、运行状态、输入队列、工具服务、网络代理、环境快照等。SessionConfiguration 是开工前的配置单,写着模型、审批规则、沙箱权限、工作目录、插件、客户端信息等。沙箱就是一圈安全围栏,限制程序能碰哪些文件、能不能访问网络。这个文件一方面提供很多小方法,让别的模块用统一方式读取这些配置;另一方面,Session::new 会真正把会话搭起来:恢复或创建历史记录,准备认证、MCP 工具服务、插件和技能,读取项目说明,建立遥测统计,选择 shell,启动网络代理,发出“会话已配置”的事件。它还很小心地处理权限更新,避免目录变了以后,文件读写范围跟着错乱。

函数细节19
SessionConfiguration::cwd113–115 ↗
fn cwd(&self) -> &AbsolutePathBuf

作用:返回这个会话默认工作的目录,也就是模型或工具在没有特别指定环境时,会把哪里当作“当前文件夹”。别人需要知道项目根在哪里时会用它。

数据流:进去的是这份会话配置本身 → 它从环境选择配置里取出 legacy_fallback_cwd 这个备用当前目录 → 出来的是一个目录引用,不复制也不修改任何东西。

调用关系:Session::new 启动会话时会用它记录和检查工作目录;apply 更新配置时用它判断目录有没有变;sandbox_policy 用它把权限围栏套到正确目录上;build_effective_session_config 也会拿它生成最终配置。

调用图:被 4 处调用(new, apply, sandbox_policy, build_effective_session_config)。

SessionConfiguration::environment_selections117–119 ↗
fn environment_selections(&self) -> &[TurnEnvironmentSelection]

作用:返回这个会话保存的环境选择列表。环境可以理解成“这一轮对话要在哪个工作空间或运行环境里办事”。

数据流:进去的是会话配置 → 它读取 environments 里的 environments 列表 → 出来的是这组环境选择的只读引用,不改变配置。

调用关系:Session::new 会在启动时把这些选择交给环境管理器;resolved_environments_for_configuration 会用它把配置里的选择解析成真正可用的运行环境。

调用图:被 2 处调用(new, resolved_environments_for_configuration)。

SessionConfiguration::codex_home121–123 ↗
fn codex_home(&self) -> &AbsolutePathBuf

作用:返回这个会话存放 Codex 自己状态文件的目录。比如配置、缓存、会话数据等都可能围绕这个目录工作。

数据流:进去的是会话配置 → 它直接取出 codex_home 字段 → 出来的是这个目录的只读引用,没有副作用。

调用关系:这是一个简单的访问口,让其他代码不用直接碰字段;当外部需要知道 Codex 的家目录时,可以从这里拿。

SessionConfiguration::permission_profile_state125–127 ↗
fn permission_profile_state(&self) -> &PermissionProfileState

作用:返回当前权限配置的完整状态。权限配置决定模型和工具能读写哪些文件、能不能联网、当前启用的是哪套权限档案。

数据流:进去的是会话配置 → 它取出 permission_profile_state → 出来的是只读引用,不做计算、不改状态。

调用关系:Session::new 在发出会话配置事件时会间接用到这类权限信息;其他需要查看权限状态的流程通过这个入口保持一致,避免绕过统一规则。

SessionConfiguration::permission_profile129–134 ↗
fn permission_profile(&self) -> PermissionProfile

作用:生成当前真正生效的权限档案。它不只是拿原始配置,还会把“项目根目录”这种占位权限换成当前会话里的真实工作区目录。

数据流:进去的是会话配置和里面保存的工作区根目录列表 → 它先从 permission_profile_state 取出权限档案,再把其中的项目根占位符替换成 workspace_roots → 出来的是一份可直接执行的权限档案副本。

调用关系:Session::new 用它告诉前端和网络代理当前权限;sandbox_policy、file_system_sandbox_policy、thread_config_snapshot、build_per_turn_config、make_turn_context、new_turn_context_from_configuration 都会用它,保证每一轮执行看到的是同一套权限解释。

调用图:调用 1 个内部函数(permission_profile);被 7 处调用(new, file_system_sandbox_policy, sandbox_policy, thread_config_snapshot, build_per_turn_config, make_turn_context, new_turn_context_from_configuration)。

SessionConfiguration::active_permission_profile136–138 ↗
fn active_permission_profile(&self) -> Option<ActivePermissionProfile>

作用:返回当前正在使用的命名权限档案,如果有的话。它回答的是“用户现在选中的是哪套权限模板”。

数据流:进去的是会话配置 → 它向 permission_profile_state 查询活动权限档案 → 出来的是一个可选结果;没有活动档案时就是空。

调用关系:Session::new 会把这个信息发给界面;thread_config_snapshot 也会把它放进线程配置快照,方便恢复或展示当前权限来源。

调用图:调用 1 个内部函数(active_permission_profile);被 2 处调用(new, thread_config_snapshot)。

SessionConfiguration::profile_workspace_roots140–142 ↗
fn profile_workspace_roots(&self) -> &[AbsolutePathBuf]

作用:返回权限档案自己声明的工作区根目录列表。它用于说明某套权限模板本来想绑定到哪些项目目录。

数据流:进去的是会话配置 → 它从 permission_profile_state 读取 profile_workspace_roots → 出来的是目录列表引用,不修改内容。

调用关系:thread_config_snapshot 会把这些目录写进快照,让外部看到权限档案和工作区之间的对应关系。

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

SessionConfiguration::apply_permission_profile_to_permissions144–149 ↗
fn apply_permission_profile_to_permissions(
        &self,
        permissions: &mut crate::config::Permissions,
    )

作用:把会话当前的权限档案状态写进另一份 Permissions 配置里。这样按轮次生成配置时,不会丢掉会话里已经调整过的权限。

数据流:进去的是会话配置和一份可修改的 Permissions → 它复制当前 permission_profile_state,并调用目标对象的设置方法 → 出来时目标 Permissions 已经带上同一份权限状态,函数本身不返回值。

调用关系:build_per_turn_config 会调用它,把线程级别的权限同步到每一轮实际执行用的配置中。它把“会话配置”这边的决定传给“执行配置”那边。

调用图:调用 1 个内部函数(set_permission_profile_state);被 1 处调用(build_per_turn_config);外部调用 1 个(clone)。

SessionConfiguration::set_permission_profile_for_tests152–158 ↗
fn set_permission_profile_for_tests(
        &mut self,
        permission_profile: PermissionProfile,
    ) -> ConstraintResult<()>

作用:这是测试专用入口,用来强行给会话配置设置一套旧格式的权限档案。正常运行代码不会依赖它。

数据流:进去的是可修改的会话配置和一份权限档案 → 它把这份档案交给 permission_profile_state 的旧格式设置方法 → 出来的是成功或约束错误;成功时配置里的权限状态被改掉。

调用关系:它只在测试编译时存在,方便测试代码快速搭出某种权限场景,而不用走完整的用户配置流程。

调用图:调用 1 个内部函数(set_legacy_permission_profile)。

SessionConfiguration::sandbox_policy160–166 ↗
fn sandbox_policy(&self) -> SandboxPolicy

作用:把当前权限档案转换成旧接口能理解的沙箱策略。沙箱策略就是“安全围栏说明书”,告诉执行命令时哪些地方可以碰、哪些不可以。

数据流:进去的是会话配置 → 它先拿到当前 permission_profile,再读取 cwd 作为目录参照,然后调用兼容转换函数 → 出来的是一份 SandboxPolicy,不改原配置。

调用关系:apply 更新配置时会用它比较旧沙箱和新权限之间的关系;Session::new 也会通过它记录启动时的权限信息。它是新权限系统和旧沙箱接口之间的桥。

调用图:调用 2 个内部函数(cwd, permission_profile);被 1 处调用(apply);外部调用 1 个(compatibility_sandbox_policy_for_permission_profile)。

SessionConfiguration::file_system_sandbox_policy168–170 ↗
fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy

作用:取出当前文件系统层面的安全规则,也就是哪些文件夹能读、哪些能写、哪些要禁止。

数据流:进去的是会话配置 → 它先生成当前 permission_profile,再从里面取文件系统沙箱策略 → 出来的是 FileSystemSandboxPolicy。

调用关系:apply 会用它判断权限更新时是否需要重新计算文件访问规则,尤其是在工作目录变化时避免把可写范围指到错误地方。

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

SessionConfiguration::network_sandbox_policy172–176 ↗
fn network_sandbox_policy(&self) -> NetworkSandboxPolicy

作用:取出当前网络访问规则,也就是工具能不能联网、能访问哪些网络资源。

数据流:进去的是会话配置 → 它从 permission_profile_state 的原始权限档案中读取网络沙箱策略 → 出来的是 NetworkSandboxPolicy,不改变配置。

调用关系:apply 在处理权限或旧沙箱更新时会用它保留现有网络规则,防止只改目录时把联网设置意外重置。

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

SessionConfiguration::thread_config_snapshot178–200 ↗
fn thread_config_snapshot(&self) -> ThreadConfigSnapshot

作用:把当前会话配置打包成一张“线程配置快照”。快照就像拍照,方便界面展示、日志记录或后续恢复时知道当时的设置。

数据流:进去的是会话配置 → 它收集模型、服务档位、审批策略、权限档案、环境、工作区、推理强度、人格偏好、线程来源等信息 → 出来的是 ThreadConfigSnapshot;原配置不变。

调用关系:它会调用 permission_profile、active_permission_profile、profile_workspace_roots 等小入口,把分散字段整理成一个完整视图。需要向外报告线程当前配置时,会用这个快照。

调用图:调用 6 个内部函数(value, active_permission_profile, permission_profile, profile_workspace_roots, model, reasoning_effort);外部调用 3 个(clone, clone, clone)。

SessionConfiguration::apply202–374 ↗
fn apply(&self, updates: &SessionSettingsUpdate) -> ConstraintResult<Self>

作用:根据一份更新请求,生成一份新的会话配置。它像“改配置的安全柜台”,负责检查哪些能改、怎么改,以及权限变化会不会破坏安全边界。

数据流:进去的是当前配置和 SessionSettingsUpdate → 它先复制当前配置,再逐项应用模型模式、摘要、服务档位、人格、审批、Windows 沙箱、环境、工作区、权限档案、旧沙箱和客户端信息等更新;遇到不合法约束就返回错误 → 出来的是新的 SessionConfiguration,原来的配置保持不动。

调用关系:它内部会调用 cwd、sandbox_policy、file_system_sandbox_policy、network_sandbox_policy 和 set_permission_profile_projection 等方法。它位于“用户或客户端想改设置”和“会话真正采用新设置”之间,负责把变更变成安全一致的新配置。

调用图:调用 10 个内部函数(active, cwd, file_system_sandbox_policy, network_sandbox_policy, sandbox_policy, from_request_value, from_runtime_permissions_with_enforcement, from_legacy_sandbox_policy, from_legacy_sandbox_policy_preserving_deny_entries, from);外部调用 2 个(new, with_capacity)。

SessionConfiguration::set_permission_profile_projection376–410 ↗
fn set_permission_profile_projection(
        &mut self,
        permission_profile: PermissionProfile,
        active_permission_profile: Option<ActivePermissionProfile>,
        profile_workspace_ro

作用:把一份高层权限档案翻译成运行时真正使用的文件和网络规则,并写回配置状态。它负责让“权限模板”落地成能执行的安全规则。

数据流:进去的是可修改的会话配置、一份权限档案、可选的活动权限档案、工作区根目录,以及可选的旧文件规则 → 它提取执行强度,拆出文件和网络规则,必要时保留原有的拒绝读取限制,再组装成权限快照 → 出来的是成功或错误;成功时 permission_profile_state 被更新。

调用关系:apply 在用户显式更新 permission_profile 时调用它。它把权限更新里最容易出错的翻译工作集中到一处,避免外面直接改多个字段导致不一致。

调用图:调用 6 个内部函数(active_with_profile_workspace_roots, legacy, set_permission_profile_snapshot, enforcement, from_runtime_permissions_with_enforcement, to_runtime_permissions)。

warm_plugins_and_skills_for_session_init438–453 ↗
async fn warm_plugins_and_skills_for_session_init(
    config: Arc<Config>,
    plugins_manager: Arc<PluginsManager>,
    skills_manager: Arc<SkillsManager>,
    turn_environments: &TurnEnvironmentSna

作用:在会话刚启动时提前加载插件和技能,并收集加载失败的信息。这样用户真正开始用工具时,很多准备工作已经做完,出错也能早点暴露。

数据流:进去的是配置、插件管理器、技能管理器和当前环境快照 → 它取主文件系统,按配置找出可用插件,再根据插件带来的技能目录加载技能 → 出来的是技能加载错误列表;插件和技能管理器内部可能完成缓存或预热。

调用关系:Session::new 在启动流程中调用它,并把失败的技能加载记录到日志里。它是会话启动阶段“准备扩展能力”的一环。

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

Session::thread_id457–459 ↗
fn thread_id(&self) -> ThreadId

作用:返回这个会话对应的具体线程编号。线程可以理解成一条具体对话线,恢复历史或区分子任务时都靠它识别。

数据流:进去的是 Session → 它直接读取 thread_id 字段 → 出来的是 ThreadId 值,不修改会话。

调用关系:这是给其他模块查身份用的小入口。需要知道“当前这条对话线是谁”时,可以调用它,而不用直接访问字段。

Session::session_id462–464 ↗
fn session_id(&self) -> SessionId

作用:返回根线程和它的子线程共同使用的会话编号。它比 thread_id 更像“一整组相关对话”的总编号。

数据流:进去的是 Session → 它向 services.agent_control 查询 session_id → 出来的是 SessionId,不改变状态。

调用关系:Session::new 会在建立 agent_control 时安排好这个编号;之后外部需要按整组会话归类事件、子代理或控制关系时,会通过这个方法拿到统一身份。

Session::new468–1192 ↗
async fn new(
        mut session_configuration: SessionConfiguration,
        config: Arc<Config>,
        user_instructions: Option<codex_extension_api::UserInstructions>,
        installation_id: S

作用:创建并启动一个完整会话。它不是简单分配一个对象,而是把历史、认证、环境、权限、插件、MCP 工具、网络代理、遥测和前端事件都一起准备好。

数据流:进去的是会话配置、全局配置、用户说明、认证管理器、模型管理器、执行策略、事件通道、历史来源、插件和技能管理器、MCP 管理器、扩展、环境管理器、线程存储、追踪信息等大量启动材料 → 它并行做线程持久化、状态库读取、认证和 MCP 配置准备,然后选择线程编号,加载项目说明,预热插件技能,检查配置锁,启动网络代理,创建服务集合和 Session 对象,发出 SessionConfigured 事件,安装 MCP 连接管理器,记录初始历史 → 出来的是 Arc<Session>;如果中途失败,会丢弃未提交的线程初始化,避免留下半成品状态。

调用关系:spawn_internal、测试辅助创建函数和相关测试会调用它来真正开会话。它会把许多具体工作交给别的模块,比如 LiveThread 创建或恢复历史、ModelClient 建模型客户端、McpConnectionManager 启动工具连接、warm_plugins_and_skills_for_session_init 预热技能。它是本文件最核心的启动编排器。

调用图:调用 34 个内部函数(new, new, new_uninitialized_with_permission_profile, new, new, session_id, with_session_id, new, new, new (+15 more));被 4 处调用(spawn_internal, make_session_with_config_and_rx, make_session_with_history_source_and_agent_control_and_rx, session_new_fails_when_zsh_fork_enabled_without_packaged_zsh);外部调用 39 个(clone, downgrade, new, new, pin, new, default, default, default, new (+15 more))。

core/src/session/input_queue.rs源码 ↗
domain_logic会话运行期间,尤其是每一轮对话接收输入、检查邮箱、取走待处理内容时

一次会话里,输入不一定只来自用户。还可能来自别的代理发来的“邮件”,也可能是运行中途追加的引导信息。这个文件就像前台接待:先把各种来件分门别类放好,等当前轮次能处理时再交出去。它有两层队列:TurnInputQueue 放当前这一轮还没处理的输入;InputQueue 放整个会话共享的代理邮件,并用 watch 通知机制(像一个广播铃,有新动静就提醒监听者)告诉外面现在是邮箱有消息,还是用户引导有消息。它还会配合 TurnState 判断当前轮是否允许收邮件;如果不允许,就把邮件留到下一轮,避免消息插队打乱当前处理。代码里大量使用 Mutex(互斥锁,一把锁,防止两个异步任务同时改同一份数据)来保证队列不会被并发改乱。

函数细节23
InputQueue::new41–47 ↗
fn new() -> Self

作用:创建一个空的输入队列。新会话开始时需要它来保存之后到来的用户输入和代理消息。

数据流:进去不需要任何参数 → 它建立一个活动通知通道,默认状态是“邮箱活动”,再准备一个空的邮箱消息队列 → 出来一个可以被会话共享使用的 InputQueue。

调用关系:它是整个输入队列的起点。测试代码和会话创建流程会先调用它,之后其他函数才会往里面放消息、订阅通知或取走输入。

调用图:被 8 处调用(input_queue_drains_mailbox_in_delivery_order, input_queue_notifies_mailbox_subscribers, input_queue_notifies_steer_subscribers, input_queue_reports_already_pending_steer, input_queue_tracks_pending_trigger_turn_mail, new, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx);外部调用 3 个(new, new, channel)。

InputQueue::subscribe_activity49–70 ↗
async fn subscribe_activity(
        &self,
        turn_state: Option<&Mutex<TurnState>>,
    ) -> (
        watch::Receiver<InputQueueActivity>,
        Option<InputQueueActivity>,
    )

作用:让外部监听这个输入队列的“新动静”。它还会顺手告诉调用者:现在是不是已经有没处理的用户引导或邮箱消息。

数据流:进去一个可选的当前轮状态 → 它先创建一个监听器,再检查当前轮里有没有用户输入;如果没有,再检查邮箱队列有没有消息 → 出来一个监听器,以及一个可选的当前待处理活动类型。

调用关系:它会调用 InputQueue::has_pending_mailbox_items 来判断邮箱是否已有积压。外部调度流程可以用它决定是继续睡眠,还是马上处理已有输入。

调用图:调用 1 个内部函数(has_pending_mailbox_items);外部调用 1 个(subscribe)。

InputQueue::enqueue_mailbox_communication72–81 ↗
async fn enqueue_mailbox_communication(
        &self,
        communication: InterAgentCommunication,
    )

作用:把一封代理之间的通信消息放进邮箱队列。这样消息不会丢,稍后会按顺序交给会话处理。

数据流:进去一条 InterAgentCommunication,也就是代理发给代理的消息 → 它拿锁打开邮箱队列,把消息追加到队尾,然后发出“邮箱有活动”的通知 → 队列变长,监听者会被提醒。

调用关系:它是邮箱消息进入 InputQueue 的入口。放入消息后,它通过通知通道提醒订阅者,后续通常由检查或取输入的流程把这些消息交给当前轮。

调用图:外部调用 1 个(send_replace)。

InputQueue::has_pending_mailbox_items83–85 ↗
async fn has_pending_mailbox_items(&self) -> bool

作用:快速回答一个问题:邮箱里现在有没有还没处理的消息。

数据流:进去不需要额外数据,只读取内部邮箱队列 → 它拿锁查看队列是否为空 → 出来 true 或 false,不改动队列。

调用关系:InputQueue::subscribe_activity 和 InputQueue::has_pending_input 会用它做判断:如果没有当前轮输入,但邮箱里有东西,就说明系统仍然有活要干。

调用图:被 2 处调用(has_pending_input, subscribe_activity)。

InputQueue::has_trigger_turn_mailbox_items87–93 ↗
async fn has_trigger_turn_mailbox_items(&self) -> bool

作用:检查邮箱里有没有会“唤醒一轮对话”的消息。有些代理邮件只是普通信息,有些则明确要求触发处理。

数据流:进去不需要参数,只读取邮箱队列 → 它逐封查看消息的 trigger_turn 标记 → 只要发现一封需要触发轮次的邮件,就返回 true,否则返回 false。

调用关系:调用图里没有显示它调用本文件其他函数。它通常作为调度判断用:决定邮箱消息是否强到需要启动或唤醒一轮处理。

InputQueue::drain_mailbox_input_items95–102 ↗
async fn drain_mailbox_input_items(&self) -> Vec<TurnInput>

作用:一次性取走邮箱里所有待处理消息,并把它们包装成当前轮可处理的输入。

数据流:进去不需要参数,只操作内部邮箱队列 → 它拿锁把队列从头到尾清空,每封代理邮件都变成 TurnInput::InterAgentCommunication → 出来一个按原顺序排列的输入列表,邮箱队列被清空。

调用关系:InputQueue::get_pending_input 会调用它。当当前轮允许接收邮箱消息时,get_pending_input 会把这些邮件和当前轮已有输入合并后交出去。

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

InputQueue::turn_state_for_sub_id104–117 ↗
async fn turn_state_for_sub_id(
        &self,
        active_turn: &Mutex<Option<ActiveTurn>>,
        sub_id: &str,
    ) -> Option<Arc<Mutex<TurnState>>>

作用:根据一个子会话 ID 找到当前活动轮次的状态。这样只有匹配的那一轮才会被改动,避免改错对象。

数据流:进去当前活动轮的共享容器和一个 sub_id → 它拿锁查看当前是否有活动轮,再比较活动轮任务里的 sub_id 是否一致 → 如果匹配,出来当前轮状态的共享引用;不匹配或没有活动轮就出来 None。

调用关系:InputQueue::defer_mailbox_delivery_to_next_turn 和 InputQueue::accept_mailbox_delivery_for_current_turn 都先调用它定位正确的 TurnState,然后才改变邮箱投递策略。

调用图:被 2 处调用(accept_mailbox_delivery_for_current_turn, defer_mailbox_delivery_to_next_turn)。

InputQueue::clear_pending120–124 ↗
async fn clear_pending(&self, active_turn: &ActiveTurn)

作用:清掉当前轮里还没处理的等待者和输入。通常用于一轮被取消、结束或需要重置时,避免旧东西影响后续处理。

数据流:进去一个当前活动轮 → 它锁住该轮状态,清空等待中的唤醒点,再清空当前轮 pending_input 里的所有输入 → 出来没有返回值,但当前轮的积压内容被清干净。

调用关系:调用图里没有显示它调用本文件其他函数。它直接操作 TurnState,属于清理当前轮内部状态的工具。

InputQueue::defer_mailbox_delivery_to_next_turn126–140 ↗
async fn defer_mailbox_delivery_to_next_turn(
        &self,
        active_turn: &Mutex<Option<ActiveTurn>>,
        sub_id: &str,
    )

作用:把邮箱消息的投递推迟到下一轮。这样当前轮如果不适合被代理邮件打断,就不会被强行插入新消息。

数据流:进去当前活动轮容器和 sub_id → 它先用 InputQueue::turn_state_for_sub_id 找到匹配的轮状态;如果找不到就什么也不做;如果当前轮已经有待处理输入,也不改;否则把邮箱投递阶段设成“下一轮” → 当前轮会暂时拒收邮箱消息。

调用关系:它依赖 InputQueue::turn_state_for_sub_id 防止改错轮次。它本身不取走邮箱消息,只改变 TurnState 里的投递策略,让之后的取输入流程按这个策略办。

调用图:调用 1 个内部函数(turn_state_for_sub_id)。

InputQueue::accept_mailbox_delivery_for_current_turn142–153 ↗
async fn accept_mailbox_delivery_for_current_turn(
        &self,
        active_turn: &Mutex<Option<ActiveTurn>>,
        sub_id: &str,
    )

作用:允许当前这一轮接收邮箱消息。之前被推迟的邮箱投递,可以通过它重新打开。

数据流:进去当前活动轮容器和 sub_id → 它先找到匹配的轮状态;找不到就返回;找到后把工作交给 InputQueue::accept_mailbox_delivery_for_turn_state → 对应轮次的邮箱投递许可被打开。

调用关系:它先调用 InputQueue::turn_state_for_sub_id 定位轮次,再调用 InputQueue::accept_mailbox_delivery_for_turn_state 做实际修改。它是按 sub_id 操作当前轮的安全入口。

调用图:调用 2 个内部函数(accept_mailbox_delivery_for_turn_state, turn_state_for_sub_id)。

InputQueue::accept_mailbox_delivery_for_turn_state155–163 ↗
async fn accept_mailbox_delivery_for_turn_state(
        &self,
        turn_state: &Mutex<TurnState>,
    )

作用:直接把某个 TurnState 设置为“本轮可以收邮箱消息”。这是更底层的版本,调用者已经拿到了具体轮状态。

数据流:进去一个 TurnState 的锁 → 它锁住状态并调用状态自己的接受邮箱投递方法 → 出来没有返回值,但该轮的邮箱投递标记被更新。

调用关系:InputQueue::accept_mailbox_delivery_for_current_turn 会调用它。它不负责找是哪一轮,只负责对已经给定的 TurnState 执行修改。

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

InputQueue::extend_pending_input_and_accept_mailbox_delivery_for_turn_state165–176 ↗
async fn extend_pending_input_and_accept_mailbox_delivery_for_turn_state(
        &self,
        turn_state: &Mutex<TurnState>,
        input: Vec<TurnInput>,
    )

作用:给某一轮追加待处理输入,并同时允许这一轮接收邮箱消息。常见场景是用户又发来了引导内容,需要马上提醒系统处理。

数据流:进去一个 TurnState 和一批 TurnInput → 它锁住轮状态,把这批输入追加到 pending_input 后面,再打开本轮邮箱投递许可;解锁后发送“用户引导有活动”的通知 → 当前轮多了新输入,监听者也会收到提醒。

调用关系:测试里用它验证 Steer 通知。它会通过通知通道发出 InputQueueActivity::Steer,让订阅者知道现在不是普通邮箱消息,而是有用户侧输入需要处理。

调用图:外部调用 1 个(send_replace)。

InputQueue::extend_pending_input_for_turn_state178–184 ↗
async fn extend_pending_input_for_turn_state(
        &self,
        turn_state: &Mutex<TurnState>,
        input: Vec<TurnInput>,
    )

作用:只给某一轮追加待处理输入,不额外发送活动通知,也不改变邮箱投递许可。

数据流:进去一个 TurnState 和一批 TurnInput → 它锁住轮状态,把输入追加到 pending_input 队尾 → 出来没有返回值,但该轮的待处理输入变多。

调用关系:调用图里没有显示它调用本文件其他函数。它适合那些调用者自己会处理通知或投递策略、这里只想安静地塞入输入的场景。

InputQueue::take_pending_input_for_turn_state186–191 ↗
async fn take_pending_input_for_turn_state(
        &self,
        turn_state: &Mutex<TurnState>,
    ) -> Vec<TurnInput>

作用:把某一轮已经排队的输入全部取走。取走后,这些输入就不再留在该轮队列里。

数据流:进去一个 TurnState → 它锁住状态,把 pending_input.items 从开头整体切出来 → 出来这批 TurnInput,原来的 pending_input 变为空。

调用关系:调用图里没有显示它调用本文件其他函数。它是一个针对指定 TurnState 的低层取件方法,和 InputQueue::get_pending_input 相比,它不自动合并邮箱消息。

InputQueue::get_pending_input197–225 ↗
async fn get_pending_input(
        &self,
        active_turn: &Mutex<Option<ActiveTurn>>,
    ) -> Vec<TurnInput>

作用:取出现在应该交给处理流程的所有输入。它会同时考虑当前轮自己的输入,以及当前轮是否允许接收邮箱消息。

数据流:进去当前活动轮容器 → 它先在一个受锁保护的步骤里取走当前轮 pending_input,并读取本轮是否允许收邮箱;如果不允许,就只返回当前轮输入;如果允许,就再清空邮箱队列,把邮箱消息接到后面 → 出来一批要处理的 TurnInput,相关队列里的这些内容被移走。

调用关系:它会调用 InputQueue::drain_mailbox_input_items 来取邮箱消息。它是输入真正从队列流向会话处理流程的关键出口。

调用图:调用 1 个内部函数(drain_mailbox_input_items);外部调用 1 个(new)。

InputQueue::has_pending_input231–252 ↗
async fn has_pending_input(&self, active_turn: &Mutex<Option<ActiveTurn>>) -> bool

作用:判断当前是否有任何输入值得处理。它会同时看当前轮输入和邮箱消息,但会尊重当前轮是否允许收邮箱。

数据流:进去当前活动轮容器 → 它先看当前轮 pending_input 是否为空,并读取本轮邮箱投递许可;如果当前轮已有输入,直接返回 true;如果不允许收邮箱,返回 false;否则再检查邮箱队列 → 出来 true 或 false,不取走任何输入。

调用关系:它会调用 InputQueue::has_pending_mailbox_items。调度器可以先用它做轻量检查,确认有活之后再调用 InputQueue::get_pending_input 真正取走内容。

调用图:调用 1 个内部函数(has_pending_mailbox_items)。

TurnInputQueue::has_user_input256–260 ↗
fn has_user_input(&self) -> bool

作用:检查当前轮待处理输入里有没有真正来自用户的输入。这个判断用来区分“用户引导”和普通代理邮件。

数据流:进去不需要参数,只读取当前 TurnInputQueue 的 items → 它逐项查看是否有 TurnInput::UserInput → 有就返回 true,没有就返回 false,队列内容不变。

调用关系:InputQueue::subscribe_activity 会通过当前轮状态里的 pending_input 使用它。如果发现已有用户输入,订阅者会立即得到 Steer 这种活动提示。

tests::make_mail269–282 ↗
fn make_mail(
        author: AgentPath,
        recipient: AgentPath,
        content: &str,
        trigger_turn: bool,
    ) -> InterAgentCommunication

作用:测试用的小工厂函数,用来快速造一封代理之间的消息。这样每个测试不用重复写一大段创建邮件的代码。

数据流:进去作者路径、收件人路径、正文和是否触发轮次的标记 → 它把正文转成字符串,并调用 InterAgentCommunication 的创建方法 → 出来一封可放进邮箱队列的测试邮件。

调用关系:多个测试会调用它来准备邮箱消息。它让测试重点放在队列行为上,而不是放在怎么构造消息对象上。

调用图:调用 1 个内部函数(new);外部调用 1 个(new)。

tests::input_queue_notifies_mailbox_subscribers285–313 ↗
async fn input_queue_notifies_mailbox_subscribers()

作用:验证邮箱收到新消息时,订阅者真的会被通知。没有这个保证,外部调度器可能不知道有新邮件到了。

数据流:进去没有外部输入 → 测试创建 InputQueue 并订阅活动,确认一开始没有待处理活动;随后构造并放入两封邮箱消息;最后等待通知并检查通知类型 → 测试通过表示邮箱活动能正确广播。

调用关系:它调用 InputQueue::new 创建队列,并使用 tests::make_mail 准备消息。它覆盖的是 InputQueue::enqueue_mailbox_communication 发出 Mailbox 通知这条路径。

调用图:调用 3 个内部函数(new, root, try_from);外部调用 2 个(assert_eq!, make_mail)。

tests::input_queue_notifies_steer_subscribers316–338 ↗
async fn input_queue_notifies_steer_subscribers()

作用:验证给当前轮追加用户输入时,订阅者会收到“用户引导”通知。这样系统能优先知道用户又插话或补充要求了。

数据流:进去没有外部输入 → 测试创建队列和默认 TurnState,先订阅活动;然后追加一条用户文本输入;最后等待通知并确认类型是 Steer → 测试通过表示用户侧输入能正确唤醒监听者。

调用关系:它调用 InputQueue::new 和 TurnState 的默认构造,并通过 InputQueue::extend_pending_input_and_accept_mailbox_delivery_for_turn_state 触发通知。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, default, assert_eq!, vec!)。

tests::input_queue_reports_already_pending_steer341–361 ↗
async fn input_queue_reports_already_pending_steer()

作用:验证如果订阅前已经有用户输入积压,订阅时也能立刻看见这个事实。这样监听者不会错过早到的输入。

数据流:进去没有外部输入 → 测试先创建队列和轮状态,再提前塞入一条用户输入;之后才订阅活动;最后检查返回的 pending_activity 是 Steer → 测试通过表示旧积压也会被报告。

调用关系:它调用 InputQueue::new,并用 InputQueue::extend_pending_input_and_accept_mailbox_delivery_for_turn_state 先制造积压,再检查 InputQueue::subscribe_activity 的即时判断。

调用图:调用 1 个内部函数(new);外部调用 4 个(new, default, assert_eq!, vec!)。

tests::input_queue_drains_mailbox_in_delivery_order364–394 ↗
async fn input_queue_drains_mailbox_in_delivery_order()

作用:验证邮箱消息被取出时保持原来的投递顺序,并且取完后邮箱会变空。顺序错了,代理之间的对话就可能乱套。

数据流:进去没有外部输入 → 测试创建两封不同方向的邮件,按顺序放入队列;然后一次性取走邮箱输入;最后比较取出的列表顺序,并确认邮箱已空 → 测试通过表示 drain 行为正确。

调用关系:它调用 InputQueue::new 和 tests::make_mail 准备场景,重点覆盖 InputQueue::drain_mailbox_input_items 的顺序和清空效果。

调用图:调用 3 个内部函数(new, root, try_from);外部调用 3 个(assert!, assert_eq!, make_mail)。

tests::input_queue_tracks_pending_trigger_turn_mail397–419 ↗
async fn input_queue_tracks_pending_trigger_turn_mail()

作用:验证队列能分辨邮箱里是否有“需要触发一轮处理”的邮件。这关系到系统该不该被一封邮件唤醒。

数据流:进去没有外部输入 → 测试先放入一封不触发轮次的邮件,确认检查结果为 false;再放入一封 trigger_turn 为 true 的邮件,确认检查结果变成 true → 测试通过表示触发标记能被正确发现。

调用关系:它调用 InputQueue::new 和 tests::make_mail 构造两种邮件,重点覆盖 InputQueue::has_trigger_turn_mailbox_items 的判断。

调用图:调用 3 个内部函数(new, root, try_from);外部调用 2 个(assert!, make_mail)。

core/src/session/inject.rs源码 ↗
orchestrationrequest handling / main loop

这里的核心问题很像前台排队:如果窗口正在办业务,新材料可以递进去;如果窗口没人,不能随便开窗口,得先看有没有用户已经排队、现在是不是“计划模式”、后台有没有活还没干完。文件给 Session 加了几种“注入输入”的办法。ResponseItem 可以理解成一条要交给会话处理的消息或结果。inject_if_running 只在已有活跃轮次时把消息塞进去;try_start_turn_if_idle 更谨慎,专门给扩展或后台自动工作用,只有系统真正空闲、没有用户触发的待办、也不在 Plan 模式(只规划不执行的模式)时才开新一轮;inject_no_new_turn 则保证不启动新任务,只记录内容。中间用 active_turn 这把异步锁保护当前轮次,避免两个地方同时抢着改“现在谁在工作”。

函数细节4
Session::inject_if_running19–36 ↗
async fn inject_if_running(
        &self,
        input: Vec<ResponseItem>,
    ) -> Result<(), Vec<ResponseItem>>

作用:这个函数尝试把一批新消息塞进当前正在运行的会话轮次里。它不会自己开新任务;如果现在没有正在运行的轮次,就把原消息退回给调用者。

数据流:进去的是一组 ResponseItem。函数先拿到 active_turn 这把锁,看当前有没有活跃轮次;有的话,就把这些消息包装成 TurnInput,追加到这个轮次对应的输入队列里,然后返回成功。没有活跃轮次时,它什么也不改,把原来的消息作为错误结果返回,表示“没人可塞”。

调用关系:它是“能塞就塞”的基础工具。Session::inject_no_new_turn 会先调用它:如果成功,事情结束;如果失败,inject_no_new_turn 再改走“只记录、不启动”的路线。

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

Session::try_start_turn_if_idle45–130 ↗
async fn try_start_turn_if_idle(
        self: &Arc<Self>,
        input: Vec<ResponseItem>,
    ) -> Result<(), TryStartTurnIfIdleError>

作用:这个函数是在系统空闲时,尝试为后台或扩展发来的内容新开一轮普通工作。它像一个门卫:只有确认没有用户排队、没有任务在跑、也不是 Plan 模式,才允许开工。

数据流:进去的是一组要处理的 ResponseItem。函数先过滤空输入;再检查输入队列里有没有用户或客户端触发的新轮次、当前协作模式是不是 Plan、当前是否已有活跃任务。都没问题时,它会先“预占”一个空的活跃轮次,创建新的轮次上下文和唯一子 ID,再反复检查中途有没有更高优先级的用户工作插进来。最后确认预占还有效后,把输入放进该轮次队列,创建 RegularTask 并启动任务。若任何检查失败,它会返回带原因和原输入的错误,必要时清掉刚才的预占位置。

调用关系:它是扩展发起“空闲时自动做点事”的共同入口。过程中它会创建错误对象、生成新的 UUID、创建普通任务,并在发现抢占或模式变化时调用 Session::clear_reserved_idle_turn 把临时占位撤掉,避免留下一个看似忙碌但其实没任务的会话。

调用图:调用 3 个内部函数(new, clear_reserved_idle_turn, new);外部调用 3 个(clone, new, new_v4)。

Session::clear_reserved_idle_turn132–140 ↗
async fn clear_reserved_idle_turn(&self, turn_state: &Arc<tokio::sync::Mutex<TurnState>>)

作用:这个函数专门清理 try_start_turn_if_idle 预先占住、但后来发现不能用的空闲轮次。它防止系统误以为还有一个任务正在准备执行。

数据流:进去的是一个 turn_state,也就是要核对的轮次状态指针。函数拿到 active_turn 锁后,只在三个条件都满足时清空它:当前确实有活跃轮次、这个轮次还没有真正启动任务、它的状态对象和传进来的正是同一个。判断“是不是同一个”用的是指针相等,也就是确认不是长得像,而是同一份对象。

调用关系:它只服务于 Session::try_start_turn_if_idle。try_start_turn_if_idle 在发现用户工作插队、进入 Plan 模式、或者预占位置被别人改动时,会请它收拾现场。

调用图:被 1 处调用(try_start_turn_if_idle);外部调用 1 个(ptr_eq)。

Session::inject_no_new_turn143–160 ↗
async fn inject_no_new_turn(
        &self,
        items: Vec<ResponseItem>,
        current_turn_context: Option<&TurnContext>,
    )

作用:这个函数用于接收一些消息,但明确保证不会因此启动新一轮工作。能塞进正在跑的任务就塞;塞不进去就只写进会话记录里。

数据流:进去的是一组 ResponseItem,以及可选的当前轮次上下文。它先调用 Session::inject_if_running;如果当前有活跃轮次,消息进入该轮次输入队列,函数结束。如果没有活跃轮次,它会使用传入的上下文;没有传入时就临时创建一个默认上下文,然后把这些消息记录到对话历史里,不启动任务。

调用关系:它是“只注入或记录,不开新活”的安全入口。它把第一步交给 Session::inject_if_running,失败后自己负责找一个合适的 TurnContext,再调用会话记录能力保存内容。

调用图:调用 1 个内部函数(inject_if_running)。

core/src/state/service.rs源码 ↗
orchestrationstartup 和整个会话运行期间都会被使用

一次对话会话不只是发消息那么简单。它可能要调用模型、跑命令、连 MCP(Model Context Protocol,一种让模型连接外部工具和服务的协议)、做权限审批、记录日志、加载插件、保存线程状态,还要处理网络代理和安全拦截。这个文件就像一个总工具柜:SessionServices 里面放着这些已经准备好的服务句柄。很多字段用 Arc(可多人共享的一份引用)或 Mutex(一把锁,防止多个任务同时改同一份数据)包起来,是因为会话里很多事情会并发发生。它唯一的函数用于安装新的 MCP 连接管理器,并马上检查必需的服务器是否可用,避免会话开始后才发现关键外部工具没连上。

函数细节1
SessionServices::install_mcp_connection_manager90–99 ↗
async fn install_mcp_connection_manager(
        &self,
        manager: McpConnectionManager,
    ) -> Result<()>

作用:把新的 MCP 连接管理器装进当前会话里,并确认必须连接的 MCP 服务器都能用。这样会话启动阶段需要外部工具时,能通过正确的管理器去连接,而不是用旧的或空的状态。

数据流:输入是一个新的 McpConnectionManager。函数先用 Arc::new 之类的共享包装把它变成可被多处安全持有的对象,再替换掉 SessionServices 里当前保存的 MCP 管理器。随后它读取刚装进去的管理器,让它检查必需服务器;如果检查通过,返回成功;如果服务器缺失或不可用,就返回错误。

调用关系:它通常在会话启动、重新配置 MCP,或准备外部工具连接时由上层启动流程调用。它先把管理器放进会话共享状态,再把后续检查交给这个管理器自己完成;这样其他同时启动的步骤如果需要 MCP,也能拿到同一份最新的连接管理器。

调用图:外部调用 1 个(new)。

core/src/state/session.rs源码 ↗
domain_logiccross-cutting

一场会话不是只发一句话就结束,它会不断积累历史、权限、模型用量、启动信息和一些临时选择。这个文件里的 SessionState 就像前台接待员手边的一本总账:谁来问,都从这里查;谁更新,也写回这里。它把聊天历史交给 ContextManager 保存,把“自动压缩窗口”(上下文太长时准备换一段新窗口)交给 AutoCompactWindow 记录,把权限按环境编号存起来,还会记住上一轮用户设置、是否第一轮、哪些 MCP 依赖已经提醒过。比较重要的一点是,它更新速率限制时不会傻傻覆盖旧信息:如果新快照缺少额度或套餐信息,会沿用上一次的值,避免界面突然显示不完整。

函数细节39
SessionState::new47–64 ↗
fn new(session_configuration: SessionConfiguration) -> Self

作用:创建一份全新的会话状态。新会话开始时需要一个干净但完整的“总账”,这里会把历史、自动压缩窗口、集合和队列都准备好。

数据流:进去的是会话配置;函数用它填入 session_configuration,并新建空历史、空权限表、空连接器集合、空待处理启动来源队列等;出来的是一个 SessionState,其中 next_turn_is_first 被设为 true,表示下一轮是第一轮。

调用关系:它是会话状态的起点,会被会话创建流程和多处测试使用,例如 make_session_and_context、带认证配置的创建流程,以及检查速率限制、连接器选择、替换历史等行为的测试。它内部把初始化工作交给 ContextManager、AutoCompactWindow 和标准集合的 new/default。

调用图:调用 2 个内部函数(new, new);被 12 处调用(new, make_session_and_context, make_session_and_context_with_auth_config_home_and_rx, set_rate_limits_retains_previous_credits, set_rate_limits_updates_plan_type_when_present, clear_connector_selection_removes_entries, merge_connector_selection_deduplicates_entries, replace_history_clears_auto_compact_window_prefill, set_rate_limits_carries_account_metadata_from_codex_to_codex_other, set_rate_limits_defaults_limit_id_to_codex_when_missing (+2 more));外部调用 4 个(new, new, new, default)。

SessionState::record_items67–73 ↗
fn record_items(&mut self, items: I, policy: TruncationPolicy)

作用:把新的模型回复或会话条目记进历史。这样后续对话才能知道前面发生过什么。

数据流:进去的是一批 ResponseItem 和一个截断策略;函数把这些交给 history.record_items,由历史管理器按策略保存或裁剪;出来没有单独返回值,但会话历史被更新了。

调用关系:它是外部流程写入聊天历史的入口,自己不判断怎么裁剪,而是把具体保存工作交给 ContextManager 的 record_items。

调用图:调用 1 个内部函数(record_items)。

SessionState::previous_turn_settings75–77 ↗
fn previous_turn_settings(&self) -> Option<PreviousTurnSettings>

作用:取出上一轮普通用户请求使用过的设置。后续轮次可以沿用这些设置,避免恢复会话或压缩上下文后忘记模型相关选择。

数据流:进去不需要参数;函数读取 previous_turn_settings,并复制一份返回;原状态不变。

调用关系:会话流程在准备下一轮请求时会查它,用来知道上一轮的模型或实时处理设置。

SessionState::set_previous_turn_settings78–83 ↗
fn set_previous_turn_settings(
        &mut self,
        previous_turn_settings: Option<PreviousTurnSettings>,
    )

作用:保存上一轮普通用户请求的设置。它相当于给下一轮留一张便签。

数据流:进去的是一个可能存在的 PreviousTurnSettings;函数直接覆盖状态里的 previous_turn_settings;出来没有返回值,但这份设置会留给之后读取。

调用关系:通常在一轮用户请求确定设置后调用,之后 previous_turn_settings 会把它读出来给后续轮次使用。

SessionState::set_next_turn_is_first85–87 ↗
fn set_next_turn_is_first(&mut self, value: bool)

作用:手动设置“下一轮是不是第一轮”的标记。某些恢复或重置场景需要重新校准这个判断。

数据流:进去的是 true 或 false;函数把 next_turn_is_first 改成这个值;出来没有返回值。

调用关系:它给会话控制流程提供一个开关,后续 take_next_turn_is_first 会读取并消费这个标记。

SessionState::take_next_turn_is_first89–93 ↗
fn take_next_turn_is_first(&mut self) -> bool

作用:询问下一轮是否是第一轮,并且问完就把标记清掉。它像一次性门票,用过后就不能再当第一轮。

数据流:进去不需要参数;函数先读出 next_turn_is_first,然后把它设为 false;返回刚才读到的值。

调用关系:会话开始处理用户轮次时会用它判断是否要执行第一轮专属行为。它和 set_next_turn_is_first 配合,一个设置标记,一个消费标记。

SessionState::clone_history95–97 ↗
fn clone_history(&self) -> ContextManager

作用:复制当前聊天历史。调用者想查看或加工历史,但不想直接改原件时会用它。

数据流:进去不需要参数;函数读取 history 并克隆一份;返回这份独立的 ContextManager 副本,原历史不变。

调用关系:它把复制工作交给 ContextManager 的 clone。会话流程需要拿历史快照时会调用它。

调用图:外部调用 1 个(clone)。

SessionState::replace_history99–108 ↗
fn replace_history(
        &mut self,
        items: Vec<ResponseItem>,
        reference_context_item: Option<TurnContextItem>,
    )

作用:用一批新条目整体替换当前历史。常见于恢复会话、压缩上下文之后重建历史。

数据流:进去的是新的 ResponseItem 列表,以及一个可选的参考上下文条目;函数先让 history.replace 换掉旧历史,再设置参考上下文条目,最后清掉自动压缩窗口里的预填令牌记录;出来没有返回值,但历史和压缩窗口状态都变了。

调用关系:它把历史替换交给 ContextManager,把预填清理交给 AutoCompactWindow。测试 replace_history_clears_auto_compact_window_prefill 会关注它是否在替换历史时同步清理压缩窗口。

调用图:调用 3 个内部函数(replace, set_reference_context_item, clear_prefill)。

SessionState::set_token_info110–112 ↗
fn set_token_info(&mut self, info: Option<TokenUsageInfo>)

作用:直接设置当前历史里的令牌用量信息。令牌可以粗略理解为模型读写文字时的计数单位。

数据流:进去的是可选的 TokenUsageInfo;函数把它交给 history.set_token_info 保存;出来没有返回值,但历史里的用量信息被覆盖。

调用关系:它是会话状态对外提供的简单转发口,真正保存由 ContextManager 完成。

调用图:调用 1 个内部函数(set_token_info)。

SessionState::set_reference_context_item114–116 ↗
fn set_reference_context_item(&mut self, item: Option<TurnContextItem>)

作用:设置一个“参考上下文条目”。它用于标记当前历史是围绕哪段上下文建立的。

数据流:进去的是一个可选 TurnContextItem;函数把它交给 history.set_reference_context_item;出来没有返回值,但历史管理器里对应标记被更新。

调用关系:它把工作交给 ContextManager。需要重建或标记上下文来源的流程会调用它。

调用图:调用 1 个内部函数(set_reference_context_item)。

SessionState::reference_context_item118–120 ↗
fn reference_context_item(&self) -> Option<TurnContextItem>

作用:读取当前保存的参考上下文条目。调用者可以用它判断现在的历史参照了哪段上下文。

数据流:进去不需要参数;函数从 history.reference_context_item 取值;返回一个可能存在的 TurnContextItem。

调用关系:它是 set_reference_context_item 的读取搭档,具体数据仍由 ContextManager 保管。

调用图:调用 1 个内部函数(reference_context_item)。

SessionState::update_token_info_from_usage123–129 ↗
fn update_token_info_from_usage(
        &mut self,
        usage: &TokenUsage,
        model_context_window: Option<i64>,
    )

作用:根据一次模型调用返回的实际用量,更新会话里的令牌统计。这样系统能知道上下文窗口还剩多少空间。

数据流:进去的是 TokenUsage 和可选的模型上下文窗口大小;函数把它们交给 history.update_token_info 计算并保存;出来没有返回值,但令牌信息更新了。

调用关系:模型返回用量后,会话流程会用它刷新统计。它不自己算细节,而是交给 ContextManager。

调用图:调用 1 个内部函数(update_token_info)。

SessionState::ensure_auto_compact_window_server_prefill_from_usage131–137 ↗
fn ensure_auto_compact_window_server_prefill_from_usage(
        &mut self,
        usage: &TokenUsage,
    )

作用:用服务器看到的用量,补齐自动压缩窗口里的“预填”记录。预填可以理解为模型开始回答前已经占用的上下文。

数据流:进去的是 TokenUsage;函数交给 auto_compact_window.ensure_server_observed_prefill_from_usage;出来没有返回值,但自动压缩窗口可能记录了服务器观察到的预填数量。

调用关系:它属于自动压缩准备流程,具体判断和记录由 AutoCompactWindow 完成。

调用图:调用 1 个内部函数(ensure_server_observed_prefill_from_usage)。

SessionState::set_auto_compact_window_estimated_prefill139–141 ↗
fn set_auto_compact_window_estimated_prefill(&mut self, tokens: i64)

作用:给自动压缩窗口设置一个估算的预填令牌数。服务器还没给准数时,可以先用估算值。

数据流:进去的是令牌数量 tokens;函数写入 auto_compact_window.set_estimated_prefill;出来没有返回值,但窗口的估算预填被更新。

调用关系:它被需要预估上下文占用的流程调用,细节交给 AutoCompactWindow 保存。

调用图:调用 1 个内部函数(set_estimated_prefill)。

SessionState::auto_compact_window_snapshot143–145 ↗
fn auto_compact_window_snapshot(&self) -> AutoCompactWindowSnapshot

作用:拿到自动压缩窗口当前状态的一张快照。快照就是只读照片,方便展示或判断,不直接改原状态。

数据流:进去不需要参数;函数调用 auto_compact_window.snapshot;返回 AutoCompactWindowSnapshot。

调用关系:会话流程或状态汇报需要查看压缩窗口情况时会用它,实际生成快照由 AutoCompactWindow 完成。

调用图:调用 1 个内部函数(snapshot)。

SessionState::auto_compact_window_id147–149 ↗
fn auto_compact_window_id(&self) -> u64

作用:读取当前自动压缩窗口的编号。编号用来区分这是第几段上下文窗口。

数据流:进去不需要参数;函数从 auto_compact_window.window_id 读取;返回一个数字编号。

调用关系:它给外部流程提供窗口身份信息,具体编号存在 AutoCompactWindow 里。

调用图:调用 1 个内部函数(window_id)。

SessionState::set_auto_compact_window_id151–153 ↗
fn set_auto_compact_window_id(&mut self, window_id: u64)

作用:手动设置自动压缩窗口编号。恢复会话或同步状态时,可能需要把编号调到指定值。

数据流:进去的是 window_id;函数交给 auto_compact_window.set_window_id 保存;出来没有返回值。

调用关系:它服务于会话恢复或状态同步流程,编号的实际保存由 AutoCompactWindow 负责。

调用图:调用 1 个内部函数(set_window_id)。

SessionState::advance_auto_compact_window_id155–157 ↗
fn advance_auto_compact_window_id(&mut self) -> u64

作用:把自动压缩窗口编号往前推进一格,并返回新编号。换到新上下文窗口时会用到。

数据流:进去不需要参数;函数调用 auto_compact_window.advance_window_id;返回推进后的窗口编号,同时内部编号已更新。

调用关系:它是切换上下文窗口时的工具。start_new_context_window_if_requested 也会在真正开始新窗口时做同样的推进动作。

调用图:调用 1 个内部函数(advance_window_id)。

SessionState::request_new_context_window159–161 ↗
fn request_new_context_window(&mut self)

作用:登记一个“需要新上下文窗口”的请求。它只是先举手,不马上切换。

数据流:进去不需要参数;函数调用 auto_compact_window.request_new_context_window;出来没有返回值,但内部请求标记被设上。

调用关系:当系统发现当前上下文该换了时会调用它。稍后 start_new_context_window_if_requested 会检查这个请求并真正执行切换。

调用图:调用 1 个内部函数(request_new_context_window)。

SessionState::start_new_context_window_if_requested163–171 ↗
fn start_new_context_window_if_requested(&mut self) -> Option<u64>

作用:如果之前有人请求新上下文窗口,就真正开始一个新窗口;如果没人请求,就什么也不做。

数据流:进去不需要参数;函数先从 auto_compact_window.take_new_context_window_request 取走请求标记;如果没有请求,返回 None;如果有,就推进窗口编号、清掉预填记录,并返回新的窗口编号。

调用关系:它是 request_new_context_window 的执行端。它把检查请求、推进编号、清理预填这些动作串起来,核心动作仍由 AutoCompactWindow 完成。

调用图:调用 3 个内部函数(advance_window_id, clear_prefill, take_new_context_window_request)。

SessionState::token_info173–175 ↗
fn token_info(&self) -> Option<TokenUsageInfo>

作用:读取当前令牌用量信息。调用者可以用它判断上下文占用情况。

数据流:进去不需要参数;函数从 history.token_info 取出信息;返回一个可能存在的 TokenUsageInfo。

调用关系:它是令牌信息的基础读取口,也会被 token_info_and_rate_limits 调用,用来和速率限制一起打包返回。

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

SessionState::set_rate_limits177–182 ↗
fn set_rate_limits(&mut self, snapshot: RateLimitSnapshot)

作用:保存最新的速率限制快照。速率限制就是服务端规定的使用额度、频率或套餐边界。

数据流:进去的是新的 RateLimitSnapshot;函数先调用 merge_rate_limit_fields,把新快照和旧快照合并,补上新快照缺失的额度、套餐等字段;然后存到 latest_rate_limits。

调用关系:服务端返回新的限制信息时会调用它。它依赖 merge_rate_limit_fields 防止新快照不完整时覆盖掉旧的有用信息;相关测试会检查额度和套餐信息是否正确保留或更新。

调用图:调用 1 个内部函数(merge_rate_limit_fields)。

SessionState::token_info_and_rate_limits184–188 ↗
fn token_info_and_rate_limits(
        &self,
    ) -> (Option<TokenUsageInfo>, Option<RateLimitSnapshot>)

作用:一次性取出令牌用量和速率限制。界面或状态上报通常两样都要看,所以这里打包给出去。

数据流:进去不需要参数;函数先调用 token_info 取令牌信息,再复制 latest_rate_limits;返回二者组成的一对值。

调用关系:它站在状态查询的出口位置,把 token_info 的结果和本文件保存的速率限制合在一起给调用者。

调用图:调用 1 个内部函数(token_info)。

SessionState::set_token_usage_full190–192 ↗
fn set_token_usage_full(&mut self, context_window: i64)

作用:把令牌使用情况标记为已经占满上下文窗口。也就是告诉系统:这段上下文空间已经没有余量了。

数据流:进去的是上下文窗口大小 context_window;函数交给 history.set_token_usage_full;出来没有返回值,但历史里的用量状态会被设为满。

调用关系:当系统判断上下文已满时会调用它。具体如何记录满载状态由 ContextManager 完成。

调用图:调用 1 个内部函数(set_token_usage_full)。

SessionState::get_total_token_usage194–197 ↗
fn get_total_token_usage(&self, server_reasoning_included: bool) -> i64

作用:计算当前总令牌用量。参数决定是否把服务器的推理内容也算进去。

数据流:进去的是 server_reasoning_included 这个布尔值;函数把它传给 history.get_total_token_usage;返回一个总令牌数。

调用关系:需要做窗口判断、显示用量或触发压缩时会查它。计算细节由 ContextManager 负责。

调用图:调用 1 个内部函数(get_total_token_usage)。

SessionState::set_server_reasoning_included199–201 ↗
fn set_server_reasoning_included(&mut self, included: bool)

作用:记录服务器推理内容是否应该计入会话状态。这个开关会影响后续令牌统计口径。

数据流:进去的是 included;函数把 server_reasoning_included 改成这个值;出来没有返回值。

调用关系:会话流程根据服务端能力或配置设置它,之后 server_reasoning_included 会读取这个口径。

SessionState::server_reasoning_included203–205 ↗
fn server_reasoning_included(&self) -> bool

作用:读取当前是否把服务器推理内容算进去。调用者用它保持统计口径一致。

数据流:进去不需要参数;函数返回 server_reasoning_included 当前值;状态不变。

调用关系:它和 set_server_reasoning_included 配套使用,常用于需要决定令牌计算方式的地方。

SessionState::record_mcp_dependency_prompted207–212 ↗
fn record_mcp_dependency_prompted(&mut self, names: I)

作用:记录哪些 MCP 依赖已经提醒过用户。MCP 可以理解为外部工具/服务接入的一套协议;这里避免同一个依赖反复弹提醒。

数据流:进去的是一批依赖名称;函数把这些名称加入 mcp_dependency_prompted 这个集合;出来没有返回值,重复名称会自然去重。

调用关系:当系统已经提示过某些 MCP 依赖后会调用它。之后 mcp_dependency_prompted 可以取出这份集合做判断。

SessionState::mcp_dependency_prompted214–216 ↗
fn mcp_dependency_prompted(&self) -> HashSet<String>

作用:读取已经提示过的 MCP 依赖名称。调用者可以据此决定不要重复打扰用户。

数据流:进去不需要参数;函数复制 mcp_dependency_prompted 集合并返回;原集合不变。

调用关系:它是 record_mcp_dependency_prompted 的读取端,供提示流程查询历史提示记录。

SessionState::set_session_startup_prewarm218–223 ↗
fn set_session_startup_prewarm(
        &mut self,
        startup_prewarm: SessionStartupPrewarmHandle,
    )

作用:保存启动时预热好的会话资源。预热就像饭店提前备菜,真正要用时可以更快。

数据流:进去的是 SessionStartupPrewarmHandle;函数把它包成 Some 存到 startup_prewarm;出来没有返回值。

调用关系:会话初始化完成预热后会调用它。稍后 take_session_startup_prewarm 会把这份预热资源取走使用。

SessionState::take_session_startup_prewarm225–227 ↗
fn take_session_startup_prewarm(&mut self) -> Option<SessionStartupPrewarmHandle>

作用:取走启动预热资源,并从状态里清空。这样同一份预热资源不会被重复使用。

数据流:进去不需要参数;函数对 startup_prewarm 执行 take;返回原来的预热句柄或 None,同时内部字段变成 None。

调用关系:它是 set_session_startup_prewarm 的消费端。会话真正需要使用预热结果时会调用它。

SessionState::merge_connector_selection230–236 ↗
fn merge_connector_selection(&mut self, connector_ids: I) -> HashSet<String>

作用:把新的连接器编号加入当前已选择的连接器集合,并返回合并后的结果。连接器可以理解为外部数据源或工具入口。

数据流:进去的是一批 connector_ids;函数把它们加入 active_connector_selection 这个集合,重复编号会自动去掉;返回合并后的集合副本。

调用关系:当用户或系统追加选择连接器时会调用它。测试 merge_connector_selection_deduplicates_entries 会关注它是否正确去重。

SessionState::get_connector_selection239–241 ↗
fn get_connector_selection(&self) -> HashSet<String>

作用:读取当前会话里选中的连接器集合。调用者可以知道接下来应该启用哪些外部入口。

数据流:进去不需要参数;函数复制 active_connector_selection 并返回;原集合不变。

调用关系:它给请求准备流程或界面展示提供当前选择,通常和 merge_connector_selection、clear_connector_selection 配合使用。

SessionState::clear_connector_selection244–246 ↗
fn clear_connector_selection(&mut self)

作用:清空当前记录的连接器选择。需要重置选择时用它。

数据流:进去不需要参数;函数清空 active_connector_selection;出来没有返回值,之后读取会得到空集合。

调用关系:当会话或某个流程需要重新选择连接器时会调用它。测试 clear_connector_selection_removes_entries 会检查清空是否生效。

SessionState::queue_pending_session_start_source248–253 ↗
fn queue_pending_session_start_source(
        &mut self,
        value: codex_hooks::SessionStartSource,
    )

作用:把一个待处理的会话启动来源排进队列。队列像排队取号,先来的会先被取走。

数据流:进去的是一个 SessionStartSource;函数用 push_back 把它放到 pending_session_start_sources 队尾;出来没有返回值。

调用关系:启动钩子或会话初始化流程发现新的启动来源时会调用它。之后 take_pending_session_start_source 会按顺序取出。

调用图:外部调用 1 个(push_back)。

SessionState::take_pending_session_start_source255–259 ↗
fn take_pending_session_start_source(
        &mut self,
    ) -> Option<codex_hooks::SessionStartSource>

作用:从待处理启动来源队列里取出最早的一项。取出后它就不在队列里了。

数据流:进去不需要参数;函数用 pop_front 从队头取值;返回一个 SessionStartSource 或 None,同时队列少一项。

调用关系:处理启动来源的流程会调用它。它和 queue_pending_session_start_source 配合,形成先进先出的处理顺序。

调用图:外部调用 1 个(pop_front)。

SessionState::record_granted_permissions261–275 ↗
fn record_granted_permissions(
        &mut self,
        environment_id: &str,
        permissions: AdditionalPermissionProfile,
    )

作用:记录某个环境已经授予的额外权限。环境可以理解为一次运行或工具执行所在的隔离空间。

数据流:进去的是 environment_id 和一份 AdditionalPermissionProfile;函数先取出这个环境旧的权限,再调用 merge_permission_profiles 合并旧权限和新权限;如果合并结果存在,就按环境编号存回表里。

调用关系:当用户或策略给某个环境放行了额外权限时会调用它。它把权限合并规则交给 sandboxing 里的 merge_permission_profiles,避免新权限覆盖掉旧权限。

调用图:调用 1 个内部函数(merge_permission_profiles)。

SessionState::granted_permissions277–284 ↗
fn granted_permissions(
        &self,
        environment_id: &str,
    ) -> Option<AdditionalPermissionProfile>

作用:读取某个环境已经拿到的额外权限。后续执行命令或工具前可以用它判断是否已经放行。

数据流:进去的是 environment_id;函数在 granted_permissions_by_environment_id 里查找并复制结果;返回权限配置或 None。

调用关系:它是 record_granted_permissions 的查询端,供执行环境准备或权限检查流程使用。

merge_rate_limit_fields290–307 ↗
fn merge_rate_limit_fields(
    previous: Option<&RateLimitSnapshot>,
    mut snapshot: RateLimitSnapshot,
) -> RateLimitSnapshot

作用:合并新旧速率限制信息,专门处理“新快照缺字段”的情况。这样不会因为服务端这次少给了套餐或额度字段,界面和逻辑就丢掉旧信息。

数据流:进去的是上一份 RateLimitSnapshot(可选)和新的 snapshot;函数先给缺失的 limit_id 填默认值 codex,再把缺失的 credits、individual_limit、plan_type 从旧快照补回来;返回补全后的新快照。

调用关系:它只被 SessionState::set_rate_limits 调用,是保存速率限制前的清洗步骤。set_rate_limits 负责存结果,它负责让结果尽量完整。

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

core/src/state/auto_compact_window.rs源码 ↗
domain_logiccross-cutting:会话运行中,只要涉及自动压缩上下文或统计 token 用量就会用到

聊天模型有一个“上下文窗口”,可以理解成一张有限大小的白纸,token 就是写在纸上的字块。聊久了纸会满,系统会自动压缩旧内容,开一个新的计算区间。这个文件里的 AutoCompactWindow 就像这个区间的计数牌:它保存当前窗口编号,记录外部有没有要求换新窗口,还保存“窗口开始时已经有多少输入 token”这个基准数。这个基准可以是服务器真实报回来的,也可以先用本地估算;一旦拿到服务器真实值,就不再被估算值覆盖。这样后面计算“这轮新增了多少内容”时,能从正确的起点扣除,避免提前或延后触发压缩。

函数细节11
AutoCompactWindow::new27–33 ↗
fn new() -> Self

作用:创建一个全新的自动压缩窗口状态。新状态从窗口编号 0 开始,没有待处理的新窗口请求,也还不知道窗口起点占了多少 token。

数据流:进去没有额外输入 → 它把窗口编号设为 0,把“需要新窗口”的标记设为否,把预填 token 基准设为空 → 出来一个干净的 AutoCompactWindow 状态对象。

调用关系:它通常在更大的状态对象初始化时被调用,也被测试用来搭一个最小场景。后面的窗口编号、请求标记、token 基准,都是在这个初始状态上逐步改出来的。

调用图:被 2 处调用(tracks_prefill_and_window_boundaries, new)。

AutoCompactWindow::clear_prefill35–37 ↗
fn clear_prefill(&mut self)

作用:清掉当前记录的预填 token 基准。换历史记录或真正开始新上下文窗口时,需要把旧窗口的起点信息丢掉,免得影响新窗口。

数据流:进去的是当前窗口状态 → 它只把 prefill_input_tokens 这一项设为空 → 出来还是同一个状态对象,但已经不再记得旧的输入 token 起点。

调用关系:它会被 replace_history 和 start_new_context_window_if_requested 使用。也就是说,当历史被替换,或系统响应“开新窗口”的请求时,会先把旧基准清理掉。

调用图:被 2 处调用(replace_history, start_new_context_window_if_requested)。

AutoCompactWindow::window_id39–41 ↗
fn window_id(&self) -> u64

作用:读取当前自动压缩窗口的编号。外部需要知道“现在是哪一轮窗口”时会用它。

数据流:进去的是当前窗口状态 → 它查看里面保存的 window_id → 返回这个数字,不改动任何状态。

调用关系:它被 auto_compact_window_id 这个更外层的读取接口调用。外部代码不用直接碰内部字段,而是通过这个小入口拿到窗口编号。

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

AutoCompactWindow::set_window_id43–45 ↗
fn set_window_id(&mut self, window_id: u64)

作用:手动设置当前窗口编号。它用于外层状态需要恢复、同步或指定窗口编号的时候。

数据流:进去一个新的窗口编号和当前状态 → 它把状态里的 window_id 改成传入的值 → 出来是同一个状态对象,但窗口编号已经更新。

调用关系:它被 set_auto_compact_window_id 调用。也就是更外层的状态管理代码收到“设置窗口编号”的需求后,把具体改字段的事交给这里。

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

AutoCompactWindow::advance_window_id47–51 ↗
fn advance_window_id(&mut self) -> u64

作用:进入下一轮自动压缩窗口,并顺手清掉“需要新窗口”的请求标记。它表示请求已经被消费,窗口确实往前走了一步。

数据流:进去的是当前窗口状态 → 它把 window_id 加 1;如果数字已经到上限,就用安全的饱和加法避免溢出;同时把 new_context_window_requested 设为否 → 返回更新后的窗口编号。

调用关系:它被 advance_auto_compact_window_id 和 start_new_context_window_if_requested 调用。前者是直接推进窗口,后者是在发现确实有人请求新窗口时推进窗口。

调用图:被 2 处调用(advance_auto_compact_window_id, start_new_context_window_if_requested)。

AutoCompactWindow::request_new_context_window53–55 ↗
fn request_new_context_window(&mut self)

作用:记录一个“请在合适时机开新上下文窗口”的请求。它只是插一面小旗,不立刻做切换。

数据流:进去的是当前窗口状态 → 它把 new_context_window_requested 改成 true → 出来同一个状态对象,但里面多了一个待处理的新窗口请求。

调用关系:它被 request_new_context_window 这个外层接口调用。之后 start_new_context_window_if_requested 会来检查这面小旗,并决定是否真正开新窗口。

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

AutoCompactWindow::take_new_context_window_request57–61 ↗
fn take_new_context_window_request(&mut self) -> bool

作用:取出并清空“是否请求了新上下文窗口”这个标记。它像取信箱里的信:取走后信箱就空了,避免同一个请求被处理两次。

数据流:进去的是当前窗口状态 → 它先记下 new_context_window_requested 当前是 true 还是 false,然后把这个标记改回 false → 返回刚才记下的结果。

调用关系:它被 start_new_context_window_if_requested 调用。这个外层流程会用返回值判断是否要开新窗口;无论结果如何,请求标记都已经被消费掉。

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

AutoCompactWindow::ensure_server_observed_prefill_from_usage66–77 ↗
fn ensure_server_observed_prefill_from_usage(&mut self, usage: &TokenUsage)

作用:用服务器真实报告的用量,记录当前窗口开始时的输入 token 基准。真实值比本地估算更可信,所以一旦有了真实值,就不会再被后来的估算或第二次真实采样覆盖。

数据流:进去的是当前窗口状态和一次 TokenUsage(服务器报告的 token 用量)→ 如果已经有 ServerObserved,也就是真实服务器基准,它直接不动;否则取 usage.input_tokens,负数按 0 处理,存成服务器观察到的基准 → 出来状态里有了更可靠的预填输入 token 数。

调用关系:它被 ensure_auto_compact_window_server_prefill_from_usage 调用,通常发生在收到服务器返回的 token 用量之后。它会构造 ServerObserved 这种内部标记,并用 matches! 判断当前是否已经有真实值。

调用图:被 1 处调用(ensure_auto_compact_window_server_prefill_from_usage);外部调用 2 个(ServerObserved, matches!)。

AutoCompactWindow::set_estimated_prefill79–88 ↗
fn set_estimated_prefill(&mut self, tokens: i64)

作用:在还没有服务器真实用量时,先放一个本地估算的输入 token 基准。这样系统不用干等服务器,也能先大致计算窗口增长。

数据流:进去的是当前窗口状态和估算 token 数 → 如果状态里已经有服务器真实基准,就什么都不改;否则把传入数字按不小于 0 处理后,存成 Estimated 估算基准 → 出来状态可能多了一个估算起点。

调用关系:它被 set_auto_compact_window_estimated_prefill 调用。它和 ensure_server_observed_prefill_from_usage 配合:先用估算顶上,等服务器真实值来了再让真实值接管。

调用图:被 1 处调用(set_auto_compact_window_estimated_prefill);外部调用 2 个(Estimated, matches!)。

AutoCompactWindow::snapshot90–99 ↗
fn snapshot(&self) -> AutoCompactWindowSnapshot

作用:把当前窗口状态中对外有用的部分打包成一个快照。快照就是一张只读的小纸条,方便外部查看当前预填 token 基准。

数据流:进去的是当前窗口状态 → 它查看 prefill_input_tokens,不管这个数来自服务器真实值还是本地估算,都取出里面的数字;如果没有记录就保持为空 → 返回 AutoCompactWindowSnapshot。

调用关系:它被 auto_compact_window_snapshot 调用。外层代码需要查看状态时,通过这个函数拿到简化后的结果,而不是直接了解内部的 ServerObserved 和 Estimated 区别。

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

tests::tracks_prefill_and_window_boundaries108–161 ↗
fn tracks_prefill_and_window_boundaries()

作用:这是这个文件的自测,确认窗口编号、请求标记、估算基准和服务器真实基准都按预期工作。它防止以后改代码时不小心破坏这些小规则。

数据流:进去没有外部输入 → 它创建一个新窗口,依次设置编号、请求新窗口、推进窗口、设置估算 token、再用服务器用量覆盖估算,并尝试用后续值覆盖服务器真实值 → 通过断言检查每一步结果是否正确;它不产出业务结果,只在测试失败时报警。

调用关系:它调用 AutoCompactWindow::new,并使用 assert_eq!、assert! 这类测试断言,还用 TokenUsage::default 补齐测试数据。它相当于把这个文件的关键规则串起来跑一遍。

调用图:调用 1 个内部函数(new);外部调用 3 个(default, assert!, assert_eq!)。

core/src/tasks/mod.rs源码 ↗
orchestration会话回合启动、运行、完成、中断和后台终端清理期间

可以把这个文件理解成 Codex 会话的“值班调度员”。真正干活的任务分散在 regular、review、compact、user_shell 等子模块里,而这里负责把它们启动起来、放到后台异步运行、给它们发取消信号,并在完成后收尾。它会记录回合开始时间、结束时间、用了多少 token(模型读写文字的计量单位)、有没有调用工具、网络代理和记忆功能是否开启等指标。任务被用户打断时,它不只是粗暴杀掉,还会先给任务一点点时间自己停下,再补一条“上一轮被中断”的历史标记,避免模型以为对话无事发生。它还管理后台终端进程,提供列出和终止这些进程的入口。整体上,这个文件让一次对话回合从“开始跑”到“正常结束或被打断”都有统一规矩。

函数细节28
InterruptedTurnHistoryMarker::from_config_and_version76–88 ↗
fn from_config_and_version(
        config: &Config,
        multi_agent_version: MultiAgentVersion,
    ) -> Self

作用:根据配置和多智能体协议版本,决定“上一轮被打断”这件事要不要写进对话历史,以及用哪种身份写。这样后续模型能知道前一轮不是自然结束,而是被人打断了。

数据流:进去的是全局配置和 MultiAgentVersion(多智能体协议版本)→ 先看配置是否允许写中断提示,不允许就返回 Disabled → 如果允许,V2 版本返回 Developer,否则返回 ContextualUser。

调用关系:当任务真正被打断时,Session::handle_task_abort 会用它选择标记格式;创建分叉线程和子智能体快照时也会用同一套判断,保证真实中断和复制出来的历史看起来一致。

调用图:被 3 处调用(handle_task_abort, fork_thread_with_initial_history, spawn_subagent)。

interrupted_turn_history_marker93–116 ↗
fn interrupted_turn_history_marker(
    marker: InterruptedTurnHistoryMarker,
) -> Option<ResponseItem>

作用:把“中断标记类型”变成模型能看到的一条对话内容。没有这一步,模型后面可能不知道上一轮为什么突然没了下文。

数据流:进去的是 Disabled、ContextualUser 或 Developer 这三种选择之一→ Disabled 直接没有输出→ ContextualUser 生成一条带上下文的用户片段→ Developer 生成一条 developer 身份的消息→ 出来是可选的 ResponseItem,也就是可放进对话历史的一项内容。

调用关系:Session::handle_task_abort 在用户打断任务后调用它,把标记写入历史;append_interrupted_boundary 和测试/辅助标记函数也复用它,避免不同地方生成的中断提示不一致。

调用图:调用 2 个内部函数(into, new);被 4 处调用(handle_task_abort, append_interrupted_boundary, contextual_user_interrupted_marker, developer_interrupted_marker);外部调用 1 个(vec!)。

emit_turn_network_proxy_metric118–133 ↗
fn emit_turn_network_proxy_metric(
    session_telemetry: &SessionTelemetry,
    network_proxy_active: bool,
    tmp_mem: (&str, &str),
)

作用:记录这一轮结束时网络代理是否处于开启状态。这个指标能帮助开发者判断某些慢、失败或网络行为是否和代理有关。

数据流:进去的是遥测对象、网络代理是否开启、以及一个临时内存功能标签→ 把布尔值转成 true/false 字符串→ 调用 counter 给 TURN_NETWORK_PROXY_METRIC 计数一次。

调用关系:Session::on_task_finished 在回合正常结束时读取网络代理状态,然后调用它把结果写入统一遥测系统。

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

emit_turn_memory_metric135–152 ↗
fn emit_turn_memory_metric(
    session_telemetry: &SessionTelemetry,
    feature_enabled: bool,
    config_enabled: bool,
    has_citations: bool,
)

作用:记录这一轮记忆功能的状态:功能开没开、配置让不让用、这一轮有没有引用记忆。这样能看出记忆功能是否真的参与了回答。

数据流:进去的是遥测对象、功能开关、配置开关、是否有记忆引用→ 计算 read_allowed,也就是功能和配置都允许才算能读记忆→ 用 bool_tag 把布尔值变成标签→ 输出是一条计数指标,没有普通返回值。

调用关系:Session::on_task_finished 在回合结束收集完本轮状态后调用它;它内部把布尔值格式化的活交给 bool_tag。

调用图:调用 2 个内部函数(bool_tag, counter);被 1 处调用(on_task_finished)。

emit_compact_metric154–164 ↗
fn emit_compact_metric(
    session_telemetry: &SessionTelemetry,
    compact_type: &'static str,
    manual: bool,
)

作用:记录一次“压缩上下文”任务发生了。压缩上下文可以理解成把很长的对话整理变短,方便模型继续接着聊。

数据流:进去的是遥测对象、压缩类型、是否人工触发→ 用 bool_tag 把 manual 转成 true/false 标签→ 给 codex.task.compact 这个指标计数一次。

调用关系:自动压缩流程 run_auto_compact 会调用它;它不做压缩本身,只负责把“发生过一次压缩”告诉统计系统。

调用图:调用 2 个内部函数(bool_tag, counter);被 1 处调用(run_auto_compact)。

bool_tag166–168 ↗
fn bool_tag(value: bool) -> &'static str

作用:把程序里的 true/false 变成指标系统喜欢的字符串标签。它很小,但能让不同指标都用同一种写法。

数据流:进去一个布尔值→ 如果是真就变成 "true",否则变成 "false"→ 返回这个静态字符串,不改动其他东西。

调用关系:emit_turn_memory_metric 和 emit_compact_metric 都用它来生成遥测标签,避免每个地方自己写一套 true/false 转换。

调用图:被 2 处调用(emit_compact_metric, emit_turn_memory_metric)。

SessionTaskContext::new178–183 ↗
fn new(session: Arc<Session>, turn_extension_data: Arc<ExtensionData>) -> Self

作用:创建一个给任务使用的轻量上下文。它把任务常用的会话对象和扩展数据打包在一起,避免任务到处直接摸完整 Session。

数据流:进去的是共享的 Session 和本回合的 ExtensionData(扩展插件可用的数据)→ 存进 SessionTaskContext → 出来一个新的上下文对象。

调用关系:Session::start_task 在启动任务前创建它,交给任务运行;Session::handle_task_abort 在调用任务自己的 abort 清理逻辑前也会重新创建它。

调用图:被 2 处调用(handle_task_abort, start_task)。

SessionTaskContext::clone_session185–187 ↗
fn clone_session(&self) -> Arc<Session>

作用:给任务一份共享会话引用,让任务能发事件、读服务或做会话级操作。这里的 clone 不是复制整个会话,而是多拿一个指向同一会话的安全指针。

数据流:进去的是上下文本身→ 克隆内部 Arc<Session>(Arc 是线程间共享指针)→ 返回同一个 Session 的新引用,不改变会话内容。

调用关系:后台任务跑完时会用它重新拿到 Session,然后刷新记录、发送完成事件和做收尾。

调用图:外部调用 1 个(clone)。

SessionTaskContext::turn_extension_data189–191 ↗
fn turn_extension_data(&self) -> Arc<ExtensionData>

作用:取出本回合的扩展数据,给任务或扩展功能使用。这样任务不用知道扩展数据藏在会话的哪个角落。

数据流:进去的是上下文本身→ 克隆内部 Arc<ExtensionData>→ 返回同一份扩展数据的共享引用。

调用关系:它是 SessionTaskContext 暴露给各类任务的便捷取口;调用图里没有显示本文件内直接调用,但任务实现可以通过它拿扩展相关信息。

调用图:外部调用 1 个(clone)。

SessionTaskContext::auth_manager193–195 ↗
fn auth_manager(&self) -> Arc<AuthManager>

作用:给任务拿登录和认证管理器。比如任务需要访问需要身份的服务时,就能从这里拿到统一的认证入口。

数据流:进去的是上下文本身→ 从 Session 的 services 里取 auth_manager,并克隆共享指针→ 返回 AuthManager 的共享引用。

调用关系:这是任务运行时可用的服务出口之一;它把任务和 Session 内部服务布局隔开,让任务只拿自己需要的东西。

调用图:外部调用 1 个(clone)。

SessionTaskContext::models_manager197–199 ↗
fn models_manager(&self) -> SharedModelsManager

作用:给任务拿模型管理器,也就是负责模型列表、选择和相关状态的共享组件。任务需要知道或切换模型信息时会用到它。

数据流:进去的是上下文本身→ 从 Session 的 services 里取 models_manager,并克隆共享指针→ 返回模型管理器的共享引用。

调用关系:它服务于具体 SessionTask 的实现;本文件只提供通道,不直接决定模型怎么用。

调用图:外部调用 1 个(clone)。

SessionTask::abort239–247 ↗
fn abort(
        &self,
        session: Arc<SessionTaskContext>,
        ctx: Arc<TurnContext>,
    ) -> impl std::future::Future<Output = ()> + Send

作用:这是任务被取消后的默认清理钩子。默认什么都不做,但某些任务可以重写它,比如关闭资源或发额外通知。

数据流:进去的是任务上下文和回合上下文→ 默认实现只是接收它们并丢弃→ 返回完成信号,不产生额外输出也不改状态。

调用关系:T::abort 这个适配层会把调用转到这里;Session::handle_task_abort 在取消任务、等待片刻并强制终止后台句柄后,会调用任务的 abort 做最后清理。

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

T::kind274–276 ↗
fn kind(&self) -> TaskKind

作用:把具体任务的 kind 方法转成统一接口上的 kind 方法。这样调度器不需要知道任务到底是 RegularTask、ReviewTask 还是别的类型。

数据流:进去的是某个实现了 SessionTask 的具体任务→ 调用它自己的 kind→ 返回 TaskKind,也就是任务类别。

调用关系:Session::start_task 把具体任务包成 AnySessionTask 后,会通过这个适配方法取得任务类别,用于记录 RunningTask 和遥测。

调用图:外部调用 1 个(kind)。

T::span_name278–280 ↗
fn span_name(&self) -> &'static str

作用:把具体任务的追踪名称取出来,给后台任务的日志和链路追踪使用。追踪名称就像给这一段后台工作贴的标签。

数据流:进去的是具体任务→ 调用它自己的 span_name→ 返回一个静态字符串名称。

调用关系:Session::start_task 会用它创建 tracing span(追踪跨度,用来把一段异步工作串起来看),方便之后查日志和性能。

调用图:外部调用 1 个(span_name)。

T::run282–296 ↗
fn run(
        self: Arc<Self>,
        session: Arc<SessionTaskContext>,
        ctx: Arc<TurnContext>,
        input: Vec<TurnInput>,
        cancellation_token: CancellationToken,
    ) -> BoxFutu

作用:把具体任务的异步运行过程包装成统一的 BoxFuture。BoxFuture 可以理解成“装进盒子里的未来结果”,让不同任务看起来像同一种形状。

数据流:进去的是具体任务、任务上下文、回合上下文、输入和取消令牌→ 调用具体 SessionTask::run→ 用 Box::pin 固定成统一返回类型→ 输出一个最终可能带有最后助手消息的异步结果。

调用关系:Session::start_task 在 tokio 后台任务里调用 AnySessionTask::run,实际就会走到这里,再转交给具体任务实现。

调用图:外部调用 2 个(pin, run)。

T::abort298–304 ↗
fn abort(
        &'a self,
        session: Arc<SessionTaskContext>,
        ctx: Arc<TurnContext>,
    ) -> BoxFuture<'a, ()>

作用:把具体任务的取消清理过程包装成统一的异步接口。这样调度器可以用同一种方式清理任何任务。

数据流:进去的是具体任务、任务上下文和回合上下文→ 调用 SessionTask::abort,也就是具体任务重写的清理逻辑或默认空操作→ 包装成 BoxFuture 返回。

调用关系:Session::handle_task_abort 通过 AnySessionTask 调用 abort 时会走到这里,然后真正交给任务自己的 abort 实现。

调用图:调用 1 个内部函数(abort);外部调用 1 个(pin)。

Session::spawn_task308–317 ↗
async fn spawn_task(
        self: &Arc<Self>,
        turn_context: Arc<TurnContext>,
        input: Vec<TurnInput>,
        task: T,
    )

作用:启动一个新任务,并先把旧任务停掉。它保证同一个会话不会同时有两个主要回合任务互相抢状态。

数据流:进去的是回合上下文、输入和一个任务→ 先用 Replaced 原因中止所有现有任务→ 清掉连接器选择→ 再调用 Session::start_task 真正启动新任务。

调用关系:外部想发起一个新的会话任务时会用它;它把“先清旧,再开新”的规矩交给 abort_all_tasks 和 start_task 分别完成。

调用图:调用 2 个内部函数(abort_all_tasks, start_task)。

Session::start_task319–445 ↗
async fn start_task(
        self: &Arc<Self>,
        turn_context: Arc<TurnContext>,
        input: Vec<TurnInput>,
        task: T,
    )

作用:真正把一个回合任务放到后台跑起来。它是本文件最核心的启动器,负责建状态、建取消令牌、建追踪信息、登记 RunningTask。

数据流:进去的是回合上下文、输入和具体任务→ 把任务包装成统一接口→ 标记回合开始时间和起始 token 用量→ 准备取消令牌、完成通知、任务状态和扩展数据→ 用 tokio::spawn 开一个后台异步任务→ 后台任务运行具体 task.run,结束后刷新记录并调用 on_task_finished→ 最后把 RunningTask 保存到 active_turn。

调用关系:Session::spawn_task 和 maybe_start_turn_for_pending_work_with_sub_id 都会调用它。它启动的后台任务完成后,会回到 Session::on_task_finished 做统一收尾。

调用图:调用 1 个内部函数(new);被 2 处调用(maybe_start_turn_for_pending_work_with_sub_id, spawn_task);外部调用 15 个(new, clone, new, new, now, new, kind, span_name, debug_assert!, format! (+5 more))。

Session::maybe_start_turn_for_pending_work453–456 ↗
async fn maybe_start_turn_for_pending_work(self: &Arc<Self>)

作用:当会话空闲但队列里有“需要触发一轮”的待办输入时,自动开一轮普通任务。它避免后台邮件或待处理消息一直躺着没人理。

数据流:进去的是 Session→ 生成一个新的随机 sub_id(本轮编号)→ 调用 maybe_start_turn_for_pending_work_with_sub_id→ 自己不直接启动任务。

调用关系:Session::abort_all_tasks 和 Session::abort_turn_if_active 在因 Interrupted 中断后会调用它,看看是否该立刻处理队列里新的触发项。

调用图:调用 1 个内部函数(maybe_start_turn_for_pending_work_with_sub_id);被 2 处调用(abort_all_tasks, abort_turn_if_active);外部调用 1 个(new_v4)。

Session::maybe_start_turn_for_pending_work_with_sub_id463–484 ↗
async fn maybe_start_turn_for_pending_work_with_sub_id(
        self: &Arc<Self>,
        sub_id: String,
    )

作用:用指定的回合编号,在合适条件下为待办工作启动一轮普通对话任务。它会先确认真的有触发项,而且当前没有任务在跑。

数据流:进去的是 Session 和 sub_id→ 检查输入队列里有没有 trigger_turn 邮件项,没有就返回→ 再检查 active_turn 是否为空,不空也返回→ 创建默认回合上下文,可能发出未知模型警告→ 调用 start_task 启动 RegularTask,输入为空。

调用关系:maybe_start_turn_for_pending_work 负责生成 sub_id 后调用它;它最终把活交给 Session::start_task。

调用图:调用 3 个内部函数(default, start_task, new);被 1 处调用(maybe_start_turn_for_pending_work);外部调用 1 个(new)。

Session::abort_all_tasks486–514 ↗
async fn abort_all_tasks(self: &Arc<Self>, reason: TurnAbortReason)

作用:中止当前会话里的所有正在运行任务。用户发起新请求、替换任务或手动打断时,需要它把旧回合干净地停下来。

数据流:进去的是中止原因→ 从 active_turn 里拿走当前回合→ 如果有任务,就调用 handle_task_abort 取消和收尾→ 发出回合中止生命周期事件→ 清掉本回合挂起输入→ 如果原因是 Interrupted,再尝试启动待办工作→ 没有普通返回值,但会改变 active_turn、输入队列和对外事件。

调用关系:Session::spawn_task 在启动新任务前会调用它。它内部用 take_active_turn 取走当前状态,再把具体中止细节交给 handle_task_abort。

调用图:调用 3 个内部函数(handle_task_abort, maybe_start_turn_for_pending_work, take_active_turn);被 1 处调用(spawn_task);外部调用 1 个(clone)。

Session::abort_turn_if_active516–555 ↗
async fn abort_turn_if_active(
        self: &Arc<Self>,
        turn_id: &str,
        reason: TurnAbortReason,
    ) -> bool

作用:只在指定 turn_id 正好是当前运行回合时,才中止它。这样可以避免误伤别的回合。

数据流:进去的是 turn_id 和中止原因→ 锁住 active_turn 检查当前任务的 sub_id 是否匹配→ 不匹配返回 false→ 匹配就拿走 active_turn,调用 handle_task_abort,发中止生命周期事件,清挂起输入→ 如果是 Interrupted,再尝试启动待办工作→ 返回 true 表示确实中止了。

调用关系:它是更精确的中止入口;和 abort_all_tasks 一样,真正取消任务的细节交给 handle_task_abort。

调用图:调用 2 个内部函数(handle_task_abort, maybe_start_turn_for_pending_work);外部调用 1 个(clone)。

Session::on_task_finished557–775 ↗
async fn on_task_finished(
        self: &Arc<Self>,
        turn_context: Arc<TurnContext>,
        last_agent_message: Option<String>,
    )

作用:统一处理任务正常结束后的收尾。它会发完成事件、记录用量和性能、处理排队输入,并把会话标记为空闲。

数据流:进去的是回合上下文和可选的最后助手消息→ 取消 git 补充信息任务→ 从 active_turn 取下已完成任务并分离后台句柄→ 取出本轮期间积累的待处理输入并通过 hook 检查→ 统计工具调用数、token 用量、网络代理、记忆引用和耗时→ 发送 TurnComplete 事件→ 清理保护器状态→ 如果确认当前 active_turn 就是这轮,置为空并可能发线程空闲生命周期事件。

调用关系:Session::start_task 创建的后台任务在 run 返回后会调用它。它会调用 inspect_pending_input、record_additional_contexts、record_pending_input,以及 emit_turn_network_proxy_metric 和 emit_turn_memory_metric 来完成附加收尾。

调用图:调用 5 个内部函数(inspect_pending_input, record_additional_contexts, record_pending_input, emit_turn_memory_metric, emit_turn_network_proxy_metric);外部调用 5 个(ptr_eq, current, try_from, TurnComplete, warn!)。

Session::take_active_turn777–780 ↗
async fn take_active_turn(&self) -> Option<ActiveTurn>

作用:把当前 active_turn 整个取出来,并把 Session 里的位置清空。它像把值班台上的当前工单拿走,后面才能安全处理或替换。

数据流:进去的是 Session→ 锁住 active_turn→ 调用 take 把 Option 里的 ActiveTurn 移出→ 返回可能存在的 ActiveTurn,同时 Session 内部 active_turn 变成 None。

调用关系:Session::abort_all_tasks 用它先把当前回合从共享状态中移除,再继续执行中止逻辑,减少别的流程同时看到半中止状态的机会。

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

Session::close_unified_exec_processes782–787 ↗
async fn close_unified_exec_processes(&self)

作用:关闭所有统一执行管理器里的后台进程。通俗说,就是把会话里还开着的后台终端或命令进程一起收掉。

数据流:进去的是 Session→ 找到 services.unified_exec_manager→ 调用 terminate_all_processes→ 没有返回业务数据,但会请求终止所有相关进程。

调用关系:它是会话级清理入口;本文件不继续下放到具体任务,而是直接把关闭进程的工作交给 unified_exec_manager。

Session::list_background_terminals789–791 ↗
async fn list_background_terminals(&self) -> Vec<BackgroundTerminalInfo>

作用:列出当前会话里还在后台运行的终端进程。前端或调用方可以用它展示“有哪些命令还没停”。

数据流:进去的是 Session→ 向 unified_exec_manager 请求进程列表→ 返回 BackgroundTerminalInfo 列表,每项描述一个后台终端。

调用关系:它是查看后台终端状态的查询入口;实际收集进程信息的工作由 unified_exec_manager 完成。

Session::terminate_background_terminal793–798 ↗
async fn terminate_background_terminal(&self, process_id: i32) -> bool

作用:按进程号终止某一个后台终端。用户想关掉某个长时间运行的命令时会用到它。

数据流:进去的是 process_id 进程号→ 交给 unified_exec_manager 尝试终止对应进程→ 返回布尔值,表示是否成功找到并发起终止。

调用关系:它和 list_background_terminals 配套:先列出后台终端,再按某个 process_id 调用这里关闭指定进程。

Session::handle_task_abort800–874 ↗
async fn handle_task_abort(self: &Arc<Self>, task: RunningTask, reason: TurnAbortReason)

作用:执行一次任务中止的完整流程。它不是简单杀掉任务,而是先通知任务取消、等它短暂自我收尾,再做强制停止、写历史标记、发中止事件。

数据流:进去的是 RunningTask 和中止原因→ 如果取消令牌已经取消就直接返回→ 取消令牌发信号→ 停掉 git 补充任务→ 最多等待 100 毫秒让任务自己完成→ 仍没完就 abort 后台句柄→ 创建 SessionTaskContext 并调用任务自己的 abort 清理→ 如果是 Interrupted,就根据配置和版本生成中断历史标记,写入对话并刷新落盘→ 记录完成时间和性能画像→ 发送 TurnAborted 事件→ 清理保护器状态。

调用关系:Session::abort_all_tasks 和 Session::abort_turn_if_active 都把具体中止工作交给它。它会调用 InterruptedTurnHistoryMarker::from_config_and_version 和 interrupted_turn_history_marker 来决定是否给模型留下“刚才被打断”的说明。

调用图:调用 3 个内部函数(from_config_and_version, new, interrupted_turn_history_marker);被 2 处调用(abort_all_tasks, abort_turn_if_active);外部调用 7 个(clone, new, TurnAborted, select!, from_ref, trace!, warn!)。

core/src/state/turn.rs源码 ↗
domain_logic每一轮对话或任务执行期间活跃

可以把一轮对话理解成一次办事流程:用户说一句话,系统可能要调用工具、等用户批准、等外部服务回答,最后再给出结果。这个文件就是这次办事流程的“临时登记本”。ActiveTurn 记录当前有没有正在执行的任务,以及这轮共享的 TurnState。TurnState 里放了很多“还没等到回复”的事项,比如审批、权限申请、用户输入、外部服务询问、动态工具返回等,每一项都用一个 key 存起来,等回复来了再取出来通知对应等待者。它还记录邮箱消息是否还能并入当前轮次,避免系统已经给出最终回答后又把迟到消息硬塞进去。权限部分会把同一环境里本轮新批准的权限合并保存,后续工具调用可以继续用。还有一个“严格自动审查”开关,用来让这一轮走更谨慎的审查流程。

函数细节19
ActiveTurn::default57–62 ↗
fn default() -> Self

作用:创建一个“当前没有任务”的默认活动轮次。系统启动新会话或测试时需要一个干净的起点,先把任务位置空出来,同时准备好一份可共享的本轮状态。

数据流:进去没有额外输入 → 它把 task 设成 None,并新建一个带互斥锁的 TurnState;互斥锁就是一把锁,防止多个异步任务同时改同一份状态 → 出来一个 ActiveTurn,表示当前还没有正在跑的任务,但已经有地方记录之后这一轮的临时信息。

调用关系:很多请求处理和测试会从这里开始拿到一个空的 ActiveTurn。后续真正开始执行任务时,别的流程会把 RunningTask 填进去,并通过里面的 turn_state 记录审批、权限、输入等等待事项。

调用图:被 17 处调用(handle_request_permissions_uses_tool_call_id_for_round_trip, codex_apps_auth_elicitation_feature_enabled_requests_elicitation, prompt_mode_waits_for_approval_when_annotations_do_not_require_approval, enable_strict_auto_review_for_turn_uses_originating_turn, request_permissions_guardian_review_stops_when_cancelled, request_permissions_routes_to_guardian_when_reviewer_is_enabled, shell_command_allows_sticky_turn_permissions_without_inline_request_permissions_feature, strict_auto_review_turn_grant_forces_guardian_for_shell_command_policy_skip, notify_request_permissions_response_ignores_unmatched_call_id, record_granted_request_permissions_for_turn_uses_originating_turn (+7 more));外部调用 3 个(new, new, default)。

TurnState::insert_pending_approval109–115 ↗
fn insert_pending_approval(
        &mut self,
        key: String,
        tx: oneshot::Sender<ReviewDecision>,
    ) -> Option<oneshot::Sender<ReviewDecision>>

作用:登记一个还在等待回复的审批请求。比如系统要执行某个可能有风险的动作,需要用户或审查器点头,这个函数就把“等谁回复”先记下来。

数据流:进去一个 key 和一个 oneshot 发送端;oneshot 可以理解成一次性传话筒,只能传回一次结果 → 它把发送端放进 pending_approvals 这张表里 → 出来的是同一个 key 下原来旧的发送端,如果有的话,同时状态表被更新。

调用关系:当某个流程发起审批时会调用它保存等待通道。等审批结果回来时,配套的 TurnState::remove_pending_approval 会把通道取出,再把决定送回给当初等待的任务。

TurnState::remove_pending_approval117–122 ↗
fn remove_pending_approval(
        &mut self,
        key: &str,
    ) -> Option<oneshot::Sender<ReviewDecision>>

作用:取出并移除一个正在等待的审批请求。它通常用在审批结果回来了之后,保证同一个审批不会被重复处理。

数据流:进去一个 key → 它到 pending_approvals 表里查找并删除这一项 → 出来是对应的一次性发送端,如果没找到就返回空;状态里也不再保留这个审批等待项。

调用关系:它和 TurnState::insert_pending_approval 配对使用。发起审批时先插入,收到审批结果时取出,然后外部流程用取出的通道把 ReviewDecision 送回去。

TurnState::clear_pending_waiters124–130 ↗
fn clear_pending_waiters(&mut self)

作用:一次性清掉这一轮里所有还在等回复的临时请求。通常用于一轮被取消、结束或需要收尾时,避免旧的等待项遗留到后面造成混乱。

数据流:进去不需要参数 → 它清空审批、权限申请、用户输入、外部服务询问、动态工具回复这几类等待表 → 出来没有返回值,但 TurnState 里这些未完成等待项都被移除。

调用关系:这是本轮状态的清场动作。其他地方如果决定当前任务不再继续,就会用它把各种待回复通道都丢掉,防止之后迟到的回复误伤新一轮流程。

TurnState::insert_pending_request_permissions132–139 ↗
fn insert_pending_request_permissions(
        &mut self,
        key: String,
        pending_request_permissions: PendingRequestPermissions,
    ) -> Option<PendingRequestPermissions>

作用:登记一个还在等待结果的权限申请。比如工具想访问某个环境或做某个敏感操作,系统需要先把申请挂起来等审查结果。

数据流:进去一个 key 和 PendingRequestPermissions;后者里面有回复通道、申请的权限内容、目标环境 → 它把这份申请放进 pending_request_permissions 表 → 出来是同 key 下被替换掉的旧申请,如果存在的话。

调用关系:权限请求发起方会先调用它存档。等权限审查结果回来时,TurnState::remove_pending_request_permissions 会把对应申请取出来,并用里面的通道把结果交回给等待的执行流程。

TurnState::remove_pending_request_permissions141–146 ↗
fn remove_pending_request_permissions(
        &mut self,
        key: &str,
    ) -> Option<PendingRequestPermissions>

作用:取出并移除一个权限申请等待项。这样权限回复到达后,系统能找到原来那次申请,并且不会重复处理。

数据流:进去一个 key → 它从 pending_request_permissions 表中删除匹配项 → 出来是原来的 PendingRequestPermissions,如果没有对应申请就返回空;状态表随之少掉这一项。

调用关系:它是 TurnState::insert_pending_request_permissions 的收尾动作。权限审查流程收到结果后用它找到申请,再把 RequestPermissionsResponse 送回给原先等待的任务。

TurnState::insert_pending_user_input148–154 ↗
fn insert_pending_user_input(
        &mut self,
        key: String,
        tx: oneshot::Sender<RequestUserInputResponse>,
    ) -> Option<oneshot::Sender<RequestUserInputResponse>>

作用:登记一个正在等待用户补充输入的请求。比如系统需要用户再确认一句话、补一个参数,就用它记录“这个问题还没答”。

数据流:进去一个 key 和一次性回复通道 → 它把通道放进 pending_user_input 表 → 出来是旧的同 key 通道,如果之前已经有一项;状态里新增或替换了这次等待用户输入的记录。

调用关系:当系统向用户发出补充输入请求时调用它。用户真正回答后,TurnState::remove_pending_user_input 会取出对应通道,把 RequestUserInputResponse 送回给等待的那段流程。

TurnState::remove_pending_user_input156–161 ↗
fn remove_pending_user_input(
        &mut self,
        key: &str,
    ) -> Option<oneshot::Sender<RequestUserInputResponse>>

作用:取出并移除一个等待用户输入的请求。它让用户的回答能准确回到当初提出问题的地方。

数据流:进去一个 key → 它在 pending_user_input 表里找到并删除对应项 → 出来是一次性回复通道,如果找不到就返回空;状态里不再记录这次用户输入等待。

调用关系:它和 TurnState::insert_pending_user_input 成对出现。一个负责挂号等回答,一个负责在回答来了之后取号并传回结果。

TurnState::insert_pending_elicitation163–171 ↗
fn insert_pending_elicitation(
        &mut self,
        server_name: String,
        request_id: RequestId,
        tx: oneshot::Sender<ElicitationResponse>,
    ) -> Option<oneshot::Sender<Elicitat

作用:登记一个外部服务正在向系统“追问信息”的请求。这里的 elicitation 可以理解成某个连接的服务需要再问用户或系统一个问题,答案稍后才回来。

数据流:进去服务名、请求编号和一次性回复通道 → 它用“服务名 + 请求编号”当作复合钥匙,把通道存进 pending_elicitations 表 → 出来是旧的同钥匙通道,如果原来已有一项。

调用关系:当外部服务发起询问时,这个函数负责把等待关系记住。等回答回来时,TurnState::remove_pending_elicitation 会按同样的服务名和请求编号找到它。

TurnState::remove_pending_elicitation173–180 ↗
fn remove_pending_elicitation(
        &mut self,
        server_name: &str,
        request_id: &RequestId,
    ) -> Option<oneshot::Sender<ElicitationResponse>>

作用:取出并移除一个外部服务询问的等待项。它用服务名和请求编号一起定位,避免不同服务或不同问题互相串线。

数据流:进去服务名和请求编号 → 它把这两个值组成查找钥匙,到 pending_elicitations 表里删除对应项;过程中会复制请求编号用于查表 → 出来是对应的一次性回复通道,找不到则为空。

调用关系:它接在 TurnState::insert_pending_elicitation 后面使用。外部服务的回答抵达时,系统靠它找到当时挂起的询问,再把 ElicitationResponse 交回去。

调用图:外部调用 1 个(clone)。

TurnState::insert_pending_dynamic_tool182–188 ↗
fn insert_pending_dynamic_tool(
        &mut self,
        key: String,
        tx: oneshot::Sender<DynamicToolResponse>,
    ) -> Option<oneshot::Sender<DynamicToolResponse>>

作用:登记一个动态工具的未完成调用。动态工具可以理解成运行时临时接入或变化的工具,它的结果不是马上到,所以要先记账。

数据流:进去一个 key 和一次性回复通道 → 它把通道存入 pending_dynamic_tools 表 → 出来是同 key 下原先的通道,如果存在;状态中记录了这个工具调用正在等待结果。

调用关系:动态工具被调用时会用它保存等待通道。工具结果回来后,TurnState::remove_pending_dynamic_tool 负责取出通道,把 DynamicToolResponse 传回等待者。

TurnState::remove_pending_dynamic_tool190–195 ↗
fn remove_pending_dynamic_tool(
        &mut self,
        key: &str,
    ) -> Option<oneshot::Sender<DynamicToolResponse>>

作用:取出并移除一个动态工具的等待项。这样工具结果回来时可以准确送达,而且同一结果不会被处理两次。

数据流:进去一个 key → 它从 pending_dynamic_tools 表中删除对应记录 → 出来是保存的回复通道,如果没有找到就返回空;状态里这次工具等待被清除。

调用关系:它是 TurnState::insert_pending_dynamic_tool 的配套收尾函数。动态工具完成后,外部流程调用它找到等待者,再把结果发回去。

TurnState::accept_mailbox_delivery_for_current_turn197–199 ↗
fn accept_mailbox_delivery_for_current_turn(&mut self)

作用:把邮箱消息接收状态切回“可以并入当前轮”。简单说,就是告诉系统:接下来收到的子消息还能算进这一轮对话里。

数据流:进去没有参数 → 它调用 TurnState::set_mailbox_delivery_phase,把阶段设为 CurrentTurn → 出来没有返回值,但 mailbox_delivery_phase 被改成允许当前轮接收邮箱消息。

调用关系:它是一个更好读的快捷按钮,内部把实际设置工作交给 TurnState::set_mailbox_delivery_phase。某些后续明确属于同一轮的动作出现时,会用它重新打开当前轮的消息合并。

调用图:调用 1 个内部函数(set_mailbox_delivery_phase)。

TurnState::accepts_mailbox_delivery_for_current_turn201–203 ↗
fn accepts_mailbox_delivery_for_current_turn(&self) -> bool

作用:询问当前这一轮是否还接收邮箱消息。调用者可以用它决定迟到的子消息是马上并入当前请求,还是留到下一轮再处理。

数据流:进去没有参数,只读取当前状态 → 它检查 mailbox_delivery_phase 是否等于 CurrentTurn → 出来一个布尔值:true 表示还能并入当前轮,false 表示应该留给后面的轮次。

调用关系:消息分发或输入整理流程会在决定是否吸收邮箱消息前问它。它不改状态,只提供判断结果;状态本身由 TurnState::set_mailbox_delivery_phase 或 TurnState::accept_mailbox_delivery_for_current_turn 改。

TurnState::set_mailbox_delivery_phase205–207 ↗
fn set_mailbox_delivery_phase(&mut self, phase: MailboxDeliveryPhase)

作用:直接设置邮箱消息属于当前轮还是下一轮。它是控制“迟到消息怎么归类”的核心开关。

数据流:进去一个 MailboxDeliveryPhase,表示 CurrentTurn 或 NextTurn → 它把 mailbox_delivery_phase 字段改成这个值 → 出来没有返回值,但之后消息分发看到的规则会变。

调用关系:TurnState::accept_mailbox_delivery_for_current_turn 会调用它把阶段设回 CurrentTurn。其他流程也可以用它在当前轮已经输出最终内容后切到 NextTurn,避免继续扩展一个已经展示给用户的回答。

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

TurnState::record_granted_permissions209–223 ↗
fn record_granted_permissions(
        &mut self,
        environment_id: &str,
        permissions: AdditionalPermissionProfile,
    )

作用:记录这一轮在某个环境里已经获得的新权限,并和旧权限合并。这样同一轮后续操作可以复用刚刚批准过的权限,不必反复问。

数据流:进去一个环境 ID 和一份新增权限 → 它先取出这个环境之前已记录的权限,再调用 merge_permission_profiles 把旧权限和新权限合在一起 → 如果合并后有结果,就把它按环境 ID 存回 granted_permissions_by_environment_id。

调用关系:权限申请通过后会调用它更新本轮权限账本。后续工具或审查流程可以用 TurnState::granted_permissions 查询这些已批准权限,判断是否还需要再申请。

调用图:调用 1 个内部函数(merge_permission_profiles)。

TurnState::granted_permissions225–232 ↗
fn granted_permissions(
        &self,
        environment_id: &str,
    ) -> Option<AdditionalPermissionProfile>

作用:查询某个环境在这一轮里已经拿到的额外权限。它让后续操作知道“之前已经批准过什么”。

数据流:进去一个环境 ID → 它到 granted_permissions_by_environment_id 表里查找对应权限,并复制一份返回 → 出来是一份 AdditionalPermissionProfile,如果这个环境还没有记录则为空;它不会改动状态。

调用关系:它通常跟 TurnState::record_granted_permissions 配合。前者负责写入权限账本,后者负责在工具调用或权限判断时读取这本账。

TurnState::enable_strict_auto_review234–236 ↗
fn enable_strict_auto_review(&mut self)

作用:打开这一轮的严格自动审查模式。打开后,后续判断会把这一轮当成需要更谨慎审查的流程来处理。

数据流:进去没有参数 → 它把 strict_auto_review_enabled 设为 true → 出来没有返回值,但 TurnState 之后会记住这个开关已经打开。

调用关系:当某个入口或策略决定本轮必须严格审查时会调用它。后续流程通过 TurnState::strict_auto_review_enabled 查询这个开关,再决定是否走更严格的审查路线。

TurnState::strict_auto_review_enabled238–240 ↗
fn strict_auto_review_enabled(&self) -> bool

作用:查看这一轮是否已经启用严格自动审查。它给后续权限或工具执行判断提供一个简单的是/否信号。

数据流:进去没有参数,只读取当前状态 → 它返回 strict_auto_review_enabled 的当前值 → 出来一个布尔值;不会修改任何状态。

调用关系:它和 TurnState::enable_strict_auto_review 配套。一个负责打开开关,一个负责让其他流程在需要时检查这个开关。

code-mode/src/service.rs源码 ↗
orchestrationrequest handling, main loop, shutdown

可以把这个文件理解成代码执行的“调度台”。外部提交一段代码后,它会给这次执行分配一个 cell_id,也就是一个小任务编号,然后启动底层运行时。运行时会不断吐出事件,比如有文本输出、需要调用工具、代码暂停了、代码结束了。这里的核心循环 run_cell_control 像值班员一样,一边听运行时消息,一边听外部的 wait、terminate 等命令,并决定什么时候返回结果、什么时候先让出控制权、什么时候缓存最终结果。它还保存同一会话里的共享值,让前一个代码单元写入的值能被后一个读到,但不同会话互不影响。文件里也包含默认的空代理:没有外部工具时,工具调用会被拒绝,普通通知会被忽略。底部测试覆盖了同步结束、暂停、终止、图片输出、国际化格式等关键行为。

函数细节61
NoopCodeModeSessionDelegate::invoke_tool44–53 ↗
fn invoke_tool(
        &'a self,
        _invocation: CodeModeNestedToolCall,
        cancellation_token: CancellationToken,
    ) -> ToolInvocationFuture<'a>

作用:这是默认的“工具调用处理器”。当代码里想调用外部工具,但外面没有真正提供工具时,它会一直等到取消,然后返回“不可用”的错误。

数据流:进去的是一次工具调用信息和一个取消令牌(一种通知任务该停下来的信号)→ 它不真正执行工具,只等待取消信号 → 出来的是错误文字,说明代码模式里的嵌套工具不可用。

调用关系:run_cell_control 在收到 RuntimeEvent::ToolCall 时会通过 delegate 调用工具;如果会话用的是默认代理,就会走到这里,最后把错误送回运行时。

调用图:外部调用 2 个(pin, cancelled)。

NoopCodeModeSessionDelegate::notify55–63 ↗
fn notify(
        &'a self,
        _call_id: String,
        _cell_id: CellId,
        _text: String,
        _cancellation_token: CancellationToken,
    ) -> NotificationFuture<'a>

作用:这是默认的“通知处理器”。代码发出的通知在没有外部接收者时,会被安全地当成成功处理掉。

数据流:进去的是调用编号、代码单元编号、通知文字和取消令牌 → 它不保存也不转发这些内容 → 出来是成功结果,不改动任何状态。

调用关系:run_cell_control 收到 RuntimeEvent::Notify 时会调用 delegate.notify;默认代理让通知不会因为没人接收而打断代码执行。

调用图:外部调用 1 个(pin)。

NoopCodeModeSessionDelegate::cell_closed65–65 ↗
fn cell_closed(&self, _cell_id: &CellId)

作用:这是默认的“代码单元关闭提醒”。它什么也不做,用来保证即使没人关心关闭事件,流程也能正常结束。

数据流:进去的是已关闭的 cell_id → 它不读取额外信息,也不修改状态 → 没有返回值。

调用关系:run_cell_control 在清理完一个代码单元后会通知 delegate;默认代理接到通知后直接忽略。

InProcessCodeModeSessionProvider::create_session72–81 ↗
fn create_session(
        &'a self,
        delegate: Arc<dyn CodeModeSessionDelegate>,
    ) -> CodeModeSessionProviderFuture<'a>

作用:它创建一个本进程内的代码模式会话。也就是说,不启动远程服务,直接在当前程序里组装 CodeModeService。

数据流:进去的是一个 delegate,也就是外部工具和通知的接收者 → 它用这个 delegate 构造 CodeModeService,并包装成通用会话接口 → 出来的是一个可共享的会话对象。

调用关系:上层需要新建代码模式会话时会调用 provider;它把真正的会话工作交给 CodeModeService::with_delegate。

调用图:调用 1 个内部函数(with_delegate);外部调用 2 个(new, pin)。

CodeModeService::new105–107 ↗
fn new() -> Self

作用:创建一个最简单的代码模式服务,不连接任何真实外部工具。测试和不需要工具的场景会直接用它。

数据流:没有输入 → 它准备一个默认的 NoopCodeModeSessionDelegate → 出来是新的 CodeModeService,里面有空的共享值表、空的代码单元表和从 1 开始的编号器。

调用关系:它是 with_delegate 的简化入口;大量测试用它快速搭出一个可运行的会话。

调用图:被 29 处调用(date_locale_string_formats_with_icu_data, execute_to_pending_excludes_delayed_timeout_tool_calls_until_wait, execute_to_pending_identifies_tool_calls_in_paused_frontier, execute_to_pending_returns_completed_for_synchronous_results, execute_to_pending_returns_once_the_runtime_is_quiescent, generated_image_helper_appends_image_and_output_hint, image_helper_accepts_low_detail, image_helper_accepts_raw_mcp_image_block_with_original_detail, image_helper_rejects_raw_mcp_result_container, image_helper_rejects_unsupported_detail (+15 more));外部调用 2 个(new, with_delegate)。

CodeModeService::with_delegate109–119 ↗
fn with_delegate(delegate: Arc<dyn CodeModeSessionDelegate>) -> Self

作用:创建一个带外部代理的代码模式服务。这个代理负责接收通知、执行代码里发起的工具调用、知道代码单元何时关闭。

数据流:进去的是 delegate → 它初始化内部共享状态,包括 stored_values、cells、shutting_down 标记和 next_cell_id → 出来是可用于执行代码的 CodeModeService。

调用关系:InProcessCodeModeSessionProvider::create_session 和 CodeModeService::new 都依赖它;带真实工具能力的会话会从这里开始。

调用图:被 5 处调用(create_session, natural_completion_cleans_up_callbacks_before_responding, repeated_termination_is_rejected_while_callback_cleanup_is_pending, termination_cancels_pending_callbacks_before_responding, new);外部调用 5 个(new, new, new, new, new)。

CodeModeService::allocate_cell_id121–128 ↗
fn allocate_cell_id(&self) -> CellId

作用:给每次代码执行分配一个新的编号。这样 wait 或 terminate 才知道要找哪一个正在跑的代码单元。

数据流:它读取内部的递增数字 → 原子地加 1,避免并发时拿到重复编号 → 出来是字符串形式的 CellId。

调用关系:execute 和 execute_to_pending 在启动新代码单元前都会先调用它,然后把编号传给 start_cell。

调用图:调用 1 个内部函数(new);被 2 处调用(execute, execute_to_pending)。

CodeModeService::execute_to_pending149–167 ↗
async fn execute_to_pending(
        &self,
        request: ExecuteRequest,
    ) -> Result<ExecuteToPendingOutcome, String>

作用:启动代码,并等到它“结束”或“暂时没事可做”。这适合想知道代码当前是否卡在等待工具、定时器或异步任务的场景。

数据流:进去的是 ExecuteRequest,里面有源码、工具列表、输出限制等 → 它分配 cell_id,启动运行时,并让运行时在 pending 状态暂停 → 出来是 Completed 最终结果,或者 Pending 暂停信息。

调用关系:它把启动工作交给 start_cell;后续 run_cell_control 会在运行时进入 Pending 或 Result 时把结果通过一次性通道送回来。

调用图:调用 2 个内部函数(allocate_cell_id, start_cell);外部调用 2 个(ExecuteToPending, channel)。

CodeModeService::start_cell169–222 ↗
async fn start_cell(
        &self,
        cell_id: CellId,
        request: ExecuteRequest,
        initial_response_tx: CellResponseSender,
        initial_yield_time_ms: Option<u64>,
        pendi

作用:真正启动一个代码单元。它把请求、共享值、消息通道和取消信号都接好,然后把运行时和控制循环同时安排起来。

数据流:进去的是 cell_id、执行请求、初始响应通道、是否设置初始让出时间、pending 模式 → 它复制当前共享值,启动底层运行时,把 cell handle 登记到 cells 表,再启动 run_cell_control → 成功时返回空结果,失败时返回错误文字。

调用关系:execute 和 execute_to_pending 都靠它开工;它调用 spawn_runtime 创建底层执行环境,并把后续所有事件处理交给 run_cell_control。

调用图:调用 2 个内部函数(spawn_runtime, run_cell_control);被 2 处调用(execute, execute_to_pending);外部调用 8 个(clone, new, new, new, clone, format!, unbounded_channel, spawn)。

CodeModeService::begin_wait228–249 ↗
async fn begin_wait(
        &self,
        request: WaitRequest,
    ) -> CodeModeSessionResultFuture<'static, WaitOutcome>

作用:开始等待某个已经启动的代码单元下一次有结果。它把外部的 wait 请求变成内部控制命令。

数据流:进去的是 WaitRequest,包含 cell_id 和最多等多久才让出 → 它查找对应 CellHandle,创建响应通道,并发送 Poll 命令 → 出来是一个 future,未来会变成 LiveCell、MissingCell 或错误。

调用关系:CodeModeService::wait 调用它;它不直接处理运行时事件,而是把活交给对应代码单元的 run_cell_control。

调用图:调用 2 个内部函数(missing_wait, wait_for_response);被 1 处调用(wait);外部调用 1 个(channel)。

CodeModeService::wait_to_pending279–307 ↗
async fn wait_to_pending(
        &self,
        request: WaitToPendingRequest,
    ) -> Result<WaitToPendingOutcome, String>

作用:继续观察一个暂停模式的代码单元,直到它再次完成或再次进入 pending。它比普通 wait 更关心“安静下来”的边界。

数据流:进去的是 WaitToPendingRequest → 它找到 cell handle,发送 PollToPending 命令,并等待一次性回复 → 出来是 LiveCell 包着 Completed 或 Pending,找不到时是 MissingCell,异常时是错误。

调用关系:外部在 execute_to_pending 得到 Pending 后,可以用它继续推进;具体什么时候返回由 run_cell_control 根据 RuntimeEvent::Pending 或 Result 决定。

调用图:调用 1 个内部函数(missing_cell_response);外部调用 3 个(LiveCell, MissingCell, channel)。

CodeModeService::default335–337 ↗
fn default() -> Self

作用:提供 Rust 的默认构造方式。需要默认值时,它等同于 CodeModeService::new。

数据流:没有输入 → 调用 new → 出来是一个使用空代理的新服务。

调用关系:这是标准 Default 接口,方便框架或测试在需要默认实例时自动创建服务。

调用图:外部调用 1 个(new)。

CodeModeService::drop341–353 ↗
fn drop(&mut self)

作用:当服务对象被丢弃时,尽量通知所有正在跑的代码单元停下来。它是最后一道安全网,防止后台任务孤儿化。

数据流:进去的是即将销毁的服务本身 → 它设置 shutting_down,尝试拿到 cells 表,逐个取消令牌、发送 Terminate 控制命令和 RuntimeCommand::Terminate → 没有返回值,且发送失败会被忽略。

调用关系:正常关闭应走 shutdown;如果调用方忘了显式关闭,Drop 会尽力触发 run_cell_control 和运行时收尾。

调用图:外部调用 1 个(channel)。

CodeModeService::is_alive357–359 ↗
fn is_alive(&self) -> bool

作用:告诉调用方这个会话是否还接受工作。只要还没进入关闭状态,它就返回 true。

数据流:它读取内部 shutting_down 标记 → 取反 → 出来是布尔值,true 表示还活着,false 表示正在或已经关闭。

调用关系:这是 CodeModeSession 接口的一部分;上层可以在提交新请求前用它做快速检查。

CodeModeService::execute361–366 ↗
fn execute(
        &'a self,
        request: ExecuteRequest,
    ) -> CodeModeSessionResultFuture<'a, StartedCell>

作用:这是会话接口里的执行入口:提交一段代码,并拿到一个 StartedCell,用来接收初始结果和后续等待。

数据流:进去的是 ExecuteRequest → 它分配 cell_id,建立一次性响应通道,调用 start_cell,以普通连续运行模式启动 → 出来是 StartedCell,里面带 cell_id 和初始响应接收器。

调用关系:外部通过 CodeModeSession trait 调它;内部把实际启动交给 start_cell,后续输出由 run_cell_control 送回。

调用图:调用 3 个内部函数(from_result_receiver, allocate_cell_id, start_cell);被 1 处调用(execute);外部调用 3 个(pin, Runtime, channel)。

CodeModeService::wait368–370 ↗
fn wait(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome>

作用:等待某个代码单元继续产出、让出或结束。它是外部轮询正在运行代码的普通入口。

数据流:进去的是 WaitRequest → 它调用 begin_wait 得到等待 future,并包装成接口要求的异步结果 → 出来是 WaitOutcome 或错误。

调用关系:这是 CodeModeSession 接口的一部分;begin_wait 负责发送 Poll 命令,run_cell_control 负责真正回应。

调用图:调用 1 个内部函数(begin_wait);外部调用 1 个(pin)。

CodeModeService::terminate372–374 ↗
fn terminate(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome>

作用:请求停止某个正在运行的代码单元。它会避免同一个 cell 被重复终止两次。

数据流:进去的是 cell_id → 它查找 cell,设置 termination_requested 标记,发送 Terminate 命令并等待回应 → 出来是终止后的 RuntimeResponse、MissingCell,或“已经在终止”的错误。

调用关系:这是 CodeModeSession 接口的一部分;它把停止请求交给 run_cell_control,错误文字由 already_terminating_error 等辅助函数生成。

调用图:调用 2 个内部函数(already_terminating_error, missing_cell_response);外部调用 4 个(pin, LiveCell, MissingCell, channel)。

CodeModeService::shutdown376–378 ↗
fn shutdown(&'a self) -> CodeModeSessionResultFuture<'a, ()>

作用:关闭整个代码模式会话。它会拒绝新代码,并要求所有现有代码单元停止。

数据流:没有业务输入 → 它设置 shutting_down,复制当前所有 cell handle,逐个取消、发终止命令,再循环等待 cells 表清空 → 出来是成功或错误结果,正常情况下所有 cell 都被清理。

调用关系:这是 CodeModeSession 接口的关闭入口;每个 cell 的最终清理由 run_cell_control 完成。

调用图:外部调用 3 个(pin, channel, yield_now)。

missing_cell_response413–419 ↗
fn missing_cell_response(cell_id: CellId) -> RuntimeResponse

作用:生成“找不到代码单元”的统一响应。这样 wait、terminate 等接口能用同一种格式告诉调用方目标不存在。

数据流:进去的是 cell_id → 它组装 RuntimeResponse::Result,内容为空,error_text 写着对应 cell 不存在 → 出来是这个响应对象。

调用关系:terminate、wait_to_pending、missing_wait 和 wait_for_response 都用它,保证缺失 cell 的提示一致。

调用图:被 4 处调用(terminate, wait_to_pending, missing_wait, wait_for_response);外部调用 2 个(new, format!)。

missing_wait421–423 ↗
fn missing_wait(cell_id: CellId) -> CodeModeSessionResultFuture<'static, WaitOutcome>

作用:把“等待的 cell 不存在”包装成异步结果。这样 begin_wait 即使立刻发现缺失,也能返回同样类型的 future。

数据流:进去的是 cell_id → 它调用 missing_cell_response,并包装成 WaitOutcome::MissingCell → 出来是一个马上完成的 future。

调用关系:begin_wait 找不到 cell 或控制通道断开时会用它;它让 wait 的返回形状保持统一。

调用图:调用 1 个内部函数(missing_cell_response);被 1 处调用(begin_wait);外部调用 2 个(pin, MissingCell)。

wait_for_response425–436 ↗
fn wait_for_response(
    cell_id: CellId,
    response_rx: oneshot::Receiver<Result<RuntimeResponse, String>>,
) -> CodeModeSessionResultFuture<'static, WaitOutcome>

作用:等待 run_cell_control 通过一次性通道发回普通 wait 的结果。它也处理通道断开的情况。

数据流:进去的是 cell_id 和响应接收器 → 它等待接收器完成;收到成功响应就包成 LiveCell,收到错误就返回错误,通道断了就当作 MissingCell → 出来是 WaitOutcome 或错误。

调用关系:begin_wait 成功发出 Poll 命令后会返回它;真正的响应来自 run_cell_control。

调用图:调用 1 个内部函数(missing_cell_response);被 1 处调用(begin_wait);外部调用 3 个(pin, LiveCell, MissingCell)。

busy_observer_error438–440 ↗
fn busy_observer_error(cell_id: &CellId) -> String

作用:生成“已经有人在等这个 cell”的错误文字。一个 cell 同时只能有一个活跃观察者,避免两个请求抢同一份输出。

数据流:进去的是 cell_id → 它格式化一段错误信息 → 出来是字符串。

调用关系:run_cell_control 在收到新的 Poll、PollToPending 时,如果已有 response_tx 或 termination_response_tx,就用这个错误拒绝新观察者。

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

already_terminating_error442–444 ↗
fn already_terminating_error(cell_id: &CellId) -> String

作用:生成“这个 cell 已经在终止中”的错误文字。它让重复终止请求得到清楚反馈。

数据流:进去的是 cell_id → 它格式化一段错误信息 → 出来是字符串。

调用关系:CodeModeService::terminate 和 run_cell_control 都会在发现重复终止时用它。

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

pending_result_response446–452 ↗
fn pending_result_response(cell_id: &CellId, result: PendingResult) -> RuntimeResponse

作用:把暂存的最终结果变成标准 RuntimeResponse。暂存结果通常是在没有人等待时先放起来的。

数据流:进去的是 cell_id 和 PendingResult → 它复制 cell_id,取出内容列表和错误文字 → 出来是 RuntimeResponse::Result。

调用关系:send_or_buffer_result 以及 run_cell_control 处理已缓存结果时会用它,把内部缓存转换成外部能看懂的响应。

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

send_terminal_response454–463 ↗
fn send_terminal_response(response_tx: CellResponseSender, response: RuntimeResponse)

作用:把最终结果发给最初等待的人。它会根据等待方式不同,把结果发成普通 RuntimeResponse 或 execute_to_pending 的 Completed。

数据流:进去的是 CellResponseSender 和 RuntimeResponse → 如果是 Runtime 通道,就直接发送响应;如果是 ExecuteToPending 通道,就包装成 Completed 再发送 → 出来没有显式结果,发送失败会被忽略。

调用关系:send_or_buffer_result 和 send_termination_responses 用它统一发送最终响应,避免两种等待模式各写一遍。

调用图:被 2 处调用(send_or_buffer_result, send_termination_responses);外部调用 2 个(Completed, send)。

send_termination_responses465–476 ↗
fn send_termination_responses(
    response_tx: Option<CellResponseSender>,
    termination_response_tx: Option<oneshot::Sender<Result<RuntimeResponse, String>>>,
    response: RuntimeResponse,
)

作用:终止时可能有两个等待者:原本等输出的人,以及后来发起 terminate 的人。这个函数负责把同一个终止结果都通知到。

数据流:进去的是可选的普通响应通道、可选的终止响应通道和 RuntimeResponse → 它先给普通等待者发最终响应,再给终止请求者发成功响应 → 出来没有显式结果。

调用关系:run_cell_control 在确认运行时已停下后会调用它,确保相关调用方都收到收尾消息。

调用图:调用 1 个内部函数(send_terminal_response);外部调用 1 个(clone)。

send_or_buffer_result478–492 ↗
fn send_or_buffer_result(
    cell_id: &CellId,
    result: PendingResult,
    response_tx: &mut Option<CellResponseSender>,
    pending_result: &mut Option<PendingResult>,
) -> bool

作用:有等待者时立刻发最终结果,没人等时先缓存起来。这样结果不会因为调用方晚一点来 wait 而丢失。

数据流:进去的是 cell_id、PendingResult、当前响应通道槽、缓存槽 → 如果有 response_tx,就转成 RuntimeResponse 并发送,返回 true;如果没有,就放进 pending_result,返回 false → 可能改动 response_tx 和 pending_result。

调用关系:run_cell_control 收到 RuntimeEvent::Result 或运行时意外结束时用它;它调用 pending_result_response 和 send_terminal_response。

调用图:调用 2 个内部函数(pending_result_response, send_terminal_response)。

send_yield_response494–513 ↗
fn send_yield_response(
    cell_id: &CellId,
    content_items: &mut Vec<FunctionCallOutputContentItem>,
    response_tx: &mut Option<CellResponseSender>,
)

作用:在代码运行太久或主动让出时,把已经收集到的输出先发回去。这样调用方不用等到代码完全结束才看到内容。

数据流:进去的是 cell_id、当前输出列表和响应通道槽 → 如果等待者是普通 Runtime,就发送 Yielded 并清空输出;如果等待者是 ExecuteToPending,就不发送,继续保留等待 pending/完成 → 出来没有显式结果,但可能清空 content_items。

调用关系:run_cell_control 在计时器到期或收到 YieldRequested 事件时调用它。

调用图:外部调用 3 个(clone, ExecuteToPending, take)。

run_cell_control515–834 ↗
async fn run_cell_control(
    inner: Arc<Inner>,
    context: CellControlContext,
    mut event_rx: mpsc::UnboundedReceiver<RuntimeEvent>,
    mut control_rx: mpsc::UnboundedReceiver<CellControlComma

作用:这是每个代码单元的“大脑”和值班循环。它同时处理外部命令、运行时事件、工具任务、通知任务,并决定何时返回、暂停、缓存、终止和清理。

数据流:进去的是共享内部状态、这个 cell 的上下文、运行时事件接收器、控制命令接收器、初始响应通道和初始让出时间 → 它循环监听 Poll、PollToPending、Terminate,以及 Started、ContentItem、ToolCall、Pending、Result 等事件;期间收集输出、启动工具/通知子任务、保存 stored_values、发送响应或缓存结果 → 结束前会终止运行时、取消回调、从 cells 表移除自己,并通知 delegate.cell_closed。

调用关系:start_cell 为每个新 cell 启动它;它调用 finish_callbacks、send_yield_response、send_or_buffer_result、send_termination_responses、terminate_paused_runtime 等辅助函数,是本文件最核心的流程枢纽。

调用图:调用 2 个内部函数(finish_callbacks, terminate_paused_runtime);被 2 处调用(start_cell, terminate_waits_for_runtime_shutdown_before_responding);外部调用 3 个(new, new, select!)。

finish_callbacks842–854 ↗
async fn finish_callbacks(
    cancellation_token: &CancellationToken,
    notification_tasks: &mut JoinSet<()>,
    tool_tasks: &mut JoinSet<()>,
    completion: CallbackCompletion,
)

作用:收尾通知任务和工具任务。它确保在返回最终结果前,该等的通知已等完,该取消的工具已取消。

数据流:进去的是取消令牌、通知任务集合、工具任务集合和完成策略 → 如果策略是 Cancel,先取消;随后等通知任务结束,再取消一次并等工具任务结束 → 出来没有显式结果,但后台任务被清空。

调用关系:run_cell_control 在自然完成、终止、异常清理时调用它;它把具体等待工作交给 drain_tasks。

调用图:调用 1 个内部函数(drain_tasks);被 1 处调用(run_cell_control);外部调用 2 个(cancel, matches!)。

drain_tasks856–864 ↗
async fn drain_tasks(tasks: &mut JoinSet<()>, description: &str)

作用:把一组后台任务一个个等完,并记录非取消类失败。它像清空待办篮一样,确保没有悬着的回调任务。

数据流:进去的是 JoinSet 和任务描述文字 → 它循环 join_next 等任务结束;如果任务失败且不是正常取消,就写警告日志 → 出来时任务集合为空。

调用关系:finish_callbacks 分别用它清理 notification 和 tool 两类任务。

调用图:被 1 处调用(finish_callbacks);外部调用 2 个(join_next, warn!)。

resume_paused_runtime866–873 ↗
fn resume_paused_runtime(
    runtime_control_tx: &std::sync::mpsc::Sender<RuntimeControlCommand>,
    pending_mode: PendingRuntimeMode,
)

作用:如果运行时处在“暂停直到恢复”模式,它发送恢复信号。普通连续运行模式下它什么也不做。

数据流:进去的是运行时控制通道和 pending 模式 → 如果模式是 PauseUntilResumed,就发送 RuntimeControlCommand::Resume → 没有返回值,发送失败被忽略。

调用关系:run_cell_control 在收到 Poll 或 PollToPending、需要继续推进暂停运行时时会调用它。

调用图:外部调用 1 个(send)。

terminate_paused_runtime875–882 ↗
fn terminate_paused_runtime(
    runtime_control_tx: &std::sync::mpsc::Sender<RuntimeControlCommand>,
    pending_mode: PendingRuntimeMode,
)

作用:如果运行时处在暂停模式,它发送终止控制信号。这样即使运行时正暂停,也能被叫醒并结束。

数据流:进去的是运行时控制通道和 pending 模式 → 如果是 PauseUntilResumed,就发送 RuntimeControlCommand::Terminate → 没有返回值。

调用关系:run_cell_control 在终止和最终清理时调用它,配合 RuntimeCommand::Terminate 和 V8 的 terminate_execution 使用。

调用图:被 1 处调用(run_cell_control);外部调用 1 个(send)。

tests::execute_request921–929 ↗
fn execute_request(source: &str) -> ExecuteRequest

作用:测试用的小工具,用最少输入快速造一个 ExecuteRequest。这样每个测试不用重复写一堆默认字段。

数据流:进去的是源码字符串 → 它填入固定 tool_call_id、空工具列表、1 毫秒让出时间和无输出上限 → 出来是 ExecuteRequest。

调用关系:许多测试先用它生成默认请求,再按需要覆盖 source、yield_time_ms 或 enabled_tools。

调用图:外部调用 1 个(new)。

tests::cell_id931–933 ↗
fn cell_id(value: &str) -> CellId

作用:测试用的小工具,把普通字符串变成 CellId。它让断言里的编号写起来更短。

数据流:进去的是字符串切片 → 它复制成 String 并调用 CellId::new → 出来是 CellId。

调用关系:测试在比较 RuntimeResponse、WaitOutcome 时大量用它构造期望值。

调用图:调用 1 个内部函数(new)。

tests::execute935–943 ↗
async fn execute(service: &CodeModeService, request: ExecuteRequest) -> RuntimeResponse

作用:测试用的快捷执行函数。它提交请求,并直接等初始响应,省去每个测试都写 unwrap 链。

数据流:进去的是服务和执行请求 → 它调用 service.execute,取得 StartedCell,再等待 initial_response → 出来是 RuntimeResponse。

调用关系:同步结果、图片 helper、国际化等测试都用它;它实际走的是 CodeModeService::execute 流程。

调用图:调用 1 个内部函数(execute)。

tests::test_inner945–953 ↗
fn test_inner() -> Arc<Inner>

作用:测试用来手工构造 Inner。这样某些测试可以直接启动 run_cell_control,而不必走完整服务启动流程。

数据流:没有输入 → 它创建空 stored_values、空 cells、默认 delegate、未关闭标记和编号器 → 出来是 Arc<Inner>。

调用关系:terminate_waits_for_runtime_shutdown_before_responding 等更底层的控制循环测试会用它。

调用图:外部调用 5 个(new, new, new, new, new)。

tests::synchronous_exit_returns_successfully956–979 ↗
async fn synchronous_exit_returns_successfully()

作用:验证代码调用 exit() 后会正常停止,并且 exit 后面的输出不会出现。

数据流:测试创建服务,执行先 text、再 exit、再 text 的源码 → 服务返回最终结果 → 断言只包含 exit 前的文本且没有错误。

调用关系:它覆盖 CodeModeService::new、测试 helper execute,以及运行时 Result 到 run_cell_control 返回结果的路径。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::stored_values_are_shared_between_cells_but_not_sessions982–1043 ↗
async fn stored_values_are_shared_between_cells_but_not_sessions()

作用:验证同一个会话里的 store/load 能共享值,但不同会话之间不会串数据。

数据流:测试创建两个服务 → 在第一个服务写入 key,再在同一个服务和另一个服务读取 → 断言同会话读到 visible,另一个会话读到 undefined。

调用关系:它检查 start_cell 复制 stored_values、run_cell_control 在 Result 时写回 stored_values 的行为。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::shutdown_interrupts_cpu_bound_cells1046–1068 ↗
async fn shutdown_interrupts_cpu_bound_cells()

作用:验证 shutdown 能打断死循环这类吃 CPU 的代码。没有这个能力,关闭服务可能永远卡住。

数据流:测试启动 while(true) 代码 → 先确认它让出了一次 → 对 service.shutdown 加 1 秒超时 → 断言能及时完成。

调用关系:它覆盖 CodeModeService::shutdown、取消令牌、RuntimeCommand::Terminate 和 V8 终止执行的配合。

调用图:调用 1 个内部函数(new);外部调用 4 个(from_secs, assert_eq!, execute_request, timeout)。

tests::start_cell_rejects_new_cell_after_shutdown_begins1071–1089 ↗
async fn start_cell_rejects_new_cell_after_shutdown_begins()

作用:验证服务一旦进入关闭状态,就不能再启动新的代码单元。

数据流:测试手动把 shutting_down 设为 true → 调用 start_cell → 得到错误,并确认 cells 表仍为空。

调用关系:它直接测试 start_cell 里的关闭检查,防止 shutdown 与新执行请求并发时留下半启动的 cell。

调用图:调用 1 个内部函数(new);外部调用 6 个(assert!, assert_eq!, Runtime, cell_id, execute_request, channel)。

tests::execute_to_pending_returns_completed_for_synchronous_results1092–1114 ↗
async fn execute_to_pending_returns_completed_for_synchronous_results()

作用:验证 execute_to_pending 遇到同步完成的代码时,会返回 Completed,而不是误报 Pending。

数据流:测试执行只输出 done 的代码 → execute_to_pending 等到结果 → 断言返回 Completed,里面有文本输出且无错误。

调用关系:它覆盖 execute_to_pending、start_cell 和 run_cell_control 处理 RuntimeEvent::Result 的路径。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, execute_request)。

tests::execute_to_pending_returns_once_the_runtime_is_quiescent1117–1152 ↗
async fn execute_to_pending_returns_once_the_runtime_is_quiescent()

作用:验证代码进入长期等待时,execute_to_pending 会在运行时安静下来后返回 Pending。

数据流:测试执行先输出 before、再等待永不完成 Promise 的代码 → 得到 Pending 和已有输出 → 再 terminate 并断言终止成功。

调用关系:它覆盖 PendingRuntimeMode::PauseUntilResumed、RuntimeEvent::Pending、execute_to_pending 和 terminate 的组合流程。

调用图:调用 1 个内部函数(new);外部调用 5 个(from_secs, assert_eq!, cell_id, execute_request, timeout)。

tests::execute_to_pending_identifies_tool_calls_in_paused_frontier1155–1199 ↗
async fn execute_to_pending_identifies_tool_calls_in_paused_frontier()

作用:验证暂停时能列出当前正在等待的工具调用编号。这样外部知道哪些工具调用挡住了代码继续走。

数据流:测试启用 echo 工具,代码并发调用两次工具 → execute_to_pending 返回 Pending → 断言 pending_tool_call_ids 是 tool-1 和 tool-2,然后终止 cell。

调用关系:它检查 run_cell_control 在 RuntimeEvent::ToolCall 且暂停模式下记录 pending_tool_call_ids 的行为。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, cell_id, execute_request, vec!)。

tests::execute_to_pending_excludes_delayed_timeout_tool_calls_until_wait1202–1281 ↗
async fn execute_to_pending_excludes_delayed_timeout_tool_calls_until_wait()

作用:验证未来定时器里的工具调用不会提前出现在第一次 Pending 里,只有恢复运行后才出现。

数据流:测试先执行包含 setTimeout 和两个立即工具调用的代码 → 第一次 Pending 只包含立即工具 → 手动发送 TimeoutFired,再 wait_to_pending → 第二次 Pending 才包含延迟工具。

调用关系:它覆盖 execute_to_pending、wait_to_pending、RuntimeCommand::TimeoutFired 和 run_cell_control 对多次 pending 边界的处理。

调用图:调用 1 个内部函数(new);外部调用 6 个(from_secs, assert_eq!, cell_id, execute_request, timeout, vec!)。

tests::wait_to_pending_returns_after_resumed_runtime_becomes_quiescent_again1284–1353 ↗
async fn wait_to_pending_returns_after_resumed_runtime_becomes_quiescent_again()

作用:验证已经 Pending 的代码被恢复后,可以再次运行到新的安静点并返回 Pending。

数据流:测试执行先等定时器、再输出 after、再永远等待的代码 → 第一次 Pending 无输出 → 触发定时器并 wait_to_pending → 断言第二次 Pending 带 after 输出。

调用关系:它检查 resume_paused_runtime 和 run_cell_control 再次处理 Pending 的能力。

调用图:调用 1 个内部函数(new);外部调用 5 个(from_secs, assert_eq!, cell_id, execute_request, timeout)。

tests::wait_to_pending_returns_completed_after_resumed_runtime_finishes1356–1416 ↗
async fn wait_to_pending_returns_completed_after_resumed_runtime_finishes()

作用:验证暂停后的代码如果恢复后直接结束,wait_to_pending 会返回 Completed。

数据流:测试执行等待定时器后输出 done 的代码 → 第一次得到 Pending → 触发定时器并 wait_to_pending → 断言返回 Completed 的最终结果。

调用关系:它覆盖 wait_to_pending 在恢复后收到 RuntimeEvent::Result 的路径。

调用图:调用 1 个内部函数(new);外部调用 5 个(from_secs, assert_eq!, cell_id, execute_request, timeout)。

tests::v8_console_is_not_exposed_on_global_this1419–1442 ↗
async fn v8_console_is_not_exposed_on_global_this()

作用:验证代码运行环境里没有暴露 V8 默认 console。这样输出必须走受控的 text/image/notify 等接口。

数据流:测试执行检查 globalThis 是否有 console 的代码 → 结果输出 false → 断言符合预期。

调用关系:它主要验证底层 runtime 的全局对象配置,但通过 CodeModeService 的执行通路观察结果。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::date_locale_string_formats_with_icu_data1445–1482 ↗
async fn date_locale_string_formats_with_icu_data()

作用:验证 Date.toLocaleString 能使用 ICU 数据格式化法语日期。ICU 可以理解成多语言日期、数字等格式规则库。

数据流:测试执行一段格式化法国法语日期的代码 → 收到文本输出 → 断言结果是法语日期字符串。

调用关系:它通过 CodeModeService 跑真实 JavaScript,确认运行时带有正确国际化数据。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::intl_date_time_format_formats_with_icu_data1485–1521 ↗
async fn intl_date_time_format_formats_with_icu_data()

作用:验证 Intl.DateTimeFormat 也能使用 ICU 数据。它和 Date.toLocaleString 是另一条常见国际化路径。

数据流:测试创建法语 DateTimeFormat 并格式化日期 → 服务返回文本 → 断言结果为预期法语字符串。

调用关系:它补充覆盖运行时国际化能力,仍然走 execute helper 和 CodeModeService 执行流程。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::output_helpers_return_undefined1524–1564 ↗
async fn output_helpers_return_undefined()

作用:验证 text、image、notify 这些输出辅助函数在 JavaScript 里返回 undefined。这样用户代码不会意外拿到奇怪返回值。

数据流:测试执行调用三个 helper 并检查返回值的代码 → 收到文本、图片、检查结果文本 → 断言三个 helper 都返回 undefined。

调用关系:它验证底层 runtime 的输出 helper 行为,并通过 run_cell_control 收集 FunctionCallOutputContentItem。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helper_accepts_raw_mcp_image_block_with_original_detail1567–1599 ↗
async fn image_helper_accepts_raw_mcp_image_block_with_original_detail()

作用:验证 image() 能接受原始 MCP 图片块,并保留 original 清晰度标记。MCP 图片块就是一种工具协议里的图片数据格式。

数据流:测试传入含 base64 数据、mimeType 和元数据的图片对象 → 服务输出 data URI 图片项 → 断言 detail 是 Original。

调用关系:它覆盖 runtime 图片 helper 的解析结果,以及 service 收集图片输出的通路。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::generated_image_helper_appends_image_and_output_hint1602–1637 ↗
async fn generated_image_helper_appends_image_and_output_hint()

作用:验证 generatedImage() 不只输出图片,还会追加保存提示文字。这样生成图像后,用户能看到后续该怎么处理。

数据流:测试传入图片 data URI 和 output_hint → 响应里先有图片项,再有提示文本 → 断言顺序和内容正确。

调用关系:它通过 CodeModeService 检查 generatedImage helper 产出的多个 content_items 是否被完整保留。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helper_second_arg_overrides_explicit_object_detail1640–1673 ↗
async fn image_helper_second_arg_overrides_explicit_object_detail()

作用:验证 image() 的第二个参数可以覆盖对象里写的 detail。调用者显式传第二参数时优先级更高。

数据流:测试传入 detail 为 high 的对象,同时第二参数传 original → 输出图片项 → 断言最终 detail 是 Original。

调用关系:它测试 runtime 图片 helper 的参数优先级,结果经 service 返回。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helper_second_arg_overrides_raw_mcp_image_detail1676–1711 ↗
async fn image_helper_second_arg_overrides_raw_mcp_image_detail()

作用:验证第二个参数也能覆盖原始 MCP 图片块里的 detail 元数据。

数据流:测试传入元数据 detail 为 original 的 MCP 图片块,同时第二参数传 high → 输出图片项 → 断言最终 detail 是 High。

调用关系:它补充验证 image helper 对不同输入格式都遵守同一优先级规则。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helper_accepts_low_detail1714–1744 ↗
async fn image_helper_accepts_low_detail()

作用:验证 image() 接受 low 清晰度选项。这个选项通常用于减少图片处理成本。

数据流:测试传入 data URI 和 detail: low → 服务返回图片输出 → 断言 detail 是 Low。

调用关系:它覆盖图片 detail 枚举中的合法值路径。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helpers_reject_remote_urls1747–1780 ↗
async fn image_helpers_reject_remote_urls()

作用:验证 image() 和 generatedImage() 都拒绝 http/https 远程图片地址。这样输出只允许可直接携带的数据 URI,避免运行时再去联网取图。

数据流:测试分别用 http 和 https URL 调用两个 helper → 服务返回错误结果且没有内容项 → 断言错误提示要求使用 base64 data URI。

调用关系:它覆盖两个图片 helper 的安全限制,结果通过 CodeModeService 执行链返回。

调用图:调用 1 个内部函数(new);外部调用 4 个(assert_eq!, execute, execute_request, format!)。

tests::image_helper_rejects_unsupported_detail1783–1812 ↗
async fn image_helper_rejects_unsupported_detail()

作用:验证 image() 会拒绝不支持的清晰度值,比如 medium。这样调用者能得到明确错误,而不是默默产生错误图片配置。

数据流:测试传入 detail: medium → 服务返回错误结果 → 断言错误列出 auto、low、high、original 这些合法值。

调用关系:它测试 runtime 输入校验,service 负责把错误文本带回 RuntimeResponse。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::image_helper_rejects_raw_mcp_result_container1815–1851 ↗
async fn image_helper_rejects_raw_mcp_result_container()

作用:验证 image() 不接受整个 MCP 结果容器,只接受里面的单个图片块或简单图片对象。

数据流:测试把带 content 数组的 MCP 结果对象传给 image → 服务返回错误 → 断言错误说明 image 期望的输入形状。

调用关系:它防止用户把协议外层对象误传给 helper,覆盖 runtime 校验和 service 错误返回路径。

调用图:调用 1 个内部函数(new);外部调用 3 个(assert_eq!, execute, execute_request)。

tests::wait_reports_missing_cell_separately_from_runtime_results1854–1873 ↗
async fn wait_reports_missing_cell_separately_from_runtime_results()

作用:验证 wait 找不到 cell 时,会返回 MissingCell,而不是普通运行时错误结果。这样调用方能区分“代码报错”和“任务不存在”。

数据流:测试对 missing 这个不存在的 cell 调 wait → 得到 WaitOutcome::MissingCell → 断言里面的 RuntimeResponse 写明 cell not found。

调用关系:它覆盖 begin_wait 找不到 handle 时调用 missing_wait 和 missing_cell_response 的路径。

调用图:调用 1 个内部函数(new);外部调用 2 个(assert_eq!, cell_id)。

tests::terminate_waits_for_runtime_shutdown_before_responding1876–1945 ↗
async fn terminate_waits_for_runtime_shutdown_before_responding()

作用:验证 terminate 不会刚发出终止命令就立刻回答,而是等运行时事件通道真正关闭后再返回。这样调用方拿到响应时,后台已经收干净了。

数据流:测试手工启动 run_cell_control 和运行时,先让初始响应 Yielded → 发送 Terminate 后短时间内确认还没返回 → 关闭事件发送端模拟运行时结束 → 最后断言收到 Terminated。

调用关系:它直接测试 run_cell_control 的终止路径,覆盖 spawn_runtime、CellControlCommand::Terminate 和运行时关闭后的收尾逻辑。

调用图:调用 2 个内部函数(spawn_runtime, run_cell_control);外部调用 12 个(new, assert!, assert_eq!, Runtime, cell_id, execute_request, test_inner, unbounded_channel, channel, pin! (+2 more))。

历史与持久化桥接

这些文件重建并持久化线程历史,为派生和恢复截断 rollout 状态,并在存储与实时运行时之间同步派生元数据。

core/src/thread_rollout_truncation.rs源码 ↗
domain_logiccross-cutting

系统里的对话记录不是简单的一串文字,而是一串 RolloutItem,可以理解成“时间线上发生过的各种事件”:用户消息、助手回复、系统事件、代理之间的通信等。这个文件要解决的问题是:从这串复杂事件里找出真正的“轮次边界”,再按边界安全地截断。比如要丢掉第 N 个用户消息之后的内容,或者只保留最近几轮,就不能随便按数组位置切,否则可能留下半截上下文。它还特别处理 ThreadRolledBack,也就是“之前几轮被撤回了”的标记;遇到这种标记时,它会把已经记下的旧边界也撤掉,保证后面看到的是回滚后的真实历史。文件里的函数像几把尺子:有的判断某个项目是不是用户轮次,有的收集边界位置,有的按这些位置裁剪前缀或后缀。

函数细节8
initial_history_has_prior_user_turns15–17 ↗
fn initial_history_has_prior_user_turns(conversation_history: &InitialHistory) -> bool

作用:这个函数用来快速判断初始历史里是不是已经有过用户轮次。这样系统在接手一段已有对话时,就能知道这不是一张白纸,而是已经聊过了。

数据流:输入是一份 InitialHistory,也就是启动时带进来的历史记录。它让这份历史自己扫描里面的 rollout 项目,并把每一项交给 rollout_item_is_user_turn_boundary 判断。最后输出一个布尔值:有用户轮次就返回 true,没有就返回 false;它不改动原历史。

调用关系:record_initial_history 在记录初始历史时会用到它,像先问一句“这里面以前有人发过话吗”。它自己不逐条处理细节,而是把判断单个项目是不是边界的活交给 rollout_item_is_user_turn_boundary。

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

rollout_item_is_user_turn_boundary19–25 ↗
fn rollout_item_is_user_turn_boundary(item: &RolloutItem) -> bool

作用:这个函数判断一条 rollout 记录是不是能算作“用户轮次边界”。它把普通用户消息和代理之间的通信都纳入考虑,因为这些都可能让对话进入新一轮。

数据流:输入是一条 RolloutItem。它先看这条是不是 ResponseItem,如果是,就交给 is_user_turn_boundary 做更具体的判断;如果是 InterAgentCommunication,也就是代理之间的通信,就直接认为是边界;其他类型返回 false。输出只是 true 或 false,不修改任何数据。

调用关系:它主要服务于 initial_history_has_prior_user_turns,是扫描初始历史时用的单项判断器。遇到 ResponseItem 时,它依赖外部的 is_user_turn_boundary 来识别真正的用户轮次。

调用图:外部调用 1 个(is_user_turn_boundary)。

user_message_positions_in_rollout35–56 ↗
fn user_message_positions_in_rollout(items: &[RolloutItem]) -> Vec<usize>

作用:这个函数从一串 rollout 记录里找出真正的用户消息所在的位置。它重要的地方在于会尊重回滚标记:如果历史说之前几轮被撤销了,它也会把那些旧位置删掉。

数据流:输入是一段 RolloutItem 列表。函数从头到尾看每一项:如果某项是消息,并且解析后确实是用户消息,就把它的下标记下来;如果遇到 ThreadRolledBack,就按回滚说明删掉最后若干个已记录的用户位置。输出是一组下标,表示回滚之后仍然有效的用户消息位置;原列表不变。

调用关系:truncate_rollout_before_nth_user_message_from_start 会调用它,先拿到所有用户消息边界,再决定从哪里切。它内部会使用 event_mapping::parse_turn_item 来把底层消息翻译成“用户消息”这种更容易判断的形式。

调用图:被 1 处调用(truncate_rollout_before_nth_user_message_from_start);外部调用 4 个(new, matches!, iter, try_from)。

fork_turn_positions_in_rollout69–109 ↗
fn fork_turn_positions_in_rollout(items: &[RolloutItem]) -> Vec<usize>

作用:这个函数找出“分叉轮次”的位置,也就是从哪些地方开始可以把线程切成一段新的有效历史。这里的边界不只是真人用户消息,还包括某些会触发新轮次的代理通信。

数据流:输入是一段 RolloutItem 列表。它一边扫描,一边记录两类位置:一类用于处理回滚的“指令轮次边界”,一类是真正要保留或裁剪用的“分叉轮次边界”。遇到用户轮次、代理通信、带 trigger_turn 标记的助手消息时,会按规则记位置;遇到 ThreadRolledBack 时,会按被撤回的轮次数删掉失效位置,并移除被回滚影响的分叉边界。最后输出一组有效的分叉轮次下标。

调用关系:truncate_rollout_to_last_n_fork_turns 会先调用它,确定最近几轮从哪里开始保留。它把细小判断交给 is_real_user_message_boundary 和 is_trigger_turn_boundary,同时也使用外部的 is_user_turn_boundary 来正确处理回滚。

调用图:调用 2 个内部函数(is_real_user_message_boundary, is_trigger_turn_boundary);被 1 处调用(truncate_rollout_to_last_n_fork_turns);外部调用 4 个(new, is_user_turn_boundary, iter, try_from)。

truncate_rollout_before_nth_user_message_from_start119–137 ↗
fn truncate_rollout_before_nth_user_message_from_start(
    items: &[RolloutItem],
    n_from_start: usize,
) -> Vec<RolloutItem>

作用:这个函数把一段 rollout 从开头保留到第 N 个用户消息之前。简单说,就是“看到第 N 个用户发言就停,连这条发言也不要”。

数据流:输入是一段 RolloutItem 列表和 n_from_start,表示从开头数第几个用户消息。若 n_from_start 是 usize::MAX,就完全不裁剪,直接返回整段复制;否则先调用 user_message_positions_in_rollout 找用户消息位置。如果用户消息数量不够,就原样返回整段复制;如果够,就在目标用户消息的下标前切开,输出前半段的新列表。

调用关系:它是面向外部使用的裁剪工具之一。它不自己判断哪些项是用户消息,而是先让 user_message_positions_in_rollout 处理好包括回滚在内的边界问题,再根据位置做最终切片。

调用图:调用 1 个内部函数(user_message_positions_in_rollout);外部调用 1 个(to_vec)。

truncate_rollout_to_last_n_fork_turns143–161 ↗
fn truncate_rollout_to_last_n_fork_turns(
    items: &[RolloutItem],
    n_from_end: usize,
) -> Vec<RolloutItem>

作用:这个函数只保留最后 N 个“分叉轮次”。它适合在对话太长时保留最近有意义的几轮,而不是保留一堆启动时的杂项上下文。

数据流:输入是一段 RolloutItem 列表和 n_from_end,表示要保留最后几轮。如果 n_from_end 是 0,直接输出空列表。否则先调用 fork_turn_positions_in_rollout 找到有效边界;如果能找到边界,就从应保留的那一轮开始切到结尾。如果总轮数不够,它也会从第一个分叉边界开始保留,从而丢掉轮次之前的启动内容。

调用关系:它是另一个主要裁剪入口,用在需要“保留最近上下文”的场景。它依赖 fork_turn_positions_in_rollout 来理解用户消息、代理触发轮次和回滚标记,然后自己只负责按算好的位置截取后缀。

调用图:调用 1 个内部函数(fork_turn_positions_in_rollout);外部调用 1 个(new)。

is_real_user_message_boundary163–168 ↗
fn is_real_user_message_boundary(item: &ResponseItem) -> bool

作用:这个小函数判断一个 ResponseItem 是不是货真价实的用户消息边界。它避免把助手消息或其他事件误当成用户发言。

数据流:输入是一条 ResponseItem。它调用 event_mapping::parse_turn_item 把消息内容解析成更明确的 TurnItem;如果结果是 TurnItem::UserMessage,就输出 true,否则输出 false。它不改动输入。

调用关系:fork_turn_positions_in_rollout 在判断分叉边界时会调用它。它只负责“这是不是用户消息”这一件小事,让主扫描函数不用塞满细节。

调用图:被 1 处调用(fork_turn_positions_in_rollout);外部调用 1 个(matches!)。

is_trigger_turn_boundary170–178 ↗
fn is_trigger_turn_boundary(item: &ResponseItem) -> bool

作用:这个函数判断一条助手消息里是否藏着“代理之间通信”,并且这次通信是否明确要求触发新一轮。它是为了兼容一种旧格式:通信内容被包在助手消息里。

数据流:输入是一条 ResponseItem。它先确认这是不是 role 为 assistant 的消息;如果不是,直接返回 false。然后尝试从消息内容里解析 InterAgentCommunication,并检查里面的 trigger_turn 标记。标记为 true 就输出 true,否则输出 false;不修改原消息。

调用关系:fork_turn_positions_in_rollout 会用它来识别旧式的代理通信边界。真正的解析工作交给 InterAgentCommunication::from_message_content,它自己负责把结果变成一个简单的是否判断。

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

thread-store/src/local/create_thread.rs源码 ↗
orchestrationrequest handling

这个文件解决的是“本地线程怎么开始记录”的问题。可以把线程想成一次任务的档案夹,而 RolloutRecorder 就是负责往档案夹里写过程记录的人。创建本地线程时,代码先从请求参数里拿当前工作目录 cwd;本地保存必须知道这个目录,因为很多文件路径和上下文都要靠它定位。没有 cwd,就返回一个清楚的错误。接着它把本地存储配置、模型来源、是否生成记忆等信息拼成 RolloutConfig,也就是记录器的启动说明书。最后,它把线程 ID、父线程、来源、基础指令、动态工具等信息装进 RolloutRecorderParams,交给 RolloutRecorder::new 去真正初始化记录器。如果初始化失败,会把底层错误包装成 ThreadStoreError::Internal,让上层知道“本地记录器没建起来”。

函数细节1
create_thread10–45 ↗
async fn create_thread(
    store: &LocalThreadStore,
    params: CreateThreadParams,
) -> ThreadStoreResult<RolloutRecorder>

作用:这个函数负责为本地线程创建一个可用的 RolloutRecorder,也就是后续用来记录线程运行过程的对象。它会先确认请求里带了当前工作目录,因为本地线程没有这个目录就不知道该以哪里为基准保存和读取东西。

数据流:进去的是一个本地线程仓库 store 和一组创建参数 params。函数先从 params.metadata.cwd 里取工作目录;如果没有,就立刻返回“请求无效”的错误。拿到 cwd 后,它读取 store 里的 codex_home、sqlite_home 等本地配置,再结合参数里的模型提供方、记忆模式等信息,拼出 RolloutConfig。然后它把线程 ID、 fork 来源、父线程、来源信息、基础指令和动态工具等打包成 RolloutRecorderParams,并补上 multi_agent_version。最后它调用 RolloutRecorder::new 初始化记录器;成功就返回记录器,失败就把错误改写成“初始化本地线程记录器失败”的内部错误。

调用关系:它是在创建本地线程的流程中被上层 create_thread 调用的具体执行步骤。它自己不直接写大量业务数据,而是先做入口检查和参数整理,再把真正初始化记录器的工作交给 RolloutRecorder::new;在组装参数时会使用 RolloutRecorderParams::new,并用 matches! 判断记忆模式是不是启用。

调用图:调用 2 个内部函数(new, new);被 1 处调用(create_thread);外部调用 1 个(matches!)。

thread-store/src/thread_metadata_sync.rs源码 ↗
domain_logiccreate/resume/append handling

一条对话保存时,系统拿到的是一串历史事件,里面可能有用户发言、会话启动信息、模型设置、token 用量等。这个文件里的 ThreadMetadataSync 就像一个“边看流水账边填档案卡”的小助手:新建对话时先填创建时间、来源、当前目录、Git 信息;恢复旧对话时会重新扫历史,补出标题、预览和第一条用户消息;追加新事件时再更新变化的字段。它不会自己写数据库,而是攒出 ThreadMetadataPatch,也就是“要改哪些元数据”的补丁,交给外面的存储流程去落盘。它还有两个重要的小心思:新建时会等历史真正存在后再刷元数据,避免空对话先留下不完整信息;如果只是普通追加、只需要碰一下 updated_at,它会把太频繁的更新时间合并,避免反复写盘。

函数细节26
ThreadMetadataSync::for_create52–91 ↗
async fn for_create(params: &CreateThreadParams) -> Self

作用:新建一条对话时,创建一个元数据同步器,并先准备好第一批元数据补丁。它会记录创建时间、来源、目录、版本号,还会尽量读出当前 Git 仓库信息。

数据流:输入是 CreateThreadParams,里面有线程编号、来源、模型提供方、工作目录等信息。函数先取当前时间,再看工作目录是不是 Git 仓库;如果是,就异步收集提交号、分支和远端地址。最后产出一个 ThreadMetadataSync,里面带着一份待保存的 ThreadMetadataPatch,并标记“先别急着写,等历史存在”。

调用关系:它由 create 流程调用,是新对话进入存储系统时的第一步元数据准备。它会借助 get_git_repo_root 和 collect_git_info 读取 Git 状态,用 now 取时间,用 env! 写入当前包版本。

调用图:被 1 处调用(create);外部调用 5 个(default, now, collect_git_info, get_git_repo_root, env!)。

ThreadMetadataSync::for_resume93–116 ↗
fn for_resume(params: &ResumeThreadParams) -> Self

作用:恢复旧对话时,创建一个元数据同步器,并根据已有历史重新推导还能补上的元数据。这样旧记录即使以前缺少标题或预览,也能在恢复时补齐。

数据流:输入是 ResumeThreadParams,包含线程编号、已有元数据和可选历史记录。函数先按已有 cwd 判断工作目录是否已经见过;如果带了历史,就扫描这些 RolloutItem,从里面提取会话时间、用户第一句话、目标摘要等,合并成待保存补丁。输出是一个准备好继续观察追加事件的 ThreadMetadataSync。

调用关系:它在 resume 流程里使用,测试也大量用它来构造恢复场景。它会把历史交给内部扫描逻辑观察,但如果这些补丁只来自恢复历史,会先暂存,等下一次真正追加事件时再一起刷出去。

调用图:被 6 处调用(resume, goal_update_sets_preview_without_overriding_existing_preview, later_user_messages_do_not_emit_existing_preview_fields, metadata_irrelevant_items_coalesce_updated_at_touches, resume_history_keeps_derived_metadata_pending_until_applied, resume_history_waits_for_append_before_flushing_metadata)。

ThreadMetadataSync::take_pending_update118–125 ↗
fn take_pending_update(&self) -> Option<PendingThreadMetadataPatch>

作用:查看当前有没有攒好的元数据补丁可以交给外面保存。注意它只是“拿一份副本给你看”,不会立刻把内部待办清掉。

数据流:它读取 ThreadMetadataSync 里的 pending_update 和 generation。若有待保存补丁,就包装成 PendingThreadMetadataPatch,带上版本号返回;若没有,就返回 None。内部状态不会因为这次读取而改变。

调用关系:observe_appended_items 会用它把本次追加后该保存的补丁交出去;take_pending_update_for_existing_history 也会在条件允许时转交给它。真正清掉待办要等 mark_pending_update_applied 被调用。

调用图:被 2 处调用(observe_appended_items, take_pending_update_for_existing_history)。

ThreadMetadataSync::take_pending_update_for_existing_history127–137 ↗
fn take_pending_update_for_existing_history(
        &self,
    ) -> Option<PendingThreadMetadataPatch>

作用:在“已有历史”场景下,判断现在能不能把暂存的元数据补丁交出去。它专门避免太早写入,比如新建对话但历史还没落下,或者恢复对话但还没发生新追加。

数据流:它先读取两个延迟标记:是否要等创建历史存在、是否要等恢复后第一次追加。如果任一条件还没满足,就返回 None;否则调用 take_pending_update,把当前待保存补丁交出去。

调用关系:它是 take_pending_update 前面的一道安全闸门。外层存储流程在处理已有历史时会用这种方式确认“现在写元数据是不是合适”。

调用图:调用 1 个内部函数(take_pending_update)。

ThreadMetadataSync::mark_pending_update_applied139–146 ↗
fn mark_pending_update_applied(&mut self, update: &PendingThreadMetadataPatch)

作用:告诉同步器:刚才那份元数据补丁已经成功保存了,可以把对应的待办清掉。它还能记录 updated_at 最近一次真正写盘的时间,用来减少频繁更新时间写入。

数据流:输入是一份 PendingThreadMetadataPatch,里面有补丁和生成号。函数比较生成号:如果和当前待办一致,就清空 pending_update;如果补丁里包含 updated_at,就把 last_touch_persisted_at 更新为当前时刻。输出为空,但会改变同步器内部状态。

调用关系:外部保存成功后应该调用它,相当于给同步器回执。observe_appended_items 后拿到的补丁,只有在这里确认后,内部才知道不用再重试。

调用图:外部调用 1 个(now)。

ThreadMetadataSync::observe_appended_items148–175 ↗
fn observe_appended_items(
        &mut self,
        items: &[RolloutItem],
    ) -> Option<PendingThreadMetadataPatch>

作用:当对话追加了新历史事件时,观察这些事件会不会改变元数据,并返回应该保存的补丁。这是对话进行中最常用的入口。

数据流:输入是一批 RolloutItem。函数先解除“等待历史/等待追加”的延迟状态;再判断这些事件是否真的影响元数据。如果会影响,就扫描事件提取标题、预览、模型等;如果不会影响,也至少准备一个 updated_at 更新时间。然后把新补丁合进待办,必要时为了减少频繁写盘会暂时不返回;否则返回当前待保存补丁。

调用关系:它把工作分给 observe_items、thread_updated_at_touch、merge_pending_update 和 take_pending_update。外层追加历史的流程会在写入新事件后调用它,用它产出的补丁继续更新对话元数据。

调用图:调用 4 个内部函数(merge_pending_update, observe_items, take_pending_update, thread_updated_at_touch);外部调用 1 个(iter)。

ThreadMetadataSync::observe_items177–185 ↗
fn observe_items(&mut self, items: &[RolloutItem]) -> Option<ThreadMetadataPatch>

作用:扫描一批新追加的事件,从里面提取会改变元数据的信息,并自动加上“更新时间”。

数据流:输入是一批 RolloutItem。函数先创建一个只带 updated_at 的补丁,然后把事件和这份补丁交给 observe_items_with_update 继续填字段。输出是可能包含标题、预览、模型、token 用量等信息的 ThreadMetadataPatch;如果没有事件则可能没有结果。

调用关系:它由 observe_appended_items 调用,是“新追加事件”专用的扫描入口。它和 observe_resume_history 的区别是:这里会把 updated_at 设为当前时间。

调用图:调用 1 个内部函数(observe_items_with_update);被 1 处调用(observe_appended_items);外部调用 2 个(default, now)。

ThreadMetadataSync::observe_resume_history187–189 ↗
fn observe_resume_history(&mut self, items: &[RolloutItem]) -> Option<ThreadMetadataPatch>

作用:扫描恢复出来的旧历史,从里面补出元数据,但不更新 updated_at。因为只是重读旧东西,不应该让对话看起来像刚刚被用户改过。

数据流:输入是一批历史 RolloutItem。函数从空补丁开始,把历史交给 observe_items_with_update 提取创建时间、预览、标题、第一条用户消息等。输出是一个待补齐的元数据补丁,或在没有内容时没有补丁。

调用关系:它在恢复对话时被 for_resume 使用。它复用 observe_items_with_update 的通用扫描逻辑,只是不给补丁预先塞入 updated_at。

调用图:调用 1 个内部函数(observe_items_with_update);外部调用 1 个(default)。

ThreadMetadataSync::observe_items_with_update191–280 ↗
fn observe_items_with_update(
        &mut self,
        items: &[RolloutItem],
        mut update: ThreadMetadataPatch,
    ) -> Option<ThreadMetadataPatch>

作用:这是实际“读流水账、填档案卡”的核心函数。它逐条看历史事件,遇到有用信息就写进元数据补丁里。

数据流:输入是一批 RolloutItem 和一份已有的 ThreadMetadataPatch。函数逐条检查:会话元信息可提供创建时间、来源、CLI 版本、Git 和记忆模式;上下文可提供 cwd、模型、审批策略和权限配置;用户消息可提供预览、标题、第一条用户消息;token 事件可提供用量;目标更新可作为预览。最后输出填好的补丁。

调用关系:observe_items 和 observe_resume_history 都把活交给它。它再调用 parse_session_timestamp、parse_memory_mode、user_message_preview、strip_user_message_prefix、git_info_patch_from_observation 等小工具,把不同格式的数据转成统一的元数据字段。

调用图:调用 5 个内部函数(git_info_patch_from_observation, parse_memory_mode, parse_session_timestamp, strip_user_message_prefix, user_message_preview);被 2 处调用(observe_items, observe_resume_history);外部调用 1 个(is_empty)。

ThreadMetadataSync::merge_pending_update282–291 ↗
fn merge_pending_update(&mut self, update: Option<ThreadMetadataPatch>)

作用:把新发现的元数据补丁合并到当前待保存补丁里。这样多次观察到的信息不会互相丢失,而是攒成一份再保存。

数据流:输入是一个可选 ThreadMetadataPatch。如果没有补丁就什么也不做;如果已有 pending_update,就把新字段合进去;如果原来没有,就把它设为新的待办。最后把 generation 加一,表示待办版本变了。

调用关系:observe_appended_items 会用它把这次追加得到的结果合到待保存队列里。generation 之后会被 take_pending_update 和 mark_pending_update_applied 用来判断“保存成功的是不是当前这一版”。

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

parse_memory_mode294–300 ↗
fn parse_memory_mode(value: &str) -> Option<ThreadMemoryMode>

作用:把历史记录里的记忆模式文字转成程序内部认识的枚举值。枚举值可以理解为几个固定选项,而不是随便写的字符串。

数据流:输入是一段字符串。如果是 enabled,就返回 ThreadMemoryMode::Enabled;如果是 disabled,就返回 ThreadMemoryMode::Disabled;其他内容无法识别,返回 None。

调用关系:observe_items_with_update 在读取会话元信息时调用它,用来把旧记录里的 memory_mode 字段安全转换成当前元数据补丁里的标准格式。

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

parse_session_timestamp302–310 ↗
fn parse_session_timestamp(value: &str) -> Option<DateTime<Utc>>

作用:把会话历史里的时间文本解析成统一的 UTC 时间。UTC 可以理解为全球统一标准时间,方便不同机器、时区之间比较。

数据流:输入是一段时间字符串。函数先尝试按 RFC3339 这种常见标准时间格式解析;失败后,再尝试一种旧格式“年-月-日T时-分-秒”。成功就返回 DateTime<Utc>,失败就返回 None。

调用关系:observe_items_with_update 在遇到 SessionMeta 时调用它,用来把会话创建时间写进元数据。它依赖外部时间库的 parse_from_rfc3339 来处理标准格式。

调用图:被 1 处调用(observe_items_with_update);外部调用 1 个(parse_from_rfc3339)。

strip_user_message_prefix312–317 ↗
fn strip_user_message_prefix(text: &str) -> &str

作用:去掉用户消息前面系统加的固定标记,只留下真正的用户内容。这样标题和预览不会带着内部标记。

数据流:输入是一段用户消息文本。函数查找 USER_MESSAGE_BEGIN 这个标记;找到了就取标记后面的内容并去掉首尾空白,没找到就直接去掉首尾空白。输出是借用原文本的一段干净内容。

调用关系:observe_items_with_update 用它生成标题,user_message_preview 也用它生成预览。它是处理用户消息文本前的清洗步骤。

调用图:被 2 处调用(observe_items_with_update, user_message_preview)。

user_message_preview319–333 ↗
fn user_message_preview(user: &UserMessageEvent) -> Option<String>

作用:从用户消息里做出列表上能显示的一句预览。如果用户只发了图片没有文字,它会用 “[Image]” 代替,避免预览空白。

数据流:输入是 UserMessageEvent。函数先用 strip_user_message_prefix 清理文字;如果清理后有文字,就返回这段文字。若没有文字但包含远程图片或本地图片,就返回 “[Image]”。如果既没文字也没图片,就返回 None。

调用关系:observe_items_with_update 在处理用户消息事件时调用它。它的结果可能成为 preview,也可能成为 first_user_message。

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

thread_updated_at_touch335–340 ↗
fn thread_updated_at_touch() -> ThreadMetadataPatch

作用:生成一个只更新 updated_at 的小补丁。它表示“这条对话刚刚有活动”,即使这次活动没有带来标题、模型等新信息。

数据流:输入为空。函数取当前 UTC 时间,放进 ThreadMetadataPatch 的 updated_at 字段,其他字段保持默认空值。输出这份补丁。

调用关系:observe_appended_items 在发现追加的事件不影响具体元数据时调用它。这样普通活动仍能刷新对话的最后更新时间。

调用图:被 1 处调用(observe_appended_items);外部调用 2 个(default, now)。

update_has_metadata_facts342–363 ↗
fn update_has_metadata_facts(update: &ThreadMetadataPatch) -> bool

作用:判断一份补丁里是否包含真正的元数据内容,而不只是 updated_at 这种“碰一下时间”。

数据流:输入是一份 ThreadMetadataPatch。函数逐个检查 rollout_path、preview、title、模型、来源、cwd、token 用量、Git 信息、记忆模式等字段有没有值。只要有一个有值就返回 true,否则返回 false。

调用关系:它配合 observe_appended_items 的节流逻辑使用:如果待办里有实质信息,就应该尽快保存;如果只是频繁更新时间,就可以短时间内合并。

git_info_patch_from_observation365–371 ↗
fn git_info_patch_from_observation(git_info: GitInfo) -> GitInfoPatch

作用:把观察到的 Git 信息转成元数据补丁里使用的格式。Git 是代码版本管理工具,这里记录当前提交号、分支和远端仓库地址。

数据流:输入是 GitInfo,里面可能有 commit_hash、branch、repository_url。函数把它们分别映射成 GitInfoPatch 的 sha、branch、origin_url 字段;缺失的信息就保持缺失。输出是可合并进线程元数据的 GitInfoPatch。

调用关系:observe_items_with_update 在扫描历史里的 Git 信息时调用它;for_create 收集到 Git 信息后也用同样格式放进初始补丁。

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

tests::resume_history_keeps_derived_metadata_pending_until_applied389–422 ↗
fn resume_history_keeps_derived_metadata_pending_until_applied()

作用:这个测试确认:恢复旧历史时推导出的元数据会一直待保存,直到外部明确说保存成功。这样临时失败时不会把补丁弄丢。

数据流:测试先造一个线程编号和包含会话元信息、用户消息的历史,再用 for_resume 创建同步器。它读取待办补丁,检查创建时间、预览、标题、第一条用户消息都正确,而且 updated_at 没被误设。最后调用保存成功标记,确认待办被清掉。

调用关系:它通过 resume_params、session_meta、user_message 等测试辅助函数搭场景,并直接验证 for_resume 和 mark_pending_update_applied 的配合是否可靠。

调用图:调用 2 个内部函数(new, for_resume);外部调用 4 个(assert!, assert_eq!, resume_params, vec!)。

tests::goal_update_sets_preview_without_overriding_existing_preview425–445 ↗
fn goal_update_sets_preview_without_overriding_existing_preview()

作用:这个测试确认:如果历史里先出现了线程目标更新,预览会用目标内容;后面的用户消息不会把这个预览覆盖掉。

数据流:测试构造一段历史:先有目标“ship the refactor”,再有用户消息“first user text”。for_resume 扫描后,测试检查 preview 是目标文本,而 first_user_message 和 title 仍然来自第一条用户消息。

调用关系:它主要验证 observe_items_with_update 处理 ThreadGoalUpdated 和 UserMessage 的先后规则,确保预览、标题、第一条消息这几个字段各司其职。

调用图:调用 2 个内部函数(new, for_resume);外部调用 3 个(assert_eq!, resume_params, vec!)。

tests::later_user_messages_do_not_emit_existing_preview_fields448–469 ↗
fn later_user_messages_do_not_emit_existing_preview_fields()

作用:这个测试确认:第一条用户消息已经生成过预览和标题后,后续用户消息不会再次改写这些“第一次”的字段。

数据流:测试先用一条历史用户消息恢复同步器,取出并标记这份元数据已保存。然后再追加一条新的用户消息。结果应该只产生 updated_at,而 preview、title、first_user_message 都保持为空,表示没有要覆盖旧值。

调用关系:它验证 for_resume、mark_pending_update_applied 和 observe_appended_items 的连续行为,特别是 first_user_message_seen、preview_seen、title_seen 这些内部标记是否生效。

调用图:调用 2 个内部函数(new, for_resume);外部调用 7 个(assert!, assert_eq!, UserMessage, EventMsg, resume_params, user_message, vec!)。

tests::metadata_irrelevant_items_coalesce_updated_at_touches472–496 ↗
fn metadata_irrelevant_items_coalesce_updated_at_touches()

作用:这个测试确认:如果追加的内容不影响元数据,只是刷新 updated_at,短时间内不会反复写盘,而是会合并。

数据流:测试创建一个不影响元数据的 CompactedItem。第一次追加时应该立刻得到 updated_at 补丁,并标记已保存;紧接着第二次追加时,因为还在合并时间窗口内,observe_appended_items 返回 None,但内部仍保留待办,等待下一个合适时机刷出。

调用关系:它验证 observe_appended_items、thread_updated_at_touch 和 last_touch_persisted_at 这套节流机制,避免系统因为普通事件太多而频繁写元数据。

调用图:调用 2 个内部函数(new, for_resume);外部调用 5 个(new, assert!, Compacted, from_ref, resume_params)。

tests::resume_history_waits_for_append_before_flushing_metadata499–520 ↗
fn resume_history_waits_for_append_before_flushing_metadata()

作用:这个测试确认:只是在恢复时扫出旧历史元数据,不会马上把它写回;必须等有新的追加事件时再一起刷。

数据流:测试先构造带会话元信息和用户消息的恢复历史。调用 take_pending_update_for_existing_history 时应得到 None,说明不能立刻刷。随后追加一条新用户消息,observe_appended_items 应返回补丁,表示恢复补丁和追加补丁可以一起保存了。

调用关系:它验证 for_resume 设置的 defer_resume_update_until_append 规则,以及 take_pending_update_for_existing_history 和 observe_appended_items 如何配合。

调用图:调用 2 个内部函数(new, for_resume);外部调用 3 个(assert!, resume_params, vec!)。

tests::resume_params522–534 ↗
fn resume_params(thread_id: ThreadId, history: Vec<RolloutItem>) -> ResumeThreadParams

作用:这个测试辅助函数用来快速组装恢复对话所需的参数。它让多个测试不用重复写一大堆固定字段。

数据流:输入是线程编号和一组历史 RolloutItem。函数把它们放进 ResumeThreadParams,并填上测试用的模型提供方、记忆模式、include_archived 等默认值。输出是一份可直接传给 for_resume 的参数。

调用关系:多个恢复相关测试都会调用它,用同一套标准测试参数搭场景,减少测试之间的无关差异。

tests::user_message536–545 ↗
fn user_message(message: &str) -> UserMessageEvent

作用:这个测试辅助函数用来造一条用户消息事件。测试只需要给文字,就能得到完整的 UserMessageEvent。

数据流:输入是一段消息文字。函数把它放进 message 字段,图片和文本元素等字段设为空或默认值。输出是 UserMessageEvent。

调用关系:恢复历史、追加消息相关测试会调用它,再把结果包装成 EventMsg::UserMessage,交给 for_resume 或 observe_appended_items 扫描。

调用图:外部调用 2 个(default, new)。

tests::session_meta547–557 ↗
fn session_meta(thread_id: ThreadId) -> SessionMetaLine

作用:这个测试辅助函数用来造一条会话元信息记录,里面带固定的线程编号和创建时间。

数据流:输入是线程编号。函数生成 SessionMetaLine,timestamp 固定为 2025-01-03T12:00:00Z,source 设为 Exec,其他字段用默认值。输出是可放进历史的 SessionMetaLine。

调用关系:测试用它验证 parse_session_timestamp 和 observe_items_with_update 能从会话元信息里正确提取 created_at 等字段。

调用图:外部调用 1 个(default)。

tests::goal_update559–574 ↗
fn goal_update(thread_id: ThreadId, objective: &str) -> ThreadGoalUpdatedEvent

作用:这个测试辅助函数用来造一条“线程目标已更新”的事件。它帮助测试预览字段是否能从目标说明里生成。

数据流:输入是线程编号和目标文本。函数生成 ThreadGoalUpdatedEvent,把 objective 设为传入文本,状态设为 Active,计数和时间字段填测试用默认值。输出是目标更新事件。

调用关系:goal_update_sets_preview_without_overriding_existing_preview 调用它,把目标更新放进历史,验证目标文本能先占住 preview。

thread-store/src/live_thread.rs源码 ↗
orchestrationsession lifecycle

一段对话在运行时会不断产生新消息,也会更新一些摘要、记忆模式之类的元数据。这个文件的作用,就是把这些动作按正确顺序串起来,避免“消息写了但元数据没跟上”或“初始化失败却留下半截脏数据”。LiveThread 像一个正在打开的文件夹,里面记着线程编号、真正干存储活儿的 ThreadStore,以及一个 ThreadMetadataSync(用来判断元数据什么时候需要补写的小账本)。追加消息时,它先把内容交给存储层,再根据真正会被持久保存的消息更新元数据。保存、刷新、关闭前,它会把还没写出去的元数据补上。LiveThreadInitGuard 则像试用期保险:初始化还没完全成功时先拿着 LiveThread;如果成功就放手,如果失败或被丢弃,就自动把刚打开的持久化状态清掉。

函数细节20
LiveThreadInitGuard::new47–49 ↗
fn new(live_thread: Option<LiveThread>) -> Self

作用:创建一个初始化保护套,把一个可能存在的 LiveThread 暂时包起来。这样在线程会话还没完全启动成功前,可以防止半成品数据被当成正式数据留下。

数据流:输入是一个可选的 LiveThread;函数只是把它放进 LiveThreadInitGuard 里;输出是这个保护套本身,不会写磁盘也不会改线程内容。

调用关系:它通常在创建或恢复线程的初始化流程里被调用。后续如果初始化成功,会走 LiveThreadInitGuard::commit;如果失败或对象被释放,会由 LiveThreadInitGuard::discard 或 LiveThreadInitGuard::drop 收尾。

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

LiveThreadInitGuard::as_ref51–53 ↗
fn as_ref(&self) -> Option<&LiveThread>

作用:让调用者临时看一眼里面的 LiveThread,但不把所有权拿走。适合初始化过程中需要借用这个线程把手,却还不想宣布它已经正式启用。

数据流:输入是这个保护套自身;它读取内部是否还有 LiveThread;输出是一个只读引用,或者在已经提交、丢弃后返回空。

调用关系:它是初始化流程里的查看窗口。它不会触发保存或丢弃,只是让外层代码在 LiveThreadInitGuard 还掌握生杀权时访问 LiveThread。

LiveThreadInitGuard::commit55–57 ↗
fn commit(&mut self)

作用:告诉保护套:初始化已经成功,这个 LiveThread 现在由正常会话接管,不要再自动丢弃它。

数据流:输入是保护套自身;它把内部保存的 LiveThread 清空;输出没有返回值,但结果是保护套之后被释放时不会再调用丢弃逻辑。

调用关系:它在会话初始化成功后调用。它和 LiveThreadInitGuard::discard、LiveThreadInitGuard::drop 是一组互斥的结局:成功就提交,失败或提前退出就清理。

LiveThreadInitGuard::discard59–66 ↗
async fn discard(&mut self)

作用:主动取消初始化中的线程持久化。比如会话启动到一半失败了,它会尽量把刚打开或刚创建的存储痕迹清掉。

数据流:输入是保护套自身;它取出里面的 LiveThread,如果有就调用 LiveThread::discard 去让存储层丢弃;输出是异步完成的结果,失败时只记录警告,不把错误继续抛出。

调用关系:它用于明确知道初始化失败的场景。它把真正的清理工作交给 LiveThread::discard;如果清理失败,会用 warn! 记录一条警告,方便排查但不让清理过程再制造新故障。

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

LiveThreadInitGuard::drop70–83 ↗
fn drop(&mut self)

作用:这是保护套被释放时的兜底清理。哪怕调用者忘了手动 discard,只要初始化没有 commit,它也会尝试异步丢弃半成品线程。

数据流:输入是即将被释放的保护套;它取出内部 LiveThread,尝试找到当前 Tokio runtime(Tokio 是 Rust 常用的异步运行环境);找到后就启动一个后台任务去丢弃,找不到或失败时记录警告。

调用关系:它是 LiveThreadInitGuard 生命周期的最后防线。它会调用 Tokio 的 try_current 找异步运行环境,并在后台交给 LiveThread::discard;如果没有运行环境,就只能用 warn! 提醒无法自动清理。

调用图:外部调用 2 个(try_current, warn!)。

LiveThread::create87–99 ↗
async fn create(
        thread_store: Arc<dyn ThreadStore>,
        params: CreateThreadParams,
    ) -> ThreadStoreResult<Self>

作用:创建一个全新的活动线程把手,并在底层存储里登记这个线程。上层会话用它开始一段新的可持久化对话。

数据流:输入是一个 ThreadStore 和创建参数,里面包含线程编号等信息;它先根据创建参数准备元数据同步小账本,再让存储层 create_thread;成功后输出 LiveThread,里面带着线程编号、存储后端和元数据同步状态。

调用关系:它是新会话接入持久化的入口之一,会被初始化流程、attach_thread_persistence 以及多项测试使用。它把实际创建交给 ThreadStore,把元数据初始状态交给 ThreadMetadataSync::for_create。

调用图:调用 1 个内部函数(for_create);被 6 处调用(new, attach_thread_persistence, shutdown_complete_does_not_append_to_thread_store_after_shutdown, live_thread_observes_appended_items_into_sqlite_metadata, live_thread_shutdown_does_not_materialize_empty_thread_metadata, live_thread_shutdown_with_buffered_items_materializes_before_metadata_read);外部调用 2 个(new, new)。

LiveThread::resume101–134 ↗
async fn resume(
        thread_store: Arc<dyn ThreadStore>,
        mut params: ResumeThreadParams,
    ) -> ThreadStoreResult<Self>

作用:恢复一个已经存在的线程,让它重新变成可继续写入和读取的活动线程。它特别注意先补齐历史记录,否则元数据判断可能会基于不完整的上下文。

数据流:输入是存储后端和恢复参数;它先让存储层 resume_thread 打开线程。如果参数里没有带历史记录,它会再向存储层 load_history;如果加载历史失败,会尝试 discard_thread 清理这次恢复状态并返回错误。成功后输出 LiveThread。

调用关系:它是继续旧会话的入口。它先依赖 ThreadStore 打开和读取历史,再调用 ThreadMetadataSync::for_resume 建立元数据同步状态;相关测试会验证恢复时确实先加载历史。

调用图:调用 1 个内部函数(for_resume);被 3 处调用(new, live_thread_resume_loads_history_before_observing_metadata, live_thread_resume_loads_history_from_explicit_external_rollout_path);外部调用 4 个(new, new, clone, warn!)。

LiveThread::append_items136–169 ↗
async fn append_items(&self, items: &[RolloutItem]) -> ThreadStoreResult<()>

作用:往当前线程里追加新的对话条目,并顺手观察这些条目是否会影响线程元数据。它解决的是“内容写进去了,标题、摘要、记忆等元数据也要及时跟上”的问题。

数据流:输入是一组 RolloutItem(可以理解为要保存的对话事件);它先筛出真正需要持久化统计的标准条目,再把原始条目追加到存储层。若这些条目触发元数据更新,它会写入 metadata patch,并在成功后标记这次更新已经落地。

调用关系:它是会话运行中最常用的写入点。它调用外部的 persisted_rollout_items 判断哪些条目参与元数据同步,再通过 ThreadStore::append_items 和 update_thread_metadata 完成保存。

调用图:外部调用 3 个(persisted_rollout_items, is_empty, to_vec)。

LiveThread::persist171–174 ↗
async fn persist(&self) -> ThreadStoreResult<()>

作用:把当前线程明确变成持久状态,并补写所有等待中的元数据更新。可以理解为按下“保存”按钮。

数据流:输入是当前 LiveThread;它先让存储层 persist_thread 保存线程主体,再调用 flush_pending_metadata_update 把元数据小账本里还没写出的更新写出去;成功后没有额外返回内容。

调用关系:它用于需要确保数据真正 durable(持久保存)的时刻。它先交给 ThreadStore 保存,再把后续元数据收尾交给 LiveThread::flush_pending_metadata_update。

调用图:调用 1 个内部函数(flush_pending_metadata_update)。

LiveThread::flush176–180 ↗
async fn flush(&self) -> ThreadStoreResult<()>

作用:把当前已经缓冲的线程数据刷新出去,但只补写适合“已有历史”的元数据更新。它像是把临时缓存倒进存储桶,保证读者能看到最新内容。

数据流:输入是当前 LiveThread;它先调用存储层 flush_thread,再调用 flush_pending_metadata_update_for_existing_history 处理那些依赖已有历史的元数据更新;输出是成功或错误。

调用关系:它常在运行中需要把缓冲内容推到存储层时使用。主体刷新交给 ThreadStore,元数据收尾交给 LiveThread::flush_pending_metadata_update_for_existing_history。

调用图:调用 1 个内部函数(flush_pending_metadata_update_for_existing_history)。

LiveThread::shutdown182–186 ↗
async fn shutdown(&self) -> ThreadStoreResult<()>

作用:关闭活动线程前做最后收尾。它先补齐该写的元数据,再通知存储层这个线程要结束使用。

数据流:输入是当前 LiveThread;它先取出并应用适合已有历史的待写元数据,再调用 shutdown_thread;输出是关闭成功或错误。

调用关系:它在会话结束时调用。它先复用 LiveThread::flush_pending_metadata_update_for_existing_history,确保元数据不落后,然后把真正关闭动作交给 ThreadStore。

调用图:调用 1 个内部函数(flush_pending_metadata_update_for_existing_history)。

LiveThread::discard188–190 ↗
async fn discard(&self) -> ThreadStoreResult<()>

作用:丢弃这个活动线程的持久化状态。它用于初始化失败、恢复失败或明确不想留下这次会话痕迹的情况。

数据流:输入是当前 LiveThread;它只把线程编号交给存储层 discard_thread;输出是清理是否成功。

调用关系:它是 LiveThreadInitGuard::discard 和 LiveThreadInitGuard::drop 的主要清理目标,也会在恢复失败时被间接使用。真正怎么删文件或通知远程服务,由 ThreadStore 决定。

LiveThread::load_history192–202 ↗
async fn load_history(
        &self,
        include_archived: bool,
    ) -> ThreadStoreResult<StoredThreadHistory>

作用:读取当前线程的历史对话。调用者可以决定是否包含已经归档的内容。

数据流:输入是 include_archived 开关和当前线程编号;它组装 LoadThreadHistoryParams 后交给存储层;输出是 StoredThreadHistory,也就是保存下来的历史条目集合。

调用关系:它是读取历史的薄封装。LiveThread::resume 内部也会在恢复参数没有历史时做类似的历史加载,而这个函数给外部会话代码提供直接读取入口。

LiveThread::read_thread204–216 ↗
async fn read_thread(
        &self,
        include_archived: bool,
        include_history: bool,
    ) -> ThreadStoreResult<StoredThread>

作用:读取当前线程的完整信息。它不只可以读元数据,也可以按需要连历史一起读出来。

数据流:输入是两个开关:是否包含归档内容、是否包含历史;它把这些条件和线程编号打包成 ReadThreadParams;输出是 StoredThread,里面是存储层返回的线程资料。

调用关系:它给上层提供“查看这个线程现在是什么样”的接口。具体查询由 ThreadStore 完成,LiveThread 只负责带上正确的 thread_id 和读取选项。

LiveThread::update_memory_mode218–235 ↗
async fn update_memory_mode(
        &self,
        mode: ThreadMemoryMode,
        include_archived: bool,
    ) -> ThreadStoreResult<()>

作用:修改当前线程的记忆模式。记忆模式可以理解为这个线程后续如何处理或保留记忆的设置。

数据流:输入是新的 ThreadMemoryMode 和是否允许更新归档线程的开关;它先 flush_pending_metadata_update,避免旧的待写元数据覆盖或干扰新设置;然后提交一个只包含 memory_mode 的元数据补丁;输出是成功或错误。

调用关系:它用于用户或系统显式改变线程记忆行为时。它先借助 LiveThread::flush_pending_metadata_update 清掉旧账,再把真正的 metadata 更新交给 ThreadStore。

调用图:调用 1 个内部函数(flush_pending_metadata_update);外部调用 1 个(default)。

LiveThread::update_metadata237–250 ↗
async fn update_metadata(
        &self,
        patch: ThreadMetadataPatch,
        include_archived: bool,
    ) -> ThreadStoreResult<StoredThread>

作用:通用地更新线程元数据,比如标题、摘要、记忆相关字段等。它适合调用者已经准备好一个元数据补丁的场景。

数据流:输入是 ThreadMetadataPatch 和 include_archived 开关;它先补写之前积压的元数据更新,再把新的补丁交给存储层 update_thread_metadata;输出是更新后的 StoredThread。

调用关系:它是外部直接改元数据的入口。为了避免顺序错乱,它先调用 LiveThread::flush_pending_metadata_update,再让 ThreadStore 执行真正更新。

调用图:调用 1 个内部函数(flush_pending_metadata_update)。

LiveThread::local_rollout_path255–267 ↗
async fn local_rollout_path(&self) -> ThreadStoreResult<Option<PathBuf>>

作用:给旧的、只认识本地文件的调用者返回当前线程的本地 rollout 文件路径。rollout 文件可以简单理解为本地保存对话展开记录的文件。

数据流:输入是当前 LiveThread;它检查底层 ThreadStore 是否真的是 LocalThreadStore。如果是,就查询 live_rollout_path 并返回路径;如果是远程存储或别的后端,就返回 Ok(None)。

调用关系:它是兼容旧本地调用方式的小桥梁。平常 LiveThread 不暴露存储细节,但这里会尝试把通用 ThreadStore 向下识别成本地存储;远程存储没有文件路径,所以不会假装有。

LiveThread::flush_pending_metadata_update269–272 ↗
async fn flush_pending_metadata_update(&self) -> ThreadStoreResult<()>

作用:取出所有等待写入的元数据更新,并把它们真正写到存储层。它主要用于保存或手动改元数据之前,先把旧账结清。

数据流:输入是当前 LiveThread;它先给 metadata_sync 加锁(互斥锁,防止两个异步任务同时改同一份小账本),取走待处理更新;再交给 apply_pending_metadata_update;输出是成功或错误。

调用关系:它被 LiveThread::persist、LiveThread::update_memory_mode 和 LiveThread::update_metadata 调用。它自己不直接写存储,而是把统一写入步骤交给 LiveThread::apply_pending_metadata_update。

调用图:调用 1 个内部函数(apply_pending_metadata_update);被 3 处调用(persist, update_memory_mode, update_metadata)。

LiveThread::flush_pending_metadata_update_for_existing_history274–281 ↗
async fn flush_pending_metadata_update_for_existing_history(&self) -> ThreadStoreResult<()>

作用:只取出那些适合在“已经有历史内容”的前提下写入的元数据更新。这样可以避免空线程关闭或刷新时,意外把本不该落地的元数据写成正式记录。

数据流:输入是当前 LiveThread;它锁住 metadata_sync,取出符合已有历史条件的待写更新;然后交给 apply_pending_metadata_update;输出是成功或错误。

调用关系:它被 LiveThread::flush 和 LiveThread::shutdown 调用。它和 flush_pending_metadata_update 很像,但选择待写更新的规则更谨慎,适合刷新和关闭阶段。

调用图:调用 1 个内部函数(apply_pending_metadata_update);被 2 处调用(flush, shutdown)。

LiveThread::apply_pending_metadata_update283–302 ↗
async fn apply_pending_metadata_update(
        &self,
        update: Option<crate::thread_metadata_sync::PendingThreadMetadataPatch>,
    ) -> ThreadStoreResult<()>

作用:真正执行一次待处理的元数据补丁写入。它把“有就写、没有就什么都不做、写完再记账”这套动作集中在一个地方。

数据流:输入是一个可选的 PendingThreadMetadataPatch;如果没有更新,就直接成功返回。如果有,它把补丁交给存储层 update_thread_metadata,写成功后再锁住 metadata_sync,标记这次待更新已经应用;输出是成功或错误。

调用关系:它是两个 flush 辅助函数的共同下游:LiveThread::flush_pending_metadata_update 和 LiveThread::flush_pending_metadata_update_for_existing_history 都把取出的更新交给它。这样元数据补写的真正执行逻辑只保留一份。

调用图:被 2 处调用(flush_pending_metadata_update, flush_pending_metadata_update_for_existing_history)。

external-agent-sessions/src/lib.rs源码 ↗
orchestrationsession import preparation

外部智能体留下的聊天记录,不能直接一股脑塞进系统里:可能已经导入过,可能文件路径不真实,可能内容格式不对,也可能记录里的工作目录已经不存在。这个文件就像收件处的验收员。它先定义几种简单的数据包:待迁移的外部会话、已经读出来的会话内容、确认可以导入的待办项。真正准备导入时,它先查“账本”看这个文件当前版本是否已经导入过;没导入过,再把路径转成真实路径,读取并解析会话,同时计算 SHA-256(一种给文件内容算“指纹”的算法);最后还会确认会话里的工作目录确实存在。只有这些都过关,才返回一个 PendingSessionImport,交给后续步骤真正导入。文件底部的测试用例验证了三件事:重复导入会跳过,缺文件会报错,有效文件会带着内容指纹准备好。

函数细节9
prepare_validated_session_import45–63 ↗
fn prepare_validated_session_import(
    codex_home: &Path,
    session: ExternalAgentSessionMigration,
) -> io::Result<Option<PendingSessionImport>>

作用:这是导入前的总把关函数。别人给它一个外部会话文件,它会判断这个会话是不是值得导入:已导入就跳过,读不了就报错或跳过,合格才包装成“待导入任务”。

数据流:输入是 Codex 的主目录 codex_home 和一个外部会话迁移信息 session。它先读取导入账本,问 has_current_session_been_imported:这个文件当前版本是不是已经导过;如果是,就输出 None。否则它调用 load_importable_session 读取文件、解析内容、算内容指纹,并检查会话是否可用。成功后输出 Some(PendingSessionImport),里面有真实来源路径、内容 SHA-256 指纹和整理好的会话内容;过程中不真正写入导入记录。

调用关系:它是本文件对外准备导入的主入口。测试里的 tests::skips_session_that_was_already_imported、tests::reports_session_preparation_errors、tests::prepares_one_validated_session_import_with_content_hash 都直接调用它,分别验证跳过、报错和成功准备三条路。它自己把“是否导过”的问题交给 has_current_session_been_imported,把“文件能不能读成会话”的问题交给 load_importable_session。

调用图:调用 2 个内部函数(has_current_session_been_imported, load_importable_session);被 3 处调用(prepares_one_validated_session_import_with_content_hash, reports_session_preparation_errors, skips_session_that_was_already_imported)。

load_importable_session65–79 ↗
fn load_importable_session(
    path: &Path,
) -> io::Result<Option<(PathBuf, ImportedExternalAgentSession, String)>>

作用:这个函数专门把一个会话文件变成“可以导入的会话”。它会把路径整理成真实路径,读取文件内容,计算内容指纹,并过滤掉工作目录不存在的会话。

数据流:输入是一个文件路径 path。它先用 canonicalize 把路径变成系统认可的真实绝对路径;如果文件不存在,这一步会返回错误。然后它调用 load_session_for_import_with_content_sha256,尝试把文件解析成 ImportedExternalAgentSession,并拿到文件内容的 SHA-256 指纹。如果解析结果为空,输出 None。如果解析成功,还会检查 imported_session.cwd 是否是一个真实目录;是的话输出 Some(真实路径、会话内容、内容指纹),不是的话输出 None。

调用关系:它被 prepare_validated_session_import 调用,是准备导入流程里的“读文件和验货”环节。它不负责查重,也不负责记录导入完成,只负责确认这个源文件本身是否能变成一份可靠的待导入会话。

调用图:调用 1 个内部函数(load_session_for_import_with_content_sha256);被 1 处调用(prepare_validated_session_import);外部调用 1 个(canonicalize)。

summarize_for_label94–97 ↗
fn summarize_for_label(text: &str) -> String

作用:这个函数把一大段文字压成适合当标题或标签的一小句。它通常用第一行内容,避免标题又长又乱。

数据流:输入是一段 text。它取第一行,去掉前后空白;如果没有内容,就当成空字符串。然后它把这一行交给 truncate,限制在 SESSION_TITLE_MAX_LEN 这个最大长度内。输出是一段短字符串,可用于显示会话标题或标签。

调用关系:它是给会话生成简短显示名的小工具。它自己不处理复杂规则,只负责选第一行;真正的长度裁剪交给 truncate。

调用图:调用 1 个内部函数(truncate)。

truncate99–108 ↗
fn truncate(text: &str, max_len: usize) -> String

作用:这个函数把太长的文字剪短,并在末尾加上省略号。它的作用是防止界面里的标题、标签或摘要撑得太长。

数据流:输入是一段 text 和最大长度 max_len。它先按“字符”数量判断长度,而不是简单按字节判断,所以对中文这类多字节文字更安全。如果没有超过限制,就原样输出。超过限制时,它取前面 max_len 减 3 个字符,再用 format! 拼上“...”,输出剪短后的字符串。

调用关系:它被 summarize_for_label 调用,承担最后的裁剪工作。summarize_for_label 决定用哪段文字,它决定这段文字最多能显示多长。

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

now_unix_seconds110–115 ↗
fn now_unix_seconds() -> i64

作用:这个函数返回当前时间,用 Unix 秒表示。Unix 秒就是从 1970 年 1 月 1 日零点到现在经过了多少秒,常用来给记录打时间戳。

数据流:它不需要外部输入。它读取系统当前时间 now,计算距离 UNIX_EPOCH 的秒数,转换成 i64 输出。如果系统时间异常导致计算失败,它会输出 0,避免程序因为拿不到时间而崩掉。

调用关系:这是一个小型时间工具函数,适合在需要给导入记录或消息补时间戳时使用。调用清单里没有显示谁调用它,但它提供的是跨本模块可复用的“当前秒数”能力。

调用图:外部调用 1 个(now)。

tests::skips_session_that_was_already_imported126–139 ↗
fn skips_session_that_was_already_imported()

作用:这个测试确认:如果某个外部会话已经被记录为导入过,准备导入时应该直接跳过,而不是重复导入。

数据流:测试先创建临时目录和一个假的 session.jsonl 文件,再用 record_imported_session 把这个源文件写进导入账本,表示它已经导过。然后它构造迁移信息,调用 prepare_validated_session_import。最后断言返回值是 None,说明系统正确跳过了这个会话。

调用关系:它模拟 prepare_validated_session_import 的查重分支。测试先用 record_imported_session 布置“已经导入”的现场,再调用主函数,看主函数是否会通过 has_current_session_been_imported 发现这件事并停止后续读取。

调用图:调用 3 个内部函数(record_imported_session, prepare_validated_session_import, new);外部调用 4 个(new, assert!, session_migration, write)。

tests::reports_session_preparation_errors142–150 ↗
fn reports_session_preparation_errors()

作用:这个测试确认:如果要导入的会话文件不存在,准备过程应该把错误报告出来,而不是悄悄当成没有会话。

数据流:测试创建一个临时目录,但故意指向一个不存在的 missing-session.jsonl。它调用 prepare_validated_session_import,并期待得到错误。最后检查错误类型是 NotFound,也就是“文件没找到”。

调用关系:它覆盖 prepare_validated_session_import 调用 load_importable_session 时的失败路径。真正发现文件不存在的是 canonicalize 这一类文件系统操作,错误一路传回给测试。

调用图:调用 1 个内部函数(prepare_validated_session_import);外部调用 3 个(new, assert_eq!, session_migration)。

tests::prepares_one_validated_session_import_with_content_hash153–174 ↗
fn prepares_one_validated_session_import_with_content_hash()

作用:这个测试确认:一个有效的外部会话能被准备成待导入项,并且会带上正确的内容 SHA-256 指纹。

数据流:测试先创建临时目录和一个 JSON 形式的会话文件,里面有用户消息、工作目录和时间戳。然后调用 prepare_validated_session_import,期待得到 Some(PendingSessionImport)。接着测试自己也对原始文件内容算 SHA-256,和返回的 source_content_sha256 对比;两者相同就说明准备流程确实按文件内容生成了指纹。

调用关系:它验证 prepare_validated_session_import 的成功路径,也间接验证 load_importable_session 会调用 load_session_for_import_with_content_sha256 来读取会话并计算内容指纹。

调用图:调用 1 个内部函数(prepare_validated_session_import);外部调用 5 个(new, assert_eq!, session_migration, json!, write)。

tests::session_migration176–185 ↗
fn session_migration(path: &Path) -> ExternalAgentSessionMigration

作用:这是测试里用的小帮手,用来快速把一个文件路径包装成 ExternalAgentSessionMigration。这样每个测试不用重复写同样的构造代码。

数据流:输入是一个会话文件路径 path。它把 path 复制成迁移对象的 path 字段,把文件的父目录作为 cwd 字段,并把 title 设为 None。输出是一个 ExternalAgentSessionMigration,供测试传给 prepare_validated_session_import。

调用关系:它被多个测试用例调用,负责准备统一格式的测试输入。它不参与正式运行流程,只是让测试更短、更清楚。

调用图:外部调用 2 个(parent, to_path_buf)。

external-agent-sessions/src/detect.rs源码 ↗
domain_logicstartup / import discovery

外部助手会把聊天会话按项目存成一堆 jsonl 文件,也就是“一行一个 JSON 记录”的文本文件。这个文件做的事,就像在一堆旧票据里挑出最近还没报销过的票据:先去 external_agent_home/projects 下面逐个项目找 .jsonl 会话文件;只看最近 30 天内改过的文件;最多一次挑 50 个,避免一下子扫太多;再查一本“导入账本”,看看这个文件是不是已经导入过、版本有没有变。如果文件没变,就跳过;如果变了,就重新列出来。最后它会读取会话摘要,拿到文件路径、工作目录和标题,并且只保留工作目录真实存在的会话。文件下半部分是测试,用临时目录造出各种会话,检查标题选择、时间过滤、分批导入和重新检测是否正确。

函数细节17
detect_recent_sessions16–109 ↗
fn detect_recent_sessions(
    external_agent_home: &Path,
    codex_home: &Path,
) -> io::Result<Vec<ExternalAgentSessionMigration>>

作用:扫描外部助手的项目目录,找出最近可以导入到 Codex 的会话。它的价值是:既不会漏掉新会话,也尽量不重复导入已经处理过的同一版文件。

数据流:输入是外部助手的家目录 external_agent_home 和 Codex 的家目录 codex_home。函数先进入 external_agent_home/projects,读取 Codex 里的导入账本,再遍历每个项目下的 .jsonl 文件;它用文件修改时间判断新旧,用账本判断是否已经导入过,用一个最多装 50 个元素的堆保留最新候选;之后对候选文件刷新账本状态、读取会话摘要,并过滤掉工作目录不存在的会话。输出是一组 ExternalAgentSessionMigration,里面包含要导入的源文件路径、会话所在工作目录和标题;如果过程中发现账本状态变了,还会把账本保存回磁盘。

调用关系:这是本文件的核心入口。测试用例会在各种临时目录场景下调用它,验证它能发现新会话、跳过旧会话、分批处理、以及在源文件变化后重新发现。它自己会把读写账本的事交给 load_import_ledger 和 save_import_ledger,把“读懂单个会话内容”的事交给 summarize_session,把当前时间交给 now_unix_seconds。

调用图:调用 2 个内部函数(load_import_ledger, save_import_ledger);被 7 处调用(detects_ai_title_over_first_user_message, detects_recent_sessions_with_existing_roots, detects_sessions_in_batches, prefers_custom_title_over_later_ai_title, prefers_latest_custom_title_over_first_user_message, redetects_sessions_when_source_contents_change_after_import, uses_file_modification_time_for_recency);外部调用 9 个(with_capacity, join, new, now_unix_seconds, summarize_session, canonicalize, read_dir, try_from, Reverse)。

tests::detects_recent_sessions_with_existing_roots124–148 ↗
fn detects_recent_sessions_with_existing_roots()

作用:检查最基本的成功场景:外部目录里有一个正常会话时,检测函数应该能把它找出来。

数据流:测试先创建一个临时目录,再用 write_session 写入包含用户消息和助手回复的 jsonl 会话文件。然后调用 detect_recent_sessions。最后断言结果正好是一条迁移记录,路径是刚写的文件,工作目录是项目目录,标题来自第一条用户消息。

调用关系:这是 detect_recent_sessions 的基础验收测试。它用 record 造聊天记录,用 write_session 放到外部助手目录结构里,然后直接检查检测结果是否符合预期。

调用图:调用 1 个内部函数(detect_recent_sessions);外部调用 4 个(new, assert_eq!, record, write_session)。

tests::prefers_latest_custom_title_over_first_user_message151–176 ↗
fn prefers_latest_custom_title_over_first_user_message()

作用:检查如果会话里有用户自己设置的标题,就应该用最新的自定义标题,而不是随便拿第一句用户消息当标题。

数据流:测试创建一个会话,里面先有用户消息,再有两个 custom-title 记录。它调用 detect_recent_sessions 后,期待返回的迁移记录标题是最后一个自定义标题 final title。结果不改真实业务数据,只验证标题选择规则。

调用关系:这个测试通过 custom_title_record 和 record 构造特殊会话,再交给 detect_recent_sessions。它间接验证 summarize_session 给出的标题会被检测流程原样带到迁移结果里。

调用图:调用 1 个内部函数(detect_recent_sessions);外部调用 5 个(new, assert_eq!, custom_title_record, record, write_session)。

tests::detects_ai_title_over_first_user_message179–203 ↗
fn detects_ai_title_over_first_user_message()

作用:检查如果源应用生成了 AI 标题,检测结果会使用这个标题,而不是只用第一条用户消息。

数据流:测试写入一个会话,里面包含一条用户消息和一条 ai-title 记录。调用 detect_recent_sessions 后,它断言返回的标题是 generated by source app。输入是临时文件内容,输出是一次断言通过或失败。

调用关系:这个测试用 ai_title_record 造出源应用生成标题的记录,再让 detect_recent_sessions 走完整扫描流程。它关注的是检测结果中的标题是否来自会话摘要。

调用图:调用 1 个内部函数(detect_recent_sessions);外部调用 5 个(new, assert_eq!, ai_title_record, record, write_session)。

tests::prefers_custom_title_over_later_ai_title206–231 ↗
fn prefers_custom_title_over_later_ai_title()

作用:检查标题优先级:即使 AI 标题出现得更晚,用户自定义标题也应该更优先。

数据流:测试写入一个会话,顺序是用户消息、自定义标题、AI 标题。然后调用 detect_recent_sessions,并断言最终标题是 custom title,而不是后面的 generated title。

调用关系:这个测试把 custom_title_record、ai_title_record 和 record 组合起来,交给 detect_recent_sessions。它保护的是“用户手动设置优先于机器生成”的规则。

调用图:调用 1 个内部函数(detect_recent_sessions);外部调用 6 个(new, assert_eq!, ai_title_record, custom_title_record, record, write_session)。

tests::uses_file_modification_time_for_recency234–260 ↗
fn uses_file_modification_time_for_recency()

作用:检查“最近会话”的判断主要看文件修改时间,而不是聊天记录内部写的时间戳。

数据流:测试写入一条内容时间戳很早的聊天记录,但文件本身是刚创建的。调用 detect_recent_sessions 后,它期待这个会话仍然会被找出来。也就是说,输入里的旧 timestamp 不会让会话被误判成旧文件。

调用关系:这个测试用 record_at 人为指定消息时间,再用 write_session 写文件,最后调用 detect_recent_sessions。它对应核心函数里读取文件 metadata.modified 的行为。

调用图:调用 1 个内部函数(detect_recent_sessions);外部调用 4 个(new, assert_eq!, record_at, write_session)。

tests::ignores_sessions_with_old_file_modification_time263–283 ↗
fn ignores_sessions_with_old_file_modification_time()

作用:检查太旧的会话文件会被跳过,避免把很久以前的历史记录又翻出来打扰用户。

数据流:测试先写一个正常会话文件,再用 set_modified_at 把文件修改时间改成 Unix 纪元刚开始后的时间,也就是非常老。随后运行检测并断言结果为空。输入是一个被伪装成老文件的会话,输出是“没有可导入会话”的判断。

调用关系:这个测试围绕 detect_recent_sessions 的 30 天时间过滤规则。它用 write_session 造文件,用 set_modified_at 改文件时间,再通过断言确认旧文件不会进入后续摘要读取和导入候选。

调用图:外部调用 6 个(from_secs, new, assert!, record, set_modified_at, write_session)。

tests::detects_sessions_in_batches286–361 ↗
fn detects_sessions_in_batches()

作用:检查一次最多只拿最新的 50 个会话,并且导入一批后,下一次还能继续找到剩下的旧一点的会话。

数据流:测试创建 51 个会话文件,给它们设置从新到旧的修改时间。第一次检测应该返回最新的 50 个;测试把这些记录为已导入后,第二次检测应该只返回剩下那个最旧的。接着它又改动所有文件内容和修改时间,确认变更后的文件会再次按批次被检测出来。

调用关系:这是最完整的流程测试之一。它反复调用 detect_recent_sessions,并用 record_imported_session 模拟“已经导入到账本”。它验证核心函数和账本机制配合得对:限制批量大小,同时不永久漏掉排在第 51 位的会话。

调用图:调用 3 个内部函数(detect_recent_sessions, record_imported_session, new);外部调用 13 个(from_secs, now, new, new, assert_eq!, now, jsonl, record, record_at, set_modified_at (+3 more))。

tests::skips_already_imported_current_session_versions364–383 ↗
fn skips_already_imported_current_session_versions()

作用:检查同一个会话文件如果已经按当前版本导入过,就不会再次出现在待导入列表里。

数据流:测试先写入一个会话文件,然后调用 record_imported_session 把它记到账本里,表示已经导入。之后检测时,期望结果为空。输入是一个已登记的文件,输出是跳过它的结果。

调用关系:这个测试验证 detect_recent_sessions 会尊重导入账本。它用 write_session 创建源文件,用 record_imported_session 写入“已导入”状态,再确认检测流程不会重复推荐同一版会话。

调用图:调用 2 个内部函数(record_imported_session, new);外部调用 4 个(new, assert!, record, write_session)。

tests::redetects_sessions_when_source_contents_change_after_import386–417 ↗
fn redetects_sessions_when_source_contents_change_after_import()

作用:检查已经导入过的会话,如果源文件后来又新增内容,系统应该能重新发现它。

数据流:测试先创建并登记一个已导入的会话。然后直接改写这个 jsonl 文件,加入新的助手回复。再调用 detect_recent_sessions,期望这个会话重新出现在结果里,标题仍然来自原来的用户消息。

调用关系:这个测试连接了账本和文件变化检测两件事。它用 record_imported_session 标记旧版本已导入,再通过写文件让源内容变化,最后确认 detect_recent_sessions 不会因为“曾经导入过”就永远忽略它。

调用图:调用 3 个内部函数(detect_recent_sessions, record_imported_session, new);外部调用 6 个(new, assert_eq!, jsonl, record, write_session, write)。

tests::write_session419–431 ↗
fn write_session(
        external_agent_home: &Path,
        project_root: &Path,
        file_name: &str,
        records: &[JsonValue],
    ) -> std::path::PathBuf

作用:测试辅助函数,用来快速造出一个外部助手风格的会话文件。

数据流:输入是外部助手家目录、项目目录、文件名和一组 JSON 记录。函数会创建项目根目录和 external_agent_home/projects/repo 目录,把记录转成 jsonl 文本写进指定文件。输出是刚写好的会话文件路径,同时磁盘上会多出对应目录和文件。

调用关系:多个测试都用它准备测试数据。它把 jsonl 负责的“记录转文本”和标准文件写入串起来,让每个测试不用重复写目录创建和文件写入代码。

调用图:外部调用 4 个(join, jsonl, create_dir_all, write)。

tests::set_modified_at433–440 ↗
fn set_modified_at(path: &Path, modified_at: SystemTime)

作用:测试辅助函数,用来把某个文件的修改时间改成指定时间。

数据流:输入是文件路径和目标 SystemTime。函数以可写方式打开文件,然后设置文件的 modified time,也就是操作系统记录的“最后修改时间”。它不返回业务数据,但会改变这个文件在磁盘上的时间属性。

调用关系:时间过滤相关测试会用它伪造新旧文件。这样 tests::ignores_sessions_with_old_file_modification_time 和批量检测测试就能稳定验证 detect_recent_sessions 对文件修改时间的判断。

调用图:外部调用 2 个(new, new)。

tests::record442–445 ↗
fn record(role: &str, text: &str, cwd: &Path) -> JsonValue

作用:测试辅助函数,用当前时间快速生成一条普通聊天记录。

数据流:输入是角色 role、文本 text 和工作目录 cwd。函数先取当前 UTC 时间,把它转成字符串,再调用 record_at 生成 JSON 记录。输出是一条可以写进 jsonl 会话文件的 JSON 值。

调用关系:很多测试用它造用户消息或助手消息。它把“当前时间”这件事封装掉,再把实际 JSON 结构交给 record_at 生成。

调用图:外部调用 2 个(now, record_at)。

tests::record_at447–454 ↗
fn record_at(role: &str, text: &str, cwd: &Path, timestamp: &str) -> JsonValue

作用:测试辅助函数,用指定时间生成一条普通聊天记录,方便测试时间相关场景。

数据流:输入是角色、文本、工作目录和时间戳字符串。函数把这些字段组装成一个 JSON 对象,里面包含 type、cwd、timestamp 和 message.content。输出是这条 JSON 记录。

调用关系:record 会调用它,部分测试也会直接调用它来指定固定时间。它提供了最基础的聊天记录积木,write_session 再把这些积木写成文件。

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

tests::custom_title_record456–461 ↗
fn custom_title_record(title: &str) -> JsonValue

作用:测试辅助函数,用来造一条“用户自定义标题”的记录。

数据流:输入是标题字符串。函数把它包装成一个 JSON 对象,type 是 custom-title,customTitle 字段保存标题。输出是可放进会话 jsonl 文件的一条记录。

调用关系:标题优先级测试会调用它,再把生成的记录交给 write_session。它帮助测试 detect_recent_sessions 经由摘要逻辑拿到正确标题。

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

tests::ai_title_record463–468 ↗
fn ai_title_record(title: &str) -> JsonValue

作用:测试辅助函数,用来造一条“AI 生成标题”的记录。

数据流:输入是标题字符串。函数把它包装成一个 JSON 对象,type 是 ai-title,aiTitle 字段保存标题。输出是可写入会话文件的一条 JSON 记录。

调用关系:AI 标题相关测试会调用它。它和 custom_title_record 常被放在同一个会话里,用来验证标题选择规则的优先级。

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

tests::jsonl470–476 ↗
fn jsonl(records: &[JsonValue]) -> String

作用:测试辅助函数,把多条 JSON 记录变成 jsonl 文本,也就是每条记录占一行。

数据流:输入是一组 JSON 值。函数逐条把 JSON 转成字符串,再用换行符拼起来。输出是一段可以直接写入 .jsonl 文件的文本。

调用关系:write_session 用它生成会话文件内容,部分测试改写文件时也会用它。它是测试数据从“内存里的 JSON 记录”变成“磁盘上的会话文件”的最后一步。

调用图:外部调用 1 个(iter)。

服务器端线程协调

这些文件管理 app 和 exec 服务器上的已加载线程运行时状态,包括监听器编排、过滤、刷新和会话附加生命周期。

app-server/src/filters.rs源码 ↗
domain_logicrequest handling

这个文件像一个门卫,专门看线程是从哪里来的。外部接口用的是 ThreadSourceKind,也就是给前端或调用方看的来源分类,比如 CLI、VS Code、子代理、未知来源等;底层核心系统用的是 CoreSessionSource,是内部真正记录会话来源的格式。问题在于,不是所有来源都能直接交给底层一次筛完:CLI 和 VS Code 这种简单来源可以提前筛;但子代理还分 review、compact、thread spawn 等细类,有些需要拿到每条记录后再逐个判断。所以 compute_source_filters 会把筛选条件拆成两部分:能提前给底层的来源列表,以及可能需要后处理的原始筛选条件。source_kind_matches 则负责后处理阶段的精确匹配,尤其能分清不同子代理类型。文件底部的测试用几个常见场景确认默认行为、空筛选、简单筛选和子代理细分都没有跑偏。

函数细节7
compute_source_filters6–51 ↗
fn compute_source_filters(
    source_kinds: Option<Vec<ThreadSourceKind>>,
) -> (Vec<CoreSessionSource>, Option<Vec<ThreadSourceKind>>)

作用:这个函数把用户传来的来源筛选条件,拆成“底层可以先筛的部分”和“之后还要自己再筛的部分”。这样既能利用底层系统少查一些数据,又不会因为底层分不清细分类而筛错。

数据流:进去的是一个可有可无的来源列表:没有传,或传了空列表,就当作只看默认的交互式来源。函数会检查里面有没有 Exec、AppServer、各种 SubAgent、Unknown 这类需要更细判断的来源;如果有,就返回一个空的底层预筛列表,并保留原始筛选条件给后面用。如果只有 Cli 和 VsCode,就把它们翻译成底层认识的 CoreSessionSource::Cli 和 CoreSessionSource::VSCode,结果是一个可直接交给底层的来源列表,以及原始条件。

调用关系:它是筛选流程的前半段,调用者在真正取线程列表前会先用它决定怎么筛。这里列出的调用者主要是测试:默认值测试、空列表测试、只含交互来源测试、子代理来源测试都会调用它,确认它在不同输入下把预筛和后筛分得正确。它内部只用到创建空列表这类基础操作,不把工作再交给项目里的其他函数。

调用图:被 4 处调用(compute_source_filters_defaults_to_interactive_sources, compute_source_filters_empty_means_interactive_sources, compute_source_filters_interactive_only_skips_post_filtering, compute_source_filters_subagent_variant_requires_post_filtering);外部调用 1 个(new)。

source_kind_matches53–82 ↗
fn source_kind_matches(source: &CoreSessionSource, filter: &[ThreadSourceKind]) -> bool

作用:这个函数用来判断一条真实记录的来源,是否符合用户选的某个来源筛选条件。它特别重要,因为它能分清子代理的大类和具体小类。

数据流:进去的是一条内部来源 CoreSessionSource,以及一组外部筛选条件 ThreadSourceKind。函数会逐个看筛选条件:Cli 对 Cli,VsCode 对 VSCode,Exec 对 Exec,AppServer 对内部的 Mcp;如果是 SubAgent,还会继续看它是 Review、Compact、ThreadSpawn 还是 Other。只要有一个条件对上,就返回 true;全都不对,就返回 false。它不改动任何数据,只给出是否匹配的判断。

调用关系:它是筛选流程的后半段,适合在 compute_source_filters 判断出需要后处理时使用:先拿到候选线程,再用它逐条检查是否真的符合条件。根据调用图,它本身只调用遍历列表的基础能力;文件里的测试会构造不同子代理来源来确认这种精确判断不会把 Review 和 ThreadSpawn 混淆。

调用图:外部调用 1 个(iter)。

tests::compute_source_filters_defaults_to_interactive_sources92–97 ↗
fn compute_source_filters_defaults_to_interactive_sources()

作用:这个测试确认:如果调用方完全没传来源筛选条件,系统会使用默认的交互式来源,而不是返回所有乱七八糟的来源。

数据流:进去的是 None,也就是没有筛选条件。测试调用 compute_source_filters 后,检查出来的预筛来源等于默认交互来源列表,并且没有额外后处理筛选条件。结果是用断言确认默认行为符合预期。

调用关系:它直接测试 compute_source_filters 的默认分支。这个测试不参与线上运行,只在开发或持续集成时帮忙防止别人改代码时破坏默认筛选规则。

调用图:调用 1 个内部函数(compute_source_filters);外部调用 1 个(assert_eq!)。

tests::compute_source_filters_empty_means_interactive_sources100–105 ↗
fn compute_source_filters_empty_means_interactive_sources()

作用:这个测试确认:传入一个空的来源列表,效果和没传一样,都会回到默认交互式来源。

数据流:进去的是 Some(Vec::new()),也就是明确传了一个空列表。测试调用 compute_source_filters,然后检查返回的底层预筛来源是默认交互来源,后处理筛选条件是 None。它不产出业务数据,只用断言证明这个边界情况处理正确。

调用关系:它测试 compute_source_filters 的空列表分支,并用创建空列表的基础能力构造输入。它的存在是为了避免空列表被误解成“什么都不要”或“什么都要”。

调用图:调用 1 个内部函数(compute_source_filters);外部调用 2 个(new, assert_eq!)。

tests::compute_source_filters_interactive_only_skips_post_filtering108–117 ↗
fn compute_source_filters_interactive_only_skips_post_filtering()

作用:这个测试确认:如果筛选条件只有 CLI 和 VS Code 这种简单来源,就不需要复杂的后处理,底层预筛就够了。

数据流:进去的是包含 Cli 和 VsCode 的列表。测试调用 compute_source_filters 后,检查返回的底层预筛列表正好是 CoreSessionSource::Cli 和 CoreSessionSource::VSCode,同时原始筛选条件被保留下来。结果是用断言确认简单来源会被正确翻译。

调用关系:它覆盖 compute_source_filters 的简单映射路径。它通过构造一个小列表来模拟用户只想看交互式来源的情况,确保这条路径不会被误判成需要后处理。

调用图:调用 1 个内部函数(compute_source_filters);外部调用 2 个(assert_eq!, vec!)。

tests::compute_source_filters_subagent_variant_requires_post_filtering120–126 ↗
fn compute_source_filters_subagent_variant_requires_post_filtering()

作用:这个测试确认:如果用户筛选的是某一种子代理来源,比如 Review,就不能只靠底层粗筛,必须保留条件给后面精确判断。

数据流:进去的是只包含 SubAgentReview 的列表。测试调用 compute_source_filters 后,检查底层预筛列表是空的,同时原始筛选条件还在。这个结果表示系统不会假装底层已经能精确区分这个细分类。

调用关系:它测试 compute_source_filters 中“需要后处理”的分支。它保护的是一个容易出错的地方:子代理不是一个简单来源,很多时候必须逐条看记录才能判断。

调用图:调用 1 个内部函数(compute_source_filters);外部调用 2 个(assert_eq!, vec!)。

tests::source_kind_matches_distinguishes_subagent_variants129–157 ↗
fn source_kind_matches_distinguishes_subagent_variants()

作用:这个测试确认:匹配函数能分清不同子代理类型,不会把 Review 子代理和 ThreadSpawn 子代理认成同一种。

数据流:测试先生成一个随机 UUID,并把它做成父线程编号,用来构造 ThreadSpawn 子代理来源;同时也构造一个 Review 子代理来源。接着分别检查 Review 是否只匹配 SubAgentReview、ThreadSpawn 是否只匹配 SubAgentThreadSpawn。结果是通过断言证明两个细分类不会串线。

调用关系:它是 source_kind_matches 的精确性测试。它会用 from_string、new_v4 和 SubAgent 这些外部构造能力准备测试数据,再用断言检查结果;它不参与线上流程,只负责在测试阶段守住子代理分类规则。

调用图:调用 1 个内部函数(from_string);外部调用 3 个(SubAgent, new_v4, assert!)。

app-server/src/thread_state.rs源码 ↗
orchestrationcross-cutting,连接建立、订阅线程、线程运行、请求处理和关闭清理时都会用到

这个文件管的是 app-server 里“线程”的运行状态。这里的线程不是操作系统线程,而是一段对话或任务的编号。一个线程可能被多个客户端连接订阅,也可能正在被一个监听器持续接收新事件。文件里的 ThreadState 记住单个线程的细节,比如当前轮次的历史、是否有取消信号、最后一次设置、未处理的中断等;ThreadEntry 把线程状态和订阅它的连接放在一起;ThreadStateManager 则像前台登记员,维护“连接到线程”“线程到连接”的双向表。这样服务器就能知道:一个连接断了要从哪些线程移除;一个线程没人看了能不能卸载;发给监听器的命令要走哪个通道。这里还特别注意顺序问题:某些命令必须排队交给线程监听器执行,保证客户端看到的恢复消息、目标更新、请求完成通知不会前后颠倒。

函数细节29
ThreadState::listener_matches92–97 ↗
fn listener_matches(&self, conversation: &Arc<CodexThread>) -> bool

作用:判断当前记录里的监听器,是否正好属于传进来的那个对话线程。这样可以避免把旧监听器或别的线程的监听器误当成当前可用的监听器。

数据流:进去的是当前 ThreadState 和一个对话线程引用 → 它把自己保存的弱引用尝试升级成真实引用,再比较两者是不是同一个对象 → 出来一个布尔值,表示监听器是否匹配;它不改动任何状态。

调用关系:这是给启动或复用线程监听器的流程做安全检查用的。它不把活儿交给别的本文件函数,只负责回答“这个监听器是不是我要的那个”。

ThreadState::set_listener99–116 ↗
fn set_listener(
        &mut self,
        cancel_tx: oneshot::Sender<()>,
        conversation: &Arc<CodexThread>,
        watch_registration: WatchRegistration,
        thread_settings_baseline: Th

作用:给一个线程登记新的监听器,并把旧监听器停掉。它保证同一个线程不会同时有两个监听器抢着发事件。

数据流:进去的是取消用的一次性发送器、对话线程、文件监听登记信息和线程设置基线 → 它先通知旧监听器退出,再生成新一代监听器编号,保存设置、命令通道、线程弱引用和监听登记 → 出来的是新监听器要接收命令的接收端,以及这一代监听器编号;ThreadState 被更新。

调用关系:当服务器要开始监听某个运行中的线程时会调用它。它内部创建一个不受容量限制的消息通道,并保存发送端,后续其他流程就能通过 listener_command_tx 或管理器登记的通道把命令排队交给监听器。

调用图:外部调用 2 个(downgrade, unbounded_channel)。

ThreadState::clear_listener118–126 ↗
fn clear_listener(&mut self)

作用:清掉当前线程的监听器状态,并尽量通知监听器退出。它用于线程卸载、服务器关闭,或重新换监听器前的收尾。

数据流:进去的是可变的 ThreadState → 它发送取消信号,移除命令通道,清空当前轮次历史,忘掉监听的线程,并把文件监听登记恢复成默认空状态 → 没有返回值,但线程状态被清理干净。

调用关系:ThreadStateManager::remove_thread_state 和 ThreadStateManager::clear_all_listeners 会在清理时调用它。它把当前轮次历史交给 ThreadHistoryBuilder 的 reset 来清空,并用默认值替换文件监听登记。

调用图:调用 2 个内部函数(reset, default)。

ThreadState::set_experimental_raw_events128–130 ↗
fn set_experimental_raw_events(&mut self, enabled: bool)

作用:打开或关闭“实验性原始事件”开关。这个开关表示客户端想不想收到更底层、更未经整理的事件。

数据流:进去的是一个 true 或 false → 它直接写入 ThreadState 里的 experimental_raw_events 字段 → 没有返回值,只改变这个线程之后发事件的模式标记。

调用关系:ThreadStateManager::try_ensure_connection_subscribed 在客户端订阅线程时会根据客户端要求调用它。它本身不发送事件,只保存后续流程会看的开关。

ThreadState::listener_command_tx132–136 ↗
fn listener_command_tx(
        &self,
    ) -> Option<mpsc::UnboundedSender<ThreadListenerCommand>>

作用:取出当前监听器的命令发送口。外部需要让监听器按顺序做事时,会先拿到这个发送口。

数据流:进去的是当前 ThreadState → 它复制一份保存着的命令发送端,如果没有监听器就得到空 → 出来是可选的发送端;不改变原状态。

调用关系:resolve_server_request_on_thread_listener 会用它找到线程监听器的队列。这样请求完成通知能被放进监听器自己的顺序里,而不是从旁边插队。

ThreadState::active_turn_snapshot138–140 ↗
fn active_turn_snapshot(&self) -> Option<Turn>

作用:拿到当前正在进行的那一轮对话的快照。快照就是当下状态的临时照片,方便外部查看但不直接修改原件。

数据流:进去的是 ThreadState → 它向 current_turn_history 询问是否有正在进行的轮次,并取出快照 → 出来是可选的 Turn;没有活动轮次时返回空。

调用关系:清理线程状态时会用它记录日志,看看清理时是否还有活动轮次。真正整理轮次内容的工作交给 ThreadHistoryBuilder::active_turn_snapshot。

调用图:调用 1 个内部函数(active_turn_snapshot)。

ThreadState::track_current_turn_event142–153 ↗
fn track_current_turn_event(&mut self, event_turn_id: &str, event: &EventMsg)

作用:把一条新事件记进“当前轮次”的临时历史里,并在这一轮结束时做收尾。它让服务器能随时知道当前对话轮次已经发生了什么。

数据流:进去的是事件所属的轮次编号和事件内容 → 如果是轮次开始事件,就记录开始时间;然后把事件交给历史构建器消化;如果事件表示中止或完成,且已经没有活动轮次,就记下最后结束的轮次编号并清空临时历史 → 没有返回值,但 ThreadState 的轮次摘要和历史会变化。

调用关系:线程监听器收到底层事件时会用它更新状态。它把事件细节交给 ThreadHistoryBuilder::handle_event,靠 has_active_turn 判断是否真的结束,必要时调用 reset 清空临时历史。

调用图:调用 3 个内部函数(handle_event, has_active_turn, reset);外部调用 1 个(matches!)。

ThreadState::note_thread_settings155–159 ↗
fn note_thread_settings(&mut self, thread_settings: ThreadSettings) -> bool

作用:记录最新线程设置,并告诉调用者这次设置和上次相比有没有实际变化。这样可以避免没变化也反复通知客户端。

数据流:进去的是一份 ThreadSettings → 它和上一次保存的设置比较,然后把新设置保存下来 → 出来一个布尔值:true 表示变了,false 表示和上次一样。

调用关系:线程监听或设置更新流程会用它判断要不要发设置变化通知。测试 tests::note_thread_settings_reports_only_effective_changes 专门验证它只在真正变化时返回 true。

resolve_server_request_on_thread_listener162–192 ↗
async fn resolve_server_request_on_thread_listener(
    thread_state: &Arc<Mutex<ThreadState>>,
    request_id: RequestId,
)

作用:把“某个服务器请求已经处理完了”这件事,排到线程监听器自己的队列里执行。这样客户端收到的“请求完成”通知不会和请求本身、线程事件发生乱序。

数据流:进去的是线程状态和请求编号 → 它创建一个一次性回执通道,锁住线程状态拿到监听器命令发送口;如果没有监听器或通道已关,就记错误日志;如果发送成功,就等待监听器回一个完成信号 → 出来没有业务结果,但监听器会按顺序处理 ResolveServerRequest 命令。

调用关系:它会被各种客户端响应处理流程调用,比如审批回复、文件变更审批回复、MCP 服务器询问回复、权限回复、用户输入回复。它把实际顺序控制交给 ThreadListenerCommand::ResolveServerRequest 和线程监听器。

调用图:被 5 处调用(on_command_execution_request_approval_response, on_file_change_request_approval_response, on_mcp_server_elicitation_response, on_request_permissions_response, on_request_user_input_response);外部调用 2 个(error!, channel)。

tests::note_thread_settings_reports_only_effective_changes206–219 ↗
fn note_thread_settings_reports_only_effective_changes()

作用:这是一个测试,确认 note_thread_settings 只在设置真的变化时报告变化。它防止以后改代码时把“重复设置也算变化”的错误带回来。

数据流:进去的是测试里新建的空 ThreadState 和两份模拟设置 → 它按“第一次保存、重复保存、换模型、重复保存”的顺序调用 note_thread_settings → 出来用断言确认结果是 true、false、true、false。

调用关系:它只在测试运行时活跃。它调用 tests::thread_settings 造测试数据,并检查 ThreadState::note_thread_settings 的行为。

调用图:外部调用 4 个(default, thread_settings, assert_eq!, vec!)。

tests::thread_settings221–245 ↗
fn thread_settings(model: &str) -> ThreadSettings

作用:这是测试用的小工厂函数,用一个模型名快速拼出完整的 ThreadSettings。它让测试不用手写一大段重复配置。

数据流:进去的是模型名字符串 → 它填入临时目录、审批策略、沙盒策略、协作模式等字段,并把模型名放到相关位置 → 出来是一份可用于测试的 ThreadSettings。

调用关系:tests::note_thread_settings_reports_only_effective_changes 调用它来构造初始设置和更新后的设置。它会用绝对路径工具把 /tmp 变成项目需要的绝对路径类型。

调用图:调用 1 个内部函数(from_absolute_path)。

ThreadEntry::default255–261 ↗
fn default() -> Self

作用:创建一个空的线程登记项。也就是:有状态容器,但还没有任何连接订阅它,并且“是否有连接”的观察值一开始是 false。

数据流:进去没有参数 → 它新建一个带异步互斥锁的 ThreadState、一个空连接集合,以及一个 watch 通道;watch 通道是一种能让别人订阅最新值变化的通知工具 → 出来是默认 ThreadEntry。

调用关系:ThreadStateManager 在第一次看到某个线程时会通过 or_default 间接用到它。它为后续订阅、取消订阅和状态观察准备好基础零件。

调用图:外部调用 5 个(new, new, new, default, channel)。

ThreadEntry::update_has_connections265–271 ↗
fn update_has_connections(&self)

作用:更新“这个线程现在有没有连接在看”的通知值。连接加入或离开线程后,需要它告诉观察者状态是否变了。

数据流:进去的是当前 ThreadEntry → 它看 connection_ids 集合是不是空,把结果写进 watch 发送端;只有值真的变化时才通知订阅者 → 没有返回值,但观察这个线程的人可能会收到变化通知。

调用关系:订阅、退订和连接断开等流程都会在改完 connection_ids 后调用它。它不决定谁该加入或离开,只负责把“是否还有人”这个结果广播出去。

调用图:外部调用 1 个(send_if_modified)。

ThreadStateManager::new296–298 ↗
fn new() -> Self

作用:创建一个新的线程状态管理器。服务器启动或测试开始时需要它来统一登记连接和线程关系。

数据流:进去没有参数 → 它使用默认值创建内部状态表和监听器命令表 → 出来是一个可克隆的 ThreadStateManager。

调用关系:应用代码和多项测试会调用它作为入口,比如测试连接订阅、监听器清理、目标更新顺序等。它本身只是建空账本,后续由其他方法往里面登记数据。

调用图:被 7 处调用(app_server_event_sink_uses_listener_fifo_for_goal_updates_and_clears, new, adding_connection_to_thread_updates_has_connections_watcher, closed_connection_cannot_be_reintroduced_by_auto_subscribe, first_attestation_capable_connection_for_thread_only_uses_thread_subscribers, removing_auto_attached_connection_preserves_listener_for_other_connections, removing_thread_state_clears_listener_and_active_turn_history);外部调用 1 个(default)。

ThreadStateManager::connection_initialized300–310 ↗
async fn connection_initialized(
        &self,
        connection_id: ConnectionId,
        capabilities: ConnectionCapabilities,
    )

作用:记录一个连接已经正式可用了,并保存它具备哪些能力。没登记的连接不能随便订阅线程,避免断开的或伪造的连接混进来。

数据流:进去的是连接编号和连接能力 → 它锁住管理器内部表,把这条连接写入 live_connections → 没有返回值,但之后订阅线程时会把它当作有效连接。

调用关系:连接初始化流程会调用它。ThreadStateManager::try_ensure_connection_subscribed 和 try_add_connection_to_thread 后面会检查这张 live_connections 表。

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

ThreadStateManager::first_attestation_capable_connection_for_thread312–330 ↗
async fn first_attestation_capable_connection_for_thread(
        &self,
        thread_id: ThreadId,
    ) -> Option<ConnectionId>

作用:找出订阅某个线程的连接里,第一个支持“请求证明”的连接。请求证明可以理解为客户端能帮服务器拿到某种可信凭据或确认信息。

数据流:进去的是线程编号 → 它先找到线程订阅者,再从这些连接中筛出 request_attestation 为 true 的连接,最后按连接编号取最小的一个 → 出来是可选的连接编号;找不到就返回空。

调用关系:request_attestation_header_value_with_timeout 会调用它,决定该向哪个连接请求证明信息。它只负责挑人,不发送请求。

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

ThreadStateManager::subscribed_connection_ids332–339 ↗
async fn subscribed_connection_ids(&self, thread_id: ThreadId) -> Vec<ConnectionId>

作用:列出当前订阅某个线程的所有连接。需要广播消息或恢复运行中线程时,会用它知道该通知谁。

数据流:进去的是线程编号 → 它在内部表里找到这个线程的连接集合并复制出来;如果线程不存在就给空列表 → 出来是连接编号列表,不改状态。

调用关系:resolve_pending_server_request 和 resume_running_thread 会调用它。它提供“收件人名单”,真正发送消息由上层流程完成。

调用图:被 2 处调用(resolve_pending_server_request, resume_running_thread)。

ThreadStateManager::thread_state341–344 ↗
async fn thread_state(&self, thread_id: ThreadId) -> Arc<Mutex<ThreadState>>

作用:取得某个线程自己的状态对象;如果还没有,就顺手创建一个。它是很多线程操作拿到“这本线程小账本”的入口。

数据流:进去的是线程编号 → 它锁住总表,找到或创建 ThreadEntry,然后复制出里面共享的 ThreadState 指针 → 出来是 Arc<Mutex<ThreadState>>,也就是可共享、需上锁访问的线程状态。

调用关系:目标快照、目标设置/清除、恢复运行中线程、回滚、列出轮次、挂监听器、中断轮次等流程都会调用它。它不直接改线程内容,只把对应状态对象交出去。

调用图:被 8 处调用(emit_thread_goal_snapshot, thread_goal_clear_inner, thread_goal_set_inner, resume_running_thread, thread_rollback_start, thread_turns_list_response_inner, try_attach_thread_listener, turn_interrupt_inner)。

ThreadStateManager::current_listener_command_tx346–355 ↗
fn current_listener_command_tx(
        &self,
        thread_id: ThreadId,
    ) -> Option<mpsc::UnboundedSender<ThreadListenerCommand>>

作用:用不需要 await 的方式取出某个线程当前监听器的命令发送口。await 是异步等待;这里避免等待,是因为有些同步回调不能停下来等锁。

数据流:进去的是线程编号 → 它用标准互斥锁读取 listener_commands 表,复制出对应发送端 → 出来是可选的命令发送端;没有登记监听器就返回空。

调用关系:emit 会调用它,把扩展事件排到当前线程监听器队列里。这个方法使用单独的同步锁,是为了给同步事件出口一个快速投递通道。

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

ThreadStateManager::register_listener_command_tx357–366 ↗
fn register_listener_command_tx(
        &self,
        thread_id: ThreadId,
        tx: mpsc::UnboundedSender<ThreadListenerCommand>,
    )

作用:把某个线程的监听器命令发送口登记到管理器里。这样其他地方以后不用拿线程状态锁,也能找到当前监听器。

数据流:进去的是线程编号和命令发送端 → 它锁住 listener_commands 表,把发送端按线程编号存进去 → 没有返回值,但之后 current_listener_command_tx 可以查到它。

调用关系:线程监听器启动后会调用它完成登记。它和 unregister_listener_command_tx 成对使用,防止旧监听器通道一直留在全局表里。

ThreadStateManager::unregister_listener_command_tx368–373 ↗
fn unregister_listener_command_tx(&self, thread_id: ThreadId)

作用:移除某个线程登记过的监听器命令发送口。监听器结束或线程被清理时必须移除,免得后续命令发到旧通道。

数据流:进去的是线程编号 → 它锁住 listener_commands 表并删除对应项 → 没有返回值,只改变管理器里的监听器通道登记。

调用关系:ThreadStateManager::remove_thread_state 和 ThreadStateManager::clear_all_listeners 会调用它。它是监听器生命周期收尾的一部分。

调用图:被 2 处调用(clear_all_listeners, remove_thread_state)。

ThreadStateManager::remove_thread_state375–401 ↗
async fn remove_thread_state(&self, thread_id: ThreadId)

作用:彻底移除一个线程的状态,并清理所有连接到这个线程的反向记录。线程卸载或最终销毁时需要它防止状态残留。

数据流:进去的是线程编号 → 它从 threads 表删除 ThreadEntry,并从每个连接的线程集合里移除这个线程;然后取消登记监听器命令通道;如果线程状态还在,就上锁记录调试信息并清掉监听器 → 没有返回值,但管理器和线程状态都被清理。

调用关系:unload_thread_without_subscribers 和 finalize_thread_teardown 会调用它。它内部调用 unregister_listener_command_tx 和 ThreadState::clear_listener,确保全局登记和单线程监听器都收干净。

调用图:调用 1 个内部函数(unregister_listener_command_tx);被 2 处调用(unload_thread_without_subscribers, finalize_thread_teardown);外部调用 1 个(debug!)。

ThreadStateManager::clear_all_listeners403–425 ↗
async fn clear_all_listeners(&self)

作用:清掉所有线程的监听器,但不一定删除线程本身。服务器关闭时用它,让后台监听任务都收到退出信号。

数据流:进去的是管理器自身 → 它先复制出所有线程编号和状态指针,避免长时间占着总表锁;然后逐个取消监听器命令登记,锁住每个线程状态,记录调试信息并清掉监听器 → 没有返回值,所有线程的监听器都被要求停止。

调用关系:clear_all_thread_listeners 会调用它。它和 remove_thread_state 相似,但目标是“全体监听器停下”,不是删除某个线程记录。

调用图:调用 1 个内部函数(unregister_listener_command_tx);被 1 处调用(clear_all_thread_listeners);外部调用 1 个(debug!)。

ThreadStateManager::unsubscribe_connection_from_thread427–459 ↗
async fn unsubscribe_connection_from_thread(
        &self,
        thread_id: ThreadId,
        connection_id: ConnectionId,
    ) -> bool

作用:让某个连接取消订阅某个线程。它同时更新两边的账本,避免出现线程说有这个连接、连接却说没有这个线程的矛盾。

数据流:进去的是线程编号和连接编号 → 它检查线程是否存在、连接是否真的订阅了这个线程;通过后从连接到线程的表里移除线程,也从线程到连接的集合里移除连接,并更新“是否还有连接”的通知 → 出来 true 表示确实退订成功,false 表示本来就不成立。

调用关系:thread_unsubscribe_response_inner 会调用它处理客户端退订请求。它改完连接集合后会调用 ThreadEntry::update_has_connections,让观察者知道线程是否变成无人订阅。

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

ThreadStateManager::has_subscribers462–469 ↗
async fn has_subscribers(&self, thread_id: ThreadId) -> bool

作用:这是测试用方法,检查某个线程现在是否还有订阅者。它帮助测试确认订阅和退订逻辑有没有把账本改对。

数据流:进去的是线程编号 → 它查看 threads 表里对应 ThreadEntry 的连接集合是否非空 → 出来一个布尔值;不改状态。

调用关系:它只在测试编译时存在。生产代码不依赖它,测试用它验证其他管理器方法的效果。

ThreadStateManager::try_ensure_connection_subscribed471–499 ↗
async fn try_ensure_connection_subscribed(
        &self,
        thread_id: ThreadId,
        connection_id: ConnectionId,
        experimental_raw_events: bool,
    ) -> Option<Arc<Mutex<ThreadState

作用:确保一个已存活连接订阅某个线程,并返回这个线程的状态。名字里的 try 表示它可能失败,比如连接根本没初始化或已经断开。

数据流:进去的是线程编号、连接编号,以及是否启用实验性原始事件 → 它先确认连接在 live_connections 里;成功后同时更新连接到线程、线程到连接两张表,通知“是否有连接”变化,并取出线程状态;如果需要,再打开该线程的原始事件开关 → 出来是可选的线程状态,连接无效时返回空。

调用关系:自动订阅或恢复类流程会用它把连接挂到线程上。它会调用 ThreadEntry::update_has_connections,并可能调用 ThreadState::set_experimental_raw_events。

ThreadStateManager::try_add_connection_to_thread501–519 ↗
async fn try_add_connection_to_thread(
        &self,
        thread_id: ThreadId,
        connection_id: ConnectionId,
    ) -> bool

作用:把一个已存活连接加入某个线程的订阅列表,但不返回线程状态。它适合只需要补登记连接关系的场景。

数据流:进去的是线程编号和连接编号 → 它检查连接是否仍然存活;如果存活,就更新两张关系表,把连接加入线程集合并通知连接状态变化 → 出来 true 表示加入成功,false 表示连接无效。

调用关系:handle_pending_thread_resume_request 会调用它,把正在恢复线程的连接补进订阅者名单。它和 try_ensure_connection_subscribed 很像,但更轻,只返回成功与否。

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

ThreadStateManager::remove_connection521–545 ↗
async fn remove_connection(&self, connection_id: ConnectionId) -> Vec<ThreadId>

作用:处理连接关闭:把这个连接从所有线程里拿掉,并告诉调用者哪些线程因此变成无人订阅。这样上层可以决定是否卸载这些线程。

数据流:进去的是连接编号 → 它从 live_connections 删除连接,取出这个连接订阅过的所有线程;再逐个从线程的连接集合里移除它并更新通知;最后筛出那些连接集合已经为空的线程 → 出来是无人订阅的线程编号列表。

调用关系:connection_closed 会调用它。它不直接卸载线程,只把“哪些线程没人了”交给连接关闭流程继续处理。

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

ThreadStateManager::subscribe_to_has_connections547–556 ↗
async fn subscribe_to_has_connections(
        &self,
        thread_id: ThreadId,
    ) -> Option<watch::Receiver<bool>>

作用:订阅某个线程“有没有连接”的变化通知。外部可以用它等待线程从有人看变成没人看,或反过来。

数据流:进去的是线程编号 → 它找到对应 ThreadEntry,并从 has_connections_watcher 复制一个接收端;如果线程不存在就返回空 → 出来是可选的 watch 接收器,不改变状态。

调用关系:需要观察线程订阅人数变化的流程会调用它。实际变化由 ThreadEntry::update_has_connections 推送,这个函数只负责发放观察入口。

app-server/src/request_processors/thread_lifecycle.rs源码 ↗
orchestrationrequest handling / main loop / teardown

这里的“线程”可以理解成一段正在运行或可恢复的对话。这个文件像会话的值班员:新连接进来时,先确认会话存在、连接还活着、会话没有正在关闭,然后启动一个后台监听任务。监听任务一边等会话产生活动事件,一边等内部命令,比如补发恢复响应、发送目标更新、标记服务器请求已解决;同时还盯着“还有没有客户端订阅”和“会话是不是还在忙”。如果一个会话既没人订阅又不活跃,持续 30 分钟后,就取消未完成请求、移除状态、通知外部线程关机,最后广播“线程已关闭”。代码里用了互斥锁(一把锁,防止两个任务同时改同一份数据)和 watch 通道(一种只关心最新状态的通知管道)来避免“刚有人连上却被卸载”这类竞态问题。

函数细节18
UnloadingState::new27–52 ↗
async fn new(
        listener_task_context: &ListenerTaskContext,
        thread_id: ThreadId,
        delay: Duration,
    ) -> Option<Self>

作用:创建一个“是否该卸载线程”的观察器。它会同时订阅两个信号:这个线程还有没有客户端在看,以及线程当前是不是活跃。

数据流:输入监听任务上下文、线程 ID 和等待时长 → 去线程状态管理器订阅连接状态,去线程观察管理器订阅运行状态 → 记录当前是否有订阅者、是否活跃,以及这些状态从什么时候开始;如果线程已经不能订阅,就返回空值。

调用关系:ensure_listener_task_running 在启动后台监听任务前会先调用它。监听任务后面就靠这个对象判断线程能不能在无人使用时被卸载。

调用图:被 1 处调用(ensure_listener_task_running);外部调用 2 个(now, matches!)。

UnloadingState::unloading_target54–61 ↗
fn unloading_target(&self) -> Option<Instant>

作用:算出最早什么时候可以卸载线程。只有“没人订阅”和“线程不活跃”两个条件都成立时,才会给出一个时间点。

数据流:输入对象里保存的最近状态和延迟时长 → 如果线程从某时起没人看、也从某时起不活跃,就取这两个时间里更晚的那个,再加上卸载等待时间 → 输出卸载目标时间;如果任一条件不满足,就输出空值。

调用关系:should_unload_now 和 wait_for_unloading_trigger 都会用它。前者用来立刻判断,后者用来决定该睡到什么时候再醒来检查。

调用图:被 2 处调用(should_unload_now, wait_for_unloading_trigger);外部调用 1 个(max)。

UnloadingState::sync_receiver_values63–73 ↗
fn sync_receiver_values(&mut self)

作用:把观察器里的状态刷新到最新。它不是盲目重置时间,而是只有状态真的变了,才更新“从什么时候开始”的时间戳。

数据流:输入内部两个 watch 接收器当前值 → 读取最新的“是否有订阅者”和“是否活跃” → 如果和旧值不同,就把新值和当前时间一起存下来;没有返回值,只更新对象内部记录。

调用关系:should_unload_now 和 wait_for_unloading_trigger 在做判断前都会先调用它,确保不是拿旧状态决定是否卸载。

调用图:被 2 处调用(should_unload_now, wait_for_unloading_trigger);外部调用 3 个(now, borrow, matches!)。

UnloadingState::should_unload_now75–79 ↗
fn should_unload_now(&mut self) -> bool

作用:判断线程现在是不是已经满足卸载条件。它是一个快速检查口,避免在真正关线程前用过期信息做决定。

数据流:输入当前观察器 → 先刷新连接和活跃状态 → 重新计算卸载目标时间 → 如果目标时间存在并且已经到了或过了,就返回 true,否则返回 false。

调用关系:监听任务在收到卸载触发后还会再次调用它,作为关机前的二次确认,防止刚好有客户端连回来却仍被关闭。

调用图:调用 2 个内部函数(sync_receiver_values, unloading_target)。

UnloadingState::note_thread_activity_observed81–85 ↗
fn note_thread_activity_observed(&mut self)

作用:告诉卸载观察器:刚刚发现线程其实还有活动,所以不能按旧的“不活跃已很久”来卸载。

数据流:输入当前观察器 → 如果它记录的是“不活跃”,就把“不活跃开始时间”改成现在 → 没有返回值,只推迟后续卸载判断。

调用关系:监听任务发现底层会话状态仍是运行中时会调用它。这样线程不会因为状态通知稍有滞后而被误卸载。

调用图:外部调用 1 个(now)。

UnloadingState::wait_for_unloading_trigger87–119 ↗
async fn wait_for_unloading_trigger(&mut self) -> bool

作用:等待“可能该卸载了”的时刻。它会同时等三件事:卸载时间到、连接状态变化、线程活跃状态变化。

数据流:输入当前观察器 → 循环刷新状态并计算目标时间;如果时间已到就返回 true;如果还没到就睡到目标时间;如果订阅通道或状态通道变化,就醒来重算;如果这些通道关闭,说明线程状态不可再可靠观察,返回 false。

调用关系:ensure_listener_task_running 生成的后台监听任务会在 select 等待中调用它。它像一个闹钟,提醒监听任务可以开始考虑卸载。

调用图:调用 2 个内部函数(sync_receiver_values, unloading_target);外部调用 3 个(now, select!, sleep_until)。

ensure_conversation_listener137–186 ↗
async fn ensure_conversation_listener(
    listener_task_context: ListenerTaskContext,
    conversation_id: ThreadId,
    connection_id: ConnectionId,
    raw_events_enabled: bool,
) -> Result<EnsureC

作用:确保某个连接已经挂到某个会话上,并且这个会话的后台监听任务正在运行。客户端打开或恢复线程时会需要它。

数据流:输入监听上下文、会话 ID、连接 ID、是否接收原始事件 → 先找会话;再检查会话是否正在卸载;然后把连接加入订阅名单;最后启动或复用监听任务 → 返回“已挂上”或“连接已关闭”;如果会话不存在或正在关闭,就返回请求错误。

调用关系:线程启动、恢复、自动附加监听等流程会走到这里。它把真正启动后台任务的工作交给 ensure_listener_task_running;如果启动失败,它会撤销刚才的订阅,避免留下半挂状态。

调用图:调用 1 个内部函数(ensure_listener_task_running);被 3 处调用(ensure_conversation_listener, thread_start_task, ensure_conversation_listener);外部调用 2 个(clone, format!)。

log_listener_attach_result188–210 ↗
fn log_listener_attach_result(
    result: Result<EnsureConversationListenerResult, JSONRPCErrorError>,
    thread_id: ThreadId,
    connection_id: ConnectionId,
    thread_kind: &'static str,
)

作用:把“挂监听器”的结果写进日志。成功时不打扰;连接已关或失败时,留下方便排查的记录。

数据流:输入挂载结果、线程 ID、连接 ID 和线程类型说明 → 根据结果选择不输出、输出调试日志,或输出警告日志 → 不改变业务状态,只留下日志。

调用关系:它通常被调用方用在 ensure_conversation_listener 之后。它不参与修复问题,只负责让运维或开发者知道自动挂监听器有没有出岔子。

调用图:外部调用 2 个(debug!, warn!)。

ensure_listener_task_running212–396 ↗
async fn ensure_listener_task_running(
    listener_task_context: ListenerTaskContext,
    conversation_id: ThreadId,
    conversation: Arc<CodexThread>,
    thread_state: Arc<Mutex<ThreadState>>,
) -

作用:启动每个会话背后的“监听值班任务”,如果已经有合适的任务在跑,就不重复启动。这个任务负责把会话事件转给客户端,也负责空闲卸载。

数据流:输入监听上下文、会话对象和该会话的共享状态 → 建立取消信号,创建卸载观察器,登记技能配置监听,记录线程设置基线;在加锁状态下检查现有监听器是否匹配,不匹配就替换并登记命令通道;随后启动后台异步任务 → 返回成功或请求错误。

调用关系:ensure_conversation_listener 会调用它。它启动的后台任务同时处理三类来源:取消信号、listener_command_rx 收到的内部命令、conversation.next_event 产生的会话事件,以及 UnloadingState 发出的卸载触发;具体命令再交给 handle_thread_listener_command,真正卸载交给 unload_thread_without_subscribers。

调用图:调用 1 个内部函数(new);被 2 处调用(ensure_conversation_listener, ensure_listener_task_running);外部调用 6 个(clone, format!, channel, select!, spawn, warn!)。

wait_for_thread_shutdown398–404 ↗
async fn wait_for_thread_shutdown(thread: &Arc<CodexThread>) -> ThreadShutdownResult

作用:等待线程自己优雅关闭,但最多等 10 秒。这样服务器不会因为一个关不掉的线程一直卡住。

数据流:输入线程对象 → 发起 shutdown_and_wait 并包上一层 10 秒超时 → 根据结果输出 Complete、SubmitFailed 或 TimedOut,分别表示关好了、关机请求提交失败、等待超时。

调用关系:unload_thread_without_subscribers 在后台清理线程时会调用它。它把复杂的关闭结果压成三个简单状态,方便后续决定是否移除线程和发通知。

调用图:被 1 处调用(unload_thread_without_subscribers);外部调用 2 个(from_secs, timeout)。

unload_thread_without_subscribers406–456 ↗
async fn unload_thread_without_subscribers(
    thread_manager: Arc<ThreadManager>,
    outgoing: Arc<OutgoingMessageSender>,
    pending_thread_unloads: Arc<Mutex<HashSet<ThreadId>>>,
    thread_stat

作用:真正执行“没人看又空闲”的线程卸载。它会先停止这个线程相关的未完成请求,再异步关闭线程,成功后通知客户端线程已关闭。

数据流:输入线程管理器、消息发送器、正在卸载集合、状态管理器、观察管理器、线程 ID 和线程对象 → 取消这个线程还没完成的服务端请求,移除线程状态;然后开一个后台任务等待线程关闭 → 成功时从线程管理器和观察器中移除并发送 ThreadClosed 通知;失败或超时时移出“正在卸载”标记并记录警告。

调用关系:ensure_listener_task_running 的后台监听任务在确认线程可卸载后调用它。它内部把等待关闭的细节交给 wait_for_thread_shutdown,并在最后维护 pending_thread_unloads,避免同一个线程被重复卸载。

调用图:调用 3 个内部函数(wait_for_thread_shutdown, remove_thread_state, remove_thread);外部调用 5 个(ThreadClosed, info!, to_string, spawn, warn!)。

handle_thread_listener_command459–522 ↗
async fn handle_thread_listener_command(
    conversation_id: ThreadId,
    conversation: &Arc<CodexThread>,
    codex_home: &Path,
    thread_state_manager: &ThreadStateManager,
    thread_state: &Ar

作用:处理发给监听任务的内部命令。可以把它看成监听任务的“指令分发台”。

数据流:输入会话信息、各种状态管理器、消息发送器、正在卸载集合和一条监听命令 → 根据命令类型分别执行:补发线程恢复响应、发送目标更新、发送目标清除、发送目标快照,或通知某个服务端请求已解决 → 输出主要是发给客户端的响应或通知,必要时也更新订阅关系。

调用关系:ensure_listener_task_running 创建的后台任务从命令通道收到消息后调用它。更重的恢复响应工作交给 handle_pending_thread_resume_request;目标快照交给 send_thread_goal_snapshot_notification;请求解决通知交给 resolve_pending_server_request。

调用图:调用 3 个内部函数(handle_pending_thread_resume_request, resolve_pending_server_request, send_thread_goal_snapshot_notification);外部调用 3 个(ThreadGoalCleared, ThreadGoalUpdated, to_string)。

handle_pending_thread_resume_request529–704 ↗
async fn handle_pending_thread_resume_request(
    conversation_id: ThreadId,
    conversation: &Arc<CodexThread>,
    _codex_home: &Path,
    thread_state_manager: &ThreadStateManager,
    thread_sta

作用:给“重新连上一个仍在运行的线程”的客户端拼出恢复响应。它要让客户端看到当前线程摘要、历史轮次、配置、权限、目标状态,以及必要的补发通知。

数据流:输入会话、线程状态、观察器、输出通道、正在卸载集合和一份待处理恢复请求 → 读取当前活跃轮次,按需要把历史记录整理成 turns;取得线程当前加载状态,并把过期的进行中轮次标成已中断;可选生成第一页历史;按需要隐藏敏感 payload;确认线程没有正在卸载且连接还活着后,把连接加入线程订阅;最后组装 ThreadResumeResponse 发回客户端,并可能补发 token 用量、目标快照和未完成服务端请求。

调用关系:handle_thread_listener_command 遇到 SendThreadResumeResponse 命令时调用它。它会使用 populate_thread_turns_from_history、set_thread_status_and_interrupt_stale_turns、send_thread_goal_snapshot_notification 等小工具,最后通过 outgoing 把恢复响应和补充通知发给指定连接。

调用图:调用 6 个内部函数(populate_thread_turns_from_history, send_thread_goal_snapshot_notification, set_thread_status_and_interrupt_stale_turns, build_thread_resume_initial_turns_page, try_add_connection_to_thread, loaded_status_for_thread);被 1 处调用(handle_thread_listener_command);外部调用 4 个(format!, matches!, debug!, warn!)。

send_thread_goal_snapshot_notification706–739 ↗
async fn send_thread_goal_snapshot_notification(
    outgoing: &Arc<OutgoingMessageSender>,
    thread_id: ThreadId,
    state_db: &StateDbHandle,
)

作用:把线程当前的“目标”状态发给客户端。目标存在就发更新;目标不存在就发清除。

数据流:输入消息发送器、线程 ID 和状态数据库句柄 → 从数据库读取该线程目标 → 如果读到目标,转成接口格式并发送 ThreadGoalUpdated;如果没有目标,发送 ThreadGoalCleared;如果读取失败,只记警告日志。

调用关系:handle_thread_listener_command 可直接调用它来响应目标快照命令;handle_pending_thread_resume_request 也会在恢复线程时调用它,让新连接一上来就知道当前目标。

调用图:被 2 处调用(handle_pending_thread_resume_request, handle_thread_listener_command);外部调用 5 个(ThreadGoalCleared, ThreadGoalUpdated, thread_goals, to_string, warn!)。

populate_thread_turns_from_history741–751 ↗
fn populate_thread_turns_from_history(
    thread: &mut Thread,
    items: &[RolloutItem],
    active_turn: Option<&Turn>,
)

作用:把底层历史记录整理成客户端能看的“轮次”列表,并把正在进行的轮次合进去。

数据流:输入可修改的线程摘要、历史 rollout 项,以及可选的活跃轮次 → 先把历史项转换成 API turn 列表;如果有活跃轮次,就用它覆盖同 ID 的旧轮次并放到列表里 → 最后把整理好的 turns 写回线程摘要。

调用关系:handle_pending_thread_resume_request 在客户端要求带历史轮次时调用它。它把合并活跃轮次这一步交给 merge_turn_history_with_active_turn。

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

resolve_pending_server_request753–776 ↗
async fn resolve_pending_server_request(
    conversation_id: ThreadId,
    thread_state_manager: &ThreadStateManager,
    outgoing: &Arc<OutgoingMessageSender>,
    request_id: RequestId,
)

作用:通知所有订阅这个线程的连接:某个服务端请求已经解决。这样界面上等待中的请求可以收尾。

数据流:输入线程 ID、线程状态管理器、消息发送器和请求 ID → 查询当前订阅这个线程的连接列表 → 创建一个只发给这些连接的线程范围发送器 → 广播 ServerRequestResolved 通知。

调用关系:handle_thread_listener_command 遇到 ResolveServerRequest 命令时调用它。它把通知范围限制在订阅该线程的连接里,避免无关客户端收到消息。

调用图:调用 2 个内部函数(new, subscribed_connection_ids);被 1 处调用(handle_thread_listener_command);外部调用 2 个(ServerRequestResolved, to_string)。

merge_turn_history_with_active_turn778–781 ↗
fn merge_turn_history_with_active_turn(turns: &mut Vec<Turn>, active_turn: Turn)

作用:把正在进行的轮次合并进历史轮次列表,避免同一个轮次出现两份。

数据流:输入一个可修改的 turn 列表和一个活跃 turn → 先删掉列表里 ID 相同的旧项 → 再把最新的活跃 turn 放进去;不返回新列表,而是直接修改原列表。

调用关系:populate_thread_turns_from_history 在整理恢复响应的历史轮次时调用它。它保证客户端看到的是最新的活跃轮次,而不是历史里的旧快照。

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

set_thread_status_and_interrupt_stale_turns783–797 ↗
fn set_thread_status_and_interrupt_stale_turns(
    thread: &mut Thread,
    loaded_status: ThreadStatus,
    has_live_in_progress_turn: bool,
)

作用:统一设置线程对外展示的状态,并把不该再显示为“进行中”的旧轮次标成“已中断”。

数据流:输入可修改的线程摘要、当前加载状态、是否确实还有实时进行中的轮次 → 先算出线程最终状态;如果最终状态不是活跃,就遍历 turns,把仍标为 InProgress 的轮次改成 Interrupted → 最后把最终状态写回线程摘要。

调用关系:handle_pending_thread_resume_request 在拼恢复响应时调用它。它让客户端恢复页面不会误以为一个已经不活跃的线程还有任务正在跑。

调用图:被 1 处调用(handle_pending_thread_resume_request);外部调用 1 个(matches!)。

app-server/src/mcp_refresh.rs源码 ↗
orchestration配置变更、插件变更或手动请求刷新时活跃

可以把这里想成一个“配置广播员”。系统里可能同时开着多个 Codex 线程,每个线程都有自己的工作目录和配置。MCP(Model Context Protocol,一种让模型连接外部工具/服务的协议)服务器配置变了以后,不能只改磁盘上的配置文件,还要通知这些已经运行的线程。这个文件提供两种刷新方式:严格刷新会先把所有线程需要的新配置都准备好,任何一步失败就整体报错;尽力刷新则遇到某个线程失败就记一条警告,然后继续刷新其他线程。它还会专门按线程重新加载最新配置,而不是直接沿用线程启动时的旧配置,所以像认证密钥后端这种全局设置改了,也能被新刷新消息带进去。最后,刷新不是直接改线程内部状态,而是向线程投递一个 RefreshMcpServers 操作,让线程自己在合适的时机处理。

函数细节9
queue_strict_refresh11–31 ↗
async fn queue_strict_refresh(
    thread_manager: &Arc<ThreadManager>,
    config_manager: &ConfigManager,
) -> io::Result<()>

作用:严格地给所有正在运行的线程安排一次 MCP 配置刷新。只要有一个线程加载失败、配置生成失败或刷新消息投递失败,它就直接返回错误,适合需要明确知道刷新是否完全成功的场景。

数据流:进去的是线程管理器和配置管理器。它先让配置管理器读取最新的全局配置,然后逐个取出线程,为每个线程生成新的 MCP 刷新配置;这些都成功后,再逐个把刷新操作塞进线程队列。出来的是成功的空结果,或者一个说明哪里失败的输入输出错误;它不直接改线程配置,只是排队通知线程刷新。

调用关系:它会调用 build_refresh_config 准备每个线程的新配置,再调用 queue_refresh 把刷新命令发给线程。测试 strict_refresh_reports_thread_planning_failures 会验证它遇到配置规划失败时会报错;外部的 mcp_server_refresh_response 也会在需要一次严格刷新时使用它。

调用图:调用 3 个内部函数(load_latest_config, build_refresh_config, queue_refresh);被 2 处调用(strict_refresh_reports_thread_planning_failures, mcp_server_refresh_response);外部调用 1 个(new)。

queue_best_effort_refresh33–56 ↗
async fn queue_best_effort_refresh(
    thread_manager: &Arc<ThreadManager>,
    config_manager: &ConfigManager,
)

作用:尽量给所有能处理的线程刷新 MCP 配置。它不会因为某一个线程失败就停下,而是记录警告后继续处理后面的线程,适合后台自动刷新。

数据流:进去的是线程管理器和配置管理器。它遍历所有线程编号,能加载线程就继续,不能加载就跳过;能生成刷新配置就继续,不能生成就跳过;投递刷新失败也只写警告。出来没有返回值;它的效果是尽可能多地把刷新操作送到可用线程里。

调用关系:它同样依赖 build_refresh_config 生成每个线程要用的新配置,并依赖 queue_refresh 发送刷新命令。测试 best_effort_refresh_attempts_every_loaded_thread 会确认它即使遇到坏线程,也会继续尝试好线程;后台任务 spawn_effective_plugins_changed_task 会在有效插件变化时调用它。

调用图:调用 2 个内部函数(build_refresh_config, queue_refresh);被 3 处调用(best_effort_refresh_attempts_every_loaded_thread, spawn_effective_plugins_changed_task, spawn_effective_plugins_changed_task);外部调用 1 个(warn!)。

build_refresh_config58–77 ↗
async fn build_refresh_config(
    thread: &CodexThread,
    config_manager: &ConfigManager,
) -> io::Result<McpServerRefreshConfig>

作用:为某一个线程制作一份“刷新 MCP 需要带上的配置包”。它会结合线程自己的配置和最新的全局配置,确保发给线程的是当前应该生效的内容。

数据流:进去的是一个 CodexThread 和配置管理器。它先读取线程当前配置,再按这个线程的上下文加载最新配置;接着让线程算出运行时 MCP 配置,把其中真正配置好的 MCP 服务器列表整理出来,并把服务器列表、OAuth 凭据存储模式、认证钥匙环后端类型转成 JSON 值。出来的是 McpServerRefreshConfig;如果读取配置或转 JSON 失败,就返回错误。

调用关系:queue_strict_refresh 和 queue_best_effort_refresh 都会在通知线程之前调用它。测试 refresh_config_uses_latest_auth_keyring_backend 会直接调用它,确认它拿到的是最新配置,而不是线程启动时的旧认证后端设置。

调用图:调用 3 个内部函数(load_latest_config_for_thread, config, runtime_mcp_config);被 3 处调用(queue_best_effort_refresh, queue_strict_refresh, refresh_config_uses_latest_auth_keyring_backend);外部调用 2 个(configured_mcp_servers, to_value)。

queue_refresh79–93 ↗
async fn queue_refresh(
    thread_id: ThreadId,
    thread: Arc<CodexThread>,
    config: McpServerRefreshConfig,
) -> io::Result<()>

作用:把一份已经做好的 MCP 刷新配置,作为一个刷新操作投递给指定线程。它是最后一步,负责把“该刷新了”这件事真正送进线程的任务队列。

数据流:进去的是线程编号、线程对象和刷新配置。它把配置包包进 Op::RefreshMcpServers 这个操作,然后提交给线程。出来是成功的空结果;如果线程拒绝或投递失败,它会把错误包装成更好读的输入输出错误,并带上线程编号。

调用关系:它被 queue_strict_refresh 和 queue_best_effort_refresh 调用。前面的函数负责找线程和准备配置,它只负责把命令送达线程,相当于广播员最后把信投进每个收件箱。

调用图:被 2 处调用(queue_best_effort_refresh, queue_strict_refresh)。

tests::strict_refresh_reports_thread_planning_failures126–135 ↗
async fn strict_refresh_reports_thread_planning_failures() -> anyhow::Result<()>

作用:这个测试确认严格刷新真的“严格”:只要某个线程的刷新配置加载失败,整个刷新就应该失败并把错误报出来。

数据流:它先用 refresh_test_state 搭一个包含好线程和坏线程的测试环境,然后调用 queue_strict_refresh。预期结果不是成功,而是得到错误;最后检查错误文字是不是“failed to load refresh config”。

调用关系:它主要验证 queue_strict_refresh 的失败路径。refresh_test_state 提供一个会故意加载失败的线程配置加载器,让这个场景稳定复现。

调用图:调用 1 个内部函数(queue_strict_refresh);外部调用 2 个(refresh_test_state, assert_eq!)。

tests::best_effort_refresh_attempts_every_loaded_thread138–146 ↗
async fn best_effort_refresh_attempts_every_loaded_thread() -> anyhow::Result<()>

作用:这个测试确认尽力刷新不会被一个坏线程拖住。即使某个线程配置加载失败,它也应该继续尝试其他线程。

数据流:它先建立测试环境,然后调用 queue_best_effort_refresh。调用结束后,它查看计数器:好线程加载过一次,坏线程也被尝试加载过一次。结果证明函数没有一遇到坏线程就提前退出。

调用关系:它验证 queue_best_effort_refresh 的容错行为。里面用到的 CountingThreadConfigLoader 会记录好目录和坏目录分别被加载了几次。

调用图:调用 1 个内部函数(queue_best_effort_refresh);外部调用 2 个(refresh_test_state, assert_eq!)。

tests::refresh_config_uses_latest_auth_keyring_backend149–178 ↗
async fn refresh_config_uses_latest_auth_keyring_backend() -> anyhow::Result<()>

作用:这个测试确认刷新配置会使用最新的认证密钥存储设置,而不是线程刚启动时的旧设置。这样用户改配置后,刷新消息才真的有意义。

数据流:它先建立测试环境,再把配置文件改成启用 secret_auth_storage。然后找到工作目录是 good 的线程,调用 build_refresh_config 生成刷新配置,并从 JSON 里读回认证钥匙环后端类型。最后比较:线程自己的旧配置还是 Direct,但刷新配置里已经是 Secrets。

调用关系:它直接测试 build_refresh_config。这个测试说明 build_refresh_config 会重新通过配置管理器加载最新配置,而不只是照抄 thread.config() 里的启动时配置。

调用图:调用 1 个内部函数(build_refresh_config);外部调用 3 个(refresh_test_state, assert_eq!, write)。

tests::refresh_test_state180–275 ↗
async fn refresh_test_state() -> anyhow::Result<(
        TempDir,
        Arc<ThreadManager>,
        ConfigManager,
        Arc<CountingThreadConfigLoader>,
    )>

作用:这个辅助函数给上面的测试搭一个完整的小型运行环境。它会造出临时配置目录、两个线程、线程管理器,以及一个会计数并故意让坏线程失败的配置加载器。

数据流:进去没有业务输入。它创建临时文件夹,分别做 good 和 bad 两个工作目录,写入初始配置;再加载两份线程配置,搭认证、状态数据库、线程存储、环境管理器和线程扩展,启动两个线程;最后创建 CountingThreadConfigLoader,并把它放进新的 ConfigManager。出来的是临时目录、线程管理器、配置管理器和计数加载器,供测试使用。

调用关系:它被三个测试函数调用,是测试的“布景师”。测试本身只关心刷新行为,复杂的线程和配置环境都由它提前准备好。

调用图:调用 9 个内部函数(new, without_managed_config_for_tests, default, without_managed_config_for_tests, default_for_tests, new_with_restriction_product, from_auth_for_testing, from_api_key, try_from);外部调用 12 个(clone, new, new_cyclic, new, new, new, default, init_state_db, thread_store_from_config, default (+2 more))。

tests::CountingThreadConfigLoader::load305–310 ↗
fn load(
            &self,
            context: ThreadConfigContext,
        ) -> codex_config::ThreadConfigLoaderFuture<'_, Vec<ThreadConfigSource>>

作用:这是测试用的假配置加载器。它不是为了加载真实配置,而是为了记录哪些线程被尝试加载,并让 bad 目录故意失败。

数据流:进去的是 ThreadConfigContext,里面包含线程的工作目录。它如果看到目录是 good,就把 good_loads 计数加一;如果看到目录是 bad,就把 bad_loads 计数加一并返回一个“failed to load refresh config”的错误;其他情况返回空配置来源列表。结果要么是空列表,要么是测试故意制造的加载失败。

调用关系:它被测试里的 ConfigManager 间接调用。queue_strict_refresh 和 queue_best_effort_refresh 在构建刷新配置时会触发配置加载,于是这个假加载器就能帮助测试观察:严格模式是否报错、尽力模式是否继续尝试其他线程。

调用图:调用 1 个内部函数(new);外部调用 4 个(fetch_add, pin, new, load)。

exec-server/src/server/session_registry.rs源码 ↗
orchestrationrequest handling / connection lifecycle

这个文件解决的是“连接会断,但工作不能乱”的问题。它把每个会话放进一张表里,每个会话里面有一个真正干活的 ProcessHandler,还有当前连接编号。新连接进来时,它要么新建会话,要么按 session id 找回旧会话;如果旧会话已经被别人连着,就拒绝,避免两个人同时操作一台机器。断线时,它不会立刻杀掉进程,而是先把会话标成“暂时没人连”,像饭店给离座客人保留座位十秒。到期还没人回来,后台任务才关闭进程并删掉会话。这里用互斥锁(一把锁,防止两个任务同时改同一份数据)保护状态,保证 attach、detach、过期清理不会互相踩踏。

函数细节17
ConnectionId::fmt39–41 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:把内部的连接编号变成可打印的文字。这样日志、返回值或调试信息里就能看到一个清楚的连接 id。

数据流:进去的是一个 ConnectionId,里面包着一个 UUID(通用唯一编号)→ 它把这个 UUID 交给标准格式化工具 → 出来的是写入 formatter 的文本形式,不改动连接本身。

调用关系:它是 ConnectionId 的显示接口。SessionHandle::connection_id 需要把连接编号转成字符串时,会间接依赖这个显示能力。

SessionRegistry::new52–56 ↗
fn new() -> Arc<Self>

作用:创建一份新的会话登记簿。服务器启动或测试准备环境时会用它,之后所有会话都记在这里。

数据流:进去不需要参数 → 它新建一张空的 HashMap(像一张按 session id 查找的表),再用异步互斥锁包住,最后放进 Arc(可共享引用)里 → 出来的是可以被多个任务共享的 SessionRegistry。

调用关系:它是整个会话系统的起点。测试和服务器初始化会调用它;后续连接进来时,再通过这个登记簿调用 SessionRegistry::attach。

调用图:被 6 处调用(active_session_resume_is_rejected, initialized_handler, long_poll_read_fails_after_session_resume, output_and_exit_are_retained_after_notification_receiver_closes, new, transport_disconnect_detaches_session_during_in_flight_read);外部调用 3 个(new, new, new)。

SessionRegistry::attach58–117 ↗
async fn attach(
        self: &Arc<Self>,
        resume_session_id: Option<String>,
        notifications: RpcNotificationSender,
    ) -> Result<SessionHandle, JSONRPCErrorError>

作用:处理“我要连上一个会话”这件事:没有旧会话就新建,有旧会话就尝试恢复。它还会防止一个会话同时被两个连接占用。

数据流:进去的是可选的 resume_session_id 和通知发送器 notifications → 它先生成一个新的连接编号;如果有 session id,就在登记簿里找旧会话,检查是否过期、是否已被别的连接占用,然后更新通知发送器并绑定新连接;如果没有 session id,就生成新会话 id、新建 ProcessHandler 和 SessionEntry,并放进登记簿 → 成功出来一个 SessionHandle;失败出来 JSON-RPC 错误,例如“未知会话”或“会话已被占用”。如果发现旧会话已经过期,还会先关闭它的进程。

调用关系:这是新连接进入会话系统时最关键的入口。它会调用 SessionEntry::new 创建新记录,调用 SessionEntry::attach 恢复旧记录,也会借助 invalid_request 生成给客户端看的错误。

调用图:调用 3 个内部函数(invalid_request, new, new);外部调用 6 个(clone, new, Attached, new_v4, format!, now)。

SessionRegistry::expire_if_detached119–136 ↗
async fn expire_if_detached(&self, session_id: String, connection_id: ConnectionId)

作用:在连接断开后等一小段时间,如果用户没有回来,就清理这个没人要的会话。它像“保留座位到点自动取消”。

数据流:进去的是 session_id 和当时断开的 connection_id → 它先睡到保留时间结束,再加锁查看登记簿;如果会话已不存在、已经被重新连接、或断开的不是同一个连接,就什么也不做;如果确实到期无人接回,就从表里移除会话 → 最后关闭对应的 ProcessHandler,释放后台进程。

调用关系:它通常由 SessionHandle::detach 通过 tokio::spawn 放到后台执行。它不会阻塞断线处理,而是在稍后独立检查并收尾。

调用图:外部调用 2 个(now, sleep)。

SessionRegistry::default140–144 ↗
fn default() -> Self

作用:提供默认的空会话登记簿。需要“默认值”的地方可以直接得到一份没有任何会话的 registry。

数据流:进去不需要参数 → 它创建一张空的会话表,并用异步互斥锁包起来 → 出来的是一个未共享包装的 SessionRegistry 实例。

调用关系:这是 Rust 的 Default 约定接口,方便测试或通用初始化代码使用。它和 SessionRegistry::new 做的核心事情类似,只是 new 会额外返回 Arc 共享包装。

调用图:外部调用 2 个(new, new)。

SessionEntry::new148–158 ↗
fn new(session_id: String, process: ProcessHandler, connection_id: ConnectionId) -> Self

作用:创建一条具体的会话记录。它把会话 id、真正跑任务的进程处理器,以及当前连接状态装在一起。

数据流:进去的是 session_id、ProcessHandler、connection_id → 它把当前连接设为这个 connection_id,并把“断开连接”和“过期时间”都清空 → 出来的是一个刚创建且已经连接上的 SessionEntry。

调用关系:它由 SessionRegistry::attach 在创建全新会话时调用。建好后,这条记录会被放进 SessionRegistry 的会话表里。

调用图:被 1 处调用(attach);外部调用 1 个(new)。

SessionEntry::attach160–168 ↗
fn attach(&self, connection_id: ConnectionId)

作用:把一个原本可恢复的会话重新绑定到新的连接上。恢复成功后,这个会话就不再算“断开等待中”。

数据流:进去的是新的 connection_id → 它锁住会话的连接状态,把 current_connection_id 改成这个新连接,并清掉 detached_connection_id 和 detached_expires_at → 出来没有返回值,但会话状态从“可恢复/断开”变成“已连接”。

调用关系:它由 SessionRegistry::attach 在恢复旧会话时使用。这样后台的过期清理再检查时,会看到会话已经重新连上,就不会误删。

调用图:外部调用 1 个(lock)。

SessionEntry::detach170–183 ↗
fn detach(&self, connection_id: ConnectionId) -> bool

作用:把当前连接从会话上摘下来,并开始一段短暂的可恢复倒计时。只有真正持有这个会话的连接才能摘。

数据流:进去的是想断开的 connection_id → 它锁住状态,先确认这个连接就是当前连接;如果不是,就返回 false,什么也不改;如果是,就清空 current_connection_id,记录 detached_connection_id,并设置过期时间 → 出来 true 或 false,表示有没有成功把会话改成断开等待状态。

调用关系:它由 SessionHandle::detach 调用。成功后,SessionHandle::detach 会关闭通知发送器,并启动 SessionRegistry::expire_if_detached 做延迟清理。

调用图:外部调用 2 个(lock, now)。

SessionEntry::has_active_connection185–191 ↗
fn has_active_connection(&self) -> bool

作用:检查这个会话现在是不是已经有人连着。它用来阻止第二个连接同时占用同一个会话。

数据流:进去不需要额外参数 → 它锁住连接状态,看 current_connection_id 是否存在 → 出来 true 表示有活跃连接,false 表示当前没人连。

调用关系:SessionRegistry::attach 在恢复旧会话前会用它检查。如果返回 true,attach 会拒绝请求,避免同一个会话被两个客户端同时控制。

调用图:外部调用 1 个(lock)。

SessionEntry::is_attached_to193–199 ↗
fn is_attached_to(&self, connection_id: ConnectionId) -> bool

作用:检查这个会话当前是否正连接在指定的 connection_id 上。它常用于确认“我手里的句柄还有效吗”。

数据流:进去的是一个 connection_id → 它锁住连接状态,把 current_connection_id 和传入的编号比较 → 出来 true 或 false,不改变任何状态。

调用关系:SessionHandle::is_session_attached 会调用它,对外提供一个简单判断:这个 SessionHandle 代表的连接是否仍然是当前连接。

调用图:外部调用 1 个(lock)。

SessionEntry::is_expired201–207 ↗
fn is_expired(&self, now: tokio::time::Instant) -> bool

作用:判断一个断开的会话是否已经过了可恢复期限。它帮助系统把“还能接回来”和“已经该清掉”分开。

数据流:进去的是当前时间 now → 它锁住状态,查看 detached_expires_at 是否存在,并判断 now 是否已经到达或超过这个期限 → 出来 true 表示过期,false 表示没过期或根本没有处在断开倒计时中。

调用关系:SessionRegistry::attach 在有人尝试恢复旧会话时会调用它。如果旧会话已经过期,attach 会移除并关闭旧进程,然后返回“未知会话”错误。

调用图:外部调用 1 个(lock)。

SessionEntry::is_detached_connection_expired209–223 ↗
fn is_detached_connection_expired(
        &self,
        connection_id: ConnectionId,
        now: tokio::time::Instant,
    ) -> bool

作用:确认某个断开的连接是否真的已经超时,而且期间没有被重新连接。它是后台清理前的最后一道保险。

数据流:进去的是 connection_id 和当前时间 now → 它锁住状态,同时检查三件事:当前没人连接、记录的断开连接就是传入这个、过期时间已经到了 → 三项都满足才返回 true;否则返回 false,不改状态。

调用关系:SessionRegistry::expire_if_detached 在睡醒后调用它。这样即使用户在倒计时期间恢复了会话,后台任务也不会把新连接的会话误删。

调用图:外部调用 1 个(lock)。

SessionHandle::session_id227–229 ↗
fn session_id(&self) -> &str

作用:取出这个会话的 session id。客户端通常需要这个 id,之后断线重连时才能说明“我要恢复哪一个会话”。

数据流:进去的是 SessionHandle 自己 → 它直接读取内部 SessionEntry 的 session_id → 出来的是 session id 的字符串引用,不复制也不修改状态。

调用关系:这是 SessionHandle 给外部代码使用的查询口。会话创建或恢复成功后,调用方可以用它把会话编号返回给客户端。

SessionHandle::connection_id231–233 ↗
fn connection_id(&self) -> String

作用:取出当前连接的编号,并转成字符串。这个编号可用于日志、调试,或让调用方区分不同连接。

数据流:进去的是 SessionHandle 自己 → 它读取保存的 ConnectionId,并调用 to_string 转成普通文本 → 出来的是一个新的 String,不改变会话状态。

调用关系:它依赖 ConnectionId 的显示格式能力。它属于 SessionHandle 的只读信息接口,不参与 attach 或 detach 的状态改变。

调用图:外部调用 1 个(to_string)。

SessionHandle::is_session_attached235–237 ↗
fn is_session_attached(&self) -> bool

作用:判断这个句柄代表的连接是否仍然连在会话上。调用方可以用它避免对已经断开的会话继续发操作。

数据流:进去的是 SessionHandle 自己 → 它把自己保存的 connection_id 拿去问 SessionEntry::is_attached_to → 出来 true 或 false,不修改任何东西。

调用关系:它是对 SessionEntry::is_attached_to 的一层易用包装。外部代码不需要直接碰 SessionEntry,就能知道当前句柄是否还有效。

SessionHandle::process239–241 ↗
fn process(&self) -> &ProcessHandler

作用:拿到这个会话背后的 ProcessHandler,也就是实际执行命令、读写输出、发送通知的对象。会话句柄通过它把具体工作交给进程层。

数据流:进去的是 SessionHandle 自己 → 它直接返回内部 ProcessHandler 的引用 → 出来的是可供调用方使用的进程处理器引用,不改变连接状态。

调用关系:SessionRegistry 负责找到或创建会话;真正的执行工作不在这里做,而是调用方拿到这个 process 后交给 ProcessHandler 继续处理。

SessionHandle::detach243–258 ↗
async fn detach(&self)

作用:处理当前连接断开。它不会马上杀掉会话,而是先停止往这个连接发通知,并安排一个后台倒计时清理任务。

数据流:进去的是 SessionHandle 自己 → 它先调用 SessionEntry::detach,确认这个连接确实能断开;如果失败就直接结束;成功后,把 ProcessHandler 的通知发送器设为空,避免继续给已经断开的连接发消息;然后复制 registry、session_id、connection_id,并启动后台任务 → 出来没有返回值,但会话进入“等待重连或过期清理”的状态。

调用关系:网络连接关闭时会走到它。它把状态更新交给 SessionEntry::detach,把延迟清理交给 SessionRegistry::expire_if_detached,并用 tokio::spawn 让清理在后台运行。

调用图:外部调用 2 个(clone, spawn)。

扩展与线程级服务

这些文件添加具备线程感知能力的扩展行为,例如目标、MCP 插件贡献、技能状态,以及叠加在核心运行时之上的会话控制工具。

ext/goal/src/extension.rs源码 ↗
orchestrationcross-cutting:在线程生命周期、配置变化、回合执行、token 统计和工具调用结束时都会活跃

这个文件是目标功能和宿主系统之间的“插线板”。主程序会在不同时间点通知扩展:新线程开始了、配置改了、一次对话回合开始或结束了、某个工具跑完了、用了多少 token(模型消耗额度)等。GoalExtension 接到这些通知后,会找到当前线程里的 GoalRuntimeHandle,也就是目标功能的运行手柄,然后决定要不要启用目标、是否登记到 GoalService、是否记录本回合消耗、是否把工具调用算作目标进度。它还会在目标达到预算限制时,把一条提醒塞回当前回合,避免系统继续无意识消耗额度。最后,install_with_backend 负责把这个扩展注册到扩展框架里,让这些钩子真正被主程序调用。

函数细节18
GoalExtensionConfig::from_enabled54–56 ↗
fn from_enabled(enabled: bool) -> Self

作用:把一个简单的开关值包装成目标扩展自己的配置对象。这样线程里的共享存储可以统一保存“目标功能当前是否开启”。

数据流:进去的是一个布尔值 enabled,表示开或关;函数只把它放进 GoalExtensionConfig 结构里;出来的是一个带有 enabled 字段的新配置对象,不会改动别的东西。

调用关系:在线程开始时 GoalExtension::on_thread_start 会用它保存初始开关状态;配置变化时 GoalExtension::on_config_changed 也会用它更新线程里的状态。

调用图:被 2 处调用(on_config_changed, on_thread_start)。

GoalExtension::fmt71–73 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:给 GoalExtension 提供调试打印方式。它不会把内部复杂对象全打出来,只告诉调试工具“这是一个 GoalExtension”。

数据流:进去的是格式化器,也就是 Rust 用来拼调试文本的对象;函数调用 debug_struct 生成一个简略的结构名;出来的是格式化结果,内部状态不会被修改。

调用关系:它属于 Rust 的 Debug 调试接口,通常在日志、调试输出或错误排查时被间接使用;它只把活交给标准的 debug_struct。

调用图:外部调用 1 个(debug_struct)。

GoalExtension::new_with_host_capabilities77–95 ↗
fn new_with_host_capabilities(
        state_dbs: Arc<codex_state::StateRuntime>,
        analytics_events_client: AnalyticsEventsClient,
        event_sink: Arc<dyn ExtensionEventSink>,
        metri

作用:创建一个完整可用的目标扩展,并把宿主系统给的能力装进去,比如状态数据库、事件发送器、指标上报、线程管理器和目标服务。

数据流:进去的是数据库、分析事件客户端、事件出口、指标客户端、线程管理器弱引用、目标服务,以及一个判断配置是否启用目标的函数;它分别创建 GoalAnalytics、GoalEventEmitter、GoalMetrics 等内部零件;出来的是一个 GoalExtension 实例。

调用关系:install_with_backend 在安装扩展时调用它。它内部调用多个 new 来组装分析、事件和指标组件,之后这个扩展会被注册成线程、配置、回合、token 和工具生命周期的贡献者。

调用图:调用 3 个内部函数(new, new, new);被 1 处调用(install_with_backend);外部调用 1 个(new)。

GoalExtension::on_thread_start102–137 ↗
fn on_thread_start(&'a self, input: ThreadStartInput<'a, C>) -> ExtensionFuture<'a, ()>

作用:在线程刚开始时,为这个线程准备目标功能的运行环境。它会决定目标是否开启、工具是否可见,并创建或取出目标运行手柄。

数据流:进去的是线程启动信息,包括配置、线程存储、会话来源、持久线程状态是否可用等;它读取配置开关,保存 GoalExtensionConfig,初始化 GoalAccountingState,把线程 ID 转成正式 ThreadId,再创建或取得 GoalRuntimeHandle;出来没有直接返回业务数据,但会改动线程存储、设置 runtime 开关,并把 runtime 注册到 GoalService。

调用关系:这是目标功能进入某个线程的入口。它调用 GoalExtensionConfig::from_enabled 保存开关,调用 ThreadId::from_string 识别线程,使用 matches! 判断是否是 review 子代理;后续的回合、工具和 token 钩子都依赖这里放进 thread_store 的 GoalRuntimeHandle。

调用图:调用 2 个内部函数(from_enabled, from_string);外部调用 2 个(pin, matches!)。

GoalExtension::on_thread_resume139–152 ↗
fn on_thread_resume(&'a self, input: ThreadResumeInput<'a>) -> ExtensionFuture<'a, ()>

作用:在线程恢复时,让目标运行时重新找回之前的状态。比如程序暂停后继续,需要把目标状态接上,不能像新线程一样从零开始。

数据流:进去的是线程恢复信息,主要用到 thread_store;它通过 goal_runtime_handle 找到运行手柄,如果找不到就什么也不做;找到后调用 restore_after_resume,失败时只写警告日志;出来没有业务返回值。

调用关系:宿主系统在线程恢复时调用它。它依赖 goal_runtime_handle 从线程存储取 runtime,然后把恢复工作交给 runtime;遇到错误时用 warn! 记录,但不让整个流程崩掉。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 2 个(pin, warn!)。

GoalExtension::on_thread_idle154–167 ↗
fn on_thread_idle(&'a self, input: ThreadIdleInput<'a>) -> ExtensionFuture<'a, ()>

作用:在线程空闲时检查是否有目标可以继续推进。它相当于问一句:现在没别的事了,目标任务要不要接着跑?

数据流:进去的是线程空闲信息;它从 thread_store 取 GoalRuntimeHandle,找不到就结束;找到后调用 continue_if_idle 尝试继续活动目标;如果失败,只记录警告日志;没有直接输出。

调用关系:宿主系统在线程进入空闲状态时调用它。它通过 goal_runtime_handle 找 runtime,再把“是否继续”的判断交给 runtime 自己完成。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 2 个(pin, warn!)。

GoalExtension::on_thread_stop169–175 ↗
fn on_thread_stop(&'a self, input: ThreadStopInput<'a>) -> ExtensionFuture<'a, ()>

作用:在线程停止时,把这个线程的目标运行时从目标服务里注销。这样服务不会继续持有已经结束的线程。

数据流:进去的是线程停止信息;它从 thread_store 查找 GoalRuntimeHandle;如果存在,就调用 GoalService 注销这个 runtime;出来没有返回数据,但会更新 GoalService 的登记表。

调用关系:这是 GoalExtension::on_thread_start 注册动作的收尾。宿主系统在线程结束时调用它,它通过 goal_runtime_handle 找到之前注册过的 runtime,然后交给 goal_service.unregister_runtime 清理。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 1 个(pin)。

GoalExtension::on_config_changed182–194 ↗
fn on_config_changed(
        &self,
        _session_store: &ExtensionData,
        thread_store: &ExtensionData,
        _previous_config: &C,
        new_config: &C,
    )

作用:当配置改变时,同步更新目标功能的开关状态。用户或系统关掉目标功能后,正在运行的线程也要立刻知道。

数据流:进去的是会话存储、线程存储、旧配置和新配置;它只用新配置判断 enabled,写入新的 GoalExtensionConfig;如果线程里已有 GoalRuntimeHandle,就调用 set_enabled 更新运行时状态;没有直接输出。

调用关系:配置系统在配置变化时调用它。它调用 GoalExtensionConfig::from_enabled 生成配置对象,并用 goal_runtime_handle 找到 runtime,让后续回合和工具逻辑按新开关工作。

调用图:调用 3 个内部函数(insert, from_enabled, goal_runtime_handle)。

GoalExtension::on_turn_start201–241 ↗
fn on_turn_start(&'a self, input: TurnStartInput<'a>) -> ExtensionFuture<'a, ()>

作用:在一次对话回合开始时,开始记录这个回合和目标之间的关系。它会记下起始 token 用量,并判断当前有没有活动目标要跟踪。

数据流:进去的是回合启动信息,包括线程存储、回合 ID、协作模式、回合开始时的 token 用量;它取 runtime,检查是否启用,启动会计记录;如果是 Plan 模式,就清掉当前回合目标并结束;否则从状态数据库读取线程目标,若目标是活动或预算受限,就标记本回合正在服务这个目标;输出为空,但会更新会计状态。

调用关系:宿主系统在每个回合开始时调用它。它通过 goal_runtime_handle 找 runtime,用 matches! 判断是否是规划模式,并读取 state_dbs.thread_goals;后面的 token 统计、工具完成和回合结束会接着使用这里建立的会计上下文。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 2 个(pin, matches!)。

GoalExtension::on_turn_stop243–269 ↗
fn on_turn_stop(&'a self, input: TurnStopInput<'a>) -> ExtensionFuture<'a, ()>

作用:在一次对话回合正常结束时,把这个回合对活动目标的进展结算掉。这样目标消耗了多少、是否还该继续,都不会漏记。

数据流:进去的是回合结束信息;它取 runtime 并确认目标功能开启;拿到 turn_id 后调用 account_active_goal_progress 结算活动目标进度,使用 ClearActive 表示结算后清掉当前活动标记;成功后调用 finish_turn 结束会计记录;失败时写警告并提前返回。

调用关系:宿主系统在回合正常停止时调用它。它依赖 goal_runtime_handle 找 runtime,使用 format! 生成这次结算的唯一标记,具体结算交给 runtime;这和 GoalExtension::on_turn_abort 是相似的收尾路径。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 3 个(pin, format!, warn!)。

GoalExtension::on_turn_abort271–297 ↗
fn on_turn_abort(&'a self, input: TurnAbortInput<'a>) -> ExtensionFuture<'a, ()>

作用:在一次对话回合被中断时,也要把已经发生的目标进展结算掉。否则中途失败的回合可能会留下不一致的账。

数据流:进去的是回合中断信息;它找到 runtime,检查开关,取 turn_id;调用 account_active_goal_progress 结算当前活动目标,并要求清掉活动标记;成功后 finish_turn,失败时写警告;没有直接返回业务数据。

调用关系:宿主系统在回合异常中断时调用它。它和 GoalExtension::on_turn_stop 走的逻辑很像,只是结算标记里写的是 turn-abort,用来区分正常结束和中断结束。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 3 个(pin, format!, warn!)。

GoalExtension::on_turn_error299–323 ↗
fn on_turn_error(&'a self, input: TurnErrorInput<'a>) -> ExtensionFuture<'a, ()>

作用:当回合因为错误结束时,主动停止当前活动目标,避免系统一直自动续跑、反复失败、继续烧 token。

数据流:进去的是回合错误信息,包括错误类型和 turn_id;它找到 runtime;如果错误是 UsageLimitExceeded,就把停止原因记为用量限制,否则记为回合错误;然后调用 stop_active_goal_for_turn 停掉目标;失败时写警告日志。

调用关系:宿主系统在回合出错时调用它。它通过 goal_runtime_handle 找 runtime,再把真正停止目标的动作交给 runtime;这里的判断能防止不可重试错误导致自动继续形成循环。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 2 个(pin, warn!)。

GoalExtension::on_token_usage330–352 ↗
fn on_token_usage(
        &'a self,
        _session_store: &'a ExtensionData,
        thread_store: &'a ExtensionData,
        turn_store: &'a ExtensionData,
        token_usage: &'a TokenUsageInfo,

作用:收到 token 用量更新时,把本回合的消耗记到目标会计里。token 可以理解为模型处理文字时花掉的额度。

数据流:进去的是会话存储、线程存储、回合存储和 TokenUsageInfo;它取 runtime,确认启用;再用 turn_store.level_id 找到当前回合,把 total_token_usage 交给 accounting_state.record_token_usage;如果没有可记录内容就直接结束。

调用关系:token 统计系统在有用量信息时调用它。它通过 goal_runtime_handle 找到 runtime,然后只更新会计状态;这些记录会被回合结束或工具完成时的目标进度结算使用。

调用图:调用 2 个内部函数(level_id, goal_runtime_handle);外部调用 1 个(pin)。

GoalExtension::on_tool_finish359–403 ↗
fn on_tool_finish(&'a self, input: ToolFinishInput<'a>) -> ToolLifecycleFuture<'a>

作用:当一个工具调用完成后,判断这次工具调用是否应该算作目标进展,并在目标撞到预算上限时提醒当前回合。

数据流:进去的是工具结束信息,包括线程存储、工具名、调用 ID、结果和 turn_id;它取 runtime,检查扩展开关、工具结果是否算数、以及是否不是 update_goal 这个目标内部工具;如果应该计入,就调用 account_active_goal_progress 结算;若结果显示目标变成 BudgetLimited,且之前没报告过,就生成预算限制提示并注入当前回合;输出为空,但可能更新目标账本并插入一条引导信息。

调用关系:工具生命周期系统在工具结束时调用它。它调用 tool_attempt_counts_for_goal_progress 判断哪些结果算进展,调用 budget_limit_steering_item 生成提醒;真正的进度计算和注入回合由 runtime 完成。

调用图:调用 3 个内部函数(goal_runtime_handle, tool_attempt_counts_for_goal_progress, budget_limit_steering_item);外部调用 2 个(pin, warn!)。

GoalExtension::tools410–448 ↗
fn tools(
        &self,
        _session_store: &ExtensionData,
        thread_store: &ExtensionData,
    ) -> Vec<Arc<dyn codex_extension_api::ToolExecutor<codex_extension_api::ToolCall>>>

作用:告诉宿主系统当前线程可以使用哪些目标工具。这里提供获取目标、创建目标、更新目标三个工具。

数据流:进去的是会话存储和线程存储;它从线程存储取 runtime,如果没有 runtime 或 runtime 判断工具不可见,就返回空列表;否则创建三个 GoalToolExecutor,分别用于 get、create、update,并把数据库、会计状态、分析、事件和指标能力传进去;出来的是工具执行器列表。

调用关系:工具系统在询问可用工具时调用它。它先用 goal_runtime_handle 判断当前线程是否有目标运行时,再创建 GoalToolExecutor;这些工具之后会被模型或宿主系统调用来读写目标。

调用图:调用 1 个内部函数(goal_runtime_handle);外部调用 2 个(new, vec!)。

install_with_backend451–477 ↗
fn install_with_backend(
    registry: &mut ExtensionRegistryBuilder<C>,
    state_dbs: Arc<codex_state::StateRuntime>,
    analytics_events_client: AnalyticsEventsClient,
    metrics_client: Option<M

作用:把目标扩展正式安装到扩展注册表里。它负责把同一个 GoalExtension 挂到多个系统通知点上。

数据流:进去的是扩展注册表、状态数据库、分析客户端、指标客户端、线程管理器、目标服务和配置开关判断函数;它从注册表拿事件出口,调用 GoalExtension::new_with_host_capabilities 创建扩展;然后把扩展依次注册为线程生命周期、配置、回合生命周期、token 用量、工具生命周期和工具提供者;没有业务返回值,但注册表会被填好。

调用关系:这是外部代码安装目标功能时会调用的函数。它是本文件的装配入口,调用 new_with_host_capabilities 造出扩展,再调用 registry 的各类 contributor 注册方法,让宿主系统之后能调用本文件里的各个钩子。

调用图:调用 8 个内部函数(config_contributor, event_sink, thread_lifecycle_contributor, token_usage_contributor, tool_contributor, tool_lifecycle_contributor, turn_lifecycle_contributor, new_with_host_capabilities);外部调用 2 个(clone, new)。

goal_runtime_handle479–481 ↗
fn goal_runtime_handle(thread_store: &ExtensionData) -> Option<Arc<GoalRuntimeHandle>>

作用:从线程存储里取出目标运行手柄。它是一个小帮手,避免每个钩子都重复写同样的取值代码。

数据流:进去的是 ExtensionData,也就是线程级别的共享小仓库;它尝试读取 GoalRuntimeHandle;出来是 Some(runtime) 或 None,表示找到了或没找到,不会创建新对象。

调用关系:几乎所有生命周期钩子都会先调用它,比如配置变化、线程恢复、空闲、停止、token 统计、工具结束、回合中断和错误处理。它让这些流程都以“先确认这个线程有没有目标运行时”为第一步。

调用图:被 11 处调用(on_config_changed, on_thread_idle, on_thread_resume, on_thread_stop, on_token_usage, on_tool_finish, on_turn_abort, on_turn_error, on_turn_start, on_turn_stop (+1 more))。

tool_attempt_counts_for_goal_progress483–495 ↗
fn tool_attempt_counts_for_goal_progress(outcome: ToolCallOutcome) -> bool

作用:判断一次工具尝试是否应该算作目标进展。不是所有工具结果都算,比如被拦截或根本没执行的失败就不算。

数据流:进去的是 ToolCallOutcome,也就是工具调用结果;如果工具完成,或失败但处理器已经执行过,就返回 true;如果被阻止、未执行就失败、或被中止,就返回 false;不改动任何状态。

调用关系:GoalExtension::on_tool_finish 会调用它,先过滤掉不该算进度的工具结果。这样目标账本只记录真正发生过、有意义的工具尝试。

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

ext/goal/src/runtime.rs源码 ↗
orchestrationcross-cutting

可以把这个文件理解成一个“目标调度员”。一个线程里可能有一个正在进行的目标,系统要记住它是否启用、当前目标是谁、已经花了多少时间和 token、状态有没有变成完成/阻塞/超预算。这里的 GoalRuntimeHandle 就是外面拿来操作这套机制的把手。它会先检查目标功能是否开启,再和状态数据库打交道,更新目标;同时通知指标系统和分析系统;必要时还会往正在运行的对话里塞一条“转向提示”,告诉模型目标改了,或者在空闲时自动开一轮继续完成目标。文件里还用了信号量(一把一次只让一个任务通过的锁)来保护关键窗口,避免“刚读到目标还没启动下一步,目标就被别人改了”这种错乱。

函数细节20
PreviousGoalSnapshot::from65–71 ↗
fn from(goal: &codex_state::ThreadGoal) -> Self

作用:把数据库里的旧目标摘出一份小快照,只保留后面比较最需要的三样:目标编号、状态、目标文字。这样设置新目标时,就能判断是新建、替换,还是只改了状态或内容。

数据流:进去的是一个完整的旧目标记录 → 它复制出 goal_id、status、objective → 出来的是 PreviousGoalSnapshot,不会改动原目标。

调用关系:它会在 set_thread_goal 这类外部设置目标的流程里被用到。后面的 apply_external_goal_set 依赖这份快照来决定要不要记一次“创建”、要不要发状态变化、要不要给正在运行的对话注入新目标提示。

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

GoalRuntimeHandle::fmt75–77 ↗
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result

作用:给 GoalRuntimeHandle 做调试打印,但故意不把里面复杂的运行状态全部展开。这样日志里能看出这是个目标运行句柄,又不会泄露或刷屏大量内部细节。

数据流:进去的是格式化器 → 它写入一个名叫 GoalRuntimeHandle 的调试结构 → 出来是格式化结果,不改变运行状态。

调用关系:这是 Rust 的 Debug 打印机制自动会用到的函数。它只把活交给标准的 debug_struct 来生成简洁输出,不参与目标执行本身。

调用图:外部调用 1 个(debug_struct)。

GoalRuntimeHandle::new81–104 ↗
fn new(
        thread_id: ThreadId,
        state_dbs: Arc<codex_state::StateRuntime>,
        event_emitter: GoalEventEmitter,
        metrics: GoalMetrics,
        thread_manager: Weak<ThreadManage

作用:创建一个新的目标运行句柄,把线程编号、状态库、事件发送器、指标、分析、用量账本等零件装到一起。外部拿到这个句柄后,就可以统一操作目标功能。

数据流:进去的是线程 ID、状态数据库、事件和指标工具、线程管理器引用、用量账本和配置 → 它把这些放进共享的内部对象里,并初始化开关和一把目标状态锁 → 出来是 GoalRuntimeHandle。

调用关系:这是目标运行时的组装入口。之后 set_enabled、apply_external_goal_set、continue_if_idle、account_active_goal_progress 等方法都通过这里保存的内部零件工作。

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

GoalRuntimeHandle::set_enabled106–108 ↗
fn set_enabled(&self, enabled: bool)

作用:动态打开或关闭目标功能。比如配置变化后,系统可以不用重建整个运行时,只改这个开关。

数据流:进去的是 true 或 false → 它把这个值写进原子布尔值(一个可以被多个任务安全读取的小开关)→ 出来没有返回值,但后续所有检查开关的流程都会看到新状态。

调用关系:它是控制总闸的函数。is_enabled 会读取它写入的值,很多目标操作都会先问 is_enabled,如果关了就直接跳过。

GoalRuntimeHandle::is_enabled110–112 ↗
fn is_enabled(&self) -> bool

作用:检查目标功能现在是否开启。很多操作第一步都会问它,避免在禁用时还去改数据库、发事件或自动继续执行。

数据流:进去没有业务输入 → 它读取内部的 enabled 开关 → 出来是一个布尔值,true 表示可以运行目标逻辑,false 表示应当跳过。

调用关系:prepare_external_goal_mutation、apply_external_goal_set、apply_external_goal_clear、restore_after_resume、stop_active_goal_for_turn 和 tools_visible 都会先通过它判断总闸是否打开。

调用图:被 6 处调用(apply_external_goal_clear, apply_external_goal_set, prepare_external_goal_mutation, restore_after_resume, stop_active_goal_for_turn, tools_visible)。

GoalRuntimeHandle::tools_visible114–116 ↗
fn tools_visible(&self) -> bool

作用:判断目标工具对当前线程是否真的可见。它不只看目标功能总开关,还要看这个线程本身是否允许使用目标工具。

数据流:进去没有参数 → 它先调用 is_enabled,再结合 tools_available_for_thread → 出来是 true 或 false,表示是否能把目标相关工具暴露给运行中的线程。

调用关系:continue_if_idle 会用它判断能不能自动继续目标。如果工具不可见,就不启动自动续跑,并清掉当前活跃目标的账本标记。

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

GoalRuntimeHandle::thread_id118–120 ↗
fn thread_id(&self) -> ThreadId

作用:取出这个运行句柄所属的线程编号。目标状态存在按线程区分的数据库里,所以很多地方都需要这个编号当钥匙。

数据流:进去没有参数 → 它读取内部保存的 thread_id → 出来是 ThreadId,不改动任何状态。

调用关系:account_active_goal_progress、account_idle_goal_progress、continue_if_idle、current_goal_status_for_metrics、restore_after_resume、stop_active_goal_for_turn 都用它去读写对应线程的目标记录。

调用图:被 6 处调用(account_active_goal_progress, account_idle_goal_progress, continue_if_idle, current_goal_status_for_metrics, restore_after_resume, stop_active_goal_for_turn)。

GoalRuntimeHandle::accounting_state122–124 ↗
fn accounting_state(&self) -> Arc<GoalAccountingState>

作用:拿到共享的目标用量账本。账本记录当前目标是哪一个、这一轮或空闲期间积累了多少时间和 token。

数据流:进去没有参数 → 它复制一份 Arc 引用(共享指针,不复制账本内容)→ 出来是同一个 GoalAccountingState 的共享句柄。

调用关系:account_active_goal_progress 和 account_idle_goal_progress 会先拿到账本,再从里面取进度快照,并在扣账后更新账本基线。

调用图:被 2 处调用(account_active_goal_progress, account_idle_goal_progress);外部调用 1 个(clone)。

GoalRuntimeHandle::goal_state_permit126–132 ↗
async fn goal_state_permit(&self) -> Result<SemaphorePermit<'_>, String>

作用:申请目标状态锁,保证一段关键操作不会和别的目标修改交叉发生。简单说就是先拿号码牌,拿到的人才能改关键状态。

数据流:进去没有业务数据 → 它等待信号量许可 → 成功出来一个 SemaphorePermit,失败出来错误字符串;许可活着时,别人不能同时进入同一段受保护流程。

调用关系:continue_if_idle 和 stop_active_goal_for_turn 会用它保护“读目标状态再启动/停止”的窗口,避免外部设置或清除目标夹在中间造成状态不一致。

调用图:被 2 处调用(continue_if_idle, stop_active_goal_for_turn)。

GoalRuntimeHandle::prepare_external_goal_mutation134–157 ↗
async fn prepare_external_goal_mutation(&self) -> Result<(), String>

作用:在外部准备修改目标前,先把当前已经产生的目标进度结算掉。这样不会因为目标马上被改掉,导致旧目标的时间或 token 花费丢失。

数据流:进去没有参数 → 它先看功能是否开启;如果当前有正在运行的 turn(一轮模型执行),就按 active 进度扣账;如果没有,就按 idle 空闲进度扣账 → 出来是成功或错误,并可能清掉活跃目标标记。

调用关系:这是外部改目标前的“先结账”步骤。它根据是否有 current_turn_id,把工作交给 account_active_goal_progress 或 account_idle_goal_progress。

调用图:调用 3 个内部函数(account_active_goal_progress, account_idle_goal_progress, is_enabled);外部调用 1 个(format!)。

GoalRuntimeHandle::apply_external_goal_set159–223 ↗
async fn apply_external_goal_set(
        &self,
        goal: codex_state::ThreadGoal,
        previous_goal: Option<PreviousGoalSnapshot>,
    ) -> Result<(), String>

作用:外部设置或更新目标后,用这个函数把运行时状态同步好。它会记录创建、恢复、结束等指标,也会决定是否自动继续目标。

数据流:进去的是新目标和可选的旧目标快照 → 它判断是不是新建/替换、状态有没有变、目标文字有没有变;然后更新指标和分析,按目标状态调整账本;如果目标是 Active,可能注入“目标已更新”的提示,并在空闲时启动继续执行 → 出来是成功或错误。

调用关系:这是设置目标后的核心收尾流程。它会调用 objective_updated_steering_item 和 inject_active_turn_steering 给正在跑的 turn 补提示,也会调用 continue_if_idle 在没有活动 turn 时尝试自动开始下一轮。

调用图:调用 5 个内部函数(continue_if_idle, inject_active_turn_steering, is_enabled, objective_updated_steering_item, protocol_goal_from_state)。

GoalRuntimeHandle::apply_external_goal_clear225–236 ↗
async fn apply_external_goal_clear(
        &self,
        goal: codex_state::ThreadGoal,
    ) -> Result<(), String>

作用:外部清除目标后,用这个函数做运行时清理。它会记一次“目标被清除”的分析事件,并把当前活跃目标标记清掉。

数据流:进去的是被清除的目标 → 它先检查功能是否开启;开启时写分析记录,并清空 accounting_state 里的活跃目标 → 出来是成功或错误。

调用关系:这是清除目标后的配套动作。它不像设置目标那样启动继续执行,只负责把目标运行时从“还在追踪这个目标”的状态里退出来。

调用图:调用 1 个内部函数(is_enabled)。

GoalRuntimeHandle::usage_limit_active_goal_for_turn238–241 ↗
async fn usage_limit_active_goal_for_turn(&self, turn_id: &str) -> Result<(), String>

作用:当某一轮执行撞到使用限制时,停止它关联的活跃目标。它是一个更直白的包装函数,专门表达“因为用量限制而停”。

数据流:进去的是 turn_id → 它把停止原因固定为 UsageLimit,然后交给 stop_active_goal_for_turn → 出来是停止流程的成功或错误。

调用关系:它本身不做复杂判断,只是把调用转发给 stop_active_goal_for_turn。这样调用方不用关心 ActiveGoalStopReason 这种内部枚举。

调用图:调用 1 个内部函数(stop_active_goal_for_turn)。

GoalRuntimeHandle::stop_active_goal_for_turn244–333 ↗
async fn stop_active_goal_for_turn(
        &self,
        turn_id: &str,
        reason: ActiveGoalStopReason,
    ) -> Result<(), String>

作用:结束某一轮正在推进的目标,并把目标状态改成合适的终止状态。比如 turn 出错就标成 Blocked,碰到使用限制就标成 UsageLimited。

数据流:进去的是 turn_id 和停止原因 → 它先检查开关并拿目标状态锁;确认这个 turn 确实是当前活跃目标;先结算这轮进度;再从数据库取当前目标,判断能不能停;能停就更新状态、记录指标和分析、清账本、发目标更新事件 → 出来是成功或错误。

调用关系:usage_limit_active_goal_for_turn 会调用它。它内部会调用 goal_state_permit 防止并发插队,调用 account_active_goal_progress 先结账,再用 protocol_goal_from_state 把数据库目标转成对外事件能发送的格式。

调用图:调用 5 个内部函数(account_active_goal_progress, goal_state_permit, is_enabled, thread_id, protocol_goal_from_state);被 1 处调用(usage_limit_active_goal_for_turn);外部调用 2 个(Turn, format!)。

GoalRuntimeHandle::restore_after_resume335–357 ↗
async fn restore_after_resume(&self) -> Result<(), String>

作用:系统恢复运行后,重新找回目标账本状态。比如进程或线程恢复时,如果数据库里还有 Active 目标,就把它重新标成空闲中的活跃目标。

数据流:进去没有参数 → 它检查开关,从状态数据库读取当前线程目标;如果目标仍是 Active,就在账本里标记它并记一次 resumed 指标;否则清掉活跃目标 → 出来是成功或错误。

调用关系:这是恢复流程里的修复步骤。它通过 thread_id 找目标,不会自动启动继续执行,只负责让内存中的 accounting_state 和数据库重新对齐。

调用图:调用 2 个内部函数(is_enabled, thread_id)。

GoalRuntimeHandle::continue_if_idle359–415 ↗
async fn continue_if_idle(&self) -> Result<(), String>

作用:如果线程现在空闲,而且有活跃目标,就自动开一轮继续完成目标。它让目标不像普通备注一样停在数据库里,而是能真正推动系统继续工作。

数据流:进去没有参数 → 它先判断目标工具是否可见;可见就拿目标状态锁,找到 live thread,读取数据库里的目标;只有目标状态是 Active 时,才生成一条“继续目标”的 steering item(给模型的引导消息),尝试在空闲线程里启动一轮;最后确认是否真的形成了活跃 turn,否则清账本 → 出来是成功或错误。

调用关系:apply_external_goal_set 在目标变为 Active 后会调用它。它会用 continuation_steering_item 生成提示,并把启动工作交给线程对象的 try_start_turn_if_idle;如果线程管理器或线程本身不可用,就只写调试日志后跳过。

调用图:调用 5 个内部函数(goal_state_permit, thread_id, tools_visible, continuation_steering_item, protocol_goal_from_state);被 1 处调用(apply_external_goal_set);外部调用 2 个(debug!, vec!)。

GoalRuntimeHandle::inject_active_turn_steering417–429 ↗
async fn inject_active_turn_steering(&self, item: ResponseItem)

作用:把一条目标相关提示塞进正在运行的 turn 里。典型场景是目标文字刚被改了,要让当前正在工作的模型马上知道。

数据流:进去的是一个 ResponseItem,也就是要注入的提示内容 → 它找到线程管理器和当前 live thread;如果线程正在运行,就尝试注入这条提示;如果找不到或没有运行中的 turn,就只记录调试信息 → 没有业务返回值。

调用关系:apply_external_goal_set 在发现 Active 目标的 objective 发生变化时会调用它。提示内容通常由 objective_updated_steering_item 生成,它负责把提示交给线程对象的 inject_if_running。

调用图:被 1 处调用(apply_external_goal_set);外部调用 2 个(debug!, vec!)。

GoalRuntimeHandle::account_active_goal_progress431–492 ↗
async fn account_active_goal_progress(
        &self,
        turn_id: &str,
        event_id: &str,
        mode: codex_state::GoalAccountingMode,
        budget_limited_goal_disposition: BudgetLimit

作用:给正在运行的 turn 结算目标进度,把这一轮花掉的时间和 token 算到目标预算里。这样目标如果超预算,状态能及时变化并通知外界。

数据流:进去的是 turn_id、事件 ID、扣账模式和“如果预算受限要怎么处理活跃目标”的策略 → 它拿到账本锁,取该 turn 的进度快照,读取扣账前目标状态,然后调用状态数据库扣时间和 token;如果目标被更新,就记录指标、分析、更新账本、发送目标更新事件,并返回对外格式的目标和 goal_id;如果没变化,返回 None。

调用关系:prepare_external_goal_mutation 和 stop_active_goal_for_turn 会调用它。它还会调用 current_goal_status_for_metrics 拿旧状态,用 protocol_goal_from_state 把内部目标变成协议目标,再通过事件发送器通知外部。

调用图:调用 4 个内部函数(accounting_state, current_goal_status_for_metrics, thread_id, protocol_goal_from_state);被 2 处调用(prepare_external_goal_mutation, stop_active_goal_for_turn);外部调用 1 个(Turn)。

GoalRuntimeHandle::account_idle_goal_progress494–556 ↗
async fn account_idle_goal_progress(
        &self,
        event_id: &str,
        mode: codex_state::GoalAccountingMode,
        budget_limited_goal_disposition: BudgetLimitedGoalDisposition,
    )

作用:给没有正在运行 turn、但目标仍处于空闲追踪中的时间结算进度。它主要计算时间消耗,不增加 token,因为空闲时没有模型实际生成内容。

数据流:进去的是事件 ID、扣账模式和预算受限处理策略 → 它拿到账本锁,取 idle 快照,读取旧状态,然后把空闲时间计入数据库里的目标用量,token 增量固定为 0;如果目标更新,就记录指标、分析、账本和事件;如果没有更新,就重置空闲基线并清掉活跃目标 → 出来是可选的 AccountedGoalProgress 或错误。

调用关系:prepare_external_goal_mutation 在没有 current_turn_id 时会调用它。它和 account_active_goal_progress 很像,只是来源是 idle_progress_snapshot,并且事件没有 turn_id。

调用图:调用 4 个内部函数(accounting_state, current_goal_status_for_metrics, thread_id, protocol_goal_from_state);被 1 处调用(prepare_external_goal_mutation)。

GoalRuntimeHandle::current_goal_status_for_metrics558–574 ↗
async fn current_goal_status_for_metrics(
        &self,
        expected_goal_id: Option<&str>,
    ) -> Result<Option<codex_state::ThreadGoalStatus>, String>

作用:读取当前目标状态,供指标系统判断状态是否真的发生了变化。它还会用 expected_goal_id 防止把别的目标误当成旧状态。

数据流:进去的是可选的 expected_goal_id → 它按 thread_id 从数据库读取目标;如果没有目标,或目标编号和期望的不一致,就返回 None;如果匹配,就返回当前状态 → 不改动数据库。

调用关系:account_active_goal_progress 和 account_idle_goal_progress 在扣账前调用它。扣账后再比较新状态,就能正确记录“是否进入终止状态”等指标。

调用图:调用 1 个内部函数(thread_id);被 2 处调用(account_active_goal_progress, account_idle_goal_progress)。

ext/goal/src/api.rs源码 ↗
domain_logicrequest handling

可以把这个文件看成“目标前台办事窗口”。外部想查目标、改目标、删目标,都会走这里。它先检查请求是不是合理,比如目标文字是否合法、预算数字能不能用;然后去状态数据库里读写真正的数据;如果这个线程背后还有一个正在跑的目标运行器,它会先拿到一把许可锁,防止运行器正好按旧目标继续干活。写完后,它会把数据库里的内部格式转换成协议里对外返回的格式。这里还保存了线程和运行器的对应关系,但用的是弱引用(一种不会强行让对象活着的引用),这样运行器结束后不会被这个表拖住不释放。整体作用就是让“外部改目标”和“后台按目标执行”这两件事安全地衔接起来。

函数细节10
GoalServiceError::fmt27–31 ↗
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result

作用:把目标服务里的错误变成一段可读的文字。这样日志、接口返回或调试信息里不会只看到生硬的错误类型。

数据流:进去的是一个错误值和一个文字输出器;函数判断错误是“请求不合法”还是“内部出错”,取出里面的人类可读消息;出来的是写入结果,实际写出的内容就是那段错误消息。

调用关系:它是错误类型展示自己的方式。其他地方把 GoalServiceError 当作普通错误打印时,会自动走到这里;它内部只把消息交给标准的 write_str 写出去。

调用图:外部调用 1 个(write_str)。

GoalSetOutcome::apply_runtime_effects64–72 ↗
async fn apply_runtime_effects(&self, goal_service: &GoalService)

作用:在目标已经写进数据库之后,把这次变化告诉对应线程的运行器。这样后台执行器能按新目标继续工作,而不是还拿旧目标行动。

数据流:进去的是一次设置目标后的结果,以及目标服务;它先用目标里的线程编号找当前运行器,找到后把新的内部目标状态和旧目标快照复制一份交给运行器;出来没有业务返回值,但可能改变运行器的后续行为,失败时只写警告日志。

调用关系:它通常接在 GoalService::set_thread_goal 成功之后使用。它会通过 GoalService::runtime_for_thread 找运行器,再把运行时副作用交给运行器处理;如果运行器不存在,说明这个线程现在没有后台任务,就什么也不做。

调用图:调用 1 个内部函数(runtime_for_thread);外部调用 2 个(clone, warn!)。

GoalService::new81–83 ↗
fn new() -> Self

作用:创建一个新的目标服务实例。外部系统启动或测试准备时会用它来得到一扇统一处理目标读写的“窗口”。

数据流:进去没有参数;它使用默认值建出服务对象,里面的运行器登记表一开始是空的;出来是一个可用的 GoalService。

调用关系:它是使用这个服务的起点。调用方创建好服务后,后续会用它来读目标、设目标、清目标,也会在运行器启动和停止时登记或注销运行器。

调用图:被 4 处调用(new, new, goal_service_sets_gets_and_clears_thread_goal, installed_tools_with_start);外部调用 1 个(default)。

GoalService::get_thread_goal85–96 ↗
async fn get_thread_goal(
        &self,
        state_db: &codex_state::StateRuntime,
        thread_id: ThreadId,
    ) -> Result<Option<ThreadGoal>, GoalServiceError>

作用:读取某个线程当前保存的目标。有人只想知道这个线程现在要做什么时,就会用它。

数据流:进去的是状态数据库和线程编号;它从数据库的线程目标区域读取记录,如果读到了,就把内部存储格式转换成对外协议格式;出来是“可能有目标”的结果,数据库读失败会变成内部错误。

调用关系:它是最简单的查询入口。它只和状态数据库打交道,不触碰运行器,因为读取目标不会改变后台执行状态。

调用图:调用 1 个内部函数(thread_goals)。

GoalService::set_thread_goal98–237 ↗
async fn set_thread_goal(
        &self,
        state_db: &codex_state::StateRuntime,
        request: GoalSetRequest<'_>,
    ) -> Result<GoalSetOutcome, GoalServiceError>

作用:设置或更新某个线程的目标,包括目标文字、状态和 token 预算。token 预算可以理解成这件事最多能花多少“模型工作量额度”。

数据流:进去的是状态数据库和一份设置请求;它先整理输入,比如去掉目标文字前后空格、把协议状态转成内部状态,再校验目标文字和预算;然后查找该线程运行器,拿到目标状态许可锁,提醒运行器准备好外部修改;接着根据有没有新目标文字,选择新建目标或更新已有目标;最后必要时补齐线程预览,并把内部目标转换成对外目标返回,同时带上旧目标快照。

调用关系:这是外部改目标的核心入口。它会用 GoalService::runtime_for_thread 找运行器,用 validate_thread_goal_objective 和 validate_goal_budget 做校验,用数据库的 thread_goals 完成读写,用 fill_empty_thread_preview_if_possible 做辅助补全,最后常常由调用方继续调用 GoalSetOutcome::apply_runtime_effects 来通知运行器。

调用图:调用 7 个内部函数(runtime_for_thread, from, fill_empty_thread_preview_if_possible, protocol_goal_from_state, validate_goal_budget, validate_thread_goal_objective, thread_goals);外部调用 1 个(warn!)。

GoalService::clear_thread_goal239–280 ↗
async fn clear_thread_goal(
        &self,
        state_db: &codex_state::StateRuntime,
        thread_id: ThreadId,
    ) -> Result<bool, GoalServiceError>

作用:清空某个线程当前的目标。用户取消目标或系统需要停止按目标推进时,会走这里。

数据流:进去的是状态数据库和线程编号;它先找运行器并拿目标状态许可锁,防止后台刚好按旧目标继续启动;然后从数据库删除目标;删除窗口结束后释放许可,再把被删掉的目标告诉运行器;出来是一个布尔值,表示之前到底有没有目标被清掉。

调用关系:它是删除目标的对外入口。它和 GoalService::set_thread_goal 一样会先通过 GoalService::runtime_for_thread 找运行器;不同的是它写完数据库后会直接请求运行器执行“目标已清空”的后续动作。

调用图:调用 2 个内部函数(runtime_for_thread, thread_goals);外部调用 1 个(warn!)。

GoalService::register_runtime282–285 ↗
fn register_runtime(&self, runtime: &Arc<GoalRuntimeHandle>)

作用:把一个线程的目标运行器登记到服务里。这样以后改这个线程目标时,服务能找到正在运行的后台执行器并通知它。

数据流:进去的是一个运行器的共享引用;它取出运行器对应的线程编号,把运行器转成弱引用后放进登记表;出来没有返回值,但服务内部的线程到运行器映射被更新了。

调用关系:它通常在目标运行器启动时被调用。它会通过 GoalService::runtimes 打开受互斥锁保护的表,互斥锁就是一把锁,防止多个任务同时改同一张表。

调用图:调用 1 个内部函数(runtimes);外部调用 1 个(downgrade)。

GoalService::unregister_runtime287–297 ↗
fn unregister_runtime(&self, runtime: &Arc<GoalRuntimeHandle>)

作用:把一个结束或不再有效的运行器从服务里注销掉。这样服务不会把后续目标变化通知给错误的旧运行器。

数据流:进去的是一个运行器引用;它根据线程编号找到登记表里的项,并确认表里存的确实是同一个运行器;如果匹配,就删除这条记录;出来没有返回值,但内部登记表可能少了一项。

调用关系:它通常在运行器停止时被调用。它通过 GoalService::runtimes 拿到登记表,并用弱引用比较来避免误删:如果同一线程后来换了新运行器,旧运行器注销时不会把新的那条删掉。

调用图:调用 1 个内部函数(runtimes);外部调用 1 个(downgrade)。

GoalService::runtime_for_thread299–307 ↗
fn runtime_for_thread(&self, thread_id: ThreadId) -> Option<Arc<GoalRuntimeHandle>>

作用:根据线程编号找到当前还活着的目标运行器。服务在设置、清空或应用目标副作用时,需要靠它把数据库变化转交给后台执行器。

数据流:进去的是线程编号;它把编号变成字符串,到登记表里找弱引用,并尝试升级成真正可用的共享引用;如果发现运行器已经没了,就顺手清理登记表里的旧项;出来是可能存在的运行器。

调用关系:它被 GoalService::set_thread_goal、GoalService::clear_thread_goal 和 GoalSetOutcome::apply_runtime_effects 使用。它自己通过 GoalService::runtimes 打开登记表,是目标服务和运行器之间的查找中介。

调用图:调用 1 个内部函数(runtimes);被 3 处调用(clear_thread_goal, set_thread_goal, apply_runtime_effects);外部调用 1 个(to_string)。

GoalService::runtimes309–311 ↗
fn runtimes(&self) -> std::sync::MutexGuard<'_, HashMap<String, Weak<GoalRuntimeHandle>>>

作用:安全地打开内部的运行器登记表,让调用者能读写它。这个表会被多个地方使用,所以必须先拿锁。

数据流:进去的是目标服务自身;它尝试锁住保存运行器映射的 Mutex,如果之前有任务拿锁时崩了,也会把里面的数据取回来继续用;出来是一把表的临时访问权限,拿着它才能改表。

调用关系:它是 register_runtime、unregister_runtime 和 runtime_for_thread 的底层小工具。其他函数不直接碰原始表,而是通过它拿到受保护的访问入口。

调用图:被 3 处调用(register_runtime, runtime_for_thread, unregister_runtime)。

ext/mcp/src/executor_plugin.rs源码 ↗
orchestrationMCP discovery / session setup

MCP 可以理解成“模型上下文协议”,就是让外部工具或服务接进系统的一套接口。这个文件做的事像是在开会前收集各个参会插件带来的“工具箱清单”。它先在每个运行线程里放一个小状态,用来缓存已经发现过的结果,避免同一轮里反复查插件。真正工作时,它会读取当前选中的能力根,也就是用户选了哪些执行器插件;然后逐个把插件解析出来,再读取插件声明的 MCP 服务器配置。读取成功的会冻结成一个快照,保留插件编号、显示名、选择顺序和服务器配置。之后贡献给系统时,它还会让全局配置再修正这些服务器要求,最后按名字排序,变成统一的 McpServerContribution。重要的是:某个插件解析或加载失败,只会记录警告并跳过,不会让其他插件的 MCP 服务一起失效。

函数细节5
seed_thread_state35–37 ↗
fn seed_thread_state(thread_init: &mut ExtensionDataInit)

作用:这个函数给当前线程的初始化数据里塞入一份空的 MCP 发现状态。没有这一步,后面贡献 MCP 服务时就找不到用来缓存结果的小盒子。

数据流:进去的是一个可修改的线程初始化数据容器。函数创建一份默认的 SelectedExecutorPluginMcpState,把它插进容器里。出来时没有返回值,但容器里多了后续会用到的状态。

调用关系:它会在 initialize_executor_plugin_thread_data 初始化执行器插件线程数据时被调用。它不做发现工作,只是提前摆好缓存位置,等 SelectedExecutorPluginMcpContributor::contribute 后面来取用。

调用图:调用 1 个内部函数(insert);被 1 处调用(initialize_executor_plugin_thread_data);外部调用 1 个(default)。

SelectedExecutorPluginMcpContributor::new45–50 ↗
fn new(environment_manager: Arc<EnvironmentManager>) -> Self

作用:这个函数创建一个“选中插件 MCP 贡献者”。简单说,就是造出一个对象,之后专门负责从已选插件里找 MCP 服务。

数据流:进去的是共享的 EnvironmentManager,也就是管理执行环境的对象。函数把它克隆一份引用交给 ExecutorPluginProvider,用来以后解析插件,同时创建 ExecutorPluginMcpProvider。出来的是一个完整的 SelectedExecutorPluginMcpContributor。

调用关系:它会在 install_executor_plugins 安装执行器插件相关能力时被调用。它搭好两个内部零件:一个负责找到插件,一个负责从插件里读 MCP 配置;后续 resolve_snapshot 和 contribute 会使用这些零件。

调用图:调用 1 个内部函数(new);被 1 处调用(install_executor_plugins);外部调用 1 个(clone)。

SelectedExecutorPluginMcpContributor::resolve_snapshot52–89 ↗
async fn resolve_snapshot(
        &self,
        selected_roots: &[SelectedCapabilityRoot],
    ) -> Vec<SelectedPluginMcpServers>

作用:这个函数把“用户选中的插件列表”变成一份固定的 MCP 服务快照。它会尽量收集能用的插件服务,遇到坏的插件就记警告并跳过。

数据流:进去的是一组 SelectedCapabilityRoot,也就是当前选中的插件能力入口。函数按选择顺序逐个处理:先解析出绑定的插件,再读取这个插件声明的 MCP 服务器。成功时把插件 ID、显示名称、选择顺序和服务器配置放进快照;解析不到就跳过,出错就记录警告。出来的是一组 SelectedPluginMcpServers。

调用关系:它主要由 SelectedExecutorPluginMcpContributor::contribute 在第一次需要结果时触发。它把具体查插件、读插件 MCP 配置的工作分别交给 plugin_provider.resolve_bound 和 mcp_provider.load,自己负责把结果整理成稳定快照。

调用图:调用 2 个内部函数(resolve_bound, load);外部调用 3 个(new, iter, warn!)。

SelectedExecutorPluginMcpContributor::id93–95 ↗
fn id(&self) -> &'static str

作用:这个函数返回这个 MCP 贡献者的固定名字。系统用这个名字区分不同来源的 MCP 服务贡献者。

数据流:进去不需要额外数据。函数直接返回字符串 selected_executor_plugin_mcp。它不修改任何状态。

调用关系:它是 McpServerContributor 接口的一部分。系统登记或识别这个贡献者时会用到它,而真正生成服务清单的工作由 contribute 完成。

SelectedExecutorPluginMcpContributor::contribute97–138 ↗
fn contribute(
        &'a self,
        context: McpServerContributionContext<'a, Config>,
    ) -> ExtensionFuture<'a, Vec<McpServerContribution>>

作用:这个函数是对外提供 MCP 服务清单的入口。它把缓存的插件 MCP 快照转成系统能直接使用的 McpServerContribution 列表。

数据流:进去的是一次 MCP 贡献上下文,里面可能有线程初始化数据和全局配置。函数先取线程数据;如果没有线程数据、没有选中的插件列表,或没有提前放好的状态,就返回空列表。然后它用 OnceCell(一种只初始化一次的缓存格子)取得或生成快照。接着把每个插件的服务器配置放进临时表,让全局配置应用插件 MCP 的额外要求,再按服务器名字排序。出来的是一组 McpServerContribution::SelectedPlugin,每条都带有服务器名、插件 ID、插件显示名、选择顺序和配置。

调用关系:它是 McpServerContributor 接口里真正被系统调用来收集 MCP 服务的函数。第一次调用时它会交给 resolve_snapshot 去发现插件服务;之后同一线程里会复用缓存结果。它还会读取 context.config(),让配置文件有机会调整插件声明的 MCP 服务。

调用图:调用 2 个内部函数(config, thread_init);外部调用 3 个(pin, new, warn!)。

ext/skills/src/state.rs源码 ↗
orchestrationcross-cutting

可以把这个文件理解成技能系统的“随身记事本加小仓库”。技能系统需要知道当前配置是什么、用户选了哪些能力根目录、编排器技能开没开,还要记住已经从编排器读过的目录和资源。这里用互斥锁(一把锁,防止两个任务同时改同一份数据)保护配置和缓存,用 OnceCell(只初始化一次的小盒子)保证编排器目录只生成一次。它还会按 MCP 资源客户端的缓存标识来区分不同来源;来源变了,就换一套新缓存,避免把旧服务器的数据误当成新数据。资源缓存也有限制:最多 100 个资源,总内容最多 8MB,防止内存被无限吃掉。最后的 SkillsTurnState 则是“一轮对话里”的临时状态,记录本轮看到的技能目录、选中的条目、警告和提示是否已经注入。

函数细节11
SkillsThreadState::new35–46 ↗
fn new(
        config: SkillsExtensionConfig,
        selected_roots: Vec<SelectedCapabilityRoot>,
        orchestrator_skills_enabled: bool,
    ) -> Self

作用:创建一份新的线程级技能状态。线程刚开始或配置变更后,需要用它把配置、已选根目录、编排器开关和空缓存装到一起。

数据流:进去的是技能配置、选中的能力根目录列表,以及“编排器技能是否开启”的布尔值 → 它把配置放进互斥锁里,把缓存位置先设为空 → 出来的是一个可被后续流程共享使用的 SkillsThreadState。

调用关系:它会在 on_thread_start 启动线程时创建初始状态,也会在 on_config_changed 配置变化时创建新状态。它内部调用标准库的构造能力来准备锁和空缓存,之后其他读取目录、读取技能的函数都围绕这份状态工作。

调用图:被 2 处调用(on_config_changed, on_thread_start);外部调用 1 个(new)。

SkillsThreadState::config48–53 ↗
fn config(&self) -> SkillsExtensionConfig

作用:取出当前技能配置的一份副本。这样调用者能查看配置,但不会直接改到内部保存的那份。

数据流:进去的是当前 SkillsThreadState 本身 → 它先给配置上锁,拿到里面的配置,再复制一份 → 出来的是这份配置副本;内部状态不被改变。

调用关系:它是外部代码查看当前配置的入口。因为配置被锁保护,即使别的地方正在更新配置,这里也会按顺序安全读取。

SkillsThreadState::set_config55–60 ↗
fn set_config(&self, config: SkillsExtensionConfig)

作用:把线程里的技能配置换成新配置。配置热更新时需要它,让后面的技能行为按新规则走。

数据流:进去的是一份新的 SkillsExtensionConfig → 它锁住旧配置所在的位置,然后用新配置覆盖旧配置 → 出来没有单独返回值,但状态里的配置已经变了。

调用关系:它是更新配置的小入口。它不负责重新读技能目录,也不负责通知别人,只做一件事:安全地替换保存的配置。

SkillsThreadState::selected_roots62–64 ↗
fn selected_roots(&self) -> &[SelectedCapabilityRoot]

作用:返回当前线程选中的能力根目录。外部流程需要知道哪些范围是允许或被选中的,就会读这里。

数据流:进去的是当前状态 → 它直接借出 selected_roots 这份列表的只读引用 → 出来的是一个不能被调用者随便改的列表视图。

调用关系:它给后续技能筛选或权限判断流程提供基础信息。这个列表在创建 SkillsThreadState 时定好,之后这里负责让别人安全查看。

SkillsThreadState::orchestrator_skills_enabled66–68 ↗
fn orchestrator_skills_enabled(&self) -> bool

作用:告诉调用者编排器技能是否启用。编排器可以理解成外部统一调度技能资源的来源,这个开关决定要不要走那条路。

数据流:进去的是当前状态 → 它读取保存的布尔开关 → 出来是真或假,不修改任何东西。

调用关系:它通常被上层流程用来决定是否展示或读取编排器提供的技能。它本身不做读取,只提供开关状态。

SkillsThreadState::orchestrator_catalog_snapshot70–85 ↗
async fn orchestrator_catalog_snapshot(
        &self,
        mcp_resources: Option<&McpResourceClient>,
        initialize: impl Future<Output = Result<SkillCatalog, SkillProviderError>> + Send,

作用:拿到编排器技能目录的一份快照,并尽量复用缓存。这样 list_skills 列技能时,不必每次都重新向外部来源生成目录。

数据流:进去的是可选的 MCP 资源客户端,以及一个真正初始化目录的异步任务 → 它先按 MCP 客户端找到对应缓存,再用 OnceCell 确保目录只初始化一次;如果初始化失败,就把错误消息放进一个带警告的空目录 → 出来的是 SkillCatalog 的副本。

调用关系:它由 list_skills 在列出技能时调用。它先把找缓存的活交给 SkillsThreadState::orchestrator_cache,再在缓存里的 catalog 小盒子中取现成目录或启动初始化任务。

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

SkillsThreadState::read_skill87–117 ↗
async fn read_skill(
        &self,
        providers: &SkillProviders,
        request: SkillReadRequest,
    ) -> SkillProviderResult<SkillReadResult>

作用:读取某个技能资源,并且只对编排器来源的资源做缓存。这样读主提示词等内容时,重复读同一个资源会更快,也能少打扰外部服务。

数据流:进去的是技能提供者集合和一个读取请求 → 如果请求不是编排器来源,就直接交给 providers.read;如果是编排器来源,就先生成缓存键并查缓存,命中就直接返回;没命中才真正读取,读完后如果返回资源仍是请求的那个资源,就尝试放进缓存 → 出来的是读取结果,可能来自缓存,也可能来自真实提供者。

调用关系:它由 read_main_prompt 在读取主提示内容时调用。它会用 SkillReadCacheKey::from 把请求变成缓存钥匙,用 SkillsThreadState::orchestrator_cache 找到对应缓存;真正读内容的工作则交给 providers.read。

调用图:调用 3 个内部函数(read, from, orchestrator_cache);被 1 处调用(read_main_prompt)。

SkillsThreadState::orchestrator_cache119–142 ↗
fn orchestrator_cache(
        &self,
        mcp_resources: Option<&McpResourceClient>,
    ) -> Arc<OrchestratorGenerationCache>

作用:找到当前 MCP 资源客户端对应的编排器缓存;如果没有合适的,就新建一个。它保证不同外部资源来源不会共用同一份旧缓存。

数据流:进去的是可选的 MCP 资源客户端 → 它取出客户端的缓存标识,锁住当前保存的缓存;如果已有缓存的标识一致,就复制一份 Arc 指针返回;如果不一致或没有缓存,就新建包含目录 OnceCell 和资源缓存的新缓存并保存 → 出来的是可共享的 OrchestratorGenerationCache。

调用关系:它被 orchestrator_catalog_snapshot 和 read_skill 共同使用,是目录缓存和资源缓存的入口。前者拿它来缓存目录,后者拿它来缓存具体资源。

调用图:被 2 处调用(orchestrator_catalog_snapshot, read_skill);外部调用 5 个(clone, new, new, new, default)。

SkillReadCacheKey::from159–165 ↗
fn from(request: &SkillReadRequest) -> Self

作用:把一次技能读取请求转换成缓存用的钥匙。缓存不能只看资源名,还要看来源和包名,否则不同地方的同名资源可能会混在一起。

数据流:进去的是 SkillReadRequest → 它复制请求里的 authority、package、resource 三个关键字段 → 出来的是 SkillReadCacheKey,可用于 HashMap 查找。

调用关系:它在 SkillsThreadState::read_skill 里被调用。read_skill 用这个键先查缓存,读完后也用同一个键决定是否存入缓存。

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

OrchestratorResourceCache::get175–177 ↗
fn get(&self, key: &SkillReadCacheKey) -> Option<SkillReadResult>

作用:从编排器资源缓存里按钥匙取一个已读过的结果。命中时可以省掉一次真实读取。

数据流:进去的是缓存本身和一个 SkillReadCacheKey → 它在内部 HashMap 里查找对应结果,并复制一份 → 出来是可能存在的 SkillReadResult;缓存内容不变。

调用关系:它被 read_skill 通过缓存锁间接使用。read_skill 会先问它“这个资源以前读过没”,如果有,就直接返回,不再调用提供者读取。

OrchestratorResourceCache::insert179–197 ↗
fn insert(&mut self, key: SkillReadCacheKey, result: SkillReadResult) -> SkillReadResult

作用:把新读到的编排器资源放进缓存,但会控制数量和总大小。这样既能加速重复读取,又不会让缓存无限占内存。

数据流:进去的是一个缓存键和一个读取结果 → 它先检查这个键是否已经有旧结果,有就返回旧结果;没有则计算内容大小,看总字节数是否还能安全相加,再检查是否超过 100 个资源或 8MB 总内容限制;没超限才写入 HashMap 并增加计数 → 出来的是最终要返回给调用者的 SkillReadResult,缓存可能被更新,也可能因为超限而不更新。

调用关系:它在 read_skill 真正读取成功后被使用。read_skill 负责判断该不该缓存,insert 负责最后的容量把关和保存。

调用图:外部调用 1 个(clone)。

core/src/tools/handlers/new_context_window.rs源码 ↗
orchestrationrequest handling

这个文件解决的是“当前对话太长或需要重新开始上下文”时怎么办的问题。可以把上下文窗口理解成模型的“当前记忆纸张”:纸写满了,或者不想带着前面的内容继续,就换一张新纸。这里的 NewContextWindowHandler 就是这个工具的执行者。它先告诉系统自己对应哪个工具名,再提供这个工具的说明规格,让外部知道模型可以怎么调用它。真正执行时,它只接受函数式工具调用,也就是模型按约定格式发来的工具请求;如果收到别的格式,就返回错误,避免误操作。确认格式没问题后,它通知当前会话去申请开启新上下文窗口。最后,它返回一段明确文字,告诉模型“新的上下文窗口会启动,并且不会总结历史对话”。这个返回里还带了一个成功标记,方便上层知道这次工具调用已经正常完成。

函数细节3
NewContextWindowHandler::tool_name19–21 ↗
fn tool_name(&self) -> ToolName

作用:这个函数告诉工具系统:这个处理器对应的工具名字是什么。别人要找“新上下文窗口”这个工具时,就靠这个名字对上号。

数据流:进去的是这个处理器本身,不需要额外参数;它读取固定的工具名常量,把普通字符串包装成系统统一使用的 ToolName;出来的是一个可被工具注册表识别的工具名。

调用关系:在工具注册或查找工具时,上层会问每个处理器“你叫什么”。这个函数把 NEW_CONTEXT_WINDOW_TOOL_NAME 交给 ToolName::plain 包装好,方便后续调用能找到 NewContextWindowHandler。

调用图:调用 1 个内部函数(plain)。

NewContextWindowHandler::spec23–25 ↗
fn spec(&self) -> ToolSpec

作用:这个函数提供“新上下文窗口”工具的说明书。系统和模型需要这份说明,才知道这个工具存在、叫什么、应该怎样被调用。

数据流:进去的是这个处理器本身;它调用 create_new_context_window_tool 生成工具规格;出来的是 ToolSpec,也就是工具系统能读取的一份标准化说明。

调用关系:在向模型公布可用工具,或注册工具能力时,上层会调用它。它不自己拼说明,而是把生成说明书的工作交给 create_new_context_window_tool,保证工具定义集中在规格文件里。

调用图:调用 1 个内部函数(create_new_context_window_tool)。

NewContextWindowHandler::handle27–42 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这个函数是真正执行工具请求的地方。它收到模型发来的“开新上下文窗口”请求后,检查请求格式,然后通知会话切换到新的上下文窗口。

数据流:进去的是一次工具调用 invocation,里面带着调用内容和当前会话。函数先检查 payload 是否是函数调用格式;如果不是,就返回一个给模型看的错误。格式正确时,它让 invocation.session 申请新上下文窗口。最后,它把固定提示文字 NEW_CONTEXT_WINDOW_MESSAGE 包成 FunctionToolOutput,再包成工具系统统一的输出结果返回。

调用关系:当模型实际调用这个工具时,工具运行时会进入这个函数。它先用 matches! 做守门检查;错误时通过 RespondToModel 生成可返回给模型的错误;成功时把关键动作交给当前 session 的 request_new_context_window;结束前用 FunctionToolOutput::from_text 和 boxed_tool_output 把结果包装成工具框架能接收的形式。

调用图:调用 2 个内部函数(from_text, boxed_tool_output);外部调用 3 个(pin, matches!, RespondToModel)。

core/src/tools/handlers/plan.rs源码 ↗
orchestrationrequest handling / tool invocation

可以把这个文件想成“计划更新按钮”背后的接线员。模型想更新待办清单时,会调用 update_plan,这里先确认这个调用是不是正常的函数参数;如果不是,就把错误反馈给模型。它还会检查当前是不是处在 Plan 模式,也就是专门做规划的模式;在这个模式下反而不允许用这个 TODO/checklist 工具,避免两套规划机制混在一起。确认没问题后,它把传进来的 JSON 字符串解析成 UpdatePlanArgs,也就是结构化的计划更新内容,再通过 session 发出 PlanUpdate 事件,让系统其他部分知道计划已经改变。最后它返回一个很简单的成功结果:“Plan updated”。PlanHandler 是接收和执行工具调用的人,PlanToolOutput 是告诉日志和模型“这次更新成功了”的结果包装。

函数细节9
PlanToolOutput::log_preview25–27 ↗
fn log_preview(&self) -> String

作用:给日志系统提供一句简短预览,说明这次工具调用的结果是“计划已更新”。这样查看运行记录的人不用展开全部细节,也能知道发生了什么。

数据流:它不需要外部输入,只读取固定文字 Plan updated → 把这句话转成字符串 → 返回给日志预览使用,不改动任何状态。

调用关系:它是 PlanToolOutput 这个结果对象的一部分。工具执行成功后,外层工具框架会在记录日志时调用它,用来显示一行好懂的摘要。

PlanToolOutput::success_for_logging29–31 ↗
fn success_for_logging(&self) -> bool

作用:告诉日志系统:这次工具调用应该被算作成功。它让日志里的成功/失败状态和实际行为保持一致。

数据流:它不接收额外数据 → 直接返回 true → 日志系统据此把这次 update_plan 标成成功。

调用关系:它和 log_preview 一起服务于工具调用后的记录环节。PlanHandler::handle_call 返回 PlanToolOutput 后,外层框架会用这个方法判断日志上该显示成功还是失败。

PlanToolOutput::to_response_item33–41 ↗
fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem

作用:把“计划已更新”的结果包装成模型能收到的回复格式。也就是说,它负责把内部成功结果翻译成协议里的函数调用输出。

数据流:输入是这次工具调用的 call_id 和工具载荷信息 → 它用固定文字 Plan updated 创建一份函数输出,并把成功标记设为 true → 返回一个带有同一个 call_idResponseInputItem::FunctionCallOutput,让模型知道对应哪次调用成功了。

调用关系:它会调用 from_text 来把普通文字变成协议需要的输出对象。它通常在 PlanHandler::handle_call 成功返回 PlanToolOutput 之后,由工具框架负责调用,用来生成发回模型的结果。

调用图:调用 1 个内部函数(from_text)。

PlanToolOutput::code_mode_result43–45 ↗
fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue

作用:给代码模式准备一个结果对象,但这里没有额外数据要返回,所以给出一个空 JSON 对象。JSON 可以理解成一种常见的数据包装格式。

数据流:它不使用传入的 payload → 新建一个空的 JSON 对象 → 返回这个空对象,不改变任何状态。

调用关系:它是工具输出接口的一部分。即使 update_plan 的主要反馈只是“成功”,框架在某些模式下仍可能要求一个结构化结果,所以这里提供一个空壳来满足接口。

调用图:外部调用 2 个(Object, new)。

PlanHandler::tool_name49–51 ↗
fn tool_name(&self) -> ToolName

作用:告诉工具注册系统:这个处理器对应的工具名字叫 update_plan。没有这个名字,外部调用就找不到该交给谁处理。

数据流:它不接收业务数据 → 用 plain 创建一个普通工具名 update_plan → 返回给工具注册或匹配流程使用。

调用关系:工具框架会在注册或查找工具时调用它。它把 PlanHandler 和名为 update_plan 的工具绑定起来,后续模型调用这个名字时,才会走到这个处理器。

调用图:调用 1 个内部函数(plain)。

PlanHandler::spec53–55 ↗
fn spec(&self) -> ToolSpec

作用:提供 update_plan 这个工具的说明书。说明书会告诉模型这个工具怎么调用、参数长什么样。

数据流:它不接收业务输入 → 调用 create_update_plan_tool 生成工具规格 → 返回这份规格给工具系统。

调用关系:它把实际处理器和工具定义连接起来。工具框架会用它拿到 update_plan 的参数规则,模型也依赖这份规则来正确发起调用。

调用图:调用 1 个内部函数(create_update_plan_tool)。

PlanHandler::handle57–59 ↗
fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_>

作用:这是工具框架真正调用的入口。它把一次 update_plan 调用接过来,并转交给异步的 handle_call 去做具体检查和发送事件。

数据流:输入是一份 ToolInvocation,里面包含会话、当前轮次、调用编号和参数等信息 → 它把这些信息交给 handle_call,并用 Box::pin 包成框架需要的异步任务 → 返回一个未来会完成的执行结果。

调用关系:当模型调用 update_plan 时,工具框架会先进入这里。它自己不做细活,而是把工作交给 PlanHandler::handle_call,这样符合框架对异步工具执行的要求。

调用图:调用 1 个内部函数(handle_call);外部调用 1 个(pin)。

PlanHandler::handle_call63–96 ↗
async fn handle_call(
        &self,
        invocation: ToolInvocation,
    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError>

作用:这是更新计划的核心流程:检查调用是否合法,解析参数,然后发出计划更新事件。它保证只有格式正确、模式允许的调用,才会真的改动计划。

数据流:输入是一整份工具调用信息 → 它取出 session、turn 和 payload;如果 payload 不是函数参数,就返回给模型看的错误;如果当前是 Plan 模式,也返回错误;否则调用 parse_update_plan_arguments 把参数 JSON 解析成 UpdatePlanArgs → 通过 session 发送 EventMsg::PlanUpdate(args) 事件 → 最后返回一个 PlanToolOutput,表示更新成功。

调用关系:它由 PlanHandler::handle 调用,是这个文件里真正干活的地方。它会把参数解析交给 parse_update_plan_arguments,把系统通知交给 session 的事件发送机制,并用 boxed_tool_output 把成功结果包装成工具框架能接收的形式。遇到不该继续的情况时,它用 RespondToModel 生成错误,让模型知道哪里不对。

调用图:调用 2 个内部函数(boxed_tool_output, parse_update_plan_arguments);被 1 处调用(handle);外部调用 2 个(PlanUpdate, RespondToModel)。

parse_update_plan_arguments101–105 ↗
fn parse_update_plan_arguments(arguments: &str) -> Result<UpdatePlanArgs, FunctionCallError>

作用:把模型传来的参数字符串读成系统能理解的计划更新数据。它就像把一张手写表格誊成标准表单,方便后面可靠处理。

数据流:输入是一段 JSON 字符串形式的参数 → 它尝试用 JSON 解析器把字符串转成 UpdatePlanArgs → 成功时返回结构化数据;失败时返回一个给模型看的错误,说明函数参数解析失败以及具体原因。

调用关系:它由 PlanHandler::handle_call 在真正发送计划更新事件前调用。这样 handle_call 不需要自己关心 JSON 怎么解析,只要拿到解析好的计划数据,或者把解析错误反馈给模型。

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

TUI 线程会话状态

这些文件定义 TUI 的标准单线程会话快照、事件缓冲区,以及用于启动、切换和渲染线程支持会话的高层流程。

tui/src/session_state.rs源码 ↗
data_modelcross-cutting

这个文件像是 TUI 里的“会话档案袋”。一个对话线程进入终端界面后,界面需要知道很多事:它属于哪个线程、是不是从别的线程分叉出来的、现在用什么模型、当前工作目录在哪里、有哪些可访问的工作区、权限策略是什么、有没有消息历史、是否走网络代理等。如果这些信息散落在各处,界面很容易显示错,或者把旧权限误当成新权限。这里的核心类型是 ThreadSessionState,它保存一份标准的会话快照。SessionNetworkProxyRuntime 记录运行时代理地址,MessageHistoryMetadata 记录历史消息的编号和数量。文件里唯一的行为函数用于切换当前目录时,同时修正“隐含的工作区根目录”,避免状态栏和权限范围还停留在旧目录上。

函数细节1
ThreadSessionState::set_cwd_retargeting_implicit_runtime_workspace_root60–76 ↗
fn set_cwd_retargeting_implicit_runtime_workspace_root(
        &mut self,
        cwd: AbsolutePathBuf,
    )

作用:这个函数用来把会话的当前工作目录换成新的目录,并在必要时同步更新工作区根目录。简单说,就是“人已经搬家了,门牌和可活动范围也要跟着改”。

数据流:进去的是一个新的当前目录 cwd,以及当前 ThreadSessionState 里原本保存的目录和工作区根目录列表。函数先把旧的 cwd 换成新的 cwd;如果旧目录本来不在工作区根目录列表里,就只改当前目录,别的不动。如果旧目录确实是一个隐含的工作区根目录,它会把这个旧根目录替换成新 cwd,再把其他原有根目录按顺序放回去,并避免重复。出来的结果是:会话的当前目录变了,相关的工作区根目录也被安全地重定向到新目录。

调用关系:它会在 apply_thread_settings_to_session 应用线程设置时被调用,也就是外部设置告诉 TUI“这个会话现在应该切到另一个目录”时。函数内部只做本地状态调整,借助标准库里的 replace、take 和 clone 完成“替换旧值、临时拿走列表、复制新路径”这些小动作,不再把工作交给别的业务函数。

调用图:被 1 处调用(apply_thread_settings_to_session);外部调用 3 个(replace, take, clone)。

tui/src/app/thread_events.rs源码 ↗
domain_logiccross-cutting:线程切换、收到服务器事件、重放界面状态、会话刷新时都会用到

这个文件像给每个聊天线程准备了一个小收纳盒。服务器发来的通知、需要用户批准的请求、历史查询结果、反馈提交结果,都会先放进这里。这样用户切到别的线程再切回来时,界面可以重放还重要的内容,而不是把“等你批准”“等你输入”这类事情弄丢。ThreadEventStore 是真正存东西的地方:它记住当前会话、已有回合、缓冲事件、正在进行的回合,以及输入框状态。它还会判断哪些请求已经解决,不该再弹出来。ThreadEventChannel 则像一根管道,把新事件送进来,同时共享同一个存储盒。文件末尾的测试专门检查:回合开始和结束是否跟踪正确、刷新会话后哪些事件该保留、哪些已解决请求不该重放。

函数细节37
ThreadEventStore::event_survives_session_refresh53–62 ↗
fn event_survives_session_refresh(event: &ThreadBufferedEvent) -> bool

作用:判断一个已经缓存的事件,在会话刷新后还值不值得留下。比如用户还没处理的请求、钩子运行状态、MCP 服务状态、反馈结果,刷新后仍然有意义。

数据流:输入一个缓存事件 → 按事件种类检查它是否属于“刷新后还不能丢”的少数类型 → 输出 true 或 false,不改动任何数据。

调用关系:它是刷新缓存时的筛子,ThreadEventStore::rebase_buffer_after_session_refresh 会用它决定哪些旧事件保留。

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

ThreadEventStore::new64–75 ↗
fn new(capacity: usize) -> Self

作用:创建一个空的线程事件仓库。调用者会给一个容量上限,防止事件无限堆积。

数据流:输入容量数字 → 建好空会话、空回合列表、空事件队列、默认的待重放状态,并标记当前没有活动回合 → 输出一个新的 ThreadEventStore。

调用关系:这是很多流程和测试的起点。ThreadEventChannel::new 也会用它给新通道配一个共享仓库。

调用图:被 23 处调用(agent_status_uses_bounded_buffered_activity, agent_status_uses_reasoning_summaries_only, request_user_input_does_not_count_as_pending_thread_approval, thread_event_snapshot_drops_answered_request_user_input_for_multi_prompt_turn, thread_event_snapshot_drops_pending_approvals_when_turn_completes, thread_event_snapshot_drops_pending_requests_when_thread_closes, thread_event_snapshot_drops_resolved_elicitation_after_outbound_resolution, thread_event_snapshot_drops_resolved_exec_approval_after_outbound_approval_id, thread_event_snapshot_drops_resolved_exec_approval_after_server_resolution, thread_event_snapshot_drops_resolved_patch_approval_after_outbound_approval (+13 more));外部调用 3 个(new, new, default)。

ThreadEventStore::new_with_session78–87 ↗
fn new_with_session(
        capacity: usize,
        session: ThreadSessionState,
        turns: Vec<Turn>,
    ) -> Self

作用:创建一个已经带有会话和历史回合的仓库。适合从已有线程快照恢复,而不是从空白开始。

数据流:输入容量、会话信息、回合列表 → 先建空仓库,再塞入会话,并根据回合列表找出是否有正在进行的回合 → 输出恢复好的仓库。

调用关系:ThreadEventChannel::new_with_session 会通过它创建带历史状态的通道;测试也用它确认恢复后还能知道哪个回合正在运行。

调用图:被 2 处调用(new_with_session, thread_event_store_restores_active_turn_from_snapshot_turns);外部调用 1 个(new)。

ThreadEventStore::set_session89–92 ↗
fn set_session(&mut self, session: ThreadSessionState, turns: Vec<Turn>)

作用:把仓库切换到某个新的会话状态,并同时更新这个会话已有的回合列表。

数据流:输入新的会话信息和回合列表 → 保存会话 → 调用 ThreadEventStore::set_turns 重新整理回合和活动回合 → 仓库变成新会话对应的状态。

调用关系:安装旁支线程快照时会用到它;它把具体的回合整理工作交给 ThreadEventStore::set_turns。

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

ThreadEventStore::rebase_buffer_after_session_refresh94–96 ↗
fn rebase_buffer_after_session_refresh(&mut self)

作用:会话刷新后清理缓存,只留下刷新后仍然有意义的事件。这样旧界面事件不会重复污染新会话。

数据流:读取当前事件队列 → 用 ThreadEventStore::event_survives_session_refresh 的规则过滤 → 队列里只剩请求、钩子状态、MCP 状态、反馈等需要保留的事件。

调用关系:它通常发生在服务端重新给出线程状态之后,用来把本地缓存重新贴到新会话上。

调用图:外部调用 1 个(retain)。

ThreadEventStore::set_turns98–105 ↗
fn set_turns(&mut self, turns: Vec<Turn>)

作用:替换仓库里的回合列表,并顺手找出最后一个还在进行的回合。

数据流:输入一组回合 → 从后往前找状态是 InProgress 的回合 → 保存这个回合 id 到 active_turn_id,再保存整组回合。

调用关系:ThreadEventStore::set_session 和 ThreadEventStore::new_with_session 都依赖它恢复“当前哪个回合还没结束”。

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

ThreadEventStore::push_notification107–133 ↗
fn push_notification(&mut self, notification: ServerNotification)

作用:把服务器通知放进线程缓存,并更新一些随通知变化的状态,比如哪个回合正在进行、哪些交互请求已经解决。

数据流:输入一个服务器通知 → 先让待重放状态记录这条通知 → 如果是回合开始就记住活动回合,如果是对应回合完成或线程关闭就清空活动回合 → 把通知压入队列;如果队列超容量,就丢掉最旧事件,并在丢掉请求时通知待重放状态。

调用关系:收到服务器通知时会走这里。它一边保存可重放事件,一边维护 PendingInteractiveReplayState,避免以后重放过期请求。

调用图:调用 2 个内部函数(note_evicted_server_request, note_server_notification);外部调用 4 个(len, pop_front, push_back, Notification)。

ThreadEventStore::push_request135–146 ↗
fn push_request(&mut self, request: ServerRequest)

作用:把服务器发来的“需要用户处理”的请求放进缓存,比如批准命令或输入内容。

数据流:输入一个服务器请求 → 记录到待重放状态 → 把请求压入队列 → 如果超过容量,丢掉最旧事件;如果丢掉的是请求,就更新待重放状态,说明它已经不能再从缓存里重放。

调用关系:服务器要求用户做决定时会用它。它和 ThreadEventStore::push_notification 配合,维持“哪些请求还该出现”的判断。

调用图:调用 2 个内部函数(note_evicted_server_request, note_server_request);外部调用 4 个(len, pop_front, push_back, Request)。

ThreadEventStore::pending_replay_requests148–165 ↗
fn pending_replay_requests(&self) -> Vec<ServerRequest>

作用:找出当前缓存里还应该被重新展示的服务器请求。它不会把已经回答或已经解决的请求再拿出来烦用户。

数据流:读取事件队列 → 只挑出 Request 类型 → 再询问待重放状态这个请求是否还该重放 → 输出这些请求的副本列表。

调用关系:重建线程界面或切回线程时会用它,给界面补回仍在等待用户操作的请求。

调用图:外部调用 1 个(iter)。

ThreadEventStore::file_change_changes167–199 ↗
fn file_change_changes(
        &self,
        turn_id: &str,
        item_id: &str,
    ) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>>

作用:根据回合 id 和条目 id,找某个文件改动条目的具体改动内容。用于界面需要展开或查看一次文件变更细节时。

数据流:输入 turn_id 和 item_id → 先从最新缓存事件倒着找匹配的文件变更 → 找不到再从已保存回合里倒着找 → 输出改动列表;如果没有匹配项就输出 None。

调用关系:它依赖 turn_id_matches 和 file_change_item_changes 这两个小工具来做匹配。这样无论文件变更还在实时通知里,还是已经落到历史回合里,都能查到。

调用图:外部调用 1 个(iter)。

ThreadEventStore::apply_thread_rollback201–206 ↗
fn apply_thread_rollback(&mut self, response: &ThreadRollbackResponse)

作用:应用一次线程回滚结果,把本地状态退回服务器给出的版本。回滚后旧缓存不再可信,所以要清掉。

数据流:输入服务器的回滚响应 → 用响应里的回合列表替换本地回合 → 清空缓存事件 → 重置待重放状态和活动回合 → 仓库回到回滚后的干净状态。

调用关系:当线程发生回滚操作后会用它,避免界面继续显示回滚前的请求或通知。

调用图:外部调用 2 个(clear, default)。

ThreadEventStore::snapshot208–229 ↗
fn snapshot(&self) -> ThreadEventSnapshot

作用:把当前仓库打包成一个快照,供界面切换回来时重建显示。它会刻意过滤掉不该再重放的交互请求。

数据流:读取会话、回合、事件队列、输入框状态 → 复制这些信息 → 对 Request 事件额外检查是否仍该重放 → 输出 ThreadEventSnapshot。

调用关系:线程切换和界面重建时会用它。它是 ThreadEventStore 保存状态和 ChatWidget 重新展示状态之间的桥。

调用图:外部调用 1 个(iter)。

ThreadEventStore::note_outbound_op231–236 ↗
fn note_outbound_op(&mut self, op: T)

作用:记录应用主动发出去的操作,比如用户已经批准或拒绝某个请求。这样仓库知道某些等待项已经被处理。

数据流:输入一个可以转成 AppCommand 的操作 → 交给 pending_interactive_replay 更新内部判断 → 仓库之后生成快照时就不会重放已处理的请求。

调用关系:当 TUI 向服务器发送会影响请求状态的命令时会调用它;具体判断交给 PendingInteractiveReplayState。

调用图:调用 1 个内部函数(note_outbound_op)。

ThreadEventStore::op_can_change_pending_replay_state238–243 ↗
fn op_can_change_pending_replay_state(op: T) -> bool

作用:快速判断某个即将发出的操作,会不会改变“哪些请求还要重放”的状态。

数据流:输入一个可转成 AppCommand 的操作 → 交给 PendingInteractiveReplayState 的规则检查 → 输出 true 或 false。

调用关系:配置同步、功能开关更新、提交线程操作、尝试解决服务器请求等流程会先问它,只有相关操作才需要更新重放状态。

调用图:调用 1 个内部函数(op_can_change_state);被 5 处调用(sync_auto_review_runtime_state_from_effective_config, update_feature_flags, note_active_thread_outbound_op, submit_thread_op, try_resolve_app_server_request)。

ThreadEventStore::has_pending_thread_approvals245–248 ↗
fn has_pending_thread_approvals(&self) -> bool

作用:告诉外面:这个线程里是否还有没处理的批准请求。比如后台线程需要在列表上显示“等你批准”的提示。

数据流:读取 pending_interactive_replay 的状态 → 查询是否有线程级待批准项 → 输出布尔值。

调用关系:它是界面徽标和旁支状态判断的依据之一;真正的请求追踪由 PendingInteractiveReplayState 完成。

调用图:调用 1 个内部函数(has_pending_thread_approvals)。

ThreadEventStore::side_parent_pending_status250–264 ↗
fn side_parent_pending_status(&self) -> Option<SideParentStatus>

作用:给旁支线程的父级显示一个简短状态:是需要用户输入,还是需要用户批准,或者什么都不需要。

数据流:先检查是否有待用户输入 → 如果有,输出 NeedsInput → 否则检查是否有待批准 → 如果有,输出 NeedsApproval → 都没有则输出 None。

调用关系:旁支线程汇总状态会用它。它把复杂的待重放状态压缩成界面能直接显示的一种状态。

调用图:调用 2 个内部函数(has_pending_thread_approvals, has_pending_thread_user_input)。

ThreadEventStore::active_turn_id266–268 ↗
fn active_turn_id(&self) -> Option<&str>

作用:读取当前正在进行的回合 id。如果没有正在运行的回合,就返回空。

数据流:读取 active_turn_id → 把内部 String 以只读字符串形式返回 → 不改动仓库。

调用关系:界面或测试需要知道当前活动回合时会调用它;这个值主要由 push_notification 和 set_turns 更新。

ThreadEventStore::clear_active_turn_id270–272 ↗
fn clear_active_turn_id(&mut self)

作用:手动清掉当前活动回合记录。适合外部已经确认没有活跃回合,但缓存里还留着旧值的情况。

数据流:不需要输入 → 把 active_turn_id 设为 None → 之后 active_turn_id 会返回空。

调用关系:它是一个直接的重置开关,测试会确认它能清掉由回合开始通知写入的活动回合。

turn_id_matches275–277 ↗
fn turn_id_matches(request_turn_id: &str, candidate_turn_id: &str) -> bool

作用:比较两个回合 id 是否算匹配。特别的是,请求里的回合 id 如果是空字符串,就当作“通配”,表示都能匹配。

数据流:输入请求里的 turn_id 和候选 turn_id → 如果请求 id 为空或两者相等就返回 true,否则返回 false。

调用关系:ThreadEventStore::file_change_changes 用它在缓存事件和历史回合中匹配文件变更所属的回合。

file_change_item_changes279–287 ↗
fn file_change_item_changes(
    item: &ThreadItem,
    item_id: &str,
) -> Option<Vec<codex_app_server_protocol::FileUpdateChange>>

作用:从一个线程条目里取出指定文件变更的改动列表。只有条目确实是 FileChange 且 id 对上时才会返回内容。

数据流:输入一个 ThreadItem 和 item_id → 检查它是不是文件变更条目、id 是否相等 → 匹配则复制 changes 输出,否则输出 None。

调用关系:ThreadEventStore::file_change_changes 会反复调用它,在实时事件和历史回合里寻找目标文件改动。

ThreadEventChannel::new298–306 ↗
fn new(capacity: usize) -> Self

作用:创建一个新的线程事件通道。它同时准备发送端、接收端和一个共享的 ThreadEventStore。

数据流:输入容量 → 建立一个有容量限制的 mpsc 通道;mpsc 是“多生产者、单消费者”的消息管道 → 创建同容量的事件仓库并包进 Arc<Mutex>,也就是可共享、带锁的对象 → 输出 ThreadEventChannel。

调用关系:打开或跟踪线程时常用它。发送端负责送事件,接收端负责取事件,共享仓库负责保存可重放状态。

调用图:调用 1 个内部函数(new);被 14 处调用(discard_closed_side_thread_removes_local_state_without_server_rpc, enqueue_thread_event_does_not_block_when_channel_full, inactive_thread_approval_badge_clears_after_turn_completion_notification, inactive_thread_approval_bubbles_into_active_view, open_agent_picker_allows_existing_agent_threads_when_feature_is_disabled, open_agent_picker_clears_completed_path_backed_agent_running_state, open_agent_picker_keeps_missing_threads_for_replay, open_agent_picker_marks_loaded_threads_open, open_agent_picker_marks_terminal_read_errors_closed, open_agent_picker_preserves_cached_metadata_for_replay_threads (+4 more));外部调用 3 个(new, new, channel)。

ThreadEventChannel::mark_replay_only308–310 ↗
fn mark_replay_only(&mut self)

作用:把这个通道标记成“只用于重放”,表示它不是实时连接在收新事件的主通道上。

数据流:不需要输入 → 把 attachment 从 Live 改成 ReplayOnly → 之后 attachment 查询会看到新状态。

调用关系:在线程只需要恢复旧内容、不应该当作实时事件来源时会用它。

ThreadEventChannel::attachment312–314 ↗
fn attachment(&self) -> ThreadEventAttachment

作用:读取这个通道当前是实时模式还是只重放模式。

数据流:读取 attachment 字段 → 返回 ThreadEventAttachment 的值 → 不改动通道。

调用关系:外部流程可用它判断这个通道应不应该参与实时事件处理。

ThreadEventChannel::new_with_session317–331 ↗
fn new_with_session(
        capacity: usize,
        session: ThreadSessionState,
        turns: Vec<Turn>,
    ) -> Self

作用:创建一个带已有会话和回合历史的线程事件通道。适合从已加载的线程快照恢复。

数据流:输入容量、会话、回合列表 → 建立消息通道 → 用 ThreadEventStore::new_with_session 创建带历史状态的共享仓库 → 输出实时模式的 ThreadEventChannel。

调用关系:加载已有线程、恢复旁支线程、把非活动线程重放回界面时会用它;内部把仓库恢复工作交给 ThreadEventStore::new_with_session。

调用图:调用 1 个内部函数(new_with_session);被 16 处调用(active_turn_id_for_thread_uses_snapshot_turns, feedback_submission_for_inactive_thread_replays_into_origin_thread, inactive_thread_approval_badge_clears_after_turn_completion_notification, inactive_thread_approval_bubbles_into_active_view, inactive_thread_settings_notification_updates_cached_collaboration_mode, inactive_thread_started_notification_initializes_replay_session, inactive_thread_started_notification_preserves_primary_model_when_path_missing, refreshed_snapshot_session_persists_resumed_turns, replay_thread_snapshot_restores_draft_and_queued_input, replay_thread_snapshot_restores_pending_pastes_for_submit (+6 more));外部调用 3 个(new, new, channel)。

tests::test_thread_session359–382 ↗
fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState

作用:测试用的会话造假工具,快速生成一个看起来完整的 ThreadSessionState。

数据流:输入线程 id 和工作目录 → 填入固定的模型、权限、审批策略等测试值,并把路径转成绝对路径 → 输出一个测试会话。

调用关系:多个测试用它避免重复手写长长的会话结构;它不参与正式运行。

调用图:调用 1 个内部函数(read_only);外部调用 3 个(abs, new, new)。

tests::test_turn384–395 ↗
fn test_turn(turn_id: &str, status: TurnStatus, items: Vec<ThreadItem>) -> Turn

作用:测试用的回合造假工具,用少量参数生成一个 Turn。

数据流:输入回合 id、状态、条目列表 → 填好其余时间和错误字段的默认空值 → 输出一个测试回合。

调用关系:通知构造函数和恢复活动回合的测试会用它,让测试只关注回合状态而不是结构体细节。

tests::turn_started_notification397–405 ↗
fn turn_started_notification(thread_id: ThreadId, turn_id: &str) -> ServerNotification

作用:测试用工具,生成一条“某回合开始了”的服务器通知。

数据流:输入线程 id 和回合 id → 用 tests::test_turn 造一个进行中的回合,再包成 TurnStarted 通知 → 输出 ServerNotification。

调用关系:活动回合生命周期测试用它模拟服务器告诉 TUI:一个新回合已经开始。

调用图:外部调用 4 个(TurnStarted, new, to_string, test_turn)。

tests::turn_completed_notification407–420 ↗
fn turn_completed_notification(
        thread_id: ThreadId,
        turn_id: &str,
        status: TurnStatus,
    ) -> ServerNotification

作用:测试用工具,生成一条“某回合结束了”的服务器通知。

数据流:输入线程 id、回合 id、结束状态 → 用 tests::test_turn 造对应回合,补上完成时间和耗时 → 输出 TurnCompleted 通知。

调用关系:活动回合生命周期测试用它模拟服务器结束某个回合,检查仓库是否只清掉正确的活动回合。

调用图:外部调用 4 个(TurnCompleted, new, to_string, test_turn)。

tests::hook_started_notification422–443 ↗
fn hook_started_notification(thread_id: ThreadId, turn_id: &str) -> ServerNotification

作用:测试用工具,生成一条“钩子开始运行”的通知。钩子可以理解为用户提交前后自动跑的小检查程序。

数据流:输入线程 id 和回合 id → 填入固定的钩子名称、来源、状态、路径和提示文字 → 输出 HookStarted 通知。

调用关系:刷新缓存的测试用它确认钩子开始通知在会话刷新后仍会保留。

调用图:外部调用 4 个(HookStarted, new, test_path_buf, to_string)。

tests::hook_completed_notification445–475 ↗
fn hook_completed_notification(thread_id: ThreadId, turn_id: &str) -> ServerNotification

作用:测试用工具,生成一条“钩子运行结束”的通知,并带上警告和阻止信息。

数据流:输入线程 id 和回合 id → 填入固定的钩子运行摘要、完成时间、输出条目 → 输出 HookCompleted 通知。

调用关系:刷新缓存的测试把它和开始通知一起使用,确认完整钩子状态可以被重放。

调用图:外部调用 4 个(HookCompleted, test_path_buf, to_string, vec!)。

tests::exec_approval_request477–502 ↗
fn exec_approval_request(
        thread_id: ThreadId,
        turn_id: &str,
        item_id: &str,
        approval_id: Option<&str>,
    ) -> ServerRequest

作用:测试用工具,生成一个“执行命令前需要用户批准”的服务器请求。

数据流:输入线程 id、回合 id、条目 id、可选 approval_id → 填入固定命令、目录、理由和请求编号 → 输出 ServerRequest。

调用关系:已解决请求不应重放的测试用它模拟一个待批准命令。

调用图:外部调用 3 个(Integer, test_path_buf, to_string)。

tests::thread_event_store_tracks_active_turn_lifecycle505–526 ↗
fn thread_event_store_tracks_active_turn_lifecycle()

作用:测试仓库能否正确跟踪活动回合:开始时记录,别的回合结束时不误清,真正的活动回合结束时才清空。

数据流:创建空仓库 → 推入回合开始通知 → 检查活动回合变成 turn-1 → 推入另一个回合的完成通知 → 检查不变 → 推入 turn-1 完成通知 → 检查活动回合清空。

调用关系:它验证 ThreadEventStore::push_notification 和 ThreadEventStore::active_turn_id 的配合是否正确。

调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert_eq!, turn_completed_notification, turn_started_notification)。

tests::thread_event_store_restores_active_turn_from_snapshot_turns529–544 ↗
fn thread_event_store_restores_active_turn_from_snapshot_turns()

作用:测试从已有回合列表恢复仓库时,能找回正在进行的回合。

数据流:构造一个会话和两个回合,其中第二个是 InProgress → 用 new_with_session 建仓库并检查活动回合 → 再用 set_session 刷新另一个仓库并检查同样结果。

调用关系:它验证 ThreadEventStore::new_with_session、ThreadEventStore::set_session 和 ThreadEventStore::set_turns 的恢复逻辑。

调用图:调用 3 个内部函数(new, new, new_with_session);外部调用 4 个(assert_eq!, test_path_buf, test_thread_session, vec!)。

tests::thread_event_store_clear_active_turn_id_resets_cached_turn547–555 ↗
fn thread_event_store_clear_active_turn_id_resets_cached_turn()

作用:测试手动清除活动回合的方法确实有效。

数据流:创建仓库 → 推入回合开始通知,让 active_turn_id 有值 → 调用 clear_active_turn_id → 检查 active_turn_id 变成 None。

调用关系:它直接覆盖 ThreadEventStore::clear_active_turn_id 的行为,确保这个重置开关可靠。

调用图:调用 2 个内部函数(new, new);外部调用 2 个(assert_eq!, turn_started_notification)。

tests::thread_event_store_rebase_preserves_resolved_request_state558–579 ↗
fn thread_event_store_rebase_preserves_resolved_request_state()

作用:测试会话刷新后,即使请求事件本身被保留规则碰到,也不会把已经解决的批准请求重新显示出来。

数据流:创建仓库 → 推入一个执行批准请求 → 推入服务器请求已解决通知 → 执行 rebase → 生成 snapshot → 检查快照没有事件,并且没有待批准状态。

调用关系:它验证 rebase、push_request、push_notification、snapshot 和 has_pending_thread_approvals 一起工作时,不会让已处理请求死灰复燃。

调用图:调用 2 个内部函数(new, new);外部调用 5 个(Integer, ServerRequestResolved, assert!, assert_eq!, exec_approval_request)。

tests::thread_event_store_rebase_preserves_hook_notifications582–610 ↗
fn thread_event_store_rebase_preserves_hook_notifications()

作用:测试会话刷新不会丢掉钩子开始和结束通知。

数据流:创建仓库 → 推入钩子开始和结束通知 → 执行 rebase → 生成快照 → 把快照里的通知转成 JSON 比较,确认两条通知都还在且内容没变。

调用关系:它验证 ThreadEventStore::event_survives_session_refresh 对 HookStarted 和 HookCompleted 的保留规则。

调用图:调用 2 个内部函数(new, new);外部调用 3 个(assert_eq!, hook_completed_notification, hook_started_notification)。

tests::thread_event_store_rebase_preserves_mcp_startup_notifications613–637 ↗
fn thread_event_store_rebase_preserves_mcp_startup_notifications()

作用:测试会话刷新后,MCP 服务启动状态通知仍会保留。MCP 可以理解为外部工具服务的连接状态。

数据流:创建一条 MCP 服务失败通知 → 放进仓库 → 执行 rebase → 生成快照 → 检查快照里正好还有这条通知,内容一致。

调用关系:它验证 ThreadEventStore::event_survives_session_refresh 对 McpServerStatusUpdated 的保留规则,防止用户切换后看不到工具服务失败原因。

调用图:调用 2 个内部函数(new, new);外部调用 3 个(McpServerStatusUpdated, assert_eq!, panic!)。

tui/src/app/thread_session_state.rs源码 ↗
domain_logicmain loop / thread switching / session resume

这个文件解决的是“同一个应用里可能有多个对话线程,设置不能乱套”的问题。比如用户切换了服务档位,或改了文件访问权限,只应该更新当前活跃线程的缓存,不能顺手把旁边的支线线程也改了。它像是在给每个聊天窗口贴标签:当前用哪个模型、哪个服务档位、能不能写文件、工作目录在哪里。读取已有线程时,服务器给的 thread/read 信息不一定包含完整设置,所以这里会用当前界面里的安全设置做兜底,再从本地数据库里尽量找回当时用过的模型。文件末尾的测试专门检查这些边界:只更新活跃线程、不覆盖支线线程、保留特殊权限规则、读取线程时使用界面上的最新权限。

函数细节10
App::sync_active_thread_service_tier_to_cached_session11–33 ↗
async fn sync_active_thread_service_tier_to_cached_session(&mut self)

作用:把当前界面选择的服务档位同步到当前活跃线程的缓存会话里。服务档位可以理解成“这次对话用普通速度还是更快/更贵的通道”,缓存不更新的话,后续恢复线程时可能看到旧设置。

数据流:进去的是 App 自己保存的当前活跃线程编号、聊天组件里的当前服务档位,以及各线程的缓存通道。函数先确认现在确实有活跃线程;然后取出界面上的服务档位;接着只更新主线程缓存中匹配当前线程的那份会话,以及该线程事件通道里保存的会话。出来没有返回值,但对应 ThreadSessionState 的 service_tier 会被改成最新值。

调用关系:它通常在用户改变当前线程服务档位后被调用。它自己不再把任务交给别的业务函数,只是直接改 App 里两处可能保存会话快照的地方:主线程快照和线程事件通道里的快照。这样后面的恢复、继续对话或保存状态都会拿到一致的档位。

App::sync_active_thread_permission_settings_to_cached_session35–72 ↗
async fn sync_active_thread_permission_settings_to_cached_session(&mut self)

作用:把当前界面的权限设置同步到当前活跃线程的缓存会话里。权限设置决定模型什么时候要问用户、谁来审核、能读写哪些文件;如果缓存过期,继续这个线程时就可能用错安全规则。

数据流:进去的是 App 里的活跃线程编号、全局配置里的审批策略和审核人、聊天组件里当前真正生效的权限配置。函数把这些信息整理成一次更新动作,然后只写入当前活跃线程对应的 ThreadSessionState。出来没有返回值,但 approval_policy、approvals_reviewer、permission_profile、active_permission_profile 这些字段会变成最新值;其他线程不会被碰。

调用关系:它会在当前线程的权限设置发生变化后使用。调用事实里它会用到 from,把内部配置里的审批策略转换成应用服务器协议认识的 AskForApproval。它不负责决定权限内容本身,只负责把已经生效的权限快照写回当前线程缓存,防止支线线程被误改。

调用图:调用 1 个内部函数(from)。

App::session_state_for_thread_read74–132 ↗
async fn session_state_for_thread_read(
        &self,
        thread_id: ThreadId,
        thread: &Thread,
    ) -> ThreadSessionState

作用:根据服务器读回来的 Thread 信息,组装一份本地可用的 ThreadSessionState。简单说,就是把“服务器告诉我的线程资料”和“当前界面/本地能补上的设置”合成一张完整的线程配置卡。

数据流:进去的是要读取的 thread_id、服务器返回的 Thread,以及 App 里当前模型、权限、工作目录、本地数据库等信息。函数先拿当前界面生效的权限设置;如果已有主线程会话快照,就尽量复用,但如果线程编号不同,会清掉只属于原线程的协作模式和个性设置,避免串线;如果没有快照,就新建一份默认会话。随后它用服务器 Thread 覆盖线程名、模型提供方、工作目录、记录路径等字段,并尝试调用 read_session_model 从本地记录里找回模型名。出来是一份新的 ThreadSessionState,不会带消息历史,适合后续加载或切换到该线程。

调用关系:它在执行 thread/read,也就是读取某个线程详情时处在关键位置。它会调用 App::current_permission_profile 和 App::current_active_permission_profile 取得当前界面真正生效的权限;还会调用 read_session_model 查本地保存的模型信息;需要创建默认字段时会用到 new 这类外部构造函数。它的作用是把零散来源拼成一份安全、可继续使用的会话状态。

调用图:调用 4 个内部函数(from, current_active_permission_profile, current_permission_profile, read_session_model);外部调用 1 个(new)。

App::current_permission_profile134–140 ↗
fn current_permission_profile(&self) -> PermissionProfile

作用:取出聊天组件里当前生效的权限方案。权限方案就是一组“能不能访问网络、能读写哪些文件”的规则。

数据流:进去的是 App 自身。函数读取 chat_widget 里的配置引用,从 permissions 中取出 permission_profile,并复制一份返回。它不修改任何东西,只把当前权限快照交出去。

调用关系:它被 App::session_state_for_thread_read 调用,用来确保读取线程时使用的是界面上当前真正生效的权限,而不是可能已经过时的 App 默认配置。它是一个很小的取数辅助函数。

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

App::current_active_permission_profile142–147 ↗
fn current_active_permission_profile(&self) -> Option<ActivePermissionProfile>

作用:取出当前正在使用的“命名权限配置”的信息,如果有的话。可以把它理解成用户选择了某个预设权限套餐时,这里返回那个套餐的身份标记。

数据流:进去的是 App 自身。函数从 chat_widget 的配置引用里读取 permissions,再取 active_permission_profile。出来是一个可能为空的 ActivePermissionProfile;为空表示当前没有对应的活动预设资料。函数不改状态。

调用关系:它被 App::session_state_for_thread_read 调用,和 App::current_permission_profile 搭配使用:一个拿具体规则,一个拿当前预设资料的标记。这样恢复线程时既知道规则是什么,也知道这些规则来自哪个活动配置。

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

tests::test_thread_session173–196 ↗
fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState

作用:为测试快速造出一份假的 ThreadSessionState。这样每个测试不用重复写一大堆会话字段,只要改自己关心的部分。

数据流:进去的是线程编号和工作目录。函数把它们和一套固定的测试值组合起来,比如测试模型名、测试模型提供方、只读权限、默认审批策略,并把路径转成绝对路径。出来是一份完整的 ThreadSessionState 测试对象。

调用关系:它是后面多个测试的搭积木工具。调用事实里它会用 read_only 创建只读权限,会用 abs 处理路径,还会用 new 和 vec! 构造字段。其他测试先拿它生成基础会话,再覆盖某些字段来模拟不同场景。

调用图:调用 1 个内部函数(read_only);外部调用 4 个(abs, new, new, vec!)。

tests::permission_settings_sync_updates_active_snapshot_without_rewriting_side_thread199–287 ↗
async fn permission_settings_sync_updates_active_snapshot_without_rewriting_side_thread()

作用:验证权限同步只会更新当前活跃主线程,不会误改旁边的支线线程。这是在防止一个很危险的错误:用户改主线程权限,却把另一个线程的安全规则也改掉。

数据流:进去的是测试自己搭出来的 App、主线程、支线线程和两份会话。测试把主线程设为活跃线程,给 App 配上新的审批策略、审核人和权限资料,然后调用 App::sync_active_thread_permission_settings_to_cached_session。出来通过断言检查:主线程的主缓存和通道缓存都更新了,支线线程通道里的会话保持原样。

调用关系:它调用 make_test_app 搭测试应用,用 from_string 造线程编号,用 tests::test_thread_session 造会话,用 ThreadEventChannel::new_with_session 放入缓存,并用 workspace_write、active、allow_any 等工具设置期望权限。最后它直接验证 App::sync_active_thread_permission_settings_to_cached_session 的行为是否正确。

调用图:调用 8 个内部函数(new, allow_any, active, workspace_write, from_string, new, make_test_app, new_with_session);外部调用 4 个(new, assert_eq!, test_path_buf, test_thread_session)。

tests::permission_settings_sync_preserves_active_profile_only_rules290–353 ↗
async fn permission_settings_sync_preserves_active_profile_only_rules()

作用:验证权限同步不会把当前会话里那些只存在于活动权限资料里的细规则弄丢。比如“根目录只读”和“拒绝访问 .env 文件”这种规则,必须原样保留下来。

数据流:进去的是测试构造的 App、一条线程和一份带有复杂受限权限的会话。测试把该线程设为活跃线程,改审批策略,然后调用 App::sync_active_thread_permission_settings_to_cached_session。出来检查缓存会话的审批策略更新了,但原来的复杂 permission_profile 仍然保留。

调用关系:它用 make_test_app、from_string、tests::test_thread_session 和 ThreadEventChannel::new_with_session 搭环境,用 allow_any 设置审批策略,然后让被测函数执行。它补充覆盖了上一条测试没覆盖的情况:权限资料本身带有细粒度规则时,同步过程不能把它简化或覆盖掉。

调用图:调用 4 个内部函数(allow_any, from_string, make_test_app, new_with_session);外部调用 5 个(new, assert_eq!, test_path_buf, test_thread_session, vec!)。

tests::service_tier_sync_updates_active_cached_session356–397 ↗
async fn service_tier_sync_updates_active_cached_session()

作用:验证服务档位同步会正确更新当前活跃线程的缓存。这里特别检查从某个已有档位改成“没有指定档位”时,缓存也会跟着清空。

数据流:进去的是测试 App、一条线程和一份原本带 Fast 服务档位的会话。测试把线程设为活跃线程,把聊天组件里的服务档位设成 None,然后调用 App::sync_active_thread_service_tier_to_cached_session。出来检查主线程快照和线程通道快照里的 service_tier 都变成 None。

调用关系:它用 make_test_app、from_string、tests::test_thread_session、ThreadEventChannel::new_with_session 搭好状态,然后直接验证 App::sync_active_thread_service_tier_to_cached_session。它确保服务档位这个字段和权限字段一样,不会只改界面、不改缓存。

调用图:调用 3 个内部函数(from_string, make_test_app, new_with_session);外部调用 4 个(new, assert_eq!, test_path_buf, test_thread_session)。

tests::thread_read_fallback_uses_active_permission_settings400–453 ↗
async fn thread_read_fallback_uses_active_permission_settings()

作用:验证读取另一个线程时,如果需要补权限设置,应该用聊天界面里当前生效的权限,而不是 App 配置里可能过时的默认值。

数据流:进去的是测试 App、一个主线程会话,以及模拟服务器返回的 read_thread。测试让聊天组件先吃进主线程会话,使界面权限变成工作区可写;然后调用 App::session_state_for_thread_read 读取另一个线程。出来检查生成的 session.permission_profile 等于聊天组件当前权限,并且不等于 App 配置里的旧权限。

调用关系:它用 make_test_app、from_string、workspace_write、tests::test_thread_session 构造场景,再调用 App::session_state_for_thread_read。这个测试专门保护 App::current_permission_profile 这条取值路径,确保 thread/read 的兜底权限来自当前界面状态。

调用图:调用 3 个内部函数(workspace_write, from_string, make_test_app);外部调用 5 个(new, assert_eq!, assert_ne!, test_path_buf, test_thread_session)。

tui/src/app/loaded_threads.rs源码 ↗
domain_logic线程恢复或切换到已有线程时

应用服务器只给 TUI 一张“当前已加载线程”的平铺名单,就像把所有家庭成员都倒在一张表里;但 TUI 需要知道哪些是某个主线程的“孩子、孙子、重孙”。这个文件做的事就是顺着 ThreadSpawn(线程生成记录,说明某线程是由哪个父线程创建的)这条线往下找。它从主线程编号开始,找直接子线程,再拿子线程继续找下一层,直到没有新孩子为止。主线程自己不会放进结果。最后它只保留界面需要的少量信息,比如线程编号、智能体昵称、角色和路径,并按线程编号排序,让测试和缓存结果稳定。整个过程不访问网络、不读写文件,也不异步运行,所以很好单独测试。

函数细节6
find_loaded_subagent_threads_for_primary47–96 ↗
fn find_loaded_subagent_threads_for_primary(
    threads: Vec<Thread>,
    primary_thread_id: ThreadId,
) -> Vec<LoadedSubagentThread>

作用:从服务器给的一整包线程里,挑出属于某个主线程的所有子智能体线程。它解决的是“名单是扁平的,但界面需要一棵家族树”的问题。

数据流:输入是一组 Thread 和一个主线程编号。它先把能识别出合法编号的线程放进按编号查找的表里;然后从主线程开始,一层层检查谁的生成记录写着“我的父线程是当前这个线程”;找到后就加入结果,并继续拿它当父线程往下找。最后输出一组 LoadedSubagentThread,只带线程编号、昵称、角色和智能体路径,并把结果按线程编号转成字符串后的顺序排好;它不会改动外部状态。

调用关系:这是本文件的核心入口。界面需要重建子智能体导航信息时会用它;测试里的 tests::finds_loaded_subagent_tree_for_primary_thread 会专门喂给它一组主线程、子线程、孙线程和无关线程,确认它只挑出真正属于主线程家族的线程。它在查父子关系时把判断交给 thread_spawn_parent_thread_id,避免主流程里混入太多格式细节。

调用图:调用 2 个内部函数(from_string, thread_spawn_parent_thread_id);被 1 处调用(finds_loaded_subagent_tree_for_primary_thread);外部调用 3 个(new, new, vec!)。

thread_spawn_agent_path98–105 ↗
fn thread_spawn_agent_path(source: &SessionSource) -> Option<String>

作用:从一个线程的来源信息里,拿出“这个子智能体来自哪个路径”。如果这个线程不是由子智能体生成的,就返回没有路径。

数据流:输入是 SessionSource,也就是“这个会话从哪里来的”的记录。函数只认 SubAgent 里的 ThreadSpawn 这种来源;如果里面带有 agent_path,就复制成普通字符串返回;否则返回 None,表示没有可用路径。

调用关系:它是一个小型拆包工具,和 thread_spawn_parent_thread_id 一样,专门把复杂的来源结构翻译成主流程能直接使用的简单值。主流程在组装 LoadedSubagentThread 时需要这种信息,方便界面之后显示或缓存子智能体元数据。

thread_spawn_parent_thread_id107–114 ↗
fn thread_spawn_parent_thread_id(source: &SessionSource) -> Option<ThreadId>

作用:判断一个线程是不是由另一个线程生成的,并取出它记录的父线程编号。主查找流程靠它来认出“谁是谁的孩子”。

数据流:输入是线程来源 SessionSource。它检查来源是不是 SubAgent 里的 ThreadSpawn;如果是,就拿出 parent_thread_id 返回;如果不是,比如普通命令行会话,就返回 None。它只读取传入的数据,不做任何修改。

调用关系:find_loaded_subagent_threads_for_primary 在遍历线程名单时反复调用它。主流程每看到一个线程,就问这个函数“你有没有父线程编号”,再用答案决定这个线程是否应该加入主线程的子树。

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

tests::test_thread128–151 ↗
fn test_thread(thread_id: ThreadId, source: SessionSource) -> Thread

作用:给测试快速造一个 Thread 假数据。测试只关心线程编号和来源,但真正的 Thread 结构需要很多字段,所以这个函数帮忙填好一套默认值。

数据流:输入是线程编号和来源信息。它把编号转成字符串,给会话编号、状态、时间、工作目录等字段填上测试用的固定值,最后产出一个完整的 Thread。这个 Thread 可以再被测试按需要改昵称、角色等字段。

调用关系:它只在测试模块里服务 tests::finds_loaded_subagent_tree_for_primary_thread。这样测试正文不用被一大堆无关字段淹没,可以把重点放在父子线程关系上。

调用图:外部调用 4 个(new, new, test_path_buf, to_string)。

tests::thread_spawn_source153–170 ↗
fn thread_spawn_source(
        parent_thread_id: ThreadId,
        depth: i32,
        agent_nickname: &str,
        agent_role: &str,
    ) -> SessionSource

作用:给测试造一个“子智能体线程是从某个父线程生成的”来源记录。它让测试能很方便地搭出主线程、子线程、孙线程这种关系。

数据流:输入是父线程编号、深度、智能体昵称和角色。它先拼出一段 JSON(常见的数据文本格式),再反序列化成 SessionSource,也就是代码真正使用的来源结构;如果格式不对,测试会直接失败。

调用关系:它被 tests::finds_loaded_subagent_tree_for_primary_thread 用来生成子线程和孙线程的来源信息。这样测试能模拟服务器真实返回的 ThreadSpawn 数据,而不是手写复杂结构。

调用图:外部调用 2 个(from_value, json!)。

tests::finds_loaded_subagent_tree_for_primary_thread173–231 ↗
fn finds_loaded_subagent_tree_for_primary_thread()

作用:验证主查找函数真的能找到完整的子智能体家族树,并且不会把无关线程混进来。它覆盖了直接子线程和更深一层孙线程的情况。

数据流:它先创建一个主线程编号、一个子线程编号、一个孙线程编号,以及一组无关线程编号;再用测试辅助函数造出对应 Thread。然后把这些线程一起传给 find_loaded_subagent_threads_for_primary。最后用断言检查输出正好是子线程和孙线程,并带着正确的昵称、角色,且没有无关线程。

调用关系:这是本文件自带的安全网。它直接调用 find_loaded_subagent_threads_for_primary,并借助 tests::test_thread 和 tests::thread_spawn_source 搭测试场景;如果以后有人改坏了父子查找规则,这个测试会第一时间报错。

调用图:调用 2 个内部函数(from_string, find_loaded_subagent_threads_for_primary);外部调用 4 个(assert_eq!, test_thread, thread_spawn_source, vec!)。

tui/src/app/session_lifecycle.rs源码 ↗
orchestrationstartup、session switching、resume/fork、agent navigation、cross-cutting

可以把这个文件想成聊天应用里的“换轨调度员”。主会话、子代理会话、已保存的历史会话,都像不同轨道上的车;用户打开代理选择器、切到某个代理、恢复一个旧会话、重新开始一个新会话时,它负责确认那条轨道还在不在、是否还活着、有没有历史记录可以回放。它会向 app server(后台服务,真正保存和运行线程的地方)询问线程状态,维护本地的代理导航缓存,并在需要时重建 ChatWidget(屏幕上的聊天窗口)。它还特别处理一些容易出错的情况:比如线程已关闭但历史还能看,或者后台暂时不能返回完整消息记录时,不会贸然创建一个空白窗口挡住后续真实连接。整体上,它保证“用户当前看到的聊天窗口”和“后台实际线程状态”尽量一致。

函数细节26
App::open_agent_picker10–138 ↗
async fn open_agent_picker(&mut self, app_server: &mut AppServerSession)

作用:打开“子代理选择器”,也就是让用户挑选要查看或切换到哪个代理线程。它会先刷新代理列表和运行状态,避免把已经没了或状态过期的线程展示给用户。

数据流:进去的是当前 App 状态和 app server 连接;它先补拉后台已加载的子代理,再逐个检查线程是不是还在运行,然后整理成选择列表或状态预览;出来的是聊天窗口里出现一个选择弹窗、提示信息,或者一段正在运行代理的状态卡片。

调用关系:这是用户触发 /agent 或类似入口时的前台流程。它会把补齐缓存的活交给 App::backfill_loaded_subagent_threads,把确认线程死活的活交给 App::refresh_agent_picker_thread_liveness,最后调用聊天窗口的选择视图来展示结果。

调用图:调用 6 个内部函数(picker_subtitle, new, empty, from_store, backfill_loaded_subagent_threads, refresh_agent_picker_thread_liveness);外部调用 2 个(default, new)。

App::is_terminal_thread_read_error140–143 ↗
fn is_terminal_thread_read_error(err: &color_eyre::Report) -> bool

作用:判断一次读取线程失败,是不是“这个线程确实已经不能读了”的终局错误。这里的终局错误主要指后台明确说 thread not loaded,也就是线程没有加载。

数据流:进去的是一个错误对象;它沿着错误链一层层看文字信息;出来一个真假值,表示这个错误是不是线程不存在或已卸载这类不可继续的情况。

调用关系:它是多个恢复和刷新流程里的小判定器。App::closed_state_for_thread_read_errorApp::refresh_agent_picker_thread_liveness 会用它来决定一个线程该被标成关闭,还是只当成临时网络失败。

调用图:外部调用 1 个(chain)。

App::closed_state_for_thread_read_error145–150 ↗
fn closed_state_for_thread_read_error(
        err: &color_eyre::Report,
        existing_is_closed: Option<bool>,
    ) -> bool

作用:根据读取线程时的错误,决定这个线程在界面上要不要显示成“已关闭”。它避免把普通临时故障误判成线程结束。

数据流:进去的是错误对象,以及缓存里原本的关闭状态;它先看错误是不是终局错误,如果不是,就保留原来的关闭状态;出来一个最终的关闭标记。

调用关系:它依赖 App::is_terminal_thread_read_error 做核心判断。App::refresh_agent_picker_thread_liveness 在后台读取失败时会用它更新代理选择器里的状态。

调用图:外部调用 1 个(is_terminal_thread_read_error)。

App::can_fallback_from_include_turns_error152–158 ↗
fn can_fallback_from_include_turns_error(err: &color_eyre::Report) -> bool

作用:判断读取完整聊天记录失败时,能不能退一步只读线程基本信息。这里的 includeTurns 指“连同每轮对话内容一起读取”。

数据流:进去的是一个错误对象;它检查错误文字里是否说明“还没有第一条用户消息所以不能带对话读取”或“临时线程不支持带对话读取”;出来真假值,告诉上层是否可以安全降级。

调用关系:它主要服务 App::attach_live_thread_for_selection。当恢复实时连接失败、再尝试读取历史也失败时,这个函数帮忙判断是否还能用不带历史的方式继续确认线程。

调用图:外部调用 1 个(chain)。

App::upsert_agent_picker_thread164–179 ↗
fn upsert_agent_picker_thread(
        &mut self,
        thread_id: ThreadId,
        agent_nickname: Option<String>,
        agent_role: Option<String>,
        is_closed: bool,
    )

作用:把某个代理线程写进选择器缓存;如果已经有,就更新它的名字、角色和关闭状态。upsert 可以理解成“有就改,没有就加”。

数据流:进去的是线程 ID、代理昵称、代理角色、是否关闭;它同时更新聊天窗口里的代理名字信息和导航缓存;出来没有单独返回值,但界面后续会用最新名字和状态显示。

调用关系App::backfill_loaded_subagent_threadsApp::refresh_agent_picker_thread_liveness 都会调用它。它把“后台读到的信息”同步到“用户能看到的代理列表和页脚标签”。

调用图:被 2 处调用(backfill_loaded_subagent_threads, refresh_agent_picker_thread_liveness)。

App::mark_agent_picker_thread_closed185–188 ↗
fn mark_agent_picker_thread_closed(&mut self, thread_id: ThreadId)

作用:把代理选择器里的某个线程标记为已关闭,但不把它删掉。这样用户仍然可以回看这个代理已经完成的聊天记录。

数据流:进去的是线程 ID;它在代理导航缓存里设置关闭标记,并刷新当前活动代理的显示标签;出来是界面状态被更新。

调用关系:它通常会在其他事件发现线程结束时被使用。它和删除不同,保留了导航顺序和历史入口。

App::refresh_agent_picker_thread_liveness190–254 ↗
async fn refresh_agent_picker_thread_liveness(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> bool

作用:刷新一个代理线程的“活着还是关闭”状态,并顺手更新名字和角色。它让选择器里的状态点尽量反映后台真实情况。

数据流:进去的是 app server 连接和线程 ID;它向后台读取线程基本信息,不读取完整对话;成功时根据后台状态更新运行中、关闭、昵称和角色,失败时判断是永久不可读还是临时错误;出来一个真假值,表示这个线程是否还应该留在选择器里。

调用关系App::open_agent_picker 打开选择器前会调用它,App::select_agent_thread 真正切换前也会调用它。它内部通过 App::upsert_agent_picker_thread 写缓存,并用错误判定函数处理失败场景。

调用图:调用 2 个内部函数(upsert_agent_picker_thread, thread_read);被 2 处调用(open_agent_picker, select_agent_thread);外部调用 3 个(closed_state_for_thread_read_error, is_terminal_thread_read_error, matches!)。

App::attach_live_thread_for_selection262–319 ↗
async fn attach_live_thread_for_selection(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> Result<bool>

作用:当用户选择一个本地还没连接过的代理线程时,尝试把它接到本地界面上。成功的话,用户看到的不只是旧记录,还能继续接收实时事件。

数据流:进去的是 app server 连接和线程 ID;它先尝试 resume_thread,也就是恢复实时连接;如果失败,就尝试 thread_read 读取历史记录作为回放;然后创建或找到本地事件通道,把会话和历史轮次塞进去;出来一个结果,告诉上层是否接上了实时线程,还是只能回放历史。

调用关系:它由 App::select_agent_thread 在切换到未缓存线程时调用。它会用 App::can_fallback_from_include_turns_error 判断能不能降级读取,避免制造一个没有内容、也不能实时更新的空通道。

调用图:调用 2 个内部函数(resume_thread, thread_read);被 1 处调用(select_agent_thread);外部调用 4 个(can_fallback_from_include_turns_error, new, eyre!, warn!)。

App::replace_chat_widget327–346 ↗
fn replace_chat_widget(&mut self, mut chat_widget: ChatWidget)

作用:把当前聊天窗口换成一个新的,同时把旧窗口里不能丢的信息搬过去。比如终端标题、远程连接状态、代理昵称这些都要保留。

数据流:进去的是一个新 ChatWidget;它从旧窗口取出终端标题和远程连接信息,再把导航缓存中的代理昵称和角色写进新窗口;出来是 App 持有的新聊天窗口,并刷新活动代理标签。

调用关系App::replace_chat_widget_with_app_server_thread 在换主线程时会用它,App::select_agent_thread 在切换代理线程时也会用它。它是“重建窗口但不丢上下文”的关键中间步骤。

调用图:调用 1 个内部函数(set_collab_agent_metadata);被 2 处调用(replace_chat_widget_with_app_server_thread, select_agent_thread)。

App::select_agent_thread348–443 ↗
async fn select_agent_thread(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> Result<()>

作用:真正切换到用户选中的代理线程。它要处理很多现实情况:线程可能已关闭、可能还没本地连接、可能只能看历史、也可能已经被别处激活。

数据流:进去的是界面、app server 连接和目标线程 ID;它先确认目标不是当前线程,再刷新线程状态,必要时附加实时线程,保存当前线程接收器,激活目标线程的本地回放快照,重建聊天窗口,清屏并回放历史;出来是界面显示目标代理线程,或者显示失败原因。

调用关系:这是代理选择器条目被点中后的主流程。它会调用 App::refresh_agent_picker_thread_liveness 检查状态,调用 App::attach_live_thread_for_selection 建立连接,调用 App::replace_chat_widget 换窗口,并用 App::reset_for_thread_switch 清理切换痕迹。

调用图:调用 5 个内部函数(attach_live_thread_for_selection, refresh_agent_picker_thread_liveness, replace_chat_widget, reset_for_thread_switch, should_attach_live_thread_for_selection);外部调用 2 个(format!, new_with_app_event)。

App::should_attach_live_thread_for_selection445–451 ↗
fn should_attach_live_thread_for_selection(&self, thread_id: ThreadId) -> bool

作用:判断切换到某个线程前,是否需要尝试建立实时连接。简单说:本地没有这个线程通道,而且它看起来还没关闭,就应该尝试接上。

数据流:进去的是线程 ID;它查看本地事件通道和代理导航缓存;出来一个真假值,表示是否要走实时附加流程。

调用关系App::select_agent_thread 用它决定是否调用 App::attach_live_thread_for_selection。它让切换逻辑不用重复写这些条件判断。

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

App::reset_for_thread_switch453–458 ↗
fn reset_for_thread_switch(&mut self, tui: &mut tui::Tui) -> Result<()>

作用:线程切换后清理屏幕和旧聊天记录的显示状态,避免新线程画面里混进上一个线程的残留内容。

数据流:进去的是 App 和 TUI;它重置聊天记录状态,清掉待写入的历史行,再清理终端可见屏幕和滚动缓冲;出来是一个干净的显示环境,或者返回清理失败的错误。

调用关系App::select_agent_thread 在换好聊天窗口后调用它。它把实际清终端的工作交给 App::clear_terminal_for_thread_switch

调用图:被 1 处调用(select_agent_thread);外部调用 2 个(clear_terminal_for_thread_switch, clear_pending_history_lines)。

App::clear_terminal_for_thread_switch460–473 ↗
fn clear_terminal_for_thread_switch(
        terminal: &mut crate::custom_terminal::Terminal<B>,
    ) -> Result<()>

作用:用 ANSI 控制方式清掉终端当前画面和回滚历史。ANSI 可以理解成终端能读懂的一组控制指令。

数据流:进去的是终端对象;它清空可见屏幕和滚动缓冲区,如果视口不是从顶部开始,还把视口调回顶部;出来是干净的终端状态,或者一个错误。

调用关系:它是 App::reset_for_thread_switch 的底层工具。上层负责决定什么时候切换线程,它负责具体把终端擦干净。

调用图:调用 2 个内部函数(clear_scrollback_and_visible_screen_ansi, set_viewport_area)。

App::reset_thread_event_state475–490 ↗
fn reset_thread_event_state(&mut self)

作用:清空所有线程事件相关状态,常用于换到一个全新的主线程前。这样旧线程的监听器、缓存、待处理请求不会污染新会话。

数据流:进去的是当前 App;它停止所有线程事件监听,清空事件通道、代理导航、副线程、活动线程、待处理事件和请求,并重置聊天窗口里的待审批信息;出来是一个像刚准备连接新线程的干净状态。

调用关系App::replace_chat_widget_with_app_server_thread 会先调用它,再接入新的后台线程。它是大换血前的“清场”。

调用图:被 1 处调用(replace_chat_widget_with_app_server_thread);外部调用 1 个(new)。

App::handle_startup_thread_started492–527 ↗
async fn handle_startup_thread_started(
        &mut self,
        app_server: &mut AppServerSession,
        result: Result<AppServerStartedThread, String>,
    ) -> Result<()>

作用:处理应用启动时后台新线程创建完成的结果。它要区分这个结果是不是还需要,如果已经过期,就把多余线程退订并丢掉。

数据流:进去的是 app server 和启动线程结果;如果当前已经不等待这个启动结果,它会退订那个后台线程并清理本地状态;如果还在等待,成功就把新主线程排队接入,失败就返回启动失败错误;出来是继续运行或错误。

调用关系:这是启动阶段的异步回调处理。它会调用后台退订接口 thread_unsubscribe,成功路径会把会话交给主线程接入流程。

调用图:调用 1 个内部函数(thread_unsubscribe);外部调用 2 个(eyre!, warn!)。

App::start_fresh_session_with_summary_hint529–595 ↗
async fn start_fresh_session_with_summary_hint(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        session_start_source: Option<ThreadStartSource>,

作用:开始一个全新的会话,同时尽量给用户留下旧会话的续接提示。这样用户换新会话时,不会完全失去“怎么回到刚才那段对话”的线索。

数据流:进去的是 TUI、app server、会话来源和可选的初始用户消息;它先尽量从磁盘刷新配置,保存当前模型和旧会话摘要,关闭当前线程并退订已跟踪线程,然后请求后台创建新线程;成功就替换聊天窗口并显示旧会话摘要提示,失败就显示错误并恢复模型设置;最后请求界面刷新。

调用关系:这是用户主动新开一局时的主流程。它用 App::fresh_session_config 生成配置,用 App::replace_chat_widget_with_app_server_thread 接入新线程,并通过后台的 start_thread_with_session_start_source 真正创建线程。

调用图:调用 4 个内部函数(fresh_session_config, replace_chat_widget_with_app_server_thread, start_thread_with_session_start_source, thread_unsubscribe);外部调用 5 个(new, frame_requester, format!, warn!, vec!)。

App::replace_chat_widget_with_app_server_thread597–618 ↗
async fn replace_chat_widget_with_app_server_thread(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        started: AppServerStartedThread,
        initial_

作用:把界面切换到后台刚创建、恢复或分叉出来的主线程。它是“拿到后台线程结果后,正式装到前台”的统一入口。

数据流:进去的是 TUI、app server、后台返回的线程数据,以及可选初始消息;它先清空旧线程事件状态,创建新的聊天窗口,接入主线程会话和历史轮次,再补拉已加载的子代理;出来是前台开始显示并监听这个后台线程。

调用关系App::start_fresh_session_with_summary_hintApp::resume_target_session 都会调用它。它内部用 App::reset_thread_event_state 清场,用 App::replace_chat_widget 换窗口,再用 App::backfill_loaded_subagent_threads 补齐子代理列表。

调用图:调用 3 个内部函数(backfill_loaded_subagent_threads, replace_chat_widget, reset_thread_event_state);被 2 处调用(resume_target_session, start_fresh_session_with_summary_hint);外部调用 1 个(new_with_app_event)。

App::backfill_loaded_subagent_threads631–691 ↗
async fn backfill_loaded_subagent_threads(
        &mut self,
        app_server: &mut AppServerSession,
    ) -> bool

作用:从后台补齐本地没见过的子代理线程。特别是在恢复旧会话时,TUI 可能错过了当初创建子代理的事件,所以需要主动问后台。

数据流:进去的是 app server;它先确认当前有主线程,再向后台拿所有已加载线程 ID,逐个读取基本信息,找出属于当前主线程的子代理,然后写入代理选择器缓存和聊天窗口元数据;出来一个真假值,表示补拉过程是否完全成功。

调用关系App::open_agent_picker 打开选择器前会用它,App::replace_chat_widget_with_app_server_thread 接入主线程后会用它,App::adjacent_thread_id_with_backfill 在键盘导航找不到邻居时也会用它。它通过 App::upsert_agent_picker_thread 写入缓存。

调用图:调用 4 个内部函数(from_string, upsert_agent_picker_thread, thread_loaded_list, thread_read);被 3 处调用(adjacent_thread_id_with_backfill, open_agent_picker, replace_chat_widget_with_app_server_thread);外部调用 2 个(new, warn!)。

App::adjacent_thread_id_with_backfill701–724 ↗
async fn adjacent_thread_id_with_backfill(
        &mut self,
        app_server: &mut AppServerSession,
        direction: AgentNavigationDirection,
    ) -> Option<ThreadId>

作用:为“上一个/下一个代理”快捷键找相邻线程。如果本地缓存暂时找不到,它会先去后台补一次子代理列表再重试。

数据流:进去的是 app server 和方向,比如向前或向后;它先在本地导航缓存里找相邻线程,找不到就检查是否已经为当前主线程补拉过,没补过就调用后台补拉,再重新查;出来是相邻线程 ID,或者没有可切换目标。

调用关系:它服务键盘导航。它把懒加载思想用在子代理列表上:平时不一定提前拉,用户第一次按切换键时再通过 App::backfill_loaded_subagent_threads 补齐。

调用图:调用 1 个内部函数(backfill_loaded_subagent_threads)。

App::fresh_session_config726–730 ↗
fn fresh_session_config(&self) -> Config

作用:生成新会话要用的配置副本,并把当前聊天窗口选定的服务档位写进去。服务档位可以理解成后台服务的性能或计费级别选择。

数据流:进去的是当前 App 配置和聊天窗口里的服务档位;它复制一份配置,替换其中的 service_tier;出来是一份可用于新建线程的配置。

调用关系App::start_fresh_session_with_summary_hint 在请求后台新开线程前调用它。它让新会话沿用当前设置,同时尊重用户在界面里刚选的档位。

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

App::resume_target_session731–840 ↗
async fn resume_target_session(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        target_session: SessionTarget,
    ) -> Result<AppRunControl>

作用:恢复一个指定的旧会话。它不仅要向后台恢复线程,还要处理工作目录、配置重建、通知设置、文件搜索目录和恢复后的提示。

数据流:进去的是 TUI、app server 和目标会话;它先判断是不是重复恢复当前线程,然后决定恢复时用哪个工作目录,重建配置并套用运行期策略;接着请求后台恢复线程,成功后关闭当前线程、替换聊天窗口、更新通知和搜索目录,并可能提示是否继续暂停的目标;失败则在聊天窗口显示错误;出来是应用继续运行或按用户选择退出。

调用关系:这是恢复会话菜单或命令的主流程。它会调用外部的工作目录解析逻辑 resolve_cwd_for_resume_or_fork,用后台 resume_thread 恢复线程,并把接入前台的工作交给 App::replace_chat_widget_with_app_server_thread

调用图:调用 4 个内部函数(replace_chat_widget_with_app_server_thread, resume_thread, display_label, resolve_cwd_for_resume_or_fork);外部调用 6 个(new, frame_requester, set_notification_settings, format!, Exit, vec!)。

tests::terminal_thread_read_error_detection_matches_not_loaded_errors848–854 ↗
fn terminal_thread_read_error_detection_matches_not_loaded_errors()

作用:测试“thread not loaded”这类错误能被识别为终局读取错误。它保证已卸载线程不会被误当成临时失败。

数据流:进去的是测试里构造的一条错误消息;它调用 App::is_terminal_thread_read_error 检查;出来是断言必须为真,否则测试失败。

调用关系:这是 App::is_terminal_thread_read_error 的正向测试,用一个典型的 not loaded 错误保护主逻辑判断不被改坏。

调用图:外部调用 2 个(assert!, eyre!)。

tests::terminal_thread_read_error_detection_ignores_transient_failures857–863 ↗
fn terminal_thread_read_error_detection_ignores_transient_failures()

作用:测试普通传输错误不会被误判为线程永久不可读。比如 broken pipe 更像是连接临时断了。

数据流:进去的是测试构造的 broken pipe 错误;它调用 App::is_terminal_thread_read_error;出来是断言必须为假。

调用关系:这是 App::is_terminal_thread_read_error 的反向测试,防止刷新代理状态时因为短暂网络问题就把线程删掉或关掉。

调用图:外部调用 2 个(assert!, eyre!)。

tests::closed_state_for_thread_read_error_preserves_live_state_without_cache_on_transient_error866–874 ↗
fn closed_state_for_thread_read_error_preserves_live_state_without_cache_on_transient_error()

作用:测试在没有缓存状态时,临时读取失败不会把线程标成关闭。这样界面对不确定情况会更保守。

数据流:进去的是一个临时传输错误和空的已有关闭状态;它调用 App::closed_state_for_thread_read_error;出来是断言结果为假,也就是不关闭。

调用关系:这是 App::closed_state_for_thread_read_error 的保护测试,确保它依赖终局错误判断,而不是一失败就关。

调用图:外部调用 2 个(assert!, eyre!)。

tests::closed_state_for_thread_read_error_marks_terminal_uncached_threads_closed877–885 ↗
fn closed_state_for_thread_read_error_marks_terminal_uncached_threads_closed()

作用:测试没有缓存状态时,如果错误明确说线程未加载,就会把它标为关闭。这样界面不会继续把不存在的线程当成活的。

数据流:进去的是 thread not loaded 错误和空的已有关闭状态;它调用 App::closed_state_for_thread_read_error;出来是断言结果为真。

调用关系:这是关闭状态判断的正向测试,保证 App::refresh_agent_picker_thread_liveness 遇到终局错误时能显示正确状态。

调用图:外部调用 2 个(assert!, eyre!)。

tests::include_turns_fallback_detection_handles_unmaterialized_and_ephemeral_threads888–898 ↗
fn include_turns_fallback_detection_handles_unmaterialized_and_ephemeral_threads()

作用:测试两种可以降级的 includeTurns 错误都能被识别。也就是“还没第一条用户消息”和“临时线程不支持带历史读取”。

数据流:进去的是两条测试错误消息;它分别调用 App::can_fallback_from_include_turns_error;出来是两个断言都必须为真。

调用关系:这是 App::attach_live_thread_for_selection 降级策略的测试基础。它保证某些读不到完整历史的情况,不会直接变成不可恢复错误。

调用图:外部调用 2 个(assert!, eyre!)。

tui/src/chatwidget/session_flow.rs源码 ↗
orchestrationsession setup / thread switching

可以把这个文件理解成 ChatWidget 的“换场调度员”。当程序进入一个聊天线程、静默切换线程,或打开旁路对话时,后端会送来一份 ThreadSessionState,也就是这次会话的说明书。这里会把说明书里的内容同步到界面:当前线程编号、线程名、工作目录、可用权限、模型和推理强度、服务档位、历史记录数量等。它还会重置一些只该属于旧线程的东西,比如复制历史、自动审查拒绝记录、当前目标状态。然后它决定要不要在聊天记录顶部放一个会话头,像“你现在在哪个项目、用哪个模型聊天”的提示牌。如果这是从别的线程分叉出来的新线程,它还会插入一条“从某线程分叉”的记录。最后,它刷新底部输入区、状态栏、插件和技能,并请求界面重画。没有它,界面很容易显示旧线程的信息,甚至用错权限或工作目录。

函数细节7
ChatWidget::on_session_configured_with_display_and_fork_parent_title6–147 ↗
fn on_session_configured_with_display_and_fork_parent_title(
        &mut self,
        session: ThreadSessionState,
        display: SessionConfiguredDisplay,
        fork_parent_title: Option<String

作用:这是本文件的核心函数,用来把一份新的会话配置真正应用到聊天界面上。它像“总开关”,一次性更新线程、权限、模型、工作目录、状态栏、会话头和分叉提示。

数据流:进去的是一份 ThreadSessionState(新会话的完整信息)、一个显示方式,以及可选的父线程标题。它先清掉旧线程留下的临时痕迹,再把线程编号、历史元数据、网络代理、工作目录、权限、模型、协作模式、服务档位等写进 ChatWidget。接着它按显示方式决定是否生成会话信息头,刷新技能、插件、状态栏和命令可用性,必要时预取连接器并提交等待中的首条用户消息。出来没有返回值,但 ChatWidget 的内部状态和界面显示都被更新了,必要时还会插入“线程分叉”提示并请求重画。

调用关系:普通会话、静默会话和旁路会话都会把活交给它,只是传入的显示方式不同。它内部会调用 ChatWidget::set_skills 清空或设置技能,遇到分叉线程时调用 ChatWidget::emit_forked_thread_event 写一条提示;它还会借助外部的会话头生成函数、权限快照转换函数和协作模式初始化逻辑,把后端给的会话资料翻译成界面能用的状态。

调用图:调用 4 个内部函数(allow_only, from_session_snapshot, emit_forked_thread_event, set_skills);被 3 处调用(handle_side_thread_session, handle_thread_session, handle_thread_session_quiet);外部调用 5 个(initial_collaboration_mask, new_session_info, error!, warn!, default)。

ChatWidget::handle_thread_session149–157 ↗
fn handle_thread_session(&mut self, session: ThreadSessionState)

作用:这是处理普通聊天线程配置的入口。用户正常进入或恢复一个聊天线程时,会走这里。

数据流:进去的是一份 ThreadSessionState。它先保存这次会话里指令来源文件的路径,再取出可能存在的父线程标题,然后把整份会话交给核心配置函数,并要求用正常方式显示。出来没有返回值,但界面会像进入一个正式会话那样更新,并可能显示会话头和分叉提示。

调用关系:它本身不做复杂更新,只是把普通场景包装好后交给 ChatWidget::on_session_configured_with_display_and_fork_parent_title。这样普通会话和其他会话类型能复用同一套真正的同步逻辑。

调用图:调用 1 个内部函数(on_session_configured_with_display_and_fork_parent_title)。

ChatWidget::handle_thread_session_quiet159–166 ↗
fn handle_thread_session_quiet(&mut self, session: ThreadSessionState)

作用:这是静默应用会话配置的入口。它用于需要更新内部状态,但不想像正常进入线程那样大张旗鼓刷新会话头的场景。

数据流:进去的是一份 ThreadSessionState。它保存指令来源路径,然后把会话交给核心配置函数,显示方式设为 Quiet,并且不传父线程标题。出来没有返回值,但内部线程、权限、模型等状态会被同步;如果当前有会话头,核心逻辑还可能把它移走,避免显示不该显示的提示。

调用关系:它调用 ChatWidget::on_session_configured_with_display_and_fork_parent_title,并通过 Quiet 这个标记告诉核心函数:这次配置要低调处理,不按普通会话那样插入完整的会话信息。

调用图:调用 1 个内部函数(on_session_configured_with_display_and_fork_parent_title)。

ChatWidget::handle_side_thread_session168–176 ↗
fn handle_side_thread_session(&mut self, session: ThreadSessionState)

作用:这是处理旁路对话会话配置的入口。旁路对话可以理解成主聊天之外的临时或辅助线程,它也需要同步状态,但显示方式和普通线程不完全一样。

数据流:进去的是一份 ThreadSessionState。它保存指令来源路径,取出父线程标题,然后把会话交给核心配置函数,显示方式标成 SideConversation。出来没有返回值,但 ChatWidget 会切到这份旁路会话对应的线程、目录、权限和模型状态。

调用关系:它和普通入口一样,最终都依赖 ChatWidget::on_session_configured_with_display_and_fork_parent_title。区别在于它传入的是 SideConversation,让核心函数按旁路对话的规则处理界面展示。

调用图:调用 1 个内部函数(on_session_configured_with_display_and_fork_parent_title)。

ChatWidget::emit_forked_thread_event178–207 ↗
fn emit_forked_thread_event(
        &mut self,
        forked_from_id: ThreadId,
        fork_parent_title: Option<String>,
    )

作用:这个函数负责在聊天记录里插入一条“当前线程是从哪个线程分叉来的”提示。这样用户不会迷路,知道这个对话不是凭空开始的。

数据流:进去的是父线程的 ThreadId,以及可选的父线程标题。它把线程编号转成文字,如果标题存在且不为空,就生成一行带标题和编号的提示;否则只显示编号。然后它把这行文字包成一个普通历史记录单元,通过应用事件通道发出去。出来没有直接返回值,但聊天历史里会新增一条说明性记录。

调用关系:它由 ChatWidget::on_session_configured_with_display_and_fork_parent_title 在普通显示的新分叉线程场景下调用。它不负责判断什么时候该提示,只负责把提示文字做漂亮,并通过 InsertHistoryCell 事件交给界面历史区显示。

调用图:调用 1 个内部函数(new);被 1 处调用(on_session_configured_with_display_and_fork_parent_title);外部调用 4 个(new, InsertHistoryCell, to_string, vec!)。

ChatWidget::on_thread_name_updated209–224 ↗
fn on_thread_name_updated(
        &mut self,
        thread_id: ThreadId,
        thread_name: Option<String>,
    )

作用:这个函数处理“线程名称被更新了”这件事。它确保只有当前正在看的线程改名时,界面才跟着变,并给用户一个改名成功的提示。

数据流:进去的是被改名的线程编号和新的线程名。它先检查这个编号是不是当前线程;如果不是,就什么也不做。如果是当前线程,并且新名字存在,它会生成一条改名确认记录放进聊天历史。随后它更新内部保存的线程名,刷新状态栏,请求界面重画,并尝试发送下一个排队中的输入。出来没有返回值,但当前界面的标题和历史提示会变成新名字对应的状态。

调用关系:它通常在外部收到线程改名结果后被调用。它会用 rename_confirmation_cell 生成用户能看懂的确认消息,并接着触发状态刷新和队列推进,保证改名后的界面和输入流程继续往前走。

调用图:外部调用 2 个(new, rename_confirmation_cell)。

ChatWidget::set_skills226–228 ↗
fn set_skills(&mut self, skills: Option<Vec<SkillMetadata>>)

作用:这个小函数把当前可用的技能列表交给底部输入区显示或使用。技能可以理解成聊天时可调用的一组能力提示。

数据流:进去的是可选的 SkillMetadata 列表;有列表就表示当前有哪些技能,没有就表示先清空或不展示。它不自己保存这些技能,而是直接转交给 bottom_pane。出来没有返回值,但底部输入区看到的技能状态会被更新。

调用关系:它被 ChatWidget::on_session_configured_with_display_and_fork_parent_title 调用,用来在切换或配置新会话时先同步技能显示。这个函数本身只是一个简单转接点,让会话配置逻辑不用直接碰底部面板的内部细节。

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

tui/src/chatwidget/turn_lifecycle.rs源码 ↗
domain_logic聊天回合开始、结束、恢复、重置,以及用户切换防睡眠设置时

在聊天界面里,一次用户提问到智能体完成回答,可以看成一个“回合”。这个文件里的 TurnLifecycleState 就是这个回合的小记事本:它记住当前回合是不是还在跑、上一轮的编号、哪些回合因为预算限制被标记过,以及目标状态提示是从什么时候开始显示的。它还带着 SleepInhibitor,也就是“防睡眠开关”:如果设置允许,它会在回合运行时告诉系统别因为空闲而睡着。这样做的好处是,界面不会误以为任务结束,也不会在长任务中途让电脑休眠。文件还提供了重置线程、恢复运行状态、切换防睡眠设置、标记和取走预算限制标记等小动作。末尾的测试确认两件关键事:开始和结束会正确改状态;预算限制标记被取走后不会重复生效。

函数细节10
TurnLifecycleState::new19–27 ↗
fn new(prevent_idle_sleep: bool) -> Self

作用:创建一个全新的回合状态记录本。调用者可以决定是否启用“防止空闲睡眠”,也就是任务运行时尽量别让电脑睡着。

数据流:输入是一个布尔值 prevent_idle_sleep,表示要不要防睡眠。函数用它创建 SleepInhibitor,把“正在运行”设为否,把上一轮编号清空,把预算受限回合列表清空,把目标状态开始时间清空。输出是一个准备好使用的 TurnLifecycleState。

调用关系:这是整个状态对象的起点。聊天组件初始化时会通过 new_with_op_target 间接用到它;测试也会用它先造出一个干净状态,再检查后续开始、结束和预算标记是否正常。

调用图:调用 1 个内部函数(new);被 3 处调用(new_with_op_target, budget_limited_turn_ids_are_consumed, start_and_finish_update_running_state);外部调用 1 个(new)。

TurnLifecycleState::start29–33 ↗
fn start(&mut self, now: Instant)

作用:告诉状态记录本:新一轮智能体工作开始了。它会同时打开防睡眠状态,避免任务还没完电脑就休眠。

数据流:输入是当前时间 now。函数把 agent_turn_running 改成 true,把目标状态开始时间记为 now,然后通知 SleepInhibitor 当前有回合正在运行。结果是状态从“空闲”变成“运行中”。

调用关系:当聊天组件收到或发起一轮新的智能体工作时,会调用这个函数。它自己不再分派复杂工作,只把防睡眠这件事交给 SleepInhibitor 的 set_turn_running。

调用图:外部调用 1 个(set_turn_running)。

TurnLifecycleState::finish35–40 ↗
fn finish(&mut self)

作用:告诉状态记录本:这一轮智能体工作结束了。它会清掉运行中的标记,并关闭这一轮带来的防睡眠状态。

数据流:没有额外输入,只使用当前对象里的状态。函数把 agent_turn_running 改成 false,把目标状态开始时间清空,并通知 SleepInhibitor 当前没有回合在跑。结果是状态回到“空闲”。

调用关系:聊天回合正常结束时会用它;reset_thread 也会先调用它,确保重置线程前一定把运行状态收干净。它把系统睡眠状态的更新交给 SleepInhibitor。

调用图:被 1 处调用(reset_thread);外部调用 1 个(set_turn_running)。

TurnLifecycleState::restore_running42–46 ↗
fn restore_running(&mut self, running: bool, now: Instant)

作用:把“是否正在运行”的状态恢复成外部告诉它的样子。常见场景是界面或线程状态重建时,需要把记录本重新对齐到真实状态。

数据流:输入是 running 和当前时间 now。函数把 agent_turn_running 设为 running;如果 running 为 true,就把目标状态开始时间设为 now,否则清空;然后把同样的运行状态同步给 SleepInhibitor。输出没有返回值,但对象内部状态被更新了。

调用关系:它用于恢复或重新同步状态,而不是开始一个全新的回合。它和 start、finish 做的是同一类事:维护运行标记,并把防睡眠开关同步给 SleepInhibitor。

调用图:外部调用 1 个(set_turn_running)。

TurnLifecycleState::reset_thread48–52 ↗
fn reset_thread(&mut self)

作用:在切换或重置聊天线程时,把跟旧线程有关的回合状态清掉。这样新线程不会继承旧线程的运行状态或预算限制标记。

数据流:没有额外输入。函数先调用 finish,让当前回合无论如何都变成已结束;然后把 last_turn_id 清空,并清空 budget_limited_turn_ids。结果是这个状态对象回到适合新线程使用的干净状态。

调用关系:它是一次更大范围重置动作里的收尾工具。它把“结束当前运行状态”的细节交给 finish,自己再负责清理线程级别的旧记录。

调用图:调用 1 个内部函数(finish)。

TurnLifecycleState::set_prevent_idle_sleep54–58 ↗
fn set_prevent_idle_sleep(&mut self, enabled: bool)

作用:在用户或配置改变“是否防止空闲睡眠”时,更新这个设置。重要的是,如果当前回合正在运行,新设置会立刻按当前运行状态生效。

数据流:输入是 enabled,表示新的防睡眠开关。函数用 enabled 重新创建一个 SleepInhibitor,然后把当前 agent_turn_running 的值同步进去。结果是防睡眠工具换成了新配置,同时不丢失“现在是否有任务在跑”的事实。

调用关系:它通常在设置变更时被调用。它会创建新的 SleepInhibitor,并调用 set_turn_running,让外部睡眠控制和聊天回合状态保持一致。

调用图:调用 1 个内部函数(new);外部调用 1 个(set_turn_running)。

TurnLifecycleState::mark_budget_limited60–62 ↗
fn mark_budget_limited(&mut self, turn_id: String)

作用:给某个回合打上“预算受限”的标记。这里的预算可以理解成可用资源或额度不够,之后界面或流程可以据此做特殊提示或处理。

数据流:输入是一个 turn_id,也就是回合编号。函数把这个编号放进 budget_limited_turn_ids 这个集合里。输出没有返回值,但状态里多了一个待处理的预算受限标记。

调用关系:当系统发现某一轮因为预算限制受到影响时,会调用它登记下来。之后 take_budget_limited 会来取走这个标记,避免同一个标记被反复处理。

TurnLifecycleState::take_budget_limited64–66 ↗
fn take_budget_limited(&mut self, turn_id: &str) -> bool

作用:检查某个回合是否曾被标记为预算受限,并且一旦查到就把标记拿走。它像一次性票据,用过就作废。

数据流:输入是 turn_id 的字符串引用。函数去 budget_limited_turn_ids 里找这个编号;如果找到了就删除并返回 true,如果没找到就返回 false。结果既告诉调用者有没有这个标记,也会改变集合内容。

调用关系:它通常在需要消费预算限制信息时使用,和 mark_budget_limited 配对。测试 budget_limited_turn_ids_are_consumed 专门确认它第一次返回 true、第二次返回 false,说明标记不会重复生效。

tests::start_and_finish_update_running_state74–86 ↗
fn start_and_finish_update_running_state()

作用:这是一个自动测试,用来确认“开始回合”和“结束回合”真的会改对状态。它保护的是最核心的生命周期行为。

数据流:测试先创建一个关闭防睡眠配置的 TurnLifecycleState。然后调用 start,检查正在运行标记、开始时间、防睡眠运行标记都变成了运行中;再调用 finish,检查这些状态又回到未运行和空值。测试本身不产出业务结果,只在不符合预期时失败。

调用关系:它由测试框架运行。它先调用 TurnLifecycleState::new 准备状态,再直接验证 start 和 finish 造成的变化,确保以后改代码时不会把这条基本规则弄坏。

调用图:调用 1 个内部函数(new);外部调用 2 个(now, assert!)。

tests::budget_limited_turn_ids_are_consumed89–96 ↗
fn budget_limited_turn_ids_are_consumed()

作用:这是一个自动测试,用来确认预算受限标记是“一次性”的。也就是说,同一个回合的标记被取走后,不能再被取第二次。

数据流:测试先创建一个干净状态,再用 mark_budget_limited 给 turn-1 做标记。第一次调用 take_budget_limited 应该返回 true,并删除标记;第二次再取同一个 turn-1 应该返回 false。测试通过说明标记消费逻辑正确。

调用关系:它由测试框架运行,用来保护 mark_budget_limited 和 take_budget_limited 这对函数的配合方式。这样后续界面不会因为同一个预算限制事件重复提示或重复处理。

调用图:调用 1 个内部函数(new);外部调用 1 个(assert!)。

TUI 侧线程与设置

这些文件涵盖 UI 中侧对话线程的行为,以及线程设置和目标操作与应用服务器之间的同步。

tui/src/app/side.rs源码 ↗
orchestrationrequest handling / thread switching / cleanup

可以把这里的 side conversation 理解成“主会议旁边临时开的小讨论”。它会继承主对话的历史,但会明确告诉模型:以前的内容只能当参考,不能继续执行旧任务,尤其不要乱改文件。这个文件主要做几件事:判断什么时候允许开旁路对话;复制当前线程配置并加上安全提示;创建分叉线程;切换界面到旁路线程;用户按 Ctrl+C 或切换回别的线程时,停止并丢弃这个临时线程。它还会在旁路对话界面上显示“来自主线程”“主线程需要审批”等提示,避免用户忘了自己在哪个上下文里。另一个重要点是清理失败时不会假装成功,而是尽量把旁路对话留在屏幕上,让用户知道它还没关掉。

函数细节31
SideParentStatus::label65–80 ↗
fn label(self, parent_is_main: bool) -> &'static str

作用:把父线程的状态变成给人看的短文字,比如“main needs approval”。这样旁路对话顶部能清楚告诉用户:主线现在发生了什么。

数据流:输入是一个父线程状态,以及这个父线程是不是主线程 → 函数按状态和身份选一句固定英文标签 → 输出这句标签,不改任何数据。

调用关系:它是界面提示的一块小翻译器。App::sync_side_thread_ui 在拼旁路对话顶部说明时会用它,把内部状态变成用户能看到的文字。

SideParentStatus::is_actionable82–87 ↗
fn is_actionable(self) -> bool

作用:判断某个父线程状态是不是需要用户立刻处理。这里主要指“需要输入”或“需要审批”。

数据流:输入是一个状态 → 函数检查它是不是 NeedsInput 或 NeedsApproval → 输出 true 或 false,不改任何东西。

调用关系:它帮助 App::clear_side_parent_action_status 决定哪些提示可以清掉。比如父线程原来要审批,后来审批请求被解决了,就可以把这个提醒移除。

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

SideParentStatus::for_request89–102 ↗
fn for_request(request: &ServerRequest) -> Option<Self>

作用:把服务器发来的“请求”翻译成父线程状态。比如服务器要用户输入,就变成 NeedsInput;要批准命令或文件改动,就变成 NeedsApproval。

数据流:输入是一个 ServerRequest,也就是服务器向用户发出的请求 → 函数按请求类型分类 → 输出对应的 SideParentStatus,或者在这个请求不需要旁路提示时输出 None。

调用关系:它会被 enqueue_thread_request 使用。当父线程在后台收到需要人处理的请求时,这个函数帮旁路对话知道该显示什么提醒。

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

tests::side_boundary_prompt_marks_inherited_history_reference_only111–132 ↗
fn side_boundary_prompt_marks_inherited_history_reference_only()

作用:这是一个测试,确认旁路对话开头注入的隐藏提示确实说清楚:继承来的历史只能当参考,不能当当前任务继续执行。

数据流:测试调用 App::side_boundary_prompt_item 拿到隐藏消息 → 检查它是不是用户消息、内容是不是文本、文本里是否包含关键安全说明 → 如果不符合就让测试失败。

调用关系:它保护旁路对话最重要的安全边界。以后有人改提示词时,这个测试能及时发现是否删掉了关键规则。

调用图:外部调用 4 个(assert!, assert_eq!, side_boundary_prompt_item, panic!)。

tests::side_start_error_message_explains_missing_first_prompt135–144 ↗
fn side_start_error_message_explains_missing_first_prompt()

作用:这是一个测试,确认当主对话还没真正开始时,启动旁路对话会给出好懂的提示,而不是暴露底层错误。

数据流:测试造出一个类似“找不到 rollout”的错误 → 交给 App::side_start_error_message → 检查输出是否提示用户先发一条消息再用 /side。

调用关系:它对应 App::handle_start_side 里启动失败后的错误显示,保证新用户看到的是可操作的说明。

调用图:外部调用 2 个(assert_eq!, eyre!)。

tests::side_start_error_message_uses_generic_start_wording147–154 ↗
fn side_start_error_message_uses_generic_start_wording()

作用:这是一个测试,确认普通启动失败会保留真实错误,并加上“启动旁路对话失败”的前缀。

数据流:测试造出一个“连接断开”的错误 → 交给 App::side_start_error_message → 检查返回文案是否包含通用失败说明和原始错误。

调用关系:它保证错误处理不过度美化。不是“还没发第一条消息”这种特殊情况时,用户仍能看到真实原因。

调用图:外部调用 2 个(assert_eq!, eyre!)。

tests::side_developer_instructions_appends_existing_policy157–168 ↗
fn side_developer_instructions_appends_existing_policy()

作用:这是一个测试,确认旁路对话的开发者指令不会覆盖已有策略,而是追加在后面。

数据流:测试传入一段已有开发者策略 → 调用 App::side_developer_instructions → 检查结果同时包含旧策略和旁路对话的限制说明。

调用关系:它保护 App::side_fork_config 的配置行为,避免创建旁路线程时把原本重要的策略弄丢。

调用图:外部调用 2 个(assert!, side_developer_instructions)。

SideParentStatusChange::for_notification179–200 ↗
fn for_notification(notification: &ServerNotification) -> Option<Self>

作用:把服务器通知翻译成“父线程状态该怎么变”。比如一轮对话开始就清掉旧状态,一轮失败就标记为 Failed,线程关闭就标记为 Closed。

数据流:输入是一个 ServerNotification,也就是服务器发来的事件通知 → 函数查看通知类型和回合状态 → 输出 Set、Clear、ClearActionable 这样的状态变更指令,或者输出 None 表示不用处理。

调用关系:它会被 enqueue_thread_notification 使用。后台父线程有新进展时,它负责把事件转成 App::apply_side_parent_status_change 能执行的简单动作。

调用图:被 1 处调用(enqueue_thread_notification);外部调用 1 个(Set)。

SideThreadState::new212–217 ↗
fn new(parent_thread_id: ThreadId) -> Self

作用:创建一份旁路线程状态,记住它是从哪个父线程分叉出来的。刚创建时,父线程还没有额外提醒状态。

数据流:输入是父线程 ID → 函数生成 SideThreadState,填入 parent_thread_id,并把 parent_status 设为空 → 输出这份新状态。

调用关系:App::handle_start_side 创建新旁路线程时会用它。测试和其他旁路场景也用它来搭建一条“子线程指向父线程”的关系。

调用图:被 14 处调用(handle_start_side, active_side_thread_renders_live_mcp_startup_notifications, discard_closed_side_thread_removes_local_state_without_server_rpc, discard_side_thread_keeps_local_state_when_server_close_fails, discard_side_thread_removes_agent_navigation_entry, side_defers_parent_approval_overlay_until_parent_replay, side_defers_subagent_approval_overlay_until_side_exits, side_discard_selection_keeps_current_side_thread, side_parent_status_prioritizes_input_over_approval, side_parent_status_tracks_parent_turn_lifecycle (+4 more))。

App::sync_side_thread_ui221–261 ↗
fn sync_side_thread_ui(&mut self)

作用:同步旁路对话相关的界面状态。它决定顶部是否显示“Side from main thread · Ctrl+C to return”,以及是否禁止重命名。

数据流:输入来自 App 当前状态:当前显示的线程、side_threads 表、主线程 ID、父线程状态 → 如果当前不是旁路线程,就清掉旁路 UI;如果是,就设置旁路标识、重命名拦截、隐藏中断提示,并拼出上下文标签 → 结果是 chat_widget 的显示状态被更新。

调用关系:它被 set_side_parent_status、clear_side_parent_action_status 和 handle_start_side 调用。每当旁路身份或父线程提醒变化时,它负责把内部变化反映到屏幕上。

调用图:被 3 处调用(clear_side_parent_action_status, handle_start_side, set_side_parent_status);外部调用 2 个(new, format!)。

App::active_side_parent_thread_id263–267 ↗
fn active_side_parent_thread_id(&self) -> Option<ThreadId>

作用:查看当前显示的线程是不是旁路线程;如果是,返回它的父线程 ID。

数据流:函数读取当前显示线程 ID → 到 side_threads 里查这条线程有没有登记为旁路线程 → 找到就输出父线程 ID,找不到就输出 None。

调用关系:App::maybe_return_from_side 会用它判断 Ctrl+C 这类返回动作是否应该把用户带回父线程。

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

App::set_side_parent_status269–288 ↗
fn set_side_parent_status(
        &mut self,
        parent_thread_id: ThreadId,
        status: Option<SideParentStatus>,
    )

作用:给某个父线程名下的旁路对话设置最新状态,比如“父线程失败了”或“父线程需要审批”。

数据流:输入是父线程 ID 和一个可选状态 → 函数遍历所有旁路线程,找到父线程相同的记录并更新 parent_status → 如果确实发生变化,就调用 App::sync_side_thread_ui 刷新界面。

调用关系:它由 App::apply_side_parent_status_change 调用,是实际落地状态变化的函数。

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

App::clear_side_parent_action_status290–308 ↗
fn clear_side_parent_action_status(&mut self, parent_thread_id: ThreadId)

作用:清除父线程里“需要用户动作”的提醒,比如“需要输入”或“需要审批”。不会清除失败、关闭这类结果状态。

数据流:输入是父线程 ID → 函数找到这个父线程关联的旁路线程 → 如果它们的 parent_status 是可处理状态,就设为空 → 有变化时刷新旁路 UI。

调用关系:它由 App::apply_side_parent_status_change 调用。服务器通知说某个请求开始处理或已解决时,就可能走到这里。

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

App::apply_side_parent_status_change310–326 ↗
fn apply_side_parent_status_change(
        &mut self,
        parent_thread_id: ThreadId,
        change: SideParentStatusChange,
    )

作用:执行一条父线程状态变更命令。它像一个小分派器:该设置就设置,该清空就清空,该只清可操作提醒就只清那部分。

数据流:输入是父线程 ID 和 SideParentStatusChange → 函数按变更类型调用 App::set_side_parent_status 或 App::clear_side_parent_action_status → 输出为空,但会更新内部状态并可能刷新 UI。

调用关系:它承接 SideParentStatusChange::for_notification 产生的结果,把“服务器通知”真正变成旁路对话上的提示变化。

调用图:调用 2 个内部函数(clear_side_parent_action_status, set_side_parent_status)。

App::maybe_return_from_side328–349 ↗
async fn maybe_return_from_side(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
    ) -> bool

作用:在合适的时候从旁路对话回到父线程。它很谨慎:只有没有弹窗、输入框为空、当前确实在旁路线程里,才会返回。

数据流:输入是当前 App、TUI 和服务器会话 → 函数检查界面是否空闲,再查当前旁路线程的父线程 ID → 如果可以返回,就调用 App::select_agent_thread_and_discard_side 切回父线程并丢弃旁路线程 → 输出 true 表示确实回去了,false 表示没有或失败。

调用关系:它通常服务于用户按 Ctrl+C 之类的返回动作。真正的切换和清理由 App::select_agent_thread_and_discard_side 完成。

调用图:调用 2 个内部函数(active_side_parent_thread_id, select_agent_thread_and_discard_side)。

App::side_thread_to_discard_after_switch351–361 ↗
fn side_thread_to_discard_after_switch(
        &self,
        target_thread_id: ThreadId,
    ) -> Option<ThreadId>

作用:判断切换到目标线程后,当前旁路线程是否应该被丢弃。旁路对话是临时的,离开它通常就要关掉。

数据流:输入是目标线程 ID → 函数读取当前显示线程 ID,并确认当前线程是不是旁路线程 → 如果目标不是它自己,就输出当前旁路线程 ID;否则输出 None。

调用关系:App::select_agent_thread_and_discard_side 在切换前会问它:切完以后有没有旧旁路线程需要清理。

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

App::discard_side_thread363–382 ↗
async fn discard_side_thread(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> bool

作用:关闭并清理一个旁路线程。它先让服务器停止这个线程,再取消订阅,最后删除本地记录。

数据流:输入是服务器会话和要丢弃的线程 ID → 函数先调用 App::interrupt_side_thread 尝试中断,再调用服务器的 thread_unsubscribe 关闭订阅 → 成功后调用 App::discard_thread_local_state 清理本地状态 → 输出 true 表示关干净了;失败时显示错误并输出 false。

调用关系:它被 App::discard_side_thread_or_keep_visible 和 App::select_agent_thread_and_discard_side 调用,是旁路线程退出时的核心清理步骤。

调用图:调用 3 个内部函数(discard_thread_local_state, interrupt_side_thread, thread_unsubscribe);被 2 处调用(discard_side_thread_or_keep_visible, select_agent_thread_and_discard_side);外部调用 2 个(format!, warn!)。

App::discard_closed_side_thread384–386 ↗
async fn discard_closed_side_thread(&mut self, thread_id: ThreadId)

作用:清理一个服务器已经关闭的旁路线程的本地残留状态。因为服务器那边已经关了,所以这里不再发关闭请求。

数据流:输入是线程 ID → 函数直接调用 App::discard_thread_local_state → 本地监听器、缓存、导航记录等被删除。

调用关系:当线程已被服务器关闭时,它走更短的清理路径,避免重复请求服务器关闭。

调用图:调用 1 个内部函数(discard_thread_local_state)。

App::discard_thread_local_state388–399 ↗
async fn discard_thread_local_state(&mut self, thread_id: ThreadId)

作用:删除某个线程在 TUI 本地保存的各种记录。包括事件监听、事件通道、旁路标记、导航入口,以及当前激活状态。

数据流:输入是线程 ID → 函数停止这个线程的事件监听,移除事件通道、side_threads 记录和 agent_navigation 记录 → 如果它正是当前活动线程,就清空活动线程;否则刷新待处理审批 → 最后同步当前代理标签。

调用关系:它是本地清扫工具,被 App::discard_side_thread 和 App::discard_closed_side_thread 调用。服务器关闭只是第一步,本地也必须清干净,否则界面会残留旧线程。

调用图:被 2 处调用(discard_closed_side_thread, discard_side_thread)。

App::interrupt_side_thread401–415 ↗
async fn interrupt_side_thread(
        &self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> std::result::Result<(), String>

作用:请求服务器停止一个旁路线程当前正在做的事。它会区分线程已经开始一轮回复,还是还在启动阶段。

数据流:输入是服务器会话和线程 ID → 函数查询这个线程是否有正在运行的 turn_id;turn 可以理解为一次模型回答流程 → 有就调用 turn_interrupt,中断这一轮;没有就调用 startup_interrupt,中断启动过程 → 成功输出 Ok,失败输出带人类可读文案的 Err。

调用关系:App::discard_side_thread 在真正取消订阅前会先调用它,避免线程还在后台运行就被本地忘掉。

调用图:调用 2 个内部函数(startup_interrupt, turn_interrupt);被 1 处调用(discard_side_thread)。

App::keep_side_thread_visible_after_cleanup_failure417–430 ↗
async fn keep_side_thread_visible_after_cleanup_failure(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    )

作用:如果旁路线程清理失败,就尽量把它重新显示出来。这样用户不会误以为它已经关闭。

数据流:输入是 TUI、服务器会话和线程 ID → 如果当前活动线程不是这个旁路线程,就尝试切回它 → 成功则用户能继续看到未关闭的旁路线程;失败则只记录警告。

调用关系:它被 App::discard_side_thread_or_keep_visible 和 App::select_agent_thread_and_discard_side 在清理失败时调用,是一种“失败也别隐藏问题”的补救措施。

调用图:被 2 处调用(discard_side_thread_or_keep_visible, select_agent_thread_and_discard_side);外部调用 1 个(warn!)。

App::discard_side_thread_or_keep_visible432–445 ↗
async fn discard_side_thread_or_keep_visible(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> bool

作用:尝试丢弃旁路线程;如果丢弃失败,就把它留在界面上。它把“清理”和“失败补救”打包成一个安全动作。

数据流:输入是 TUI、服务器会话和线程 ID → 先调用 App::discard_side_thread → 成功输出 true;失败则调用 App::keep_side_thread_visible_after_cleanup_failure,再输出 false。

调用关系:App::handle_start_side 在创建旁路线程的中途失败时会用它,确保半创建的线程要么被关掉,要么被明确留给用户处理。

调用图:调用 2 个内部函数(discard_side_thread, keep_side_thread_visible_after_cleanup_failure);被 1 处调用(handle_start_side)。

App::side_developer_instructions447–454 ↗
fn side_developer_instructions(existing_instructions: Option<&str>) -> String

作用:生成旁路对话专用的开发者指令。开发者指令可以理解成给模型看的高优先级行为规则。

数据流:输入是已有开发者指令,可有可无 → 如果已有且不是空白,就把旧指令和旁路限制拼在一起;否则只用旁路限制 → 输出完整字符串。

调用关系:App::side_fork_config 会调用它,把旁路对话的安全边界写进新线程配置里。测试也会检查它不会覆盖已有策略。

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

App::side_boundary_prompt_item456–466 ↗
fn side_boundary_prompt_item() -> ResponseItem

作用:生成一条隐藏的边界提示消息,插入旁路对话历史里。它告诉模型:边界之前都是参考资料,边界之后才是当前任务。

数据流:没有外部输入 → 函数把 SIDE_BOUNDARY_PROMPT 包成一个 ResponseItem::Message,角色设为 user,内容是一段 InputText → 输出这条可注入线程的消息。

调用关系:App::handle_start_side 创建旁路线程后会把它注入进去。测试会检查这条提示是否包含关键安全说明。

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

App::side_fork_config468–481 ↗
fn side_fork_config(&self) -> Config

作用:为旁路线程准备一份配置。它从当前聊天配置复制大部分设置,但会标记为临时线程,并加入旁路专用规则。

数据流:函数读取 chat_widget 里的当前配置、模型、推理强度和服务档位 → 复制成 fork_config,补上当前模型信息,把 ephemeral 设为 true,并用 App::side_developer_instructions 添加安全指令 → 输出这份用于 fork_thread 的 Config。

调用关系:App::handle_start_side 在请求服务器分叉线程前会调用它。它确保旁路对话和主线程体验一致,但行为边界更严格。

调用图:被 1 处调用(handle_start_side);外部调用 1 个(side_developer_instructions)。

App::side_start_block_message483–491 ↗
fn side_start_block_message(&self) -> Option<&'static str>

作用:判断现在能不能开启旁路对话,并在不能时给出固定原因。

数据流:函数读取 primary_thread_id 和 side_threads → 如果主线程还不可用,输出“/side 暂不可用”;如果已经有旁路对话打开,输出“先 Ctrl+C 返回”;否则输出 None。

调用关系:App::handle_start_side 一开始就调用它。这样不满足条件时可以立刻拦住,不会向服务器发没意义的分叉请求。

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

App::side_start_error_message493–503 ↗
fn side_start_error_message(err: &color_eyre::Report) -> String

作用:把启动旁路对话失败的底层错误,改写成用户能看懂的提示。

数据流:输入是错误报告 → 函数检查错误链里是否包含“还没有第一条用户消息”相关的底层字样 → 如果是,输出“先发消息再试 /side”;否则输出“启动旁路对话失败”加原始错误。

调用关系:App::handle_start_side 在 fork_thread 失败时会用它。相关测试保证特殊错误和普通错误都显示得合适。

调用图:外部调用 2 个(chain, format!)。

App::restore_side_user_message505–513 ↗
fn restore_side_user_message(
        &mut self,
        user_message: Option<crate::chatwidget::UserMessage>,
    )

作用:如果启动旁路对话失败,把用户刚才准备发送的消息放回输入框,避免用户白打。

数据流:输入是一个可选的 UserMessage → 如果有,就调用 chat_widget 把它恢复到 composer,也就是输入框 → 如果没有,就什么也不做。

调用关系:App::handle_start_side 在各种失败路径都会调用它。它是用户体验上的保险丝。

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

App::install_side_thread_snapshot515–524 ↗
fn install_side_thread_snapshot(
        store: &mut ThreadEventStore,
        mut session: ThreadSessionState,
        _forked_turns: Vec<Turn>,
    )

作用:安装旁路线程的初始本地快照,但让界面看起来从旁路边界开始,而不是显示全部继承历史。

数据流:输入是事件存储、线程会话状态和分叉出来的历史回合 → 函数把 session.forked_from_id 清空,并用空回合列表写入 store → 结果是模型仍能在核心状态里拿到继承历史,但 TUI 视觉上从新旁路对话开始。

调用关系:App::handle_start_side 在服务器成功分叉后调用它。它解决“模型需要参考历史”和“用户界面不要被旧历史淹没”之间的矛盾。

调用图:调用 1 个内部函数(set_session);外部调用 1 个(new)。

App::select_agent_thread_and_discard_side526–551 ↗
async fn select_agent_thread_and_discard_side(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    ) -> Result<()>

作用:切换到指定线程,并在离开旁路线程后把旧旁路线程丢弃。它让“返回主线”这件事既切屏又清理垃圾。

数据流:输入是 TUI、服务器会话和目标线程 ID → 先记录当前活动线程,再用 App::side_thread_to_discard_after_switch 判断是否有旁路线程要清理 → 调用 select_agent_thread 切到目标线程 → 如果切换成功且有旧旁路线程,就调用 App::discard_side_thread;清理失败时可能调用 App::keep_side_thread_visible_after_cleanup_failure → 成功返回 Ok。

调用关系:它被 App::handle_start_side 用来进入新旁路线程,也被 App::maybe_return_from_side 用来返回父线程。它是线程切换和旁路生命周期之间的连接点。

调用图:调用 3 个内部函数(discard_side_thread, keep_side_thread_visible_after_cleanup_failure, side_thread_to_discard_after_switch);被 2 处调用(handle_start_side, maybe_return_from_side)。

App::handle_start_side553–646 ↗
async fn handle_start_side(
        &mut self,
        tui: &mut tui::Tui,
        app_server: &mut AppServerSession,
        parent_thread_id: ThreadId,
        mut user_message: Option<crate::chatwi

作用:处理用户输入 /side 后的完整流程:检查能不能开,创建分叉线程,注入安全边界,切到旁路对话,并把用户消息送进去。

数据流:输入是 TUI、服务器会话、父线程 ID,以及用户可能已经输入的一条消息 → 先用 App::side_start_block_message 拦截不允许的情况;允许时记录遥测并刷新配置;再用 App::side_fork_config 准备配置,调用服务器 fork_thread 创建子线程;成功后安装本地快照、登记 SideThreadState、注入 App::side_boundary_prompt_item;然后用 App::select_agent_thread_and_discard_side 切进去;如果用户一开始带了消息,就提交到旁路对话 → 任何中途失败都会尽量清理旁路线程、恢复用户消息并显示错误 → 最后返回 Continue,让应用继续运行。

调用关系:它是 /side 命令的主入口。它把本文件里的大多数零件串起来:启动检查、配置准备、服务器分叉、提示注入、界面切换、失败恢复和清理都由它调度。

调用图:调用 9 个内部函数(discard_side_thread_or_keep_visible, restore_side_user_message, select_agent_thread_and_discard_side, side_fork_config, side_start_block_message, sync_side_thread_ui, new, fork_thread, thread_inject_items);外部调用 5 个(install_side_thread_snapshot, side_start_error_message, format!, warn!, vec!)。

tui/src/chatwidget/side.rs源码 ↗
domain_logic用户交互时:进入或退出侧边对话、更新底部提示、提交侧边消息时活跃

这个文件是 ChatWidget 的一小组“侧边对话开关”。可以把它想成聊天窗口上的模式切换按钮:打开侧边对话时,输入框里的占位提示会换成侧边对话专用文字,底部面板也会知道自己进入了这种状态;关闭时,又恢复普通聊天的提示。它还提供一个只按普通用户消息提交的入口,明确禁止 shell escape(可以理解成“不要把用户输入当成特殊命令或终端指令来解释”),这样在侧边对话里发消息更安全、更直接。除此之外,它还能设置底部显示的上下文标签,比如告诉用户当前侧边对话围绕哪段内容展开。真正创建、结束侧边对话的生命周期不在这里;这里只负责聊天界面上看得见、摸得着的那些变化。

函数细节4
ChatWidget::submit_user_message_as_plain_user_turn10–15 ↗
fn submit_user_message_as_plain_user_turn(
        &mut self,
        user_message: UserMessage,
    ) -> Option<AppCommand>

作用:把用户输入当成一条普通聊天消息提交,而不是当成特殊命令处理。这样做的重点是安全和明确:用户写什么,就按普通发言送出去。

数据流:进去的是一条 UserMessage,也就是用户刚输入的内容;函数把它交给更通用的提交函数,并附带 ShellEscapePolicy::Disallow,意思是禁止把内容解释成 shell escape(类似终端特殊指令);出来的是一个可选的 AppCommand,表示这次提交可能触发应用接下来要执行的动作,也可能没有。

调用关系:它是侧边对话里提交普通发言的便捷入口。自己不做复杂发送流程,而是把活儿交给 ChatWidget::submit_user_message_with_shell_escape_policy,并固定使用“禁止 shell escape”的策略,避免调用方每次都重复写这条安全规则。

ChatWidget::set_side_conversation_active17–26 ↗
fn set_side_conversation_active(&mut self, active: bool)

作用:打开或关闭聊天界面的侧边对话状态。它不只是记一个开关,还会同步更新输入框提示和底部面板状态,让界面马上跟着变。

数据流:进去的是 active 这个真假值:true 表示进入侧边对话,false 表示退出;函数先把这个状态存到 ChatWidget 自己的 active_side_conversation 里,然后根据状态选择侧边提示文字或普通提示文字;最后把新提示文字和侧边状态传给 bottom_pane,让底部输入区显示正确的样子。

调用关系:当应用层决定进入或离开侧边对话时,会调用它来更新聊天窗口。它负责把一个高层状态变化翻译成具体界面变化,并把底部区域的细节交给 bottom_pane.set_placeholder_text 和 bottom_pane.set_side_conversation_active 去完成。

ChatWidget::side_conversation_active28–30 ↗
fn side_conversation_active(&self) -> bool

作用:告诉外部:当前聊天窗口是不是处在侧边对话模式。别人需要根据这个状态决定按钮、快捷键或发送行为时,就可以问它。

数据流:进去不需要额外输入;函数只读取 ChatWidget 里保存的 active_side_conversation;出来的是一个真假值,true 表示正在侧边对话中,false 表示不是。

调用关系:它是一个状态查询口。其他代码不必直接碰 ChatWidget 内部字段,而是通过这个函数确认当前模式,从而保持内部状态的封装,避免到处乱读同一份数据。

ChatWidget::set_side_conversation_context_label32–34 ↗
fn set_side_conversation_context_label(&mut self, label: Option<String>)

作用:设置侧边对话的上下文标签,也就是底部界面上用来提醒用户“这条侧边对话跟什么有关”的文字。没有标签时也可以清掉它。

数据流:进去的是一个可选字符串:有字符串就表示要显示的新标签,没有就是清除标签;函数不自己保存或排版,而是直接把这个值交给 bottom_pane;结果是底部面板显示、更新或移除这段上下文提示。

调用关系:当侧边对话绑定到某个上下文时,比如某段内容或某个主题,外部流程会调用它更新界面提示。它把具体显示工作交给 bottom_pane.set_side_conversation_context_label,自己只作为 ChatWidget 对外的转接口。

tui/src/app/thread_settings.rs源码 ↗
orchestrationmain loop / settings change / thread state sync

TUI 是终端用户界面,也就是在命令行里显示的聊天界面;app-server 是后面真正保存和执行对话状态的服务。这个文件就像一个“同步员”:当用户在界面里改了模型、推理强度、协作模式、人格、权限等设置时,它先找到当前活跃的 thread(线程,这里可以理解成一条对话),把改动包装成 ThreadSettingsUpdateParams,再发给 app-server。它也会反过来把 app-server 返回的 ThreadSettings 写回本地缓存的会话状态,避免界面显示和后台状态不一致。一个重要细节是,它发更新前会先检查“到底有没有改动”,空更新不会发,避免白跑一趟;如果发送失败,会在日志里记警告,并在聊天界面显示错误。

函数细节11
App::sync_active_thread_model_setting15–24 ↗
async fn sync_active_thread_model_setting(
        &mut self,
        app_server: &mut AppServerSession,
        model: String,
    )

作用:当用户给当前对话切换模型时,这个函数负责把新模型告诉后台。它先确认当前真的有活跃对话,再发送更新。

数据流:进去的是一个后台连接 app_server 和一个模型名 model → 它调用 App::active_thread_model_setting_update_params 把模型名、当前线程编号和协作模式打包 → 如果能打包成功,就交给 App::send_thread_settings_update 发给后台;如果当前没有线程,就什么也不做。

调用关系:它是“用户改模型”这条流程的入口。它自己不直接拼网络请求,而是先请 App::active_thread_model_setting_update_params 准备参数,再把发送和错误处理交给 App::send_thread_settings_update。

调用图:调用 2 个内部函数(active_thread_model_setting_update_params, send_thread_settings_update)。

App::active_thread_model_setting_update_params26–37 ↗
fn active_thread_model_setting_update_params(
        &self,
        model: String,
    ) -> Option<ThreadSettingsUpdateParams>

作用:这个函数专门为“当前对话改模型”准备一份更新单。它把模型名和当前线程编号放进后台能看懂的格式里。

数据流:进去的是模型名 model,同时它读取 App 里记录的 active_thread_id 和聊天框当前有效的协作模式 → 如果没有活跃线程,就返回 None,意思是没法更新 → 如果有线程,就生成 ThreadSettingsUpdateParams,其中 model 有值,collaboration_mode 也有值,其他字段保持默认空值。

调用关系:它只负责准备材料,被 App::sync_active_thread_model_setting 调用。准备好的材料会继续交给 App::send_thread_settings_update 去真正发送。

调用图:被 1 处调用(sync_active_thread_model_setting);外部调用 1 个(default)。

App::sync_active_thread_reasoning_setting39–48 ↗
async fn sync_active_thread_reasoning_setting(
        &mut self,
        app_server: &mut AppServerSession,
        effort: Option<codex_protocol::openai_models::ReasoningEffort>,
    )

作用:当用户修改当前对话的推理强度时,这个函数把改动同步给后台。推理强度可以理解成模型回答前“思考得多不多”的设置。

数据流:进去的是后台连接 app_server 和可选的 effort 推理强度 → 它调用 App::active_thread_reasoning_setting_update_params 生成更新参数 → 如果当前有活跃线程,就把参数发给 App::send_thread_settings_update;如果没有,就直接结束。

调用关系:它是“用户改推理强度”流程的入口。它依赖 App::active_thread_reasoning_setting_update_params 组装参数,并复用 App::send_thread_settings_update 的统一发送逻辑。

调用图:调用 2 个内部函数(active_thread_reasoning_setting_update_params, send_thread_settings_update)。

App::active_thread_reasoning_setting_update_params50–61 ↗
fn active_thread_reasoning_setting_update_params(
        &self,
        effort: Option<codex_protocol::openai_models::ReasoningEffort>,
    ) -> Option<ThreadSettingsUpdateParams>

作用:这个函数为“当前对话改推理强度”准备后台更新参数。它会把当前线程编号、推理强度和当前协作模式一起带上。

数据流:进去的是 effort,也就是新的推理强度,可能有值也可能为空 → 它读取当前活跃线程编号和聊天框里的当前协作模式 → 有线程时返回 ThreadSettingsUpdateParams;没有线程时返回 None。

调用关系:它被 App::sync_active_thread_reasoning_setting 调用,只做参数准备。真正发送更新仍由 App::send_thread_settings_update 处理。

调用图:被 1 处调用(sync_active_thread_reasoning_setting);外部调用 1 个(default)。

App::sync_active_thread_plan_mode_reasoning_setting63–76 ↗
async fn sync_active_thread_plan_mode_reasoning_setting(
        &mut self,
        app_server: &mut AppServerSession,
    )

作用:这个函数在计划模式相关的推理设置变化时,把当前有效协作模式同步到后台。它关注的重点不是单独某个模型名,而是当前对话应该按哪种协作方式运行。

数据流:进去的是后台连接 app_server → 它先读取当前活跃线程编号;没有线程就停止 → 有线程时创建 ThreadSettingsUpdateParams,只填 thread_id 和当前有效 collaboration_mode,其余字段保持默认 → 然后发送给后台。

调用关系:它直接组装参数,不经过单独的参数辅助函数。最后仍然把发送工作交给 App::send_thread_settings_update,和其他设置同步流程共用同一条出口。

调用图:调用 1 个内部函数(send_thread_settings_update);外部调用 1 个(default)。

App::sync_active_thread_personality_setting78–92 ↗
async fn sync_active_thread_personality_setting(
        &mut self,
        app_server: &mut AppServerSession,
        personality: codex_protocol::config_types::Personality,
    )

作用:当用户切换当前对话的人格设定时,这个函数把新人格同步给后台。人格可以理解成模型说话风格和行为倾向的一组配置。

数据流:进去的是后台连接 app_server 和 personality 人格值 → 它读取当前活跃线程编号;没有线程就不做事 → 有线程时把 thread_id 和 personality 放进 ThreadSettingsUpdateParams → 再交给统一发送函数。

调用关系:它是“改人格”流程的入口。它自己只拼出这次需要改的字段,然后调用 App::send_thread_settings_update 完成实际更新。

调用图:调用 1 个内部函数(send_thread_settings_update);外部调用 1 个(default)。

App::sync_override_turn_context_settings94–135 ↗
async fn sync_override_turn_context_settings(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        op: &AppCommand,
    )

作用:这个函数处理一种更大的“一次性覆盖本轮上下文设置”的命令。它把工作目录、审批规则、权限、模型、推理强度、摘要、服务档位、协作模式、人格等一整包设置同步给后台。

数据流:进去的是后台连接 app_server、目标 thread_id,以及一个 AppCommand 命令 → 它先确认这个命令确实是 OverrideTurnContext;如果不是,就直接返回 → 如果是,就从命令里取出各项设置,转换成后台协议需要的 ThreadSettingsUpdateParams → 最后发送给后台。

调用关系:它服务于更复杂的命令流程,不只是单个按钮改设置。它把 AppCommand::OverrideTurnContext 拆开,做必要转换,比如把审批人类型转成 app-server 使用的类型,然后把统一发送交给 App::send_thread_settings_update。

调用图:调用 1 个内部函数(send_thread_settings_update);外部调用 2 个(default, to_string)。

App::apply_thread_settings_to_cached_session137–154 ↗
async fn apply_thread_settings_to_cached_session(
        &mut self,
        thread_id: ThreadId,
        settings: &ThreadSettings,
    )

作用:这个函数把后台给出的线程设置写回本地缓存的会话。这样界面和本地状态不会继续拿旧模型、旧权限或旧工作目录说话。

数据流:进去的是 thread_id 和后台给出的 settings → 它先检查这个线程是不是主线程,如果是,就更新主会话缓存 → 然后再查这个线程有没有事件通道缓存;如果有,就加锁拿到里面的 session 并更新 → 更新动作由 apply_thread_settings_to_session 完成。

调用关系:它是“后台设置落到本地缓存”的入口。它可能更新两处缓存:主会话缓存和线程事件通道里的缓存;具体怎么把 ThreadSettings 写进 ThreadSessionState,则交给 apply_thread_settings_to_session。

调用图:调用 1 个内部函数(apply_thread_settings_to_session)。

App::send_thread_settings_update156–169 ↗
async fn send_thread_settings_update(
        &mut self,
        app_server: &mut AppServerSession,
        params: ThreadSettingsUpdateParams,
    )

作用:这个函数统一负责把线程设置更新发给 app-server。它还会过滤空更新,并在失败时给用户和日志都留下提示。

数据流:进去的是后台连接 app_server 和一份 ThreadSettingsUpdateParams → 它先调用 thread_settings_update_has_changes 检查里面有没有真正要改的字段;没有就直接返回 → 有改动时调用 app_server.thread_settings_update 发送 → 如果发送失败,就写警告日志,并在聊天界面显示一条错误消息。

调用关系:它是所有“从 TUI 往后台同步设置”流程的共同出口。改模型、改推理强度、改计划模式、改人格、覆盖本轮上下文,最后都会汇到这里。

调用图:调用 2 个内部函数(thread_settings_update_has_changes, thread_settings_update);被 5 处调用(sync_active_thread_model_setting, sync_active_thread_personality_setting, sync_active_thread_plan_mode_reasoning_setting, sync_active_thread_reasoning_setting, sync_override_turn_context_settings);外部调用 2 个(format!, warn!)。

apply_thread_settings_to_session172–195 ↗
fn apply_thread_settings_to_session(session: &mut ThreadSessionState, settings: &ThreadSettings)

作用:这个函数把后台的 ThreadSettings 真正写进一个本地 ThreadSessionState。简单说,它负责把“后台说现在应该是什么设置”翻译成本地会话能用的状态。

数据流:进去的是一个可修改的本地 session 和只读的 settings → 它按字段更新模型提供方、服务档位、审批策略、审批人、权限配置、工作目录重定向、人格和协作模式等 → 如果协作模式是默认模式,还会把模型和推理强度也写回 session → 出来时没有单独返回值,但 session 已被改成和 settings 对齐。

调用关系:它被 App::apply_thread_settings_to_cached_session 调用,是本地缓存更新的具体执行者。它还会调用权限转换函数和工作目录设置函数,把后台协议里的值变成本地运行时能直接使用的值。

调用图:调用 2 个内部函数(from_legacy_sandbox_policy_for_cwd, set_cwd_retargeting_implicit_runtime_workspace_root);被 1 处调用(apply_thread_settings_to_cached_session);外部调用 1 个(new)。

thread_settings_update_has_changes197–209 ↗
fn thread_settings_update_has_changes(params: &ThreadSettingsUpdateParams) -> bool

作用:这个小函数判断一份设置更新里有没有真正要改的东西。它的作用像发快递前检查箱子是不是空的,空箱子就不寄。

数据流:进去的是 ThreadSettingsUpdateParams → 它逐个查看 cwd、审批策略、权限、模型、服务档位、推理强度、摘要、协作模式、人格等字段是不是有值 → 只要有一个字段有值就返回 true;全部都是空就返回 false。

调用关系:它被 App::send_thread_settings_update 调用,位于真正发送给后台之前。它帮助所有设置同步流程避免发送无意义的空更新。

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

tui/src/app/thread_goal_actions.rs源码 ↗
orchestrationrequest handling

这个文件像“目标按钮背后的办事员”。用户在 TUI(终端用户界面,也就是命令行里的图形化界面)里点查看、编辑或替换目标时,它先问后端当前线程有没有目标,再决定显示摘要、打开编辑框、弹确认框,或者报错。它很在意一个问题:用户可能已经切到别的线程了,所以很多异步请求回来后,都会先检查“现在屏幕上看的还是不是刚才那个线程”,避免把旧结果显示到新页面上。设置目标时,它还会把草稿变成真正可用的目标内容;如果中途失败,会清理已经生成的文件,避免留下垃圾。它还特别处理临时会话:临时线程不能保存目标,所以会给用户一段更有帮助的解释,而不是只报一串技术错误。

函数细节18
App::open_thread_goal_menu24–52 ↗
async fn open_thread_goal_menu(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    )

作用:打开当前线程的目标查看入口。它会从后端读取目标;有目标就展示摘要,没目标就告诉用户怎么创建。

数据流:输入是后端会话和线程编号。它向后端请求这个线程的目标,拿到结果后先确认用户还停留在同一个线程;如果读取失败,就把错误变成适合用户看的提示;如果没有目标,就显示目标用法;如果有目标,就交给聊天区域显示目标摘要。

调用关系:这是用户想查看目标时走的入口。它把实际读取工作交给 app_server.thread_goal_get,把错误文案交给 thread_goal_error_message,最后只负责决定在界面上显示什么。

调用图:调用 2 个内部函数(thread_goal_error_message, thread_goal_get)。

App::maybe_prompt_resume_paused_goal_after_resume54–82 ↗
async fn maybe_prompt_resume_paused_goal_after_resume(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    )

作用:恢复会话后,检查目标是不是停住了;如果目标处于暂停、受阻或用量受限状态,就提示用户是否继续。

数据流:输入是后端会话和线程编号。它读取目标,确认界面还在同一线程,然后看目标状态;如果状态表示任务没正常继续,就在聊天区显示一个恢复提示;如果没有目标或目标正常,则什么也不打扰用户。

调用关系:它通常在 resume(恢复旧会话)之后被调用。它只读取目标,不修改目标;读取失败时只写日志警告,不弹错误给用户,避免恢复流程被小问题打断。

调用图:调用 1 个内部函数(thread_goal_get);外部调用 2 个(matches!, warn!)。

App::open_thread_goal_editor84–126 ↗
async fn open_thread_goal_editor(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: Option<ThreadId>,
    )

作用:打开目标编辑器,让用户修改已有目标。没有线程或没有目标时,它会明确告诉用户现在没东西可编辑。

数据流:输入是后端会话和可选的线程编号。它先确认线程存在,再从后端取目标;如果目标引用了文件里的内容,还会把文件内容读出来,换成适合编辑的文字;最后再次确认用户没切线程,然后打开编辑提示框。

调用关系:这是用户选择编辑目标时的流程。它用 show_no_thread_goal_to_edit 处理没目标的情况,用 app_server.thread_goal_get 读取目标,用 goal_files::objective_text_for_edit 准备可编辑文本,用 thread_goal_error_message 显示读取错误。

调用图:调用 5 个内部函数(show_no_thread_goal_to_edit, thread_goal_error_message, codex_home_path, thread_goal_get, objective_text_for_edit)。

App::set_thread_goal_draft128–227 ↗
async fn set_thread_goal_draft(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        draft: goal_files::GoalDraft,
        mode: ThreadGoalSetMode,
    )

作用:把用户输入或编辑出来的目标草稿真正设置到线程上。它会处理“是否替换旧目标”、生成目标文件、清理失败残留,以及成功后的界面反馈。

数据流:输入是后端会话、线程编号、目标草稿和设置模式。它可能先读取现有目标,判断是否需要用户确认替换;确认后把草稿变成实际目标内容和可能的输出目录;如果是替换,会先清掉旧目标;然后调用后端设置新目标或更新状态。成功后显示目标状态和用量摘要,并可能发送排队的下一条输入;失败时会清理已生成的目标文件,并显示合适错误。

调用关系:这是设置目标最核心的调度函数。它会调用 should_confirm_before_replacing_goal 判断是否弹确认框,调用 App::show_replace_thread_goal_confirmation 显示确认选择,调用 goal_files::materialize_goal_draft 准备目标内容,调用 thread_goal_clear 和 thread_goal_set 改后端状态,失败时交给 cleanup_materialized_goal_files 收尾。

调用图:调用 10 个内部函数(show_replace_thread_goal_confirmation, cleanup_materialized_goal_files, should_confirm_before_replacing_goal, thread_goal_error_message, codex_home_path, thread_goal_clear, thread_goal_get, thread_goal_set, goal_usage_summary, materialize_goal_draft);外部调用 2 个(format!, matches!)。

App::set_thread_goal_status229–256 ↗
async fn set_thread_goal_status(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
        status: ThreadGoalStatus,
    )

作用:只修改目标状态,比如把目标设为暂停、继续或其他状态,而不改目标内容。

数据流:输入是后端会话、线程编号和新状态。它把“目标内容为空、状态为新状态、预算不改”发给后端;结果回来后确认界面仍在同一线程。成功就显示新状态和用量摘要,失败就显示更新失败的错误。

调用关系:它用于状态按钮或命令触发的轻量更新。实际保存由 app_server.thread_goal_set 完成,错误格式化交给 thread_goal_error_message,用量说明交给 goal_usage_summary。

调用图:调用 3 个内部函数(thread_goal_error_message, thread_goal_set, goal_usage_summary);外部调用 1 个(format!)。

App::clear_thread_goal258–284 ↗
async fn clear_thread_goal(
        &mut self,
        app_server: &mut AppServerSession,
        thread_id: ThreadId,
    )

作用:清除当前线程的目标。它会告诉用户是真的清掉了,还是本来就没有目标。

数据流:输入是后端会话和线程编号。它请求后端清除目标,结果回来后确认用户还在这个线程;如果后端说已清除,就显示“Goal cleared”;如果没有可清除的目标,就提示这个线程当前没有目标;失败则显示清除失败原因。

调用关系:这是删除目标的界面动作。它把实际清除交给 app_server.thread_goal_clear,把错误说明交给 thread_goal_error_message。

调用图:调用 2 个内部函数(thread_goal_error_message, thread_goal_clear)。

App::show_replace_thread_goal_confirmation286–325 ↗
fn show_replace_thread_goal_confirmation(
        &mut self,
        thread_id: ThreadId,
        draft: goal_files::GoalDraft,
    )

作用:在用户要用新目标覆盖未完成的旧目标前,弹出一个确认菜单,防止误操作。

数据流:输入是线程编号和新目标草稿。它复制草稿内容,准备两个选项:一个是“替换当前目标”,选择后会发送 SetThreadGoalDraft 事件;另一个是“取消”,保留旧目标。最后把这些选项交给聊天界面显示。

调用关系:它只负责显示确认,不直接改目标。App::set_thread_goal_draft 发现旧目标还没完成时会调用它;用户确认后,它发出的 AppEvent::SetThreadGoalDraft 会让设置流程以 ReplaceExisting 模式重新执行。

调用图:调用 1 个内部函数(standard_popup_hint_line);被 1 处调用(set_thread_goal_draft);外部调用 3 个(default, format!, vec!)。

App::show_no_thread_goal_to_edit327–334 ↗
fn show_no_thread_goal_to_edit(&mut self)

作用:告诉用户现在没有可编辑的目标,并顺手提示应该先创建目标。

数据流:它不需要外部输入,只改界面。它先在聊天区加一条错误消息“当前没有目标”,再加一条说明目标用法的信息,帮助用户下一步创建目标。

调用关系:App::open_thread_goal_editor 在没有线程编号或后端返回没有目标时会调用它。它是编辑流程里的友好兜底提示。

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

cleanup_materialized_goal_files337–346 ↗
async fn cleanup_materialized_goal_files(
    app_server: &mut AppServerSession,
    output_dir: Option<goal_files::GoalFilePath>,
)

作用:设置目标失败时,删除刚刚为目标生成出来的文件或目录,避免磁盘上留下没用的半成品。

数据流:输入是后端会话和一个可选的目标文件路径。如果路径存在,它请求后端删除这个路径;删除失败不会再打扰用户,只写一条警告日志,说明清理没成功。

调用关系:它由 App::set_thread_goal_draft 在替换或设置失败时调用。它把真正的文件删除交给 app_server.fs_remove_path,自己负责“有路径才删、删不掉就记日志”的收尾规则。

调用图:被 1 处调用(set_thread_goal_draft);外部调用 2 个(warn!, fs_remove_path)。

thread_goal_error_message348–354 ↗
fn thread_goal_error_message(action: &str, err: &color_eyre::Report) -> String

作用:把目标操作失败的技术错误,改写成用户更容易看懂的错误消息。

数据流:输入是动作名字,比如 read、set、clear,以及一份错误报告。它先判断这是不是“临时会话不支持目标”的特殊错误;如果是,就返回一段明确告诉用户要使用保存会话的说明;否则返回“Failed to 某动作 thread goal: 原错误”的通用消息。

调用关系:查看、编辑、设置状态、设置草稿、清除目标这些流程都会用它。它内部把特殊错误识别交给 is_ephemeral_thread_goal_error,也被测试用例拿来确认显示效果。

调用图:调用 1 个内部函数(is_ephemeral_thread_goal_error);被 6 处调用(clear_thread_goal, open_thread_goal_editor, open_thread_goal_menu, set_thread_goal_draft, set_thread_goal_status, thread_goal_ephemeral_error_message_renders_snapshot);外部调用 1 个(format!)。

is_ephemeral_thread_goal_error356–362 ↗
fn is_ephemeral_thread_goal_error(err: &color_eyre::Report) -> bool

作用:判断一个错误是不是因为“临时线程不能使用目标”造成的。

数据流:输入是一份错误报告。它沿着错误链逐层查看每个原因的文字,只要发现包含临时线程不支持目标、或目标需要持久化线程的固定句子,就返回 true;否则返回 false。

调用关系:它是 thread_goal_error_message 的小帮手。外层函数靠它决定要不要把普通报错换成专门解释临时会话的友好提示。

调用图:被 1 处调用(thread_goal_error_message);外部调用 1 个(chain)。

should_confirm_before_replacing_goal364–375 ↗
fn should_confirm_before_replacing_goal(goal: &ThreadGoal) -> bool

作用:判断替换现有目标前要不要先问用户确认。它的原则是:已完成的目标可以直接换,没完成的目标要防误删。

数据流:输入是当前目标。它查看目标状态:Complete 表示已经结束,不需要确认;Active、Paused、Blocked、UsageLimited、BudgetLimited 都表示还有未完成的事情,所以返回需要确认。

调用关系:App::set_thread_goal_draft 在 ConfirmIfExists 模式下会调用它。它决定流程是直接替换,还是先交给 App::show_replace_thread_goal_confirmation 弹确认菜单。

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

tests::thread_goal_error_message_explains_temporary_session386–396 ↗
fn thread_goal_error_message_explains_temporary_session()

作用:测试临时线程相关错误会不会显示成清楚的人话,而不是原始技术错误。

数据流:它构造一个包含“ephemeral thread does not support goals”的错误,再调用 thread_goal_error_message。最后断言输出正好等于专门准备的临时会话说明文案。

调用关系:这是 thread_goal_error_message 的单元测试。它保护一个重要体验:用户在临时会话里用目标功能时,能知道该怎么解决。

调用图:外部调用 2 个(assert_eq!, eyre!)。

tests::thread_goal_ephemeral_error_message_renders_snapshot399–419 ↗
fn thread_goal_ephemeral_error_message_renders_snapshot()

作用:测试临时会话错误消息在终端界面里实际渲染出来的样子没有变坏。

数据流:它先制造临时线程错误,生成错误消息,再把这条消息变成历史记录单元,放进一个模拟终端。最后用快照测试保存并比对终端画面。

调用关系:它同样围绕 thread_goal_error_message,但关注的是显示效果。它会调用终端测试工具、插入历史行,并用快照断言来防止界面排版意外变化。

调用图:调用 4 个内部函数(thread_goal_error_message, with_options, insert_history_lines, new);外部调用 4 个(new, eyre!, new_error_event, assert_snapshot!)。

tests::thread_goal_error_message_preserves_generic_failure_context422–430 ↗
fn thread_goal_error_message_preserves_generic_failure_context()

作用:测试普通错误不会被误判成临时会话错误,并且仍然保留原本的失败上下文。

数据流:它构造一个普通的“server disappeared”错误,并包上一层“thread/goal/get failed in TUI”。调用 thread_goal_error_message 后,断言结果是通用失败消息,且包含外层上下文。

调用关系:这是 thread_goal_error_message 的另一面测试。它确保特殊友好提示只用于真正的临时会话问题,不会掩盖普通故障。

调用图:外部调用 2 个(assert_eq!, eyre!)。

tests::completed_goal_does_not_require_replace_confirmation433–437 ↗
fn completed_goal_does_not_require_replace_confirmation()

作用:测试已完成目标可以直接被新目标替换,不需要弹确认框。

数据流:它用 tests::test_goal 做出一个 Complete 状态的目标,传给 should_confirm_before_replacing_goal,然后断言结果是 false,也就是不需要确认。

调用关系:这是 should_confirm_before_replacing_goal 的规则测试。它保证“完成的工作可以自然开启下一件事”这个交互不会被改坏。

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

tests::unfinished_goals_require_replace_confirmation440–450 ↗
fn unfinished_goals_require_replace_confirmation()

作用:测试所有未完成状态的目标,在替换前都必须先问用户确认。

数据流:它依次用 Active、Paused、Blocked、UsageLimited、BudgetLimited 这些状态造目标。每个目标都传给 should_confirm_before_replacing_goal,并断言结果是 true。

调用关系:这是替换保护规则的批量测试。它确保 App::set_thread_goal_draft 不会在目标还没结束时静悄悄覆盖用户的工作。

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

tests::test_goal452–463 ↗
fn test_goal(status: ThreadGoalStatus) -> ThreadGoal

作用:为测试快速造一个假的 ThreadGoal,避免每个测试重复写一大堆字段。

数据流:输入是一个目标状态。它把这个状态和固定的线程编号、目标文字、时间、用量等字段组合成一个 ThreadGoal 返回。

调用关系:它只服务于本文件的测试。completed_goal_does_not_require_replace_confirmation 和 unfinished_goals_require_replace_confirmation 用它生成不同状态的目标,再检查替换确认规则。

调用图:调用 1 个内部函数(new)。