Maka Runtime Resume Phase 0 崩溃契约:基于已提交 RuntimeEvent 前缀的确定性回放安全
2026/9/18 13:13:36 网站建设 项目流程

Maka Runtime Resume Phase 0 崩溃契约:基于已提交 RuntimeEvent 前缀的确定性回放安全

【免费下载链接】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

Phase 0 是 Maka(Apache Maka, Incubating)崩溃恢复体系的第一道闸门:它只回答一个问题——当进程在任意时刻被杀死后,仅凭已经完整落盘的RuntimeEvent前缀,系统能否安全地把这段历史重放给模型。本文以 docs/architecture/runtime-resume-phase0-crash-contract.md 为骨架,结合 packages/runtime/src/runtime-resume.ts 与 packages/runtime/src/tests/runtime-resume-crash.test.ts 的源码实现,完整解析 P0–P11 稳定故障注入点、四条已提交前缀的判定规则、真实子进程 SIGKILL 测试方法论,以及 Phase 0 明确不承诺的能力边界。读完本文,你将掌握 Maka 如何在"崩溃后工具副作用状态未知"这一最危险场景下做到确定性、可测试、fail-closed 的恢复决策。

为什么需要 Phase 0:崩溃后"缺失结果"至少有四种解释

当一个 Agent 正在调用工具(例如通过Bash执行touch marker)时进程崩溃,重启后系统面对一个缺失的工具结果,至少有四种互斥的解释:

  1. 工具从未启动;
  2. 工具启动了但没有写入任何东西;
  3. 副作用已经完成(文件已写入),但结果事务未提交;
  4. 文件被写入后又被人或其他进程改掉了。

盲目重试会重复第 3 种情况下的副作用;盲目宣布成功则会给模型一个在第 1、2、4 种情况下完全虚假的历史。因此,恢复必须基于不可变的事实而非猜测。Phase 0 的全部工作就是:只读取已提交的RuntimeEvent前缀,把它投影成ToolOperation,再产出一个ResumePlan——要么safe_replay,要么blocked

与完整恢复体系的关系:Phase 0 不恢复执行、不调和工具副作用、也不引入未来的 SQLite 工具日志(tool journal)。这些属于后续阶段,见 docs/architecture/runtime-resume-architecture.md 中 Phase 0–4 的阶段划分。

生产 API:一条纯函数链路

Phase 0 的生产 API 是纯的(pure),即同样的输入永远得到同样的输出,且投影过程不修改任何持久化数据:

committed RuntimeEvent prefix -> ToolOperation projection -> ResumePlan -> safe_replay or blocked

这条链路在源码中的落点非常清晰:

  • projectToolOperationsFromRuntimeEvents(events)先将事件交给resolveRuntimeRecovery得到恢复决策,再投影出ToolOperation[]
  • buildResumePlanFromRuntimeEvents(events, options)聚合投影、诊断、拒绝原因,最终计算出disposition: 'safe_replay' | 'blocked'(见 packages/runtime/src/runtime-resume.ts);
  • 决策的底层依据来自RecoveryResolverresolveRuntimeRecovery,见 packages/runtime/src/recovery-resolver.ts),它把RuntimeEvent前缀解释为completed / parked / definitely_not_dispatched / indeterminate / corruption五类工具状态。

ResumePlan的关键字段(源码中定义于 packages/runtime/src/runtime-resume.ts):

