Codex 系统手册

关闭、清理与收尾

stage-174 个文件

这一段像程序“打烊”。连接要断、会话要关,或后台更新要重启服务时,先别一脚拔电源。RPC 闸门先拦住新请求,让已开工的请求做完;连接清理器把断线后的扫尾丢到后台,能收就收,超时就报错或取消;老式 agent 管家先保存状态,再通知它和子任务关机,最后清掉记录;自动更新循环则挑空档下载新版并重启服务,尽量不打扰正在干活的部分。

本阶段涉及的状态17
  • reg-local-state-runtime本地 SQLite 和 StateRuntime 打开的那套持久化句柄,负责让线程、日志、目标、记忆等数据能长期保存。
  • reg-thread-session-store当前有哪些线程和会话、哪个正在用、哪些能恢复或关闭的共享会话账本。
  • reg-agent-graph多智能体和 fork 线程之间谁带起谁、父子关系是否还活着的关系图。
  • reg-mcp-registryMCP 服务器、连接、工具和资源的登记表,模型和前端都靠它知道能调用哪些外部能力。
  • reg-exec-environment-processesexec-server 和统一执行层里正在运行或已经结束的进程、退出码、输入输出和失败原因。
  • reg-transport-rpc-connectionsstdio、本机 socket、WebSocket、JSON-RPC、relay 等通道上的连接、请求编号和收发队列。
  • reg-server-daemon-stateapp-server、daemon、exec-server 等常驻服务当前是否启动、有哪些客户端连着、处理器是否还接单的运行状态。
  • reg-ui-interaction-stateTUI 或前端当前显示哪个线程、输入框内容、弹窗、快捷键、通知和终端接管等界面状态。
  • reg-turn-execution-state一轮对话正在排队、运行、取消、追加输入、压缩上下文或等待工具结果的临时但跨模块共享状态。
  • reg-background-job-queue后台刷新云配置、同步目录、整理记忆、清理连接、检查更新等异步任务的队列和运行状态。
  • reg-observability-telemetry日志、指标、分析事件、请求耗时、工具结果和错误报告等观测数据的收集与发送状态。
  • reg-code-mode-sessions代码模式运行器里的会话、正在执行的脚本、等待结果、定时器和宿主回调状态。
  • reg-rollout-trace-logrollout 记录下来的可回放流水账和会话状态,用来恢复、排错、重连和事后分析。
  • reg-agent-orchestration-runtime多智能体运行时的活跃代理登记、并发闸门、等待区、代理间消息和等待结果状态。
  • reg-auto-update-state自动更新检查、可用版本、下载进度、安装结果和待重启标记等更新生命周期状态。
  • reg-invocation-dispatch-state本次进程启动选择的入口模式、子命令和早期解析参数,决定后续走 TUI、exec、登录、服务器或维护流程。
  • reg-async-runtime-shutdown全局异步运行时、任务执行句柄、取消信号和关机协调状态,供服务器、后台任务、工具执行和清理流程共用。

本阶段的文件4

Daemon 重启循环

处理由 updater 驱动的 daemon 生命周期,包括定期刷新检查,以及当受管理的二进制文件变化时重新 exec。

app-server-daemon/src/update_loop.rs源码 ↗
orchestrationstartup 后进入后台循环,之后定时运行,收到终止信号时退出

这个文件像一个“后台值班员”。程序启动这个更新循环后,它先等一小段时间,避免刚启动就立刻折腾更新;然后每隔一小时检查一次。每次检查时,它会从网上下载并运行官方安装脚本,确保本机的独立 Codex 程序是新的。接着它会找到当前 daemon(后台服务)的配置,确认受管理的 Codex 可执行文件在哪里,并比较“现在这个更新器自己是谁”和“受管理的程序是谁”。如果两者不同,说明更新器可能也需要换成受管理的新程序来继续工作。之后它尝试重启正在运行的服务;如果服务正忙,就每 50 毫秒等一下再试。整个过程会监听系统的终止信号,收到后就干净退出。这个文件主要只支持 Unix 系统;非 Unix 平台会直接报错说不支持。

函数细节7
run72–74 ↗
async fn run() -> Result<()>

作用:这是更新循环的主入口。它负责装好退出信号监听器,先延迟一会儿,然后不断执行“检查并更新一次”,直到系统要求它停止。

