一句话总评
分层方向是对的:ports / domain / runtime、DO 职责拆分、CMA 合规都在。但打开代码能「闻到」的味道也真实:session 逻辑仍偏胖、Computer 面未齐、CMA 特例和兼容头散落、还有空架子端口。下面 8 条都是可核对的路径+摘录,不是架构课。
1
上帝对象:SessionLogic 一人干完全部会话人生
▸
上帝对象:SessionLogic 一人干完全部会话人生
文件 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 巨大,回归面糊成一片。
2
构造函数里写库:DO new 一下就开始迁表抢租约
▸
构造函数里写库: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 把桩硬塞成端口
▸
类型谎言:as unknown as Computer 把桩硬塞成端口
文件 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 方法签名一改,这里照样绿;真正炸在运行时的第一次工具调用。
Computer,禁止跨 unknown 双断言。
4
CMA 特例迷宫:Beta 五家族 + 双 Header 别名
▸
CMA 特例迷宫:Beta 五家族 + 双 Header 别名
文件 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,而且难用「业务语义」解释。
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;自动化测试也得复制同一批魔法字符串。
6
空架子端口:SandboxRuntime {} / WorkQueue {}
▸
空架子端口: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 {}
新人会按空架子去「补实现」;或在别处再造第三套抽象。和「不必再叠空架子」的共识打架。
7
路由表 18 个 stub:协议面铺开,能力还没到
▸
路由表 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:挂载树坏了还继续跑
▸
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。
素材供微信带链接转发 · 源码以 GitHub main 为准 · 本页不提实现过程内部细节