T3 Code 源码拆解
系统全景

03 · Server 启动与 Effect 服务图

CLI 如何解析配置、Layer 如何组合服务,以及为何启动顺序是正确性的一部分。

两个入口、一个运行时

Server 的外层入口在 apps/server/src/bin.ts。 它用 Effect CLI 定义命令、选项和帮助,把端口、host、userdata、认证与远程相关配置转换成运行参数。 真正的组合根位于 server.ts: 这里选择 Node/Bun 平台 adapter、创建数据库、装配 Provider、启动 reactors,最后暴露 HTTP/WebSocket 服务。

将 CLI 和组合根分开,使同一个 Server 可以由三种宿主启动:

  • 用户直接执行 npx t3@latest;
  • Electron 主进程监督内嵌 backend;
  • 测试提供内存/临时实现后调用相同服务层。

Effect Layer 在这里解决什么

Effect 的 Layer 可以理解为“带生命周期的依赖图节点”。与普通依赖注入相比,它同时表达:

  1. 服务需要哪些依赖;
  2. 如何获取和释放文件句柄、进程、scope、订阅与数据库连接;
  3. 初始化失败如何以类型化 error channel 传播;
  4. 哪些实现只在 Node、Bun、测试或特定平台出现。
Server 组合根的概念服务图
CLI / Environment Config Node or Bun Platform Userdata / Workspace Paths HTTP + WebSocket Adapter Process / PTY / Filesystem SQLite Layer Event Store + Projection Repos Auth Stores Orchestration Engine Provider Drivers Provider Registries Provider + Checkpoint Reactors RPC Handlers Terminal / Preview / Git

图是概念压缩,不是逐行 Layer.merge 的复刻。重点在依赖方向:RPC handler 位于外层,数据库、编排与 Provider 都先于网络监听准备好。

启动阶段

1. 解析配置与确定平台

配置决定监听地址、用户数据目录、是否打开浏览器、认证模式以及远程能力。Server 同时维护 Node 与 Bun adapter, 是因为 HTTP、PTY 和进程 API 在两套运行时中并不完全相同。业务层依赖抽象服务,而不是在每个 handler 里判断 runtime。

2. 打开 SQLite 并执行迁移

数据库不是可选缓存:事件日志、幂等回执、投影、Provider session 运行信息和认证状态都依赖它。 因此迁移失败必须阻止服务“半启动”。40 个顺序迁移由 Migrations.ts 汇总,在仓库/引擎可用前完成。

3. Hydrate read model 与 Provider instance

事件和持久化投影让 Server 能在重启后恢复线程视图;Provider instance registry 则恢复用户配置的多账号实例。 这一步不等于盲目重启所有 Agent session,而是重建“哪些实例存在、thread 绑定到谁”的控制状态,实际 session 由发送 turn 或恢复流程按需确保。

4. 启动 ingestion 与 reactors

Provider runtime ingestion 订阅统一的 provider event stream;command reactor 订阅领域意图事件;checkpoint reactor 订阅 turn 边界。它们需要在网络监听前进入可消费状态,否则客户端一连接就发送命令时,事件可能已经提交却无人执行。

5. 暴露 HTTP、认证和 WebSocket RPC

最后才把 readiness 交给外部。WebSocket upgrade 先认证,再把连接交给 Effect RPC server;普通 HTTP route 处理静态 Web、 配对、ticket、健康检查等入口。

启动顺序是语义,不是微优化

先启动 reactor、再开放监听,保证“RPC 返回 accepted”时副作用消费者已经存在。若顺序反过来,启动窗口中的命令 可能只进入事件日志,直到下一次重启或补偿扫描才被发现。

Scope 与资源释放

Server 中有大量长寿命资源:

  • SQLite connection/pool;
  • Provider child process 与 JSON-RPC transport;
  • PubSub subscription fiber;
  • Terminal PTY;
  • WebSocket connection;
  • preview/browser session;
  • resource monitor native process。

Effect scope 让资源的所有权与 Layer 生命周期一致。Server shutdown 时,外层 scope 关闭会逐层执行 finalizer, 而 Provider adapter 还提供 stopAll 等显式业务清理。两者不能混为一谈:scope 负责“资源一定释放”, 业务方法负责“以协议允许的方式优雅停止”。

Reactor 为什么是独立服务

把外部调用直接写进 RPC handler 看似更短,却会破坏四个性质:

  1. handler 超时后难判断 Provider 是否已经收到请求;
  2. 无法在同一全序日志中表达用户意图与运行结果;
  3. 测试难以只验证领域决策而不启动 Agent;
  4. 重启后缺少可补偿的意图事实。

Reactor 作为事件消费者,使核心写路径只提交事实,外部副作用可以独立失败并转化为新的事件。T3 Code 还用 DrainableWorker 给测试提供确定性的 drain:测试无需依赖任意 sleep 等待后台 fiber。

配置与能力探测

不是所有 Provider、Git、PTY 或桌面能力都始终可用。Server 倾向于注册 driver/registry,再通过探测和 Schema 配置 决定某一能力是否出现,而不是让整个应用因一个 CLI 未安装而启动失败。例如用户只安装 Claude 时,Codex driver 的 不可用应成为可展示的 provider 状态,不应让终端和项目列表一起消失。

故障路径

故障所属阶段预期处置
数据库迁移失败启动前阻止 readiness,保留明确错误
某 Provider CLI 不可用能力探测其他 Provider 与 Server 继续可用
Reactor 中外部调用失败运行时写入 activity/session error,不回滚已提交用户意图
WebSocket 断开连接级释放连接 scope,后台 Provider 可继续运行
Server shutdown进程级关闭 listener,drain/interrupt fibers,释放 adapter 和数据库

源码锚点

可迁移经验

  • 用 composition root 展示真实依赖图,业务模块只声明所需服务。
  • readiness 应表示后台消费者、数据库和鉴权都已可用,而不是“端口 bind 成功”。
  • 将“某能力缺失”降级为局部状态,将“权威存储不可用”升级为启动失败。
  • 为异步 worker 设计测试时的 drain 协议,比在测试中不断增加 sleep 更可靠。

On this page