数据流:进去时没有业务参数;它会读取当前运行的更新器身份,并监听系统发来的终止信号。之后它先等待初始延迟,再反复调用单次更新流程;每轮结束后继续睡到下一次检查。出来时返回成功,或者在无法安装信号监听、无法识别当前程序等情况下返回错误;在非 Unix 平台上则直接返回“不支持”的错误。

调用关系:它由 run_pid_update_loop 启动,是整个文件的总调度者。它先找 current_updater_identity,再用 sleep_or_terminate 控制等待和退出,真正更新时把工作交给 update_once。

调用图:调用 3 个内部函数(current_updater_identity, sleep_or_terminate, update_once);被 1 处调用(run_pid_update_loop);外部调用 3 个(terminate, bail!, signal)。

sleep_or_terminate77–82 ↗
async fn sleep_or_terminate(duration: Duration, terminate: &mut Signal) -> bool

作用:这个函数用来“睡一会儿,但随时能被叫醒退出”。它避免更新循环在等待时对关闭信号没反应。

数据流:进去的是一段要等待的时间,以及一个终止信号接收器。它同时等两件事:时间到,或者收到终止信号。时间到了就返回 false,表示可以继续干活;收到终止信号就返回 true,表示应该停下。

调用关系:run 用它来做初始等待和每小时等待;update_once 在遇到服务正忙、需要短暂重试时也用它。它是更新循环里“可中断等待”的小工具。

调用图:被 2 处调用(run, update_once);外部调用 1 个(select!)。

update_once91–119 ↗
async fn update_once(
    running_updater_identity: &ExecutableIdentity,
    terminate: &mut Signal,
) -> Result<UpdateLoopControl>

作用:这个函数执行一轮真正的更新。它会安装最新版本,判断该怎么重启服务,并在服务不忙时触发重启。

数据流:进去的是当前更新器的身份,以及终止信号接收器。它先下载并运行最新安装脚本;然后从环境里读取 daemon 配置,找到受管理的 Codex 程序路径,识别这个程序的身份;再比较两个身份,决定重启策略和更新器刷新策略。接着它尝试重启正在运行的服务:如果服务忙,就短暂等待后再试;如果收到终止信号,就返回 Stop;如果重启尝试结束,就返回 Continue。

调用关系:它由 run 每轮定时调用,是单次更新工作的核心。它会调用 install_latest_standalone 做安装,调用 resolved_managed_codex_bin 和 executable_identity 找到并识别受管理程序,再用 update_modes_for_identities 决定策略,最后通过 daemon 的 try_restart_if_running 去推动重启。

调用图:调用 6 个内部函数(from_environment, executable_identity, resolved_managed_codex_bin, install_latest_standalone, sleep_or_terminate, update_modes_for_identities);被 1 处调用(run);外部调用 1 个(recv)。

current_updater_identity122–126 ↗
async fn current_updater_identity() -> Result<ExecutableIdentity>

作用:这个函数用来弄清楚“正在运行的这个更新器程序到底是哪一个”。后面要靠这个身份判断是否需要让更新器换成受管理的新程序。

数据流:进去时没有参数;它读取当前进程对应的可执行文件路径,也就是操作系统认为这个程序从哪里启动。然后它把这个路径交给 executable_identity,算出可执行文件的身份信息。出来的是这个身份,或者在找不到当前程序路径、无法识别文件时返回错误。

调用关系:run 在更新循环刚开始时调用它一次。它的结果会传给 update_once,用来和受管理 Codex 程序的身份做比较。

调用图:调用 1 个内部函数(executable_identity);被 1 处调用(run);外部调用 1 个(current_exe)。

update_modes_for_identities129–141 ↗
fn update_modes_for_identities(
    running_updater_identity: &ExecutableIdentity,
    managed_identity: &ExecutableIdentity,
) -> (RestartMode, UpdaterRefreshMode)

作用:这个函数负责决定更新后该怎么重启、要不要刷新更新器自己。它把两个程序身份做比较,然后给出简单的策略选择。

数据流:进去的是两个身份:正在运行的更新器身份,以及受管理 Codex 程序身份。如果两者相同,就说明更新器已经是受管理的那个程序,只需要在版本变化时重启服务,不需要重启更新器自己;如果两者不同,就要求总是尝试重启,并且当受管理程序变化时重新执行新的更新器。出来的是一对模式值:重启模式和更新器刷新模式。