字段含义
dispositionsafe_replayblocked
operations每个工具调用的投影结果(succeeded/failed/indeterminate/not_dispatched/parked/corruption
diagnostics人类可读的诊断码与消息
rejectionReasons机器可读的拒绝原因(如dangling_tool_stateruntime_offset_mismatch
requiresVerification是否存在未解决的工具副作用需要人工核验
sourceRuntimeEventHighWater该计划覆盖的不可变日志水位
replayRuntimeEvents重放给 provider 的合法事件子集

稳定故障注入点:P0–P11 目录

RUNTIME_RESUME_FAILPOINTS是机器可读的权威事实来源,其 TypeScript 常量定义于 packages/runtime/src/runtime-resume.ts。其中committedPrefix列表示崩溃后最后一个完整提交RuntimeEvent前缀。它刻意不假装未来的 T1/T2 日志已经存在——崩溃发生在哪个阶段,就只承认哪个阶段的事实。

ID注入边界最后完整提交的 RuntimeEvent 前缀
P0工具准备(T1)之前before_function_call
P1function_call 已提交,prepared journal 未提交after_function_call
P2prepared journal 已提交,实现未开始after_function_call
P3工具实现进行中after_function_call
P4副作用完成,outcome 事务(T2)未提交after_function_call
P5function_response 已提交,outcome journal 未提交after_function_response
P6outcome 已提交,结果未送达模型after_function_response
P7结果已送达,下一步 provider 调用未开始after_function_response
P8terminal RuntimeEvent 提交after_function_response
P9terminal run-header 提交after_terminal_event
P10恢复决策提交after_terminal_event
P11continuation-run 创建after_terminal_event

关于 P8 需要特别注意:Phase 0 只对 terminal append之前的前缀做推理,terminal 之后的合法前缀由 P9 表示。一条撕裂(torn)的 JSON 行属于存储损坏(storage corruption),不是合法的已提交前缀,绝不能把它升级为恢复事实——这正是"fail-closed"的第一道体现。

测试中对该目录的稳定性有专门断言:runtime-resume.test.ts验证RUNTIME_RESUME_FAILPOINTS恰好包含 P0–P11 十二个 ID,且去重后的 committedPrefix 恰好是before_function_callafter_function_callafter_function_responseafter_terminal_event四种(见 packages/runtime/src/tests/runtime-resume.test.ts)。

必选决策:四种前缀如何判定

Phase 0 对每种已提交前缀给出确定性的判定结果:

前缀期望结果
before_function_callsafe_replay;不存在任何工具操作
after_function_callblocked;操作状态为indeterminate;拒绝原因为dangling_tool_state;未解决的调用不出现在 provider 重放历史中
after_function_responsesafe_replay;操作状态为succeededfailed;调用与响应在 provider 重放中保持配对
after_terminal_event工具判定与前缀相同;terminal 事实保留在 canonical ledger 中

此外,若重新打开的已提交前缀与期望的 RuntimeEvent 高水位(high-water)不一致,一律以runtime_offset_mismatch拒绝。源码中collectResumeDiagnostics会在expectedRuntimeEventHighWater !== events.length时产出该诊断(见 packages/runtime/src/runtime-resume.ts)。

源码视角:这些决策是如何算出来的

buildResumePlanFromRuntimeEvents的判定逻辑(见 packages/runtime/src/runtime-resume.ts)可以概括为:

disposition = safe_replay 当且仅当 rejectionReasons 为空 且 无 requiresVerification(没有 indeterminate 操作) 且 无 recovery.hasCorruption 否则 = blocked

拒绝原因dangling_tool_statederiveRejectionReasons统一归并(见 packages/runtime/src/runtime-resume.ts):以下任何诊断都会映射到它——pending_tool_result(function_call 无匹配的已提交 function_response)、tool_not_dispatchedtool_recovery_parkedtool_recovery_corruptiontool_ledger_corruptionduplicate_event_idsemantic_lane_conflictprotocol_marker_invalidunmatched_tool_resulttool_name_mismatch。也就是说,"悬空工具状态"不是单一情形,而是一整类不确定状态的共同落点,任何一类出现都会让重放被阻断。

provider 重放历史如何构建

buildResumeReplayRuntimeEvents(见 packages/runtime/src/runtime-resume.ts)按三条规则裁剪重放事件:

  1. 丢弃partial事件(流式中间态);
  2. 丢弃modelVisibility === 'hidden'的事件(如 T1 dispatch 等系统内部事实);
  3. 只保留已配对function_callfunction_response——未解决的调用绝不会被喂回 provider。

这与契约表完全一致:after_function_call前缀重放时只包含 user 事件,调用被排除;after_function_response前缀重放时 user/call/response 三者保持配对。

进程级测试 Harness:真实 SIGKILL 下的语义验证

Phase 0 的崩溃测试必须使用真实文件后端RuntimeEventStorecreateWorkspaceRuntimeStore),而不是内存 mock。契约规定的九个步骤在 packages/runtime/src/tests/runtime-resume-crash.test.ts 中被完整实现:

  1. 创建临时工作区(mkdtemp);
  2. 启动一个子 Node.js 进程(spawn(process.execPath, ...),以环境变量MAKA_RUNTIME_RESUME_CRASH_CHILD=1进入崩溃子进程模式);
  3. 通过RuntimeEventStore.appendRuntimeEvent写入该 failpoint 的完整前缀;
  4. 只有当所有 append promise resolve 之后,子进程才向 stdout 输出READY通知父进程;
  5. 父进程用SIGKILL终止子进程(Windows 下经terminateChildProcessTree处理进程树);
  6. 断言finally清理标记文件(child-finally-ran未被写入——这证明 kill 发生在追加全部完成之后、任何清理之前,正是"已提交前缀"的精确位置;
  7. 用新的RuntimeEventStore实例重新打开工作区,读回事件并断言与提交前缀逐 ID 一致;
  8. 对重开的前缀投影两次,要求两次的ResumePlan深度相等(assert.deepEqual(second, first)),验证确定性;
  9. 再次读取 ledger,断言投影没有改动持久化数据(assert.deepEqual(await reopened.readRuntimeEvents(...), recoveredEvents))。

该 harness 在 Windows、macOS、Linux 三个平台上覆盖全部十二个稳定 failpoint ID。它验证的是进程崩溃恢复语义,而不是断电持久性或文件系统fsync保证——后者需要单独的硬件与文件系统层测试。

针对每种前缀,assertResumePlanForPrefix(见 packages/runtime/src/tests/runtime-resume-crash.test.ts)还做了行为断言:

  • after_function_calldisposition === 'blocked'、操作status === 'indeterminate'rejectionReasons === ['dangling_tool_state']、重放事件仅['user']
  • before_function_callsafe_replay、无操作;
  • after_function_response/after_terminal_eventsafe_replay、操作status === 'succeeded'

Phase 边界:Phase 0 不做什么

Phase 0不改变任何工具执行行为。以下能力明确超出范围:

  • 自动续跑(automatic continuation);
  • 工作区恢复(workspace restoration);
  • T1/T2 事务化工具边界;
  • 副作用对账(side-effect reconciliation);
  • 幂等工具重执行;
  • 以 SQLite 作为 canonical 的 RuntimeEvent 与工具日志存储。

这些能力依赖后续阶段。Phase 0 的定位是:仅基于当前可得证据,让恢复决策变得确定且 fail-closed。也就是说,它在"证据不足"与"证据充分"之间的任何模糊地带,一律选择阻断(blocked/park),而不是猜测"应该没事"。

与后续阶段的关系

从 docs/architecture/runtime-resume-architecture.md 可以看到,Phase 0 是五阶段恢复路线的第一环:Phase 0 解释已提交历史 → Phase 1 在完整安全边界上创建新执行(RuntimeContinuationPlanner)→ Phase 2 用 SQLite 的 T1/T2 收窄副作用窗口 → Phase 3A 原子化提交恢复事实 → 后续 Phase 3/4 做工具级证据与工作区检查点。Phase 2 不会取代 Phase 0/1,它只是给这些闸门提供更精确的"是否跨过工具派发边界"的证据。

相关延伸资料:

  • Runtime Resume 总体架构
  • Phase 1 安全边界契约
  • RecoveryResolver ADR
  • RecoveryResolver 实现
  • Phase 0 单元与契约测试
  • Phase 0 进程崩溃 harness 测试

小结:Phase 0 一句话契约

Phase 0 的价值不在于"崩溃后能自动继续",而在于让恢复决策本身变得可证明、可测试、可复现:

  • 只承认完整提交RuntimeEvent前缀,撕裂行永远不算数;
  • 只有四种合法前缀,每种前缀的ResumePlan结果完全确定;
  • 任何悬空工具状态一律blocked并给出稳定的机器可读原因(dangling_tool_state等);
  • 投影是纯函数:重复投影结果一致,且绝不改写持久化 ledger;
  • 用真实子进程 + SIGKILL + 文件后端 store 在三大平台验证语义,且明确不把"进程崩溃"与"断电持久性"混为一谈。

这是整个 Maka 恢复体系"fail-closed"的基石:先证明安全,才允许重放。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询