服务器更新
稳定启动器拥有由 systemd 或 launchd 选定的运行时。它是唯一会写入持久服务状态的运行时。服务器子进程通过继承的 IPC 请求更新;它们从不改写自己的服务定义,也不自行选定替换版本。本地服务命令可以替换启动器和状态,替换时可以选择推迟重启。前台 CLI 进程不会自行更新。
两个写入方都通过文件租约串行化状态写入和单元写入。安装程序先取得状态租约,再取得单元租约;启动器从不同时持有两者,因此单元失败不会挡住一次持久提交。管理器的停止和启动命令在两份租约之外运行,因为停掉一个正在等待租约的启动器会造成死锁。本地命令一旦改变了它的选择,正在运行的启动器就拒绝后续更新。
精确版本安装使重启独立于 npm 缓存回收,也独立于会移动的发布标签。安装和预检先在暂存目录完成,再发布不可变运行时。预检会检查启动器协议,因为需要新回滚保证的目标,无法在较旧的启动器下安全运行。远程更新只替换子进程,因此升级启动器需要一次会重启的本地服务更新。
提交或回滚之后,启动器把单元指向选定的运行时并重新加载管理器,于是下一次服务启动会运行该版本的启动器;协议检查使这一步是安全的。一次更新完成之后,随后的每次启动都会重试这一步,因为上一次重新加载可能失败;指向更新运行时或另一个 T3 home 的单元则保持不动。在被推迟的本地升级真正重启之前,systemd(daemon-reload 之前)和 launchd(bootstrap 之前)仍可能重新拉起旧启动器,而新运行时拒绝在它下面运行。
提交边界
启动器先把待处理的更新持久记录下来,再确认这次更新,然后停止旧子进程,把目标作为试运行启动。服务状态写入采用同目录替换,并对文件和目录执行 fsync。状态无效时会阻止启动,而不是猜测该引导哪一个运行时。
试运行必须先完成迁移、取得依赖、绑定 HTTP,并把每个长时间运行的根停在激活门处,然后才报告 prepared。启动器随后把目标版本持久提交,并回复 committed。只有在此之后,子进程才可以释放门闩、接受命令(command),并发布就绪。可能失败的启动获取要放在这条边界之前。仅有监听器,并不能证明运行时已经可以提交。
失败或超时的试运行会回到旧版本。提交之后,目标成为权威版本,并适用服务管理器通常的重启策略。
准备阶段的截止时间很长,因为迁移会随数据库规模增长,并阻塞试运行的主线程,使它无法报告进度。错过截止时间就会恢复快照,重试还要再付出同样的代价。崩溃的试运行在退出时就会被发现;截止时间只约束卡住不退出的那一种。客户端(client)等待更新的时间必须长于该截止时间,否则一次缓慢的更新会显示为失败。
数据库回滚
在停止旧子进程之前,若快照放不进磁盘,启动器会拒绝这次更新,这样磁盘写满就不会造成停机。旧子进程退出后,启动器为 SQLite 的主文件、WAL 和共享内存文件建立快照。因此,试运行中的迁移可以回退,而不需要向下迁移。每个更新只建立一次快照,并且启动器重启后仍然保留;如果在重试期间替换这份快照,就可能把失败试运行产生的变更也拍进去。
回滚在恢复之前先停止试运行。持久的恢复标记使被中断的恢复在任一版本启动之前完成。快照要保留到提交,或者保留到恢复和终态回滚状态都已持久化。附件以及 SQLite 之外的其他文件,不在这条回滚边界之内。
客户端确认
已被接受的更新仍然处于待处理状态。客户端(client)在重新连接之后,把启动器的更新 ID 与就绪事件(event)关联起来,然后核对结果和目标版本。仅凭重新连接,无法区分替换成功和回滚。没有更新 ID 的较旧服务器,仍然只按版本关联。
桌面更新另有一套两阶段交接,因为安装应用会停止其捆绑的后端。准备阶段在连接仍然存活时返回一个令牌;客户端只有在收到该令牌之后才提交它。否则,后端关闭可能会丢失唯一一次成功的 RPC 结果。客户端随后必须在重新连接之后观察到已准备的版本。如果安装失败,桌面端会重启已停止的后端,并针对同一个令牌重放这次失败。
恢复被中断的对话线程
重启续跑是归环境(environment)所有的一项偏好,默认开启。settings.json 若无法解码,就会关掉这项偏好,因为该文件中可能保存了关闭此项的选择。v2 恢复服务要求四项彼此匹配:持久运行、提供方对话线程(provider thread)、提供方会话(session),以及原生恢复身份。排队中的运行从未开始,因此恢复会保留它们,并继续它们所等候的、排在前面的那次运行。已结算的运行只有同时满足两点才符合条件:这次重启取消了智能体正在等待的工作(子智能体、后台任务,或智能体用 t3_background_wait 标记过的 shell 或监视器),并且其提供方会话当时是活跃的。这样,更早一次崩溃留下的过期记录就不会把它唤醒。它的续跑提示,就是说明哪些工作不会再回报的那句话。未标记的 shell 或监视器不会唤醒已结算的运行:它们大多是开发服务器及其看门狗,没有人在等待,因此那句话会留到下一条用户消息。
恢复会退役与丢失进程绑定的效应,并把续跑意图记入持久发件箱。这份意图在提供方启动之前,还能再经受一次重启。续跑效应等待激活;提供方再慢,也不得拖延服务器的就绪,或拖延启动器的提交边界。优雅关闭会在关闭提供方之前捕获意图,然后在摄入停止之后进行调和,以免迟到的完成被过期的取消覆盖。
续跑处理器在分发之前会重新检查这项偏好、提供方选择、更新的用户工作、用户请求的停止,以及 /compact 这类维护回合(turn)。意图和投递都会拒绝这些情况:已归档、已结算或仍在休眠的对话线程;提供方回合已经结束的被切断回合;以及正在等待用户的运行。所等待的,可以是当时仍待处理的批准、问题或密钥,也可以是恢复在被切断回合上关闭的批准、问题或密钥。能够以消息作答的问题仍然可以回答,因此它们不会阻塞。在同一条 restartContinuationOfRunId 链中连续续跑三次之后,投递也会停止,因此导致服务器崩溃的回合无法循环。稳定的命令(command)ID 和消息 ID 可以防止发件箱重试之后重复提交。Codex 恢复时不会添加提供方提示文本,除非该回合丢失了后台工作;其他适配器(adapter)通过各自正常的回合路径接收续跑消息。
委派任务(delegate_task 子对话线程)作为各自的对话线程进行调和,从不当作父级的后台工作。编排器(orchestrator)在调和之后的一轮启动处理中结算子结果和完成投递,因为终态运行监听器会忽略调和产生的取消。已取消的子级,如果其重启续跑仍在发件箱中待处理,就还不是结果;由这次续跑的运行结算它,或者在处理器拒绝继续时,由处理器结算它。调度器会等待激活,这样它们就不会启动随后会被调和取消的运行。