调用关系:update_once 在识别完两个程序后调用它。它不做安装、不做重启,只负责把“身份是否相同”翻译成后续 daemon 重启流程能理解的策略。

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

reexec_managed_updater144–154 ↗
fn reexec_managed_updater(managed_codex_bin: &std::path::Path) -> Result<()>

作用:这个函数把当前更新器进程替换成受管理的 Codex 程序。简单说,就是“不要继续用旧的自己跑了,直接变成新程序继续跑”。

数据流:进去的是受管理 Codex 可执行文件的路径。它用这个路径启动同一个更新循环命令,并使用 exec 方式替换当前进程;exec 可以理解为进程原地换壳,进程号通常不变,但运行的程序变了。如果替换成功,这个函数不会正常返回;如果失败,就返回带有路径说明的错误。

调用关系:它由 try_restart_if_running 在需要刷新更新器时调用。update_once 会先决定 UpdaterRefreshMode,后续 daemon 重启逻辑根据这个模式决定是否走到这里。

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

install_latest_standalone157–193 ↗
async fn install_latest_standalone() -> Result<()>

作用:这个函数负责下载并执行官方安装脚本,用来把独立版 Codex 更新到最新。没有它,更新循环只会重启服务,不能真正把新版本装到机器上。

数据流:进去时没有参数;它从固定网址下载 install.sh 脚本,确认 HTTP 请求成功,读取脚本内容。然后它启动 /bin/sh,把脚本内容写进 shell 的标准输入,隐藏脚本输出,等待脚本执行完。执行成功就返回成功;下载失败、启动 shell 失败、写入失败,或者脚本退出码表示失败,都会返回错误。

调用关系:它由 update_once 每轮最先调用。只有安装脚本跑完后,update_once 才继续查找受管理程序、判断身份、尝试重启服务。

调用图:被 1 处调用(update_once);外部调用 5 个(null, piped, bail!, new, get)。

连接关闭门控

阻止新的按连接 RPC 工作启动,然后通过优雅完成或中止来管理剩余清理任务。

app-server/src/connection_rpc_gate.rs源码 ↗
domain_logicrequest handling / shutdown

可以把它想成地铁站的闸机。平时闸机打开,请求处理函数可以进来执行;一旦连接开始关闭,闸机立刻关上,新来的处理函数直接被丢掉,不会偷偷开始跑。但已经刷卡进站的人,也就是已经拿到“令牌”的处理函数,会被允许走完。这里用互斥锁(一把锁,防止两个任务同时改同一份状态)保护“还接不接新活”的开关;用 TaskTracker(任务计数器,知道还有多少活没做完)记录已经开始的处理。close 只负责关门,不等人走完;shutdown 会先关门,再等所有已开始的处理结束。文件后半部分是测试,专门确认这些边界情况不会出错,比如关闭后不会执行新任务、关闭时仍会等待老任务结束。

函数细节13
ConnectionRpcGate::new17–23 ↗
fn new() -> Self

作用:创建一个新的 RPC 闸门,默认是“打开”的,可以接收并执行请求处理。连接刚建立、需要准备处理 RPC 请求时会用它。

数据流:进去没有额外参数 → 它把“是否接受新任务”设为 true,并创建一个新的任务计数器 → 出来一个可用的 ConnectionRpcGate,之后可以用来放行、关闭和等待请求处理。

调用关系:它是整个闸门对象的起点。测试会直接调用它来造闸门;默认构造函数也会转到这里,保证默认创建出来的闸门和手动 new 出来的一样。

调用图:被 8 处调用(close_returns_while_started_run_remains_active, run_drops_future_without_polling_after_close, run_executes_while_open, run_is_counted_before_handler_body_continues, shutdown_drops_late_runs_while_waiting_for_inflight_work, shutdown_waits_for_started_run_to_finish, new, gate);外部调用 2 个(new, new)。

ConnectionRpcGate::run25–39 ↗
async fn run(&self, future: F)

作用:尝试执行一个请求处理任务。闸门开着时,它会让任务开始并把它记为“正在运行”;闸门关了时,它会直接放弃这个任务,连任务内容都不启动。

数据流:进去一个 future(可以理解为一段将来要异步执行的活)→ 它先拿锁检查闸门是否还接活;如果不接了,就直接返回;如果还接,就从任务计数器拿一个令牌,开始等待并执行这段活 → 活结束后令牌被丢掉,正在运行的数量随之减少。

调用关系:这是请求处理进入闸门的主要入口。它和 close 配合:run 开始前必须先看 close 有没有关门;它和 shutdown 配合:run 拿到的令牌会让 shutdown 知道还有任务没结束,需要继续等。

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

ConnectionRpcGate::close41–45 ↗
async fn close(&self)

作用:关闭闸门,让之后来的请求处理不再开始。它不会硬停已经开始的任务,只是停止接新活。

数据流:进去没有参数,只使用闸门内部状态 → 它拿到锁,把“接受新任务”改成 false,并通知任务计数器进入关闭状态 → 出来没有返回值,但闸门状态已经变成关闭。

调用关系:它是停机流程的第一步。shutdown 会先调用它;普通关闭场景也可以只调用它,用来做到“别接新活,但让老活自己结束”。

调用图:被 1 处调用(shutdown);外部调用 1 个(close)。

ConnectionRpcGate::shutdown47–50 ↗
async fn shutdown(&self)

作用:完整关闭闸门:先不再接新任务,再等已经开始的任务全部结束。适合连接真正要收尾时使用。

数据流:进去没有参数 → 它先调用 close 关门,再等待任务计数器里的所有令牌消失,也就是等所有已开始的处理完成 → 等待结束后返回,表示这个连接上的 RPC 处理已经清干净。

调用关系:它把 close 和 TaskTracker 的等待能力串起来,是“安全收尾”的方法。测试会用它确认:关闭期间新任务不会启动,但老任务没结束前 shutdown 不会提前返回。

调用图:调用 1 个内部函数(close);外部调用 1 个(wait)。

ConnectionRpcGate::is_accepting53–55 ↗
async fn is_accepting(&self) -> bool

作用:测试用的小工具,用来查看闸门现在是不是还在接收新任务。它只在测试编译时存在,不给正式运行代码使用。

数据流:进去没有参数 → 它读取内部的 accepting 开关 → 出来一个布尔值:true 表示还开着,false 表示已经关门。

调用关系:它服务于测试,用来验证 close 或 shutdown 之后闸门确实变成“不接新活”。正式流程里,外部不会靠它做判断。

ConnectionRpcGate::inflight_count58–60 ↗
fn inflight_count(&self) -> usize

作用:测试用的小工具,用来数当前有多少个请求处理已经开始但还没结束。它帮助测试确认任务计数是否准确。

数据流:进去没有参数 → 它读取任务计数器的长度 → 出来一个数字,表示正在运行中的任务数量。

调用关系:它被多个测试用来观察 run、close、shutdown 之间的配合是否正确。比如 run 开始后数量应变成 1,任务结束后应回到 0。

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

ConnectionRpcGate::default64–66 ↗
fn default() -> Self

作用:提供标准的默认创建方式。别人需要“默认闸门”时,不必记住具体初始化细节。

数据流:进去没有参数 → 它走和 new 相同的创建路径 → 出来一个打开状态的新 ConnectionRpcGate。

调用关系:它让这个类型符合 Rust 的 Default 习惯用法。实际创建逻辑仍集中在 new,避免两处初始化规则不一致。

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

tests::run_executes_while_open81–92 ↗
async fn run_executes_while_open()

作用:测试闸门刚创建、还开着时,run 确实会执行传进去的任务。

数据流:进去由测试框架启动 → 它创建闸门和一个原子布尔值(可安全跨任务读写的 true/false 标记),把设置标记的任务交给 run → 最后检查标记变成 true,说明任务真的跑过。

调用关系:这是最基础的正向测试。它验证 ConnectionRpcGate::new 创建出的默认状态是可执行任务的,也验证 ConnectionRpcGate::run 在开门时会放行。

调用图:调用 1 个内部函数(new);外部调用 4 个(clone, new, new, assert!)。

tests::run_drops_future_without_polling_after_close95–108 ↗
async fn run_drops_future_without_polling_after_close()

作用:测试闸门关闭后,run 不会启动后来交进来的任务。这里特别确认任务体没有被执行过。

数据流:进去由测试框架启动 → 它创建闸门后先 close,再交给 run 一个会改标记的任务 → 最后标记仍是 false,并且闸门状态是关闭,说明任务被丢掉而不是执行了。

