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 可以理解为“带生命周期的依赖图节点”。与普通依赖注入相比,它同时表达:
- 服务需要哪些依赖;
- 如何获取和释放文件句柄、进程、scope、订阅与数据库连接;
- 初始化失败如何以类型化 error channel 传播;
- 哪些实现只在 Node、Bun、测试或特定平台出现。
图是概念压缩,不是逐行 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 看似更短,却会破坏四个性质:
- handler 超时后难判断 Provider 是否已经收到请求;
- 无法在同一全序日志中表达用户意图与运行结果;
- 测试难以只验证领域决策而不启动 Agent;
- 重启后缺少可补偿的意图事实。
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 更可靠。