
Maka Computer Use 运行时生命周期加固从 stop 墓碑到代际释放的完整修复实录【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka本指南基于 Maka 开源仓库中的设计评审文档 computer-use-runtime-hardening.md系统梳理 Computer Use 运行时在 PR #892 评审后暴露出的生命周期缺陷、根因与修复方案并结合 packages/runtime 与 packages/computer-use 的源码实现与测试用例进行纵深解析。读完本文你将理解为什么清理不能只清理已存在的状态记录、为什么只读操作也必须持有会话租约、为什么释放原因不足以推断执行器代际是否更替以及一套可落地的终态吸收 租约围栏 显式代际事实加固范式。背景一次评审暴露出的生命周期缺口Computer Use 是 Maka 中让 Agent 直接操作真实屏幕观察窗口、点击元素、发送按键的能力层。它由两层组成Runtime 侧的会话状态机负责面向模型的语义围栏如user_stopped、blocked_url以及maka/computer-use包内的执行器服务MakaCuService负责与原生maka-cu执行器子进程的 JSON-RPC 通信。在 PR #892 的评审中审查者发现该运行时存在一批清理与围栏方向的缺口会话清理、只读操作、生命周期事件覆盖、执行器队列作用域、元素身份歧义、光标结果测试与释放原因语义。这些缺口单独看都是边角问题但组合起来会形成真实的竞态用户点了停止被排队的调用却依然在清理之后激活执行一个会话被清理却可能连带丢弃另一个无关会话的观察结果与键盘所有权。本文按问题 → 根因 → 修复 → 验证的脉络完整还原这次加固。一、修复前的问题清单七类生命周期缺陷原文档列出的问题可以归为七类每一类都对应一个具体竞态stop 墓碑缺失clearSession()在尚不存在 session-state 记录时不创建停止墓碑tombstone因此清理之后首个被排队的调用仍可能激活执行。也就是说停止这个事实没有被记录到可以被排队调用读取的地方。只读操作无租约只读的 host 动作如观察窗口、等待条件不获取会话租约session lease在用户已经user_stopped之后仍可能继续执行——只有观察与变更两类操作被认为需要生命周期围栏这是根因之一。终态可被覆盖后续到达的生命周期事件如一次新的observe成功、一次screen_unlocked可能把blocked_url或user_stopped这样的终态覆盖掉导致已被禁止/已被停止的会话被重新激活。队列作用域错误进程级process-wide的 executor 队列会阻塞无关窗口而第一版按会话per-session替换又走向另一极端——允许两个会话对同一个窗口交错执行 snapshot/validation/dispatch。歧义门混入临时 ID歧义判定ambiguity gate把临时元素 IDephemeral element IDs也纳入候选即便当时已经存在稳定的元素身份stable element identity造成不必要的失败关闭fail closed。光标结果无生产来源cursor_position的格式化逻辑存在但没有任何生产后端的真实结果支撑属于格式先行、数据缺席。释放原因语义混淆服务释放原因没有区分仅会话通知与真实的子代际释放。clearSession(A)可能因为释放事件携带的会话列表覆盖到 B而丢弃会话 B 保留的观察结果与键盘所有权。二、根因分析围栏视野过窄状态与代际事实不足原文档对根因的剖析非常简洁但直指要害租约围栏覆盖不足Runtime 把观察租约与变更租约当作仅有的两类需要生命周期围栏的操作而等待读取等 host 动作被遗漏同时清理逻辑只对已创建的状态记录做变更无法在状态记录尚未创建时就留下停止痕迹。终态与非终态共用同一迁移助手可恢复状态如reobserve_required、screen_locked与终态blocked_url、user_stopped走同一个无限制的 transition helper导致终态被后来的可恢复迁移覆盖。队列作用域绑错了对象executor 队列的作用域是调用者caller而不是被验证的资源bound window因此出现全局阻塞或同窗交错两个方向的错误。身份签名混合歧义签名把稳定身份与 snapshot 局部 ID 混在一起无法区分同一个控件换了标签与控件根本没变。代际推断不可靠执行器服务从释放原因release reason推断代际是否更替。但在无在途请求no-in-flight的clearSession()路径上只发出session_cleared通知而不停止子进程——因此释放原因本身无法证明共享的执行器状态发生了变化。换句话说session_cleared既可能伴随代际更替也可能不伴随单看原因是推断不出来的。三、修复方案与源码级实现原文档给出七条修复措施下面逐条对应到当前仓库的源码实现。3.1 无条件创建同轮 stop 墓碑修复clearSession()期间无条件创建同轮same-turn停止墓碑。在 computer-use-tools.ts 中tools.clearSession的实现正是这一语义tools.clearSession (sessionId: string) { if (invocationQueues.has(sessionId)) { presentationGenerations.set(sessionId, (presentationGenerations.get(sessionId) ?? 0) 1); } for (const wake of presentationQueueWaiters.get(sessionId) ?? []) wake(); for (const wake of presentationWaiters.get(sessionId) ?? []) wake(); const current sessionStates.get(sessionId); if (current) { current.state.userStopped(); } else { // 关键状态记录不存在时从待处理调用中取一个 turn // 创建会话状态并立即写入 userStopped 墓碑 const pendingTurn pendingInvocationTurns.get(sessionId)?.values().next().value; if (pendingTurn) sessionState(sessionId, pendingTurn).userStopped(); } invalidateObservation(sessionId); observations.delete(sessionId); deps.backend.clearSession?.(sessionId); };要点在于else分支即使sessionStates中还没有该会话的状态记录只要存在待处理调用pendingInvocationTurns就通过sessionState()创建状态并写入userStopped()。这样停止这个事实在同一轮内对后续排队的调用可见——排队调用在真正执行前会经过租约校验而被拦下而不是在清理完成后悄悄激活。测试用例 computer-use-tools.test.ts 中多个用例专门覆盖这条路径clearSession keeps a same-turn tombstone but a new turn can reopen同一轮内调用被user_stopped拦截而新的一轮可以重新打开会话这正是原文档结尾新的一轮仍然创建全新的会话状态保留显式恢复边界的验证。clearSession fences a first invocation that is already queued首个排队调用在清理后同样被墓碑拦截。clearSession fences a later turn that was queued before stop停止前已排队的后续轮次同样被拦截。clearSession after a non-CU turn does not block the next turn observe反过来验证墓碑不会误伤下一个正常轮次的观察。3.2 只读 host 动作也必须持有观察租约修复每个 host 读取或等待动作都必须先获取观察租约。会话状态机 cua-session-state.ts 中定义了完整的租约模型export interface CuaActionLease { sessionId: string; generation: number; } beforeAction(): CuaActionLeaseResult { return this.status active ? { ok: true, lease: { sessionId: this.sessionId, generation: this.generation } } : { ok: false, reason: blockReason(this.status) }; } beforeObservation(): CuaObservationLeaseResult { return this.canObserve() ? { ok: true, lease: { sessionId: this.sessionId, generation: this.generation } } : { ok: false, reason: blockReason(this.status) }; }观察租约与动作租约的差异在于允许的初始状态集合canObserve()允许unobserved、active、reobserve_required而beforeAction()只在active时放行。这说明观察的准入面比变更宽你总得先看才能动但两者都返回{ sessionId, generation }代际绑定的租约。在工具层 computer-use-tools.ts 中observe路径会先取租约const lease state.beforeObservation(); if (!lease.ok) { stopped lease.reason; ... }并且在观察完成后、以及在element_sequence等跨步骤操作中都会用validateObservationLease(lease)二次校验租约是否仍然有效例如 computer-use-tools.ts。这样即使观察请求在排队期间用户按下了停止等它真正执行时generation已因userStopped()的迁移而递增租约校验失败调用以user_stopped拒绝——只读操作再也不能在停止后继续。测试clearSession fences host-reading results that complete after stop与clearSession fences failed host-reading results that complete after stop见 computer-use-tools.test.ts专门验证在停止之后才完成的 host 读取结果无论成功或失败都必须被user_stopped围栏拦截。3.3 终态吸收后续事件blocked_url与user_stopped不可覆盖修复blocked_url与user_stopped吸收absorb后续生命周期事件。这一条在 cua-session-state.ts 中有直接体现。状态机先定义终态集合再让所有可恢复迁移在终态下变成 no-opprivate isTerminal(): boolean { return this.status blocked_url || this.status user_stopped; } blockedUrlDetected(): CuaSessionSnapshot { if (this.isTerminal()) return this.snapshot(); // 终态下不迁移 return this.transition(blocked_url); } userStopped(): CuaSessionSnapshot { if (this.isTerminal()) return this.snapshot(); // 终态下不迁移 return this.transition(user_stopped); } screenLocked(): CuaSessionSnapshot { if (this.isTerminal()) return this.snapshot(); return this.transition(screen_locked); } freshObservationSucceeded(): CuaSessionSnapshot { if (!this.canObserve()) return this.snapshot(); return this.transition(active); }canObserve()只允许unobserved/active/reobserve_required而blocked_url与user_stopped不在其中——因此一次新的观察成功、一次屏幕解锁、一次重新观察要求都无法把终态会话拉回active。blockedUrlDetected()/userStopped()内部的isTerminal()守卫则保证了两个终态互相之间也不覆盖一旦进入任一终态后续任何生命周期事件都只返回当前快照snapshot()不递增 generation、不改变 status。工具层配套的模型可见文案定义在 computer-use-tools.ts 的SESSION_BLOCK_RECOVERY中blocked_url: this target is refused for the rest of this session, and so is every other one — ..., user_stopped: the user stopped computer use for this session. Do not send any further computer action; report that it was stopped.,并且由applyTypedOutcomeStatecomputer-use-tools.ts把后端的blocked_url/screen_locked/user_intervened等结果映射为状态机迁移从而把屏幕上的事实沉淀为会话状态。3.4 变更操作按绑定 PID/窗口串行化修复变更操作按绑定的 PID/窗口串行化未绑定unbound的变更使用一个保守队列无关的绑定窗口仍可独立推进。原设计是进程级全局队列——一个窗口的慢操作会阻塞所有窗口第一版 per-session 替换又允许两个会话对同一窗口交错执行 snapshot/validation/dispatch。修复方向是以被验证的资源绑定窗口为队列键同一窗口的变更严格串行不同窗口互不阻塞。值得说明的是在执行器层maka/computer-use的后续加固文档 computer-use-executor-hardening.md 记录了一个审慎的结论进程级操作队列在 executor 层暂时保持全局原因在于 Maka 当前只持有一个 action-child stdio 连接fresh-snapshot/action 对必须在该共享连接上保持原子真正的并发需要引入独立的服务连接。这体现了以资源为键的串行化与共享连接约束之间的权衡——host 层按窗口分队列、executor 层在单一连接上保守串行两层各守各的边界。在 maka-cu-backend.ts 中withOperationQueue的实现queueKey __executor__用一条 Promise 链保证同一 executor 上同一时刻只有一个操作在途同时在操作真正开始前检查会话代际active.generation ! sessionGeneration时抛出MakaCuSessionCleared把排队期间会话被清理变成显式的拒绝路径。3.5 稳定元素身份优先临时 ID 不进歧义门修复存在稳定元素身份时忽略临时元素 ID。歧义门ambiguity gate用于判定观测到的控件是否唯一对应模型要操作的目标。旧实现把 snapshot 局部的临时元素 ID 也纳入候选集合导致即便已经具备稳定的身份信息role label value 等也会因为临时 ID 的干扰而错误地失败关闭。maka/computer-use的语义加固computer-use-executor-hardening.md进一步收紧了这条规则语义重取semantic refetch现在要求一个唯一的 role/label/value 候选并验证其 frame、depth、value 仍与观测到的控件一致同标签替换或候选集歧义一律失败关闭原生内容指纹content fingerprint包含 label 与 value因此同一结构槽位上控件含义的变化会使坐标动作失效。也就是说身份判定从有候选就行演进为唯一且可验证。在 maka-cu-backend.ts 中modelIds映射模型看到的短 ID → 线上 token专门处理模型引用的 ID与线上绑定 token的分离——token 长达 53 字符且同快照共享 45 字符前缀直接交给模型会导致复制失败映射保证了模型面对的是稳定、可复制的短 ID而线上绑定仍使用完整的稳定 token。3.6 光标位置读取走固定驱动修复通过固定的 cua-driver 读取get_cursor_position返回解析后的坐标点而不移动指针。原文档指出cursor_position的格式化逻辑存在但没有生产后端结果测试只用 fake backend。修复要求把光标位置读取固定到 pinned cua-driver 上返回值是解析后的坐标点且不产生指针移动副作用——即读取必须是纯粹的观测不能改变屏幕状态否则就会与只读操作也需围栏的原则冲突。这与本文 3.2 的租约原则一致host 读取动作要么持有租约要么是纯函数式读取二者缺一不可。3.7 显式携带generationReleased释放事实修复把generationReleased作为显式的服务释放事实携带。仅会话通知session-only的释放只使列出的会话失效真实的子进程退出child exit才清扫所有保留的会话观察与键盘目标。这是整篇加固中最核心的语义修正。旧实现从释放原因推断代际更替但session_cleared在无在途请求时并不停止子进程因此原因无法作为代际事实。在 maka-cu-service.ts 中释放事件被显式建模export interface MakaCuReleaseEvent { generation: number; generationReleased: boolean; // 显式代际事实 reason: | child_exit | request_timeout | protocol_violation | session_cleared | restart_exhausted | disposed; sessionIds: readonly string[]; outcomeUnknown: boolean; }两个发射点给出了不同的事实clearSession()maka-cu-service.ts对会话在途请求逐个发起$/cancel随后emitRelease(session_cleared, [sessionId], owned.length 0, false)——generationReleased false因为子进程没有被停止共享的执行器状态代际没有变化只是该会话被放弃。onExit()maka-cu-service.ts任何子进程退出路径child_exit/request_timeout/protocol_violation/ 超时强杀都调用emitRelease(reason, sessionIds, potentiallyDelivered.length 0, true)——generationReleased true因为子进程的代际确实终结了。消费侧 maka-cu-backend.ts 的applyServiceRelease据此分派function applyServiceRelease(events: readonly MakaCuReleaseEvent[]): void { const generationReleased events.some((event) event.generationReleased); const sessions [ ...new Set([ ...events.flatMap((event) event.sessionIds), // 代际释放上一代的所有会话与快照全部失效 ...(generationReleased ? begunSessions : []), ...(generationReleased ? [...snapshots.values()].map((snapshot) snapshot.sessionId) : []), ]), ]; ... }只有generationReleased为真时begunSessions与所有快照所属会话才会被一并清扫——这正是真实子进程退出清扫所有保留观察与键盘目标。而clearSession(A)产生的 session-only 释放只会让 A以及事件中显式列出的会话失效无关会话 B 的观察与键盘所有权不受影响。对应测试 maka-cu-backend.test.tsends cleared sessions without re-notifying them and invalidates known sessions on generation loss完整验证了这条语义clearSession后执行器确实收到session.end通过日志记录等待该消息对已清理会话再次clearSession、以及对从未开始的会话clearSession都不会重复通知观察者unknown cleanup must not notify observers已完成的会话仍持有 begun 状态当执行器被SIGKILL导致代际丢失时onSessionInvalidated会准确收到该会话的失效回调——即使它的操作围栏早已释放。四、验证方式原文档给出四项验证命令与当前仓库的包结构完全对应# Runtime 类型检查 npm --workspace maka/runtime run typecheck # Runtime 单元测试含上述 clearSession 墓碑、租约围栏用例 npm --workspace maka/runtime test # Computer Use 后端测试含代际释放、session.end、键盘目标回归用例 npm --workspace maka/computer-use test # 变更检查 git diff --check后续的 executor 加固computer-use-executor-hardening.md记录的聚焦测试套件达到 111 个用例覆盖语义替换、注册表不匹配、观察淘汰、窗口压缩、共享客户端排序、生命周期错误与键盘目标回归等场景与本文的生命周期加固互为补充。五、总结一套可复用的运行时加固范式回顾整个修复核心可以提炼为四条原则这也是任何Agent 操作真实资源的运行时都值得借鉴的停止必须是先验事实而不是事后动作。clearSession在状态记录缺失时也要留下同轮墓碑让排队中的调用在真正执行前就能读到已被停止。租约围栏覆盖所有操作类别。不只是变更需要租约读取与等待同样需要租约绑定{sessionId, generation}代际递增即事实变更。终态不可被恢复事件覆盖。blocked_url与user_stopped一旦进入后续任何生命周期事件都被吸收absorb杜绝已禁止/已停止的会话被意外复活。代际事实必须显式不能从原因推断。generationReleased作为独立字段随释放事件携带session-only 释放与真实子进程退出严格分流避免一个会话的清理误伤另一个无关会话。从 cua-session-state.ts 的状态机、computer-use-tools.ts 的工具层围栏到 maka-cu-service.ts 的释放事件建模与 maka-cu-backend.ts 的消费分派再到两份加固文档与配套测试Maka 为Agent 控制屏幕这个高风险场景给出了一个边界清晰、可验证的生命周期治理样例——新的一轮对话仍然会创建全新的会话状态显式恢复边界由此得以保留。【免费下载链接】makaApache Maka (Incubating) is a high-performance agent workspace that keeps a complete record of everything it did.项目地址: https://gitcode.com/GitHub_Trending/mak/maka创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考