调用关系:它覆盖 close 和 run 的关键配合:close 关门以后,run 必须在任务真正开始前拦住它。is_accepting 只在这里辅助确认状态。

调用图:调用 1 个内部函数(new);外部调用 4 个(clone, new, new, assert!)。

tests::close_returns_while_started_run_remains_active111–135 ↗
async fn close_returns_while_started_run_remains_active()

作用:测试 close 只是关门,不会等待已经开始的任务结束。也就是说,关门动作应该很快返回,老任务还可以继续挂着。

数据流:进去由测试框架启动 → 它启动一个会卡住等待信号的 run 任务,确认任务已开始后调用 close → 检查闸门已关闭且正在运行数是 1,再发信号让任务结束 → 最后等待任务收尾并调用 shutdown 清理。

调用关系:它验证 close 的定位:只阻止新任务,不负责等老任务。这里用 channel(一次性信号通道)控制任务什么时候开始、什么时候结束,用 spawn 把任务放到异步运行环境里跑。

调用图:调用 1 个内部函数(new);外部调用 6 个(clone, new, assert!, assert_eq!, channel, spawn)。

tests::shutdown_waits_for_started_run_to_finish138–170 ↗
async fn shutdown_waits_for_started_run_to_finish()

作用:测试 shutdown 会等待已经开始的任务完成,而不是一关门就立刻返回。

数据流:进去由测试框架启动 → 它启动一个会停住的 run 任务,确认正在运行数为 1,然后另起一个 shutdown → 用很短的 timeout(超时限制)确认 shutdown 没有提前结束;之后发信号让任务完成,再确认清理后数量变成 0。

调用关系:它验证 shutdown 和 TaskTracker 等待机制的配合。run 拿到的令牌让 shutdown 知道还有任务在跑,所以 shutdown 必须等令牌释放后才算完成。

调用图:调用 1 个内部函数(new);外部调用 7 个(clone, new, from_millis, assert_eq!, channel, spawn, timeout)。

tests::shutdown_drops_late_runs_while_waiting_for_inflight_work173–212 ↗
async fn shutdown_drops_late_runs_while_waiting_for_inflight_work()

作用:测试 shutdown 正在等待老任务时,新来的 run 也不能偷偷开始。这个场景很重要,因为关闭过程中最容易出现“又进来一个新活”的竞态问题。

数据流:进去由测试框架启动 → 它先让一个任务开始并卡住,再启动 shutdown;确认 shutdown 正在等待后,又提交一个会改标记的新任务 → 标记保持 false,说明新任务没运行;最后放行老任务,等待全部收尾并确认运行数为 0。

调用关系:它把 run、shutdown、close 的边界放在一起测。shutdown 内部会先 close,所以等待老任务时,后来的 run 应该被关在门外。

调用图:调用 1 个内部函数(new);外部调用 9 个(clone, new, new, from_millis, assert!, assert_eq!, channel, spawn, timeout)。

tests::run_is_counted_before_handler_body_continues215–237 ↗
async fn run_is_counted_before_handler_body_continues()

作用:测试 run 在执行任务体时,已经先把这个任务计入“正在运行”。这能保证 shutdown 不会漏掉刚开始的请求处理。

数据流:进去由测试框架启动 → 它启动一个进入后会暂停的任务,等任务体已经进入后检查运行数 → 此时数量应为 1;随后发信号让任务继续结束,再确认数量回到 0。

调用关系:它专门验证 run 的计数顺序:必须先拿到任务令牌,再让处理逻辑继续跑。这样 shutdown 才能可靠地看到所有已经开始的任务。

调用图:调用 1 个内部函数(new);外部调用 5 个(clone, new, assert_eq!, channel, spawn)。

app-server/src/connection_cleanup.rs源码 ↗
orchestrationconnection teardown / server shutdown

服务器处理连接时,断开连接不代表事情马上结束,可能还要释放资源、通知别的部分、清掉临时状态。这个文件就像一个“后台清洁工登记本”:ConnectionCleanupTasks 里面放着一组异步任务。异步任务可以理解成“先安排好,之后慢慢做的工作”。新任务来了就用 spawn 放进去;主流程想等一个任务结束时,用 reap_next 收一个结果;服务器准备停机或彻底收尾时,用 drain 把剩下的都等完;如果不想等了,用 abort 一口气取消。这里还特别处理了报错:如果清理任务是真的失败,会写警告日志;如果只是因为被取消,那是正常停机的一部分,不会吓人地报错。

