一句话总评

分层方向是对的:ports / domain / runtime、DO 职责拆分、CMA 合规都在。但打开代码能「闻到」的味道也真实:session 逻辑仍偏胖、Computer 面未齐、CMA 特例和兼容头散落、还有空架子端口。下面 8 条都是可核对的路径+摘录,不是架构课。

高 · 日常改动会踩 中 · 拖复杂度 / 埋雷 低 · 小刺但刺眼 无勾选清单 · 只感受
1

上帝对象:SessionLogic 一人干完全部会话人生

高God Object / 胖模块
▸

文件 apps/managed-agents/src/session/session-logic.ts · 约 L240–L2316 · 整文件 ~2335 行 · 私有方法约 100+

为什么是味道

一个类同时管:路由、排队、模型回合、计费、SSE 订阅、outbox、重试、Computer 工具、线程索引……字段表本身就像一张控制台。分层名义上拆了 Store/Actor,真正「会话会怎样」仍全挤在这里。

代码摘录
240export class SessionLogic {
241  private readonly store: SessionRepository;
242  private readonly sink: SessionIndexSink;
243  private readonly models: PiModels | undefined;
244  private readonly alarms: SessionAlarmControl;
250  private active: ActiveTurn | null = null;
251  private draining = false;
252  private deliverChain: Promise<void> | null = null;
256  private readonly subscribers = new Set<StreamSubscriber>();
260  private readonly spend: OrgSpend | undefined;
262  private readonly computer: ToolComputer | undefined;
264  private readonly cleanup: SessionComputerCleanup | undefined;
266  private usageChain: Promise<void> | null = null;
   // … 其下还有 drain / turn / retry / SSE / outbox 等约百个 private 方法
不改会怎样

任何「只动流式 / 只动计费 / 只动工具」的改动都要在同一文件里找缝;评审 diff 巨大,回归面糊成一片。

收敛方向(一句):按生命周期切成 turn / stream / outbox 几个协作对象,SessionLogic 只编排。
2

构造函数里写库:DO new 一下就开始迁表抢租约

高隐式副作用
▸

文件 apps/managed-agents/src/session/session-do.ts · L201–L205(Computer DO 同款,约 L95)

为什么是味道

「创建一个对象」本该是装配;这里构造路径会 ensureSchema、claimEpoch、甚至释放无主租约。测试、冷启动、代码升级都绑在构造时序上——注释自己也在解释 workerd / node 行为不一致。

代码摘录
201    ctx.blockConcurrencyWhile(() => {
202      const prep = this.store.ensureSchema();
203      if (prep.ownerlessLease) this.logic.releaseOwnerless();
204      this.logic.bindEpoch(this.store.claimEpoch());
205    });
不改会怎样

任何「只想读一下 DO」的入口都可能触发写;迁移/租约 bug 会伪装成「偶发构造失败」,排查极费神。

收敛方向(一句):显式 boot() / 首请求路径负责 schema+epoch,构造只接线。
3

类型谎言:as unknown as Computer 把桩硬塞成端口

中any / 类型谎言
▸

文件 apps/managed-agents/src/session/session-do.ts · L65、L138;computer-do.ts L62 同模式

为什么是味道

编译器被绕过了:RPC stub 和 Computer 接口之间没有真实类型桥梁。端口写得再漂亮,边界上仍靠「我保证它长得像」。

代码摘录
 62  const stub = (): Computer => {
 63    const id = sessionIdOf(store);
 64    if (!id) throw new Error("session is gone");
 65    return namespace.get(namespace.idFromName(id)) as unknown as Computer;
 66  };
不改会怎样

Computer 方法签名一改,这里照样绿;真正炸在运行时的第一次工具调用。

收敛方向(一句):薄适配层(或生成的 stub 类型)显式实现 Computer,禁止跨 unknown 双断言。
4

CMA 特例迷宫:Beta 五家族 + 双 Header 别名

中重复特例 / CMA
▸

文件 apps/managed-agents/src/models/anthropic.ts · L40、L124–L125、L164–L178;路由表 http/routes.ts 每条挂 beta

为什么是味道

合规本身没错,但规则散在「家族枚举 + gate 分支 + 路由表标注」三处。尤其 memory-list:「两个 beta 头都能用,但不能同时用」——读代码要连注释一起背 CMA 细则。官方头和 kulullin-* 别名还要双读双写。

代码摘录
 40export type BetaFamily = "managed" | "memory" | "memory-list" | "files" | "skills";
124export const WORKSPACE_HEADER = "anthropic-workspace-id";
125export const WORKSPACE_HEADER_ALIAS = "kulullin-workspace-id";
171  if (family === "managed" && !tokens.has(MANAGED_BETA)) { … }
174  if (family === "memory" || family === "memory-list") {
175    if (tokens.has(MEMORY_BETA) && tokens.has(MANAGED_BETA)) { … }
177    const managedAllowed = family === "memory-list" && tokens.has(MANAGED_BETA);
178    if (!tokens.has(MEMORY_BETA) && !managedAllowed) …
不改会怎样

每加一条 CMA 边角,gate 和路由表各改一刀;漏一处就是线上 400,而且难用「业务语义」解释。

收敛方向(一句):把「此路径要哪些 beta / 可否组合」收成一张声明表,gate 只查表。
5

错误靠英文句子比对:改文案就改语义

中脆弱契约 / 跨层耦合
▸

文件 apps/managed-agents/src/http/serve.ts · L267–L288(sessionFailure 同文件紧随其后)

为什么是味道

Actor/Store 抛 Error("workspace not found"),HTTP 层用 message === "…" 映射成 404/409。协议不是类型,是一串英文字——两边谁改措辞,另一边静默变成 500。

代码摘录
267function storeFailure(caught: unknown, id: string): Response {
268  const message = caught instanceof Error ? caught.message : "";
269  if (
270    message === "workspace not found"
271    || message === "vault not found"
272    || message === "memory store not found"
273    || message === "agent not found"
       // … 一长串字符串分支
278  ) return notFound(id);
279  if (message === "agent version conflict") return conflict(…);
288  throw caught;
289}
不改会怎样

「修一下错误提示」这种 innocuous PR 能把客户端从 404 打成 500;自动化测试也得复制同一批魔法字符串。

收敛方向(一句):有限错误码 / 带 code 的问题类型,HTTP 只 switch code。
6

空架子端口:SandboxRuntime {} / WorkQueue {}

中泄漏的抽象 / 空架子
▸

文件 packages/ma-core/src/ports/index.ts · L334、L337

为什么是味道

注释写明「Still empty」。真正的沙箱已经是 Computer;这两个空接口占着 ports 名分,却没有任何方法。分层图好看了,读代码的人多绕一圈「这东西将来是啥」。

代码摘录
331 * Object per session. `self_hosted` work stays on `WorkQueue`.
332 */
334export interface SandboxRuntime {}
336/** Official self-hosted work items. The poller is a later adapter. */
337export interface WorkQueue {}
不改会怎样

新人会按空架子去「补实现」;或在别处再造第三套抽象。和「不必再叠空架子」的共识打架。

收敛方向(一句):用不到就删;要用等有第一个真实方法再进 ports。
7

路由表 18 个 stub:协议面铺开,能力还没到

中未齐能力 / 占位
▸

文件 apps/managed-agents/src/http/routes.ts · 约 L157–L223 · 命中 kind: "stub" 共 18 条

为什么是味道

self-hosted work 全套、session resources、mcp_oauth、memory redact、deployment pause……对外路径都挂着,真正实现是「没绑就 unattached / 绑了也 not implemented」。Computer / 自托管未齐,但 CMA 表面积已经铺开——文档与能力落差一眼可见。

代码摘录
157  route("GET", `/v1/environments/${id}/work/stats`, "managed", { kind: "stub", stub: "work" }),
158  route("GET", `/v1/environments/${id}/work/poll`, "managed", { kind: "stub", stub: "work" }),
   // … work ack/heartbeat/stop/… 共 8 条
180  route("GET", `/v1/sessions/${id}/resources/${id}`, "managed", { kind: "stub", stub: "actor" }),
194  route("POST", `…/mcp_oauth_validate`, "managed", { kind: "stub", stub: "store" }),
222  route("POST", `/v1/deployments/${id}/pause`, "managed", { kind: "stub", stub: "store" }),
223  route("POST", `/v1/deployments/${id}/unpause`, "managed", { kind: "stub", stub: "store" }),
不改会怎样

客户/SDK 对着「有路径」试,得到的是协议级拒绝;内部也分不清「刻意未做」和「忘了做」。

收敛方向(一句):未交付的面要么不挂路由,要么统一成明确的 not_supported 产品语义,并和进度页对齐。
8

restore 失败只 warn:挂载树坏了还继续跑

低错误吞掉
▸

文件 apps/managed-agents/src/session/session-do.ts · L76–L78

为什么是味道

每回合第一次工具调用前应 restoreMounts。失败被 .catch 成 console.warn,然后照样把 Computer 交出去。模型在一份过期/残缺的树上干活,上层还以为挂载成功了。

代码摘录
 74    if (restoreDue) {
 75      restoreDue = false;
 76      restoring = computer.restoreMounts().catch((error: unknown) => {
 77        console.warn(`computer restore: ${error instanceof Error ? error.message : String(error)}`);
 78      });
 79    }
 80    if (restoring) await restoring;
不改会怎样

偶发 restore 失败会变成「模型胡说 / 写错目录」类玄学问题,日志里只有一行 warn。

收敛方向(一句):restore 失败应让本回合工具失败或进入明确降级,而不是静默继续。

素材供微信带链接转发 · 源码以 GitHub main 为准 · 本页不提实现过程内部细节