函数细节6
ConnectionCleanupTasks::new13–17 ↗
fn new() -> Self

作用:创建一个空的清理任务登记本。服务器启动主流程时会先准备好它,之后才能把连接清理任务放进去。

数据流:进去没有额外输入 → 它新建一个空的 JoinSet,也就是 Tokio 用来装一批后台异步任务的容器 → 出来一个 ConnectionCleanupTasks,里面暂时没有任何清理任务。

调用关系:它被 run_main_with_transport_options 调用,说明主运行流程一开始会准备这个清理任务集合。它内部只把活交给 Tokio 的 new 来创建任务容器。

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

ConnectionCleanupTasks::spawn19–21 ↗
fn spawn(&mut self, future: impl Future<Output = ()> + Send + 'static)

作用:把一个新的连接清理工作丢到后台去跑。调用者不用当场等它做完,服务器可以继续处理别的事。

数据流:进去一个 future,也就是一段将来会完成的异步工作 → 它把这个工作交给内部的 JoinSet 去启动 → 出来没有返回值,但内部任务列表多了一个正在跑的清理任务。

调用关系:它是外部代码把清理工作加入这套机制的入口。它自己不做清理内容,只把任务转交给 Tokio 的 spawn 去真正运行。

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

ConnectionCleanupTasks::reap_next23–30 ↗
async fn reap_next(&mut self)

作用:等待并收走下一个完成的清理任务结果。这样后台任务完成后不会没人管,失败时也能记一条警告。

数据流:进去的是当前保存着的一批清理任务 → 如果里面一个任务都没有,它会一直等着,不会立刻返回;如果有任务,就等下一个结束 → 拿到结束结果后交给 log_cleanup_result 检查,必要时写警告日志。

调用关系:它处在后台清理任务的日常回收流程里。它先问 JoinSet 是否为空,再等 join_next 拿结果,最后把结果交给 log_cleanup_result 判断该不该报警。

调用图:调用 1 个内部函数(log_cleanup_result);外部调用 2 个(is_empty, join_next)。

ConnectionCleanupTasks::drain32–36 ↗
async fn drain(&mut self)

作用:把所有还在登记本里的清理任务一个个等完。通常用于服务器收尾时,尽量让已经开始的清理工作正常结束。

数据流:进去的是当前剩下的所有清理任务 → 它反复等待下一个任务结束,直到没有任务为止 → 每个任务的结果都会交给 log_cleanup_result;函数结束时,内部任务集合已经被清空。

调用关系:它用于集中收尾。它不断调用 JoinSet 的 join_next 收任务结果,并把每个结果交给 log_cleanup_result 做统一的错误记录。

调用图:调用 1 个内部函数(log_cleanup_result);外部调用 1 个(join_next)。

ConnectionCleanupTasks::abort38–40 ↗
fn abort(&mut self)

作用:立刻取消所有还没完成的清理任务。适合不能再等、需要快速停下来的场景。

数据流:进去的是当前还在运行或等待的清理任务集合 → 它调用 abort_all 要求这些任务全部停止 → 出来没有返回值,但这些后台任务会被标记为取消。

调用关系:它是强制停止清理任务的按钮。它不逐个检查结果,只把取消命令交给 Tokio 的 abort_all。之后如果这些取消结果被回收,log_cleanup_result 会把“被取消”当作正常情况,不写失败警告。

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

log_cleanup_result43–49 ↗
fn log_cleanup_result(result: Result<(), JoinError>)

作用:统一检查清理任务的结束结果。真正失败就写警告;如果只是被取消,就不当成错误。

数据流:进去一个任务结束结果:要么成功,要么带着 JoinError 失败信息 → 它判断是不是错误,以及这个错误是不是“任务被取消” → 如果是非取消类失败,就通过 warn! 写一条警告日志;否则什么也不做。

调用关系:它被 ConnectionCleanupTasks::reap_next 和 ConnectionCleanupTasks::drain 调用,负责把任务结果翻译成日志行为。这样两个回收流程不用各自写一遍错误判断。

调用图:被 2 处调用(drain, reap_next);外部调用 1 个(warn!)。

Agent 线程关闭

关闭旧版 agent 线程,并持久化由此产生的已关闭线程树状态,包括后代拆除。

core/src/agent/control/legacy.rs源码 ↗
orchestrationteardown / request handling

这里的 agent 可以理解成系统里一个正在干活的“小工人”。关闭它不能只是粗暴断电,因为它可能还有没写完的记录,也可能派生了子工人。这个文件给 AgentControl 加了三个关闭相关的方法。shutdown_live_agent 负责关一个具体还活着的工人:先把会话里的 rollout(可以理解成执行过程记录)落到持久存储里,再发送 Shutdown(关机)命令,等它真的停下,然后清理内存里的线程记录。close_agent 更像“用户明确点了关闭”:它会先把这个 agent 的派生关系标成 Closed(已关闭),这样以后系统知道它是被主动关掉的,不是丢了或崩了。shutdown_agent_tree 则先找出内存里还能看到的子孙 agent,再从父到子逐个关闭。一个重要细节是:关闭子孙时,如果发现它已经不见了或内部死掉了,通常不再当成大失败,因为目标本来就是让它停下来。

函数细节3
AgentControl::shutdown_live_agent6–25 ↗
async fn shutdown_live_agent(&self, agent_id: ThreadId) -> CodexResult<String>

作用:关掉一个指定的、可能还活着的 agent,但不把它标记成“用户明确关闭”。有人需要单独停掉某个后台小工人时会用它。

数据流:输入是一个 agent_id,也就是要关闭的线程编号。函数先拿到当前控制状态;如果这个线程还在,就先确保它的执行记录已经生成并写出去,然后看它是不是已经处于 Shutdown 状态;没关的话就发一个 Shutdown 命令,并等待它真正结束。如果线程已经找不到,也会尝试直接发 Shutdown 命令。最后,不管前面结果如何,都会从内存线程表、v2 驻留记录和 spawned thread 记录里清掉这个编号;输出是关机命令返回的字符串,或者错误。

调用关系:它是最底层的“关一个 agent”的动作,AgentControl::shutdown_agent_tree 会反复调用它来关闭父 agent 和它的子孙 agent。它内部会借助系统状态发送 Op::Shutdown,并在结束后做清理。

调用图:被 1 处调用(shutdown_agent_tree);外部调用 2 个(new, matches!)。

AgentControl::close_agent29–70 ↗
async fn close_agent(&self, agent_id: ThreadId) -> CodexResult<String>

作用:执行一次“明确关闭”某个 agent 的操作。它不只是停掉进程,还会把持久化状态写成 Closed,让系统以后知道这是一次有意的关闭。

数据流:输入是要关闭的 agent_id。函数先判断系统是否认识这个 agent;如果线程还活着,就通过它的状态数据库把派生边状态写成 Closed。如果线程已经不在但元数据还在,就尝试通过全局状态数据库补写 Closed;如果这个补写失败,会返回严重错误。之后它调用 AgentControl::shutdown_agent_tree 去关闭这个 agent 以及内存树里能找到的子孙。输出是关闭结果;如果系统本来认识它,但关闭时发现线程没了或内部 agent 已死,函数会把这当成“已经达到关闭效果”,返回空字符串成功。

调用关系:它是面向“关闭请求”的入口方法,先做持久化标记,再把真正停机的活交给 AgentControl::shutdown_agent_tree。遇到写状态失败或检查失败时,它会记录警告,必要时返回 Fatal 级别错误。

调用图:调用 1 个内部函数(shutdown_agent_tree);外部调用 5 个(pin, new, format!, Fatal, warn!)。

AgentControl::shutdown_agent_tree73–83 ↗
async fn shutdown_agent_tree(&self, agent_id: ThreadId) -> CodexResult<String>

作用:关闭一个 agent 以及它在内存派生树里还能看到的所有后代 agent。适合处理“关掉这一整支任务”的场景。

数据流:输入是根 agent_id。函数先查出这个 agent 当前还活着的子孙线程编号列表;然后调用 AgentControl::shutdown_live_agent 关闭根 agent;接着逐个关闭子孙。关闭子孙时,如果发现某个已经找不到或内部死掉了,就忽略,因为目标只是让它不再运行;如果遇到其他错误,就立刻返回错误。最终输出主要是根 agent 的关闭结果。

调用关系:它处在中间层:上面由 AgentControl::close_agent 调用,用来完成整棵派生树的停机;下面反复调用 AgentControl::shutdown_live_agent,真正执行单个 agent 的关机和清理。

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