# M-Agent Durable Run 视觉图示与动画制作说明

> 本文是《M-Agent Durable Run 源码解析》的配套视觉规格，基于 M-Agent
> `v0.5.1`。它单独整理正文中的 11 个 Mermaid 图示、对应最终版 PNG 与
> 5 段精修 2D 动画，供技术写作、插图设计和视觉验收使用。
>
> 本文不新增 Runtime 行为宣称。图示中的状态、顺序和恢复分支应以
> [`v0.5.1` 源码](https://github.com/lingdu0007/M-Agent/tree/v0.5.1)及其测试为准。

> **精校补注：** 本地正文第 7.6、12.8 节补充了 `call_id` 作用域风险和已复现的
> Tool 分步提交恢复缺口。PNG 与视频按原资源保留，没有重新渲染；它们表达的是
> 指定故障窗口，不能作为任意 Checkpoint 前退出都安全的证据。M08 的 Mermaid
> 已与精校正文同步，补明冻结声明检查的顺序。

**当前交付：** [图文与视频正文](durable-run-source-analysis.md)已完成媒体嵌入；
[动画合集](../remotion_video/out/refined/index.html)收录 A01-A05 的精修成片。
图片采用 [最终配图清单](imgs/outline.md)中的版本，视频统一引用
`../remotion_video/out/refined/`，不使用 `docs/animations/` 中的初版视频。
当前脚本、分镜与验收记录分别见
[script.md](../remotion_video/script.md)、
[storyboard.md](../remotion_video/storyboard.md)和
[精修报告](../remotion_video/refinement-report.md)。
第 4 节折叠保留的长分镜仅供追溯，不是当前成片的时长或实现要求。

## 1. 使用原则

### 1.1 视觉系统必须表达的三类信息

所有图示都应明确区分：

1. **权威持久化事实**：Run、Step、Attempt、Checkpoint、Policy Decision、
   Lease 和 version；
2. **进程内或实时信息**：Runner 内存、Cancellation Event、Run Update、
   Streaming Delta；
3. **Run Store 事务之外的外部事实**：Model Provider 调用、Tool Effect、
   外部 journal、外部幂等 ledger。

这三类信息不能使用相同的容器或相同的确认标记。特别是：

- Tool 函数在内存中返回，不等于 Tool Checkpoint 已提交；
- `StepAttempt(RUNNING)` 不单独证明外部 dispatch 已发生；
- Streaming Delta 不等于完整 Model Response；
- Lease 过期不等于旧进程或旧外部请求已经停止；
- `WAITING` 不等于失败，也不表示外部 Effect 已发生或未发生。

### 1.2 建议的统一视觉语义

| 视觉元素 | 建议语义 |
|---|---|
| 实线箭头 | 执行路径上的调用、返回或权威 mutation |
| 虚线箭头 | 可选观测、应用组合关系或非权威通知 |
| 数据库圆柱 | Run Store 中已经提交的权威记录 |
| 绿色确认标记 | Checkpoint 或终态 mutation 已提交 |
| 黄色/琥珀色状态 | `WAITING`、未确认 Attempt、Lease 即将过期 |
| 红色断裂或停止线 | 进程退出、写入被拒、协议失败 |
| 灰色内容 | 已失败 Attempt 的历史证据或已失效的临时 UI |
| 蓝色内容 | 当前有效 Runner、当前 Attempt 或正常执行路径 |

颜色只能辅助表达，不能作为唯一编码。状态、Attempt identity 和关键分支必须有
文字标签，以便黑白打印和无障碍阅读。

### 1.3 制作约束

- Mermaid 图优先保持静态、可打印、可在 Markdown 中直接渲染。
- 2D 动画应使用稳定布局，避免 Actor、Store 行或状态标签在镜头间跳位。
- 状态转换使用源码中的精确枚举值，如 `RUNNING`、`WAITING`、`SUCCEEDED`。
- 首次出现时写完整术语，后续可缩写，但不要将 Attempt、Checkpoint 和 Update
  统称为“日志”。
- 所有时间线都必须标出 Checkpoint 提交边界，而不只标出 Python 调用返回。
- 不得用动画补间暗示跨 Run Store 与外部系统的原子事务。
- 不得用“回滚”动画表现不可逆的外部 Tool Effect。

## 2. 视觉项总览

### 2.1 Mermaid 图示

| ID | 关联章节 | 图形类型 | 核心问题 |
|---|---|---|---|
| M01 | 开场 | Sequence Diagram | 非幂等通知在 Tool Checkpoint 前崩溃后如何处置 |
| M02 | 第 1 章 | Flowchart | Runtime Core 与应用、Adapter、Store 的边界 |
| M03 | 第 1 章 | ER Diagram | Run、Step、Attempt、Checkpoint 如何归属 |
| M04 | 第 1 章 | State Diagram | 七状态 Run 状态机及合法转换 |
| M05 | 第 4 章 | Sequence Diagram | 正常启动与 Model/Tool 主循环的完整时序 |
| M06 | 第 6 章 | Flowchart | Metadata/Payload 拆分与恢复组合 |
| M07 | 第 7 章 | Flowchart | `resume_run()` 的 Run 级入口决策 |
| M08 | 第 8 章 | Flowchart | 缺少 Tool Checkpoint 时如何按 Effect 分流 |
| M09 | 第 10 章 | Sequence Diagram | Streaming Retry 时 Attempt 如何替换 |
| M10 | 第 11 章 | Sequence Diagram | Lease 接管与旧 owner 迟到提交 |
| M11 | 第 12 章 | Flowchart | 真实跨进程崩溃测试如何组织证据 |

### 2.2 2D 动画

| ID | 关联章节 | 精修成片时长 | 核心问题 |
|---|---|---:|---|
| A01 | 开场 | 9 秒 | Checkpoint 前后崩溃的恢复差异 |
| A02 | 第 5 章 | 11 秒 | Context、Model、Tool 三类 Step 的恢复分流 |
| A03 | 第 8 章 | 12 秒 | 非幂等 Effect 如何进入 WAITING 并被应用确认 |
| A04 | 第 10 章 | 9 秒 | 部分 Streaming 输出如何按 Attempt 替换 |
| A05 | 第 11 章 | 11 秒 | Lease 接管后旧 owner 的迟到写为何被拒 |

五段合计 **52 秒**，均为 1920 × 1080、30 fps、无音轨 MP4；下方按需播放，
不自动播放或循环。每段附结论静帧与手机竖版图，便于无视频环境阅读。

## 3. Mermaid 图示说明

### M01：非幂等通知在 Checkpoint 前崩溃

**关联章节：** 开场“这条通知到底发送了几次”

**表达目标：** 用一个完整事故链说明 Durable Run 的核心问题不是“进程能否
重启”，而是“外部 Effect 已可能发生、Run Store 却没有确认时，Runtime 能否
诚实地保留不确定性”。

**阅读顺序：**

1. Runner 先持久化 Tool Step/Attempt identity；
2. Tool 调用外部通知系统，外部系统接受请求；
3. Tool 在内存中返回 `ToolOutcome.SUCCESS`；
4. Runner 在 Tool Checkpoint 前退出；
5. 新 Runner 从 Store 看到 in-flight Attempt，但看不到 Tool Checkpoint；
6. Run 进入 `WAITING(UNCERTAIN_NON_IDEMPOTENT)`；
7. 应用查询外部证据后提交 `CONFIRM_STEP`；
8. Runtime 写入 Tool Outcome Checkpoint 并继续 Run。

**必须强调：**

- `ToolOutcome.SUCCESS in memory` 和 Tool Checkpoint 是两个不同事件；
- 独立外部系统的 `accepted` 不在 Run Store 事务中；
- 新 Runner 不自动重放非幂等通知；
- `CONFIRM_STEP` 不再次调用 Tool。

**不得暗示：**

- `StepAttempt(RUNNING)` 单独证明 Tool 已 dispatch；
- 进程退出会自动写 `FAILED`；
- Runtime 能直接查询任意外部通知系统。

**依据：**

- [`Runner._run_tool_step()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L3268)
- [`Runner.resolve_run()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L722)
- [`test_crash_before_tool_checkpoint_resumes_to_waiting`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resolution.py#L795)
- [`test_sqlite_confirm_after_crash_completes_run`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resolution.py#L866)

```mermaid
sequenceDiagram
    autonumber
    participant R as Runner A
    participant S as Run Store
    participant T as Notification Tool
    participant E as External System
    participant R2 as Runner B
    participant A as Application

    R->>S: persist Tool Step/Attempt RUNNING
    R->>T: invoke(notification)
    T->>E: send notification
    E-->>T: accepted
    T-->>R: ToolOutcome.SUCCESS in memory
    Note over R,S: crash before Tool Checkpoint
    R-xS: process exits
    R2->>S: resume_run(run_id)
    S-->>R2: RUNNING + inflight Attempt + no Tool Checkpoint
    R2->>S: WAITING(UNCERTAIN_NON_IDEMPOTENT)
    A->>R2: resolve_run(CONFIRM_STEP, result)
    R2->>S: write Tool Outcome Checkpoint
    R2->>S: continue Run
```

![M01：外部通知已接受、Runner 内存返回与 Tool Checkpoint 提交是独立事实](imgs/01-infographic-crash-evidence-boundary-v3.png)

### M02：Runtime Core 的所有权边界

**关联章节：** 第 1 章“Runtime 的边界”

**表达目标：** 说明 M-Agent 是嵌入应用的 Runtime，不是 scheduler、worker
平台、消息队列或密钥管理系统。

**阅读顺序：**

1. Application 创建、启动、恢复、处置和取消 Run；
2. Application 向 Registry 注册精确 Definition；
3. Runner 调用 Registry、Run Store、Context、Model 和 Tool；
4. Telemetry 与 Run Update 位于非权威观测边界；
5. Queue、Scheduler、Worker Deployment 属于应用；
6. Credentials 属于 Adapter 的外部配置边界。

**线型语义：**

- 实线表示执行路径上的直接契约；
- 虚线表示不参与 Run 权威推进的观测或应用组合关系。

**不得暗示：**

- Run Lease 等于 worker assignment；
- Run Update 是持久化事件总线；
- Telemetry 或 Trace 是恢复来源；
- Runner 会自动扫描 Store 并接管任务。

**依据：**

- [`ADR-0001`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0001-agent-application-runtime-boundary.md)
- [`ADR-0009`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0009-async-first-embedded-runner.md)
- [`RunStore`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_store.py#L173)

```mermaid
flowchart LR
    App[Application / Service] -->|create, start, resume, resolve, cancel| Runner
    App -->|register exact definition| Registry[Definition Registry]
    Runner --> Registry
    Runner --> Store[(Run Store)]
    Runner --> Context[Context Provider]
    Runner --> Model[Model Adapter]
    Runner --> Tool[Tools]
    Runner -. metadata events .-> Telemetry[Telemetry Sink]
    Runner -. live-only updates .-> UI[Application UI / Transport]

    Queue[Queue / Scheduler] -. application owned .-> App
    Workers[Worker deployment] -. application owned .-> App
    Secrets[Credentials / Secret Store] -. adapter owned .-> Model
    Secrets -. adapter owned .-> Tool
```

![M02：Runtime 推进 Run，应用拥有调度、部署与恢复触发，观测不参与权威恢复](imgs/02-framework-runtime-ownership-boundary-v3.png)

### M03：Run、Step、Attempt 与 Checkpoint 的归属关系

**关联章节：** 第 1 章“五个必须分开的对象”

**表达目标：** 建立 Durable Run 最重要的数据模型：Run 拥有 Step，一个逻辑
Step 可以有多个 Attempt，但最多形成一个已确认 Checkpoint。

**阅读提示：**

- 这张图只表达归属关系，不表达写入顺序；
- Step 是逻辑工作单元；
- Attempt 是一次具体执行尝试及其持久化 identity；
- Checkpoint 是已确认的完整结果；
- 重试或恢复 replay 保持 `step_id`，创建新的 `attempt_id`；
- Run-scoped identity 使用 `(run_id, step_id)` 或
  `(run_id, attempt_id)`，不是全局单列 identity。

**不得暗示：**

- Attempt 一定意味着外部 dispatch 已发生；
- 多个 Attempt 是以并发竞争选择胜者的结果机制；但 Lease 接管后，旧外部请求
  与新 Attempt 在时间上仍可能重叠；
- 每个 Attempt 都会产生 Checkpoint；
- Checkpoint 是一条只有时间和状态的普通日志。

**依据：**

- [`StepRecord`、`StepAttempt`、`StepCheckpoint`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_steps.py#L56)
- [`RunStore` identity contract](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_store.py#L173)
- [`SQLite schema`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/adapters/_sqlite_store.py#L75)

```mermaid
erDiagram
    RUN ||--o{ STEP : owns
    STEP ||--o{ ATTEMPT : retries_as
    STEP ||--o| CHECKPOINT : confirms
    ATTEMPT ||--o| CHECKPOINT : produces

    RUN {
        string run_id PK
        string definition_id
        string definition_version
        string status
        int version
        string lease_owner
        datetime lease_expires_at
    }
    STEP {
        string run_id PK
        string step_id PK
        string step_type
        string status
    }
    ATTEMPT {
        string run_id PK
        string attempt_id PK
        string step_id
        string status
        string classification
        string error_code
    }
    CHECKPOINT {
        string run_id PK
        string step_id PK
        string attempt_id
        string step_type
    }
```

![M03：Run 拥有 Step，Step 保留多次 Attempt，至多一个 Checkpoint 确认完整结果](imgs/03-framework-run-step-attempt-checkpoint-v3.png)

### M04：七状态 Run 状态机

**关联章节：** 第 1 章“七状态 Run 状态机”

**表达目标：** 展示当前公开 Run 状态以及 Runtime 允许的生命周期转换。

**关键解释：**

- `CREATED` 表示执行 identity 和冻结语义已经持久化，但尚未进入执行；
- `RUNNING` 表示已进入执行状态，不等于当前调用者持有有效 Lease；
- `WAITING` 是需要外部条件或应用决策的可恢复中间态；
- `SUCCEEDED`、`REJECTED`、`FAILED`、`CANCELLED` 都是终态；
- 进程崩溃本身不产生状态转换，Run 可以继续保持 `RUNNING`。

**不得暗示：**

- `WAITING` 是一种失败；
- 终态可以再次进入 `RUNNING`；
- Lease owner 是状态机的一部分；
- 每个 `RUNNING -> WAITING` 都来自同一种原因。

**依据：**

- [`RunStatus`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_status.py#L18)
- [`LEGAL_TRANSITIONS`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_status.py#L47)
- [`ADR-0027`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0027-distinguish-rejected-runs.md)

```mermaid
stateDiagram-v2
    [*] --> CREATED
    CREATED --> RUNNING
    CREATED --> WAITING
    CREATED --> REJECTED
    CREATED --> FAILED
    CREATED --> CANCELLED

    RUNNING --> SUCCEEDED
    RUNNING --> REJECTED
    RUNNING --> FAILED
    RUNNING --> WAITING
    RUNNING --> CANCELLED

    WAITING --> RUNNING
    WAITING --> FAILED
    WAITING --> CANCELLED

    SUCCEEDED --> [*]
    REJECTED --> [*]
    FAILED --> [*]
    CANCELLED --> [*]
```

![M04：WAITING 是可恢复中间态，与 SUCCEEDED、REJECTED、FAILED、CANCELLED 四种终态分开](imgs/04-flowchart-run-state-machine-v2.png)

### M05：正常启动与 Model/Tool 主循环

**关联章节：** 第 4 章“正常执行时序”

**表达目标：** 以公开 API 为入口，串联创建、Lease、Policy、Context、
Compression、业务 Model Step、Tool Calls 和最终输出。

**阅读顺序：**

1. `create_run()` 持久化 `CREATED` 与冻结 Snapshot；
2. `start_run()` 获取 Lease；
3. INPUT Policy 允许后进入 `RUNNING`；
4. 可选执行 `RUN_INPUT` Context Stage；
5. 可选执行独立 `CONTEXT_COMPRESSION` Model Step；
6. 每轮先完成一个 `PRIMARY` Model Checkpoint；
7. Tool Calls 按原顺序逐个执行并 Checkpoint；
8. 无 Tool Calls 时经过 FINAL_OUTPUT Policy；
9. 提交 `SUCCEEDED` 并释放 Lease。

**制作建议：**

- Context、Compression、Primary Model、Tool 使用不同泳道或不同标签；
- `reserve Model attempt` 应放在 Model dispatch 之前；
- Tool Call 循环必须表现为严格串行；
- Checkpoint 应先于依赖该结果的下一步。

**不得暗示：**

- 这是 `resume_run()` 的完整恢复流程；
- Tool Calls 可以在当前实现中并行推进；
- Compression 是进程内字符串处理；
- FINAL_OUTPUT Policy 通过后一定成功，忽略 Output Contract/Repair 分支。

**依据：**

- [`Runner.start_run()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L383)
- [`Runner._execute_steps()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L1819)
- [`Runner._run_agent_loop()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L2484)

```mermaid
sequenceDiagram
    autonumber
    participant App
    participant Runner
    participant Store
    participant Policy
    participant Context
    participant Model
    participant Tool

    App->>Runner: create_run(definition_id, version, input)
    Runner->>Store: create CREATED + frozen snapshot
    App->>Runner: start_run(run_id)
    Runner->>Store: acquire lease(expected_version)
    Runner->>Policy: INPUT
    Policy-->>Runner: ALLOW
    Runner->>Store: CREATED -> RUNNING

    opt RUN_INPUT Context
        Runner->>Store: Context Step/Attempt RUNNING
        Runner->>Context: provide()
        Context-->>Runner: ContextStageResult
        Runner->>Store: Context Step/Attempt/Checkpoint SUCCEEDED
    end

    opt Semantic Compression
        Runner->>Store: reserve CONTEXT_COMPRESSION attempt
        Runner->>Model: dispatch compression request
        Model-->>Runner: complete ModelResponse
        Runner->>Store: Model Step/Attempt/Checkpoint
    end

    loop until response has no tool_calls
        Runner->>Store: reserve PRIMARY Model attempt
        Runner->>Model: generate or stream
        Model-->>Runner: complete ModelResponse
        Runner->>Store: Model Step/Attempt/Checkpoint
        alt tool_calls exist
            loop each ToolCall in original order
                Runner->>Policy: TOOL_REQUEST
                Runner->>Store: Tool Step/Attempt RUNNING
                Runner->>Tool: invoke()
                Tool-->>Runner: ToolOutcome
                Runner->>Policy: TOOL_OUTCOME
                Runner->>Store: Tool Step/Attempt/Checkpoint
            end
        else final response
            Runner->>Policy: FINAL_OUTPUT
            Note over Runner,Policy: success path: Policy ALLOW and Output Contract valid
            Runner->>Store: RUNNING -> SUCCEEDED(output)
            Runner->>Store: release lease
        end
    end
```

![M05：正常主循环先确认 Context、Model、Tool 的完整结果，再推进依赖它的下一步](imgs/05-flowchart-normal-run-checkpoints-v2.png)

### M06：Metadata 与 Payload 的拆分

**关联章节：** 第 6 章“Metadata 与 Payload 为什么分表”

**表达目标：** 展示公共对象如何被拆成可查询 metadata 和受
`PayloadCodec` 保护的内容 Payload，并在读取时重新组合。

**关键解释：**

- status、identity、version、classification、usage 等字段进入 metadata；
- input、output、instructions、error 和 Checkpoint body 进入 Payload；
- `PayloadCodec` 只作用于内容路径；
- `run_payloads` 是编码内容区；
- 恢复时 metadata 与解码后的 Payload 重新构造公共对象。

**不得暗示：**

- metadata 中完全没有 Snapshot 信息；`runs.snapshot_json` 仍有不含
  instructions 的可查询 Snapshot metadata；
- Plaintext Codec 提供保密性；
- Split/Restore helper 自身是数据库事务；
- Trace 或 Update 参与对象恢复。

**依据：**

- [`ADR-0033`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0033-separate-protected-run-payloads.md)
- [`RunStore split helpers`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_store.py#L36)
- [`SQLite run_payloads schema`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/adapters/_sqlite_store.py#L75)

```mermaid
flowchart TB
    Public[RunRecord / StepAttempt / Checkpoint]
    Split[Shared split/restore helpers]
    Codec[PayloadCodec]
    Meta[(Queryable metadata tables)]
    Payload[(run_payloads encoded BLOB)]

    Public --> Split
    Split -->|status, ids, version, classification, usage| Meta
    Split -->|input, output, instructions, errors, checkpoint body| Codec
    Codec --> Payload
    Meta --> Restore[Restore public objects]
    Payload --> Codec
    Codec --> Restore
```

![M06：公共对象拆分为可查询 metadata 与编码 payload，读取时经 Codec 重建](imgs/06-framework-metadata-payload-boundary-v2.png)

### M07：`resume_run()` 的 Run 级入口决策

**关联章节：** 第 7 章“第一层决策：Run 当前是什么状态”

**表达目标：** 先按 Run 状态和 `waiting_reason` 选择入口分支，再进入证据级
恢复，避免把 `resume_run()` 误解为无条件重新执行。

**阅读顺序：**

1. 读取权威 `RunRecord`；
2. 终态直接拒绝；
3. 普通 WAITING 原样返回；
4. `DEFINITION_UNAVAILABLE` 只有在精确 Definition 恢复后才继续；
5. `CREATED` 获取 Lease 后走共享启动路径；
6. `RUNNING` 获取或接管 Lease，再进入 `_resume_running()`。

**制作建议：**

- 终态拒绝使用明确的停止节点；
- 两类 WAITING 应视觉分开；
- `CREATED` 和 `RUNNING` 最终进入不同 helper；
- Lease 与 Contract guard 应位于继续推进之前。

**不得暗示：**

- 普通 WAITING 会被重复 `resume_run()` 绕过；
- 精确 Definition 缺失时会使用最新版；
- `RUNNING` 调用者天然拥有 Lease；
- `resume_run()` 会读取 Trace、Update 或旧进程内存。

**依据：**

- [`Runner.resume_run()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L444)
- [`Runner._resume_running()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L1439)
- [`ADR-0023`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0023-exact-definition-resolution.md)

```mermaid
flowchart TD
    Start[resume_run(run_id)] --> Load[load authoritative RunRecord]
    Load --> Terminal{terminal?}
    Terminal -->|yes| Reject[IllegalRunTransitionError]
    Terminal -->|no| Waiting{WAITING?}

    Waiting -->|yes| DefWait{reason = DEFINITION_UNAVAILABLE?}
    DefWait -->|no| ReturnWaiting[return unchanged WAITING]
    DefWait -->|yes| ResolveOld{exact definition available?}
    ResolveOld -->|no| ReturnWaiting
    ResolveOld -->|yes| Acquire1[acquire lease + contract guard]
    Acquire1 --> ToRunning[WAITING -> RUNNING]
    ToRunning --> ResumeFacts[_resume_running]

    Waiting -->|no| Created{CREATED?}
    Created -->|yes| Acquire2[acquire lease]
    Acquire2 --> ExactCreated{exact definition available?}
    ExactCreated -->|no| WaitDefinition[WAITING DEFINITION_UNAVAILABLE]
    ExactCreated -->|yes| StartPath[_start_with_lease]

    Created -->|no: RUNNING| GuardIfAvailable[guard exact definition if available]
    GuardIfAvailable --> Acquire3[acquire or take over lease]
    Acquire3 --> ResumeFacts
```

![M07：resume_run 按权威状态、精确定义和 Lease 条件进入启动、恢复或等待](imgs/07-flowchart-resume-entry-decision-v2.png)

### M08：缺少 Tool Checkpoint 时的 Effect 决策

**关联章节：** 第 8 章“恢复矩阵”

**表达目标：** 展示最后一个 Model Response 已请求 Tool，但没有对应 Tool
Checkpoint 时，Runtime 如何结合 in-flight 证据、冻结 Tool Effect 和 Retry
authority 决定首次执行、恢复 replay、WAITING 或失败。

**关键分支：**

- 没有 in-flight Tool Step/Attempt：这是尚未建立执行 identity 的首次调用；
- 冻结声明缺失或歧义：保留可关联证据进入 WAITING，或在无法关联时 fail closed；
- `READ_ONLY` / `IDEMPOTENT`：保留旧 Attempt 的不确定证据，在 Retry Policy
  允许时创建新 Attempt；
- `NON_IDEMPOTENT`：进入 `WAITING(UNCERTAIN_NON_IDEMPOTENT)`；
- WAITING 由应用选择 Retry、Confirm、Fail 或 Cancel。

**不得暗示：**

- Tool Effect 本身就是 Retry Policy；
- `UNCERTAIN + READ_ONLY` 在普通失败路径中一定自动重试；
- `IDEMPOTENT` 声明自动实现外部去重；
- `CONFIRM_STEP` 会执行 Tool；
- 冻结声明缺失时可以读取当前 callable 的新声明猜测。

**依据：**

- [`Runner._frozen_tool_effect()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L4125)
- [`Runner._run_tool_step()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L3268)
- [`ADR-0007`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0007-fail-closed-tool-effect.md)
- [`ADR-0008`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0008-external-resolution-for-uncertain-effects.md)

```mermaid
flowchart TD
    Missing[Model requested ToolCall but no Tool Checkpoint] --> Frozen{unique frozen ToolEffect?}
    Frozen -->|no| Evidence{RUNNING Tool Step?}
    Evidence -->|yes| PreserveWait[WAITING preserving evidence]
    Evidence -->|no| DeclarationFail[FROZEN_TOOL_DECLARATION_UNAVAILABLE]
    Frozen -->|yes| Inflight{RUNNING Tool Step?}
    Inflight -->|no| Fresh[first-dispatch path; see commit-gap caveat]
    Inflight -->|yes| Effect{frozen effect}
    Effect -->|READ_ONLY| Preserve1[preserve old Attempt if still RUNNING]
    Effect -->|IDEMPOTENT| Preserve2[preserve old Attempt if still RUNNING]
    Effect -->|NON_IDEMPOTENT| Wait[WAITING UNCERTAIN_NON_IDEMPOTENT]
    Preserve1 --> Budget1{frozen retry authority remains?}
    Preserve2 --> Budget2{frozen retry authority remains?}
    Budget1 -->|yes| Replay1[new Attempt, same Step, replay]
    Budget2 -->|yes| Replay2[new Attempt, same Step, replay]
    Budget1 -->|no| Fail1[FAILED]
    Budget2 -->|no| Fail2[FAILED]
    Wait --> Resolution[application Resolution]
    Resolution --> Retry[RETRY_STEP]
    Resolution --> Confirm[CONFIRM_STEP]
    Resolution --> FailRun[FAIL_RUN]
    Resolution --> CancelRun[CANCEL_RUN]
```

![M08：缺少 Tool Checkpoint 时，依据 in-flight 证据、冻结 Effect 与 Retry Policy 决定重放、WAITING 或失败](imgs/08-flowchart-tool-effect-resolution-v2.png)

“无 in-flight”在当前源码中只是没有找到 `RUNNING` Tool Step，不能证明没有
发生过调用。成功状态先于 Checkpoint 提交的情况不在原 PNG 的安全故事内。
只有存在 `RUNNING` Attempt 时才启用恢复重放的 Retry Policy 检查。

### M09：Streaming Retry 与 Attempt Replacement

**关联章节：** 第 10 章“Attempt replacement 是 UI 必须理解的协议”

**表达目标：** 说明 Streaming Delta 是按 Attempt 分组的临时视图。Attempt A
部分输出后失败，Attempt B 必须以新的 identity 重新开始；UI 替换临时内容，
Run Store 则保留两个 Attempt 的历史。

**阅读顺序：**

1. Attempt A 发布 `STEP_STARTED`；
2. A 产生 `A`、`B` 两个 delta；
3. A 发生 TRANSIENT failure；
4. Store 保存 Attempt A 的失败证据；
5. Attempt B 使用新 `attempt_id`；
6. B 产生完整 `ABC`；
7. 完整 ModelResponse 形成 Checkpoint；
8. `STEP_COMPLETED` 在 Checkpoint 后发布。

**不得暗示：**

- 第二次流式输出追加成 `ABABC`；
- `ATTEMPT_FAILED` 删除 Attempt A；
- Delta 被写入 Model Checkpoint；
- Runtime 会在流结束时自行拼接 delta；
- `STEP_COMPLETED` 先于 Checkpoint。

**依据：**

- [`RunUpdate`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_updates.py#L32)
- [`Runner._stream_model()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L1144)
- [`test_partial_stream_retry_new_attempt_replaces_output`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_stream_cancel.py#L751)

```mermaid
sequenceDiagram
    autonumber
    participant R as Runner
    participant U as Update Consumer
    participant S as Run Store
    participant M as Model Adapter

    R->>U: STEP_STARTED(step-1, attempt-A)
    R->>M: stream attempt-A
    M-->>R: delta A
    R-->>U: MODEL_DELTA(attempt-A, A)
    M-->>R: delta B
    R-->>U: MODEL_DELTA(attempt-A, B)
    M-xR: TRANSIENT failure
    R->>S: Attempt-A FAILED
    R-->>U: ATTEMPT_FAILED(attempt-A)

    R->>U: STEP_STARTED(step-1, attempt-B)
    R->>M: stream attempt-B
    M-->>R: delta A, B, C
    R-->>U: MODEL_DELTA(attempt-B, A/B/C)
    M-->>R: complete ModelResponse ABC
    R->>S: Attempt-B SUCCEEDED + Checkpoint ABC
    R-->>U: STEP_COMPLETED(attempt-B)
```

![M09：新 Attempt 从空白替换旧临时输出，旧失败记录保留，完整响应才可形成 Checkpoint](imgs/09-comparison-stream-attempt-replacement-v2.png)

### M10：Lease 接管与迟到写入拒绝

**关联章节：** 第 11 章“Takeover 时序”

**表达目标：** 说明 Lease 只控制谁可以继续 dispatch 和提交权威 mutation，
不会停止旧进程，也不会取消已经发出的 Provider 请求。

**阅读顺序：**

1. Runner A 获取 Lease，并将 Run 从 `CREATED` 转为 `RUNNING`；
2. A 预留 Attempt-A 并 dispatch Provider；
3. A 卡住或变得不可达，Lease 过期；
4. Runner B 读取当前 version 并接管 Lease；
5. B 将旧 Attempt 归一为不确定证据；
6. 冻结规则允许时，B 创建 Attempt-B 并 replay；
7. B Checkpoint 成功结果并提交终态；
8. A 的迟到响应随后返回；
9. A 的旧 owner/version 写入被 Store 拒绝。

**制作建议：**

- version 使用 `v`、`v+1`、`current_version`，不要绑定易误导的固定整数；
- Lease 倒计时只画在 Store 一侧；
- A 的 Provider 请求在 Lease 过期后仍可继续；
- 迟到写被拒应与外部请求返回分开表现。

**不得暗示：**

- Lease 过期会向 A 发送中断信号；
- B 接管证明 A 的请求没有产生外部 Effect；
- 拒绝 A 的 Checkpoint 能撤销外部结果；
- Lease acquisition 本身会递增 Run progress version。

**依据：**

- [`Runner._assert_step_dispatch()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L1088)
- [`SQLiteRunStore.acquire_lease()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/adapters/_sqlite_store.py#L445)
- [`test_expired_lease_takeover_and_stale_owner_rejected`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_lease.py#L286)

```mermaid
sequenceDiagram
    autonumber
    participant A as Runner A
    participant S as SQLite Run Store
    participant P as Provider
    participant B as Runner B

    A->>S: acquire lease(owner=A, expected_version=v)
    A->>S: CREATED to RUNNING, version becomes v+1
    A->>S: reserve Attempt-A
    A->>P: dispatch
    Note over A,P: A stalls or process becomes unreachable
    Note over S: lease expires
    B->>S: resume_run + acquire expired lease(owner=B, current_version)
    S-->>B: lease granted
    B->>S: mark Attempt-A UNCERTAIN
    B->>S: reserve Attempt-B
    B->>P: replay if frozen policy permits
    P-->>B: complete response
    B->>S: Checkpoint Attempt-B
    B->>S: Run to SUCCEEDED using current version

    P-->>A: late response
    A->>S: late Checkpoint Attempt-A
    S--xA: reject stale version / lease owner
```

![M10：B 接管有效 Lease 后可以推进，A 的旧请求仍可返回，但迟到写入被拒](imgs/10-flowchart-lease-takeover-guard-v2.png)

### M11：真实跨进程崩溃测试拓扑

**关联章节：** 第 12 章“真实进程退出，而不是模拟异常”

**表达目标：** 展示高价值 Durable 测试如何通过真实子进程退出、独立
SQLite、外部 journal、FakeClock 和新 Runner，排除同进程内存状态造成的
假阳性。

**阅读顺序：**

1. Test Process 启动 Worker Process；
2. Worker 使用 SQLite Run Store 和独立 effect/call journal；
3. Worker 在确定性 `CrashPoint` 调用 `os._exit`；
4. Test Process 重新打开数据库检查崩溃现场；
5. FakeClock 越过崩溃遗留 Lease 的 TTL；
6. 新进程语义由新 Registry、Store 和 Runner 组成；
7. 新 Runner 读取数据库恢复；测试进程独立读取 journal 核对外部证据；
8. `inspect_run()` 与外部调用计数共同形成断言。

**制作建议：**

- 第一个 Worker 和恢复 Runner 使用不同进程框；
- Run Store 与 Journal 画成两个独立持久化介质；
- `os._exit` 后不要画 finally、Lease release 或失败状态写入；
- Clock 应位于接管前，而不是恢复完成后；
- Inspect 同时读取内部状态与外部计数。

**不得暗示：**

- 同进程抛异常等价于 `os._exit`；
- 新 Runner 可以在旧 Lease 未过期时立即接管；
- 外部 journal 是 Run Store 的一张表；
- 单个测试必须同时使用所有四类证据。

**依据：**

- [`tests/test_m_agent_resume.py`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resume.py#L1)
- [`test_second_process_reuses_checkpoint_without_model_call`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resume.py#L164)
- [`test_crash_before_checkpoint_replays_model_with_budget`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resume.py#L223)

```mermaid
flowchart LR
    Test[Test process] --> Spawn[spawn worker process]
    Spawn --> DB[(SQLite Run Store)]
    Spawn --> Journal[(independent call/effect journal)]
    Spawn --> Crash{os._exit at CrashPoint}
    Crash --> Probe[Test reopens DB]
    Probe --> Clock[advance FakeClock beyond lease TTL]
    Clock --> Resume[new Registry + Store + Runner]
    Resume --> DB
    Test --> Journal
    Resume --> Inspect[inspect_run + external counts]
```

![M11：用真实进程退出、SQLite、独立 journal、FakeClock 与新 Runner 组合验证恢复证据](imgs/11-framework-cross-process-crash-test-v2.png)

## 4. 2D 动画成片与历史分镜

### A01：Checkpoint 前后的崩溃分界

**关联章节：** 开场

**精修成片：** 9 秒。两条独立时间线对照提交前后，恢复只复用 Store 已确认的完整结果。

<video controls playsinline muted preload="none" width="1920" height="1080" style="display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;" poster="../remotion_video/out/refined/a01-checkpoint-boundary-poster.png" aria-label="A01：Checkpoint 提交前后崩溃的恢复对照">
  <source src="../remotion_video/out/refined/a01-checkpoint-boundary.mp4" type="video/mp4">
  <a href="../remotion_video/out/refined/a01-checkpoint-boundary.mp4">播放 A01</a>
</video>

[播放 / 下载 MP4](../remotion_video/out/refined/a01-checkpoint-boundary.mp4) · [结论静帧](../remotion_video/out/refined/a01-checkpoint-boundary-poster.png) · [手机竖版图](../remotion_video/out/refined/a01-checkpoint-boundary-mobile.png)

<details>
<summary>初版长分镜（历史参考，已由 9 秒精修版取代）</summary>

**初版建议时长：** 25-35 秒

**表达目标：** 用同一 Tool 调用的两个崩溃点做对照，说明恢复依据是
Checkpoint 是否提交，而不是 Python 函数是否已经返回。

**固定布局：**

- 左侧：外部 Tool Effect 时间线；
- 中间：Runner 进程和调用栈；
- 右侧：Run Store 的 Step、Attempt、Checkpoint 三行；
- 底部：恢复动作结果。

**分镜：**

1. **0-5 秒：建立身份。** Step 与 Attempt 行从空白变为 `RUNNING`；
   Checkpoint 行保持空白。
2. **5-10 秒：外部调用。** Runner 箭头进入 Tool；外部系统标记
   “accepted”。Tool 在内存中返回 SUCCESS。
3. **10-16 秒：Checkpoint 前退出。** 在 Runner 与 Store 之间放置红色断点；
   进程消失，Store 仍只有 `RUNNING` Attempt。
4. **16-23 秒：恢复判断。** 新 Runner 读取 Store；根据 Tool Effect 显示
   “replay / WAITING / fail closed”三类可能结果。
5. **23-30 秒：Checkpoint 后对照。** 时间线回到 Tool 返回点，这次先提交
   Checkpoint 再退出；新 Runner 直接复用完整 Outcome。
6. **30-35 秒：结论定格。** 显示：
   “Checkpoint decides reuse; function return alone does not.”

**关键标签：**

- `ToolOutcome returned in memory`
- `Checkpoint committed`
- `No Tool Checkpoint`
- `WAITING(UNCERTAIN_NON_IDEMPOTENT)`

**禁止表现：**

- Checkpoint 与外部系统被同一绿色事务框包住；
- 进程退出时自动出现 `FAILED`；
- `RUNNING` Attempt 被直接标成“Tool 已执行”。

**原占位说明：**

> checkpoint 前后的崩溃分界；左侧展示 Tool 外部效果时间线，右侧展示 Run
> Store 中 Step、Attempt、Checkpoint 三行记录；先在 Checkpoint 前切断，再在
> Checkpoint 后切断并对比恢复路径。Checkpoint 是否已提交决定“复用”还是
> “不确定/重放”，而不是 Python 调用是否已经返回。

</details>

### A02：Context、Model、Tool 三类 Step 的恢复分流

**关联章节：** 第 5 章

**精修成片：** 11 秒。分别呈现 Context 重读、Model 的策略与预算门槛，以及
Tool 按冻结 Effect 与恢复授权分流；各分支是对照情形，不是同一调用反复改变属性。

<video controls playsinline muted preload="none" width="1920" height="1080" style="display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;" poster="../remotion_video/out/refined/a02-step-recovery-matrix-poster.png" aria-label="A02：三类 Step 缺少 Checkpoint 时的不同恢复规则">
  <source src="../remotion_video/out/refined/a02-step-recovery-matrix.mp4" type="video/mp4">
  <a href="../remotion_video/out/refined/a02-step-recovery-matrix.mp4">播放 A02</a>
</video>

[播放 / 下载 MP4](../remotion_video/out/refined/a02-step-recovery-matrix.mp4) · [结论静帧](../remotion_video/out/refined/a02-step-recovery-matrix-poster.png) · [手机竖版图](../remotion_video/out/refined/a02-step-recovery-matrix-mobile.png)

<details>
<summary>初版长分镜（历史参考，已由 11 秒精修版取代）</summary>

**初版建议时长：** 35-45 秒

**表达目标：** 对比三类 Step 在“有 Attempt、无 Checkpoint”时为什么不能使用
同一种恢复规则。

**固定布局：**

- 三条水平泳道：Context、Model、Tool；
- 每条泳道都有 Attempt、Dispatch、External Result、Checkpoint 四个标记；
- 右侧固定一个“恢复结果”面板；
- 顶部显示统一时间轴。

**分镜：**

1. **0-6 秒：共同协议。** 三条泳道都显示先建立 Attempt、后执行外部调用、
   最后写 Checkpoint。
2. **6-14 秒：Context。** 在 Provider 返回后、Checkpoint 前冻结；右侧显示
   “at-least-once 重新读取”。随后切换到 Checkpoint 后，显示“复用 Stage
   Result”。
3. **14-24 秒：Model。** 在 reservation 后冻结；预算计数立即增加且不退款。
   Retry Policy 允许时创建新 Attempt；预算耗尽则失败。
4. **24-34 秒：Tool。** 在 Attempt 持久化后冻结；画面同时保留“尚未真正
   dispatch”和“已产生未确认 Effect”两种可能。
5. **34-41 秒：Effect 分流。** `READ_ONLY` / `IDEMPOTENT` 指向受限 replay，
   `NON_IDEMPOTENT` 指向 WAITING。
6. **41-45 秒：结论。** 三条泳道共同显示：
   “Missing Checkpoint is evidence, not a universal retry command.”

**必须表现：**

- Model reservation 在真实 Adapter dispatch 前就消耗 Runtime 预算；
- Tool Attempt 不能单独证明外部 Effect；
- 每次 replay 使用新的 `attempt_id`，但保持逻辑 `step_id`。

**禁止表现：**

- 所有未确认 Attempt 都自动重试；
- Model 预算只统计成功调用；
- Context Checkpoint 是通用缓存；
- Tool Effect 与 Failure Classification 被画成同一枚举。

**原占位说明：**

> Context、Model、Tool 三条 checkpoint 时间轴；三条水平泳道分别移动 dispatch
> 指针和 commit 指针，在每个可注入崩溃点冻结画面，右侧弹出恢复动作。相同的
> “缺 Checkpoint”在不同 Step 类型下需要不同安全判断。

</details>

### A03：非幂等通知进入 WAITING 并由应用确认

**关联章节：** 第 8 章

**精修成片：** 12 秒。应用独立查证后提交 CONFIRM_STEP，确认仍经过
TOOL_OUTCOME Policy；旧 Attempt 的不确定证据保留，通知计数始终为 1。

<video controls playsinline muted preload="none" width="1920" height="1080" style="display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;" poster="../remotion_video/out/refined/a03-uncertain-effect-resolution-poster.png" aria-label="A03：不确定通知进入 WAITING，由应用查证并确认，不重复调用 Tool">
  <source src="../remotion_video/out/refined/a03-uncertain-effect-resolution.mp4" type="video/mp4">
  <a href="../remotion_video/out/refined/a03-uncertain-effect-resolution.mp4">播放 A03</a>
</video>

[播放 / 下载 MP4](../remotion_video/out/refined/a03-uncertain-effect-resolution.mp4) · [结论静帧](../remotion_video/out/refined/a03-uncertain-effect-resolution-poster.png) · [手机竖版图](../remotion_video/out/refined/a03-uncertain-effect-resolution-mobile.png)

<details>
<summary>初版长分镜（历史参考，已由 12 秒精修版取代）</summary>

**初版建议时长：** 30-40 秒

**表达目标：** 展示 Runtime 无法从自身 Store 判断外部通知是否发生时，为什么
必须保留不确定性，并把 Resolution 交给应用。

**固定布局：**

- 左列：通知服务或独立 notification journal；
- 中列：Runner 进程及其内存返回；
- 右列：SQLite Run Store；
- 顶部：Application 控制面。

**分镜：**

1. **0-6 秒：Attempt identity。** Store 写入 Tool Step/Attempt `RUNNING`。
2. **6-12 秒：外部 Effect。** 通知服务接受请求，journal 计数从 0 变为 1。
3. **12-17 秒：崩溃。** Runner 在 Tool Checkpoint 前退出；SQLite 的
   Checkpoint 行仍为空。
4. **17-24 秒：新 Runner 恢复。** 新 Runner 只读取 SQLite，显示
   “external effect unknown”，Run 进入 WAITING。
5. **24-31 秒：应用查证。** Application 独立查询 notification journal，
   得到计数 1。
6. **31-36 秒：显式确认。** Application 调用
   `resolve_run(CONFIRM_STEP, result)`。
7. **36-40 秒：继续执行。** Runtime 不调用 Tool，写入成功 Tool Outcome
   Checkpoint 并继续 Model 循环。

**必须表现：**

- journal 与 SQLite 是两个独立事实来源；
- `WAITING` 是琥珀色中间态，不是红色失败态；
- `CONFIRM_STEP` 固定构造成功 Outcome，但仍需经过 `TOOL_OUTCOME` Policy；
- 通知计数在 Confirm 前后都保持 1。

**禁止表现：**

- Runtime 自动读取任意外部系统；
- 人工确认直接修改数据库；
- `CONFIRM_STEP` 再调用一次通知工具；
- WAITING 表示通知一定已发送。

**依据：**

- [`RunResolution`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_resolution.py#L43)
- [`test_sqlite_confirm_after_crash_completes_run`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_resolution.py#L866)

**原占位说明：**

> 非幂等通知从“不确定”进入 WAITING 再由应用确认；通知 journal、Runner
> 内存、SQLite 三层并排。通知 journal 先增加，Runner 在写 Checkpoint 前
> 消失；新 Runner 只读取 SQLite 后停止，应用查询 journal 并提交
> `CONFIRM_STEP`。

</details>

### A04：Streaming Attempt 替换

**关联章节：** 第 10 章

**精修成片：** 9 秒。AB 与 attempt-A 一起移入历史，attempt-B 从空白开始，
完整 ModelResponse 与 UI delta 分开展示。本片停在 Checkpoint 提交前的边界，
不把临时文本画成已持久化结果。

<video controls playsinline muted preload="none" width="1920" height="1080" style="display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;" poster="../remotion_video/out/refined/a04-attempt-replacement-poster.png" aria-label="A04：新 Attempt 替换活动文本，旧 AB 保留历史，完整响应与临时 delta 分开">
  <source src="../remotion_video/out/refined/a04-attempt-replacement.mp4" type="video/mp4">
  <a href="../remotion_video/out/refined/a04-attempt-replacement.mp4">播放 A04</a>
</video>

[播放 / 下载 MP4](../remotion_video/out/refined/a04-attempt-replacement.mp4) · [结论静帧](../remotion_video/out/refined/a04-attempt-replacement-poster.png) · [手机竖版图](../remotion_video/out/refined/a04-attempt-replacement-mobile.png)

<details>
<summary>初版长分镜（历史参考，已由 9 秒精修版取代）</summary>

**初版建议时长：** 25-35 秒

**表达目标：** 让 UI 实现者理解 delta 属于 Attempt，不属于逻辑 Step 的可追加
永久文本；Retry 必须替换临时输出，同时保留旧 Attempt 证据。

**固定布局：**

- 上方：UI 文本区域；
- 中间：Attempt A / Attempt B 两条流式泳道；
- 下方：Run Store 的 Attempt 与 Checkpoint 行；
- 右上角：当前 `step_id` 与 `attempt_id`。

**分镜：**

1. **0-6 秒：Attempt A。** UI 显示 `A`，随后显示 `AB`；Store 只有
   Attempt A `RUNNING`。
2. **6-11 秒：A 失败。** Attempt A 变为 `FAILED(TRANSIENT)`；UI 的 `AB`
   整块变灰或收起，不进入 Checkpoint。
3. **11-17 秒：Attempt B。** 相同 `step_id`，新的 `attempt_id`；UI 清空
   该 Step 的活动文本区域。
4. **17-25 秒：B 输出。** UI 依次显示 `A`、`AB`、`ABC`，不得显示
   `ABABC`。
5. **25-30 秒：完整响应。** Adapter 产出完整 ModelResponse；Store 写入
   Attempt B `SUCCEEDED` 与 Checkpoint `ABC`。
6. **30-35 秒：历史与最终视图。** UI 最终显示 `ABC`；检查面板仍能看到
   Attempt A 的失败记录。

**必须表现：**

- Delta 带 `step_id + attempt_id`；
- Attempt A 的失败历史没有被删除；
- 只有完整 ModelResponse 形成 Checkpoint；
- `STEP_COMPLETED` 在 Checkpoint 后。

**禁止表现：**

- 第二次输出追加到第一次临时文本；
- Runtime 将 delta 自动拼成 Checkpoint；
- 新 Attempt 覆盖 Store 中旧 Attempt；
- Retry 沿用相同 `attempt_id`。

**依据：**

- [`Runner._stream_model()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/_runner.py#L1144)
- [`test_deltas_are_not_checkpointed_while_streaming`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_stream_cancel.py#L504)
- [`test_partial_stream_retry_new_attempt_replaces_output`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_stream_cancel.py#L751)

**原占位说明：**

> 流式 Attempt 替换；UI 文本区先按 attempt-A 显示 AB，失败后整块变灰并由
> attempt-B 的 ABC 替换；下方 Store 始终只显示 Attempt 状态，直到完整响应
> 才出现 Checkpoint。新的 `attempt_id` 代表替换而不是续写。

</details>

### A05：Lease 接管与旧 owner 迟到提交

**关联章节：** 第 11 章

**精修成片：** 11 秒。A 的旧请求跨越 Lease 过期继续存在，B 取得有效 Lease
后按冻结规则重放并提交；A 的迟到响应返回，但写入被 owner guard 拒绝。

<video controls playsinline muted preload="none" width="1920" height="1080" style="display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;" poster="../remotion_video/out/refined/a05-lease-takeover-guard-poster.png" aria-label="A05：Lease 过期不终止旧请求，B 接管提交后 A 的迟到写入被拒">
  <source src="../remotion_video/out/refined/a05-lease-takeover-guard.mp4" type="video/mp4">
  <a href="../remotion_video/out/refined/a05-lease-takeover-guard.mp4">播放 A05</a>
</video>

[播放 / 下载 MP4](../remotion_video/out/refined/a05-lease-takeover-guard.mp4) · [结论静帧](../remotion_video/out/refined/a05-lease-takeover-guard-poster.png) · [手机竖版图](../remotion_video/out/refined/a05-lease-takeover-guard-mobile.png)

<details>
<summary>初版长分镜（历史参考，已由 11 秒精修版取代）</summary>

**初版建议时长：** 30-40 秒

**表达目标：** 用时间轴解释 Lease 的真实能力边界：它可以让新 owner 在过期后
接管，并拒绝旧 owner 的权威写入，但不能停止旧进程或远端请求。

**固定布局：**

- 左列：Runner A；
- 中列：Run Store，顶部显示 Lease owner 与倒计时；
- 右列：Runner B；
- 最右侧或背景层：Provider；
- 底部：Run version 和 Attempt/Checkpoint 记录。

**分镜：**

1. **0-6 秒：A 持有 Lease。** Store 显示 `owner=A`、TTL 倒计时；A 创建
   Attempt-A 并 dispatch Provider。
2. **6-12 秒：A 停滞。** A 变灰或断开，但 Provider 请求仍继续；TTL 继续减少。
3. **12-18 秒：Lease 过期。** 倒计时到 0；只改变 Store 中的推进权条件，
   不向 A 或 Provider 发出取消信号。
4. **18-25 秒：B 接管。** B 用当前 version 获取 Lease，旧 Attempt 被保留为
   `UNCERTAIN`；规则允许时创建 Attempt-B。
5. **25-31 秒：B 提交。** Attempt-B 完成并形成 Checkpoint；Run 进入终态。
6. **31-36 秒：A 迟到。** Provider 的旧响应返回 A；A 尝试写入
   Attempt-A Checkpoint。
7. **36-40 秒：mutation 被拒。** 写入箭头撞上 owner/version guard；画面同时
   保留“外部响应已返回”和“权威 Store 未接受”两个事实。

**必须表现：**

- Lease acquisition/release 不递增 progress version；
- 状态转换使用当前 authoritative version；
- 旧请求可以在 Lease 过期后继续；
- Store 拒绝的是迟到权威写入，不是外部 Effect。

**禁止表现：**

- Lease 是跨进程中断信号；
- expiry 能证明旧调用已经结束；
- B 接管自动回滚 Attempt-A；
- 拒绝迟到 Checkpoint 能撤销 Provider 结果。

**依据：**

- [`ADR-0013`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/docs/adr/0013-run-lease-and-version-control.md)
- [`SQLiteRunStore.assert_lease()`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/src/m_agent/adapters/_sqlite_store.py#L539)
- [`test_expired_lease_takeover_and_stale_owner_rejected`](https://github.com/lingdu0007/M-Agent/blob/v0.5.1/tests/test_m_agent_lease.py#L286)

**原占位说明：**

> Lease 接管与旧 owner 迟到提交；A、Store、B 三列，Lease 倒计时归零后 B
> 接管并提交新 Checkpoint；随后 A 的迟到箭头撞上 version/owner guard 被拒。
> Lease 控制权威推进与提交，不会物理停止旧进程或远端请求。

</details>

## 5. 初版制作优先级（历史）

以下是初版制作顺序，现已完成 11 张配图与 5 段精修成片，不再作为待办清单：

1. **第一批：M01、M03、M07、M08、M10。** 这五张图定义 Durable Run 的
   核心证据模型和恢复决策。
2. **第二批：A01、A02、A03、A05。** 这些动画负责解释静态图最难表现的时间
   窗口和未知外部事实。
3. **第三批：M02、M04、M05、M06、M09、M11。** 用于建立边界、完整调用链、
   存储结构、UI 协议和测试方法。
4. **第四批：A04。** 在 Streaming 章节发布或制作交互式阅读版本时完成。

## 6. 验收清单

### 6.1 Mermaid 验收

- [x] 共 11 张 Mermaid，编号为 M01-M11，源码保留并附最终版配图；
- [ ] 每张图能脱离正文看懂主要 Actor、状态和方向；
- [ ] Mermaid 源码可以通过目标发布环境的 parser；
- [ ] 静态截图在桌面和窄屏上没有文字互相覆盖；
- [ ] 图中状态名、API 名和错误名与 `v0.5.1` 一致；
- [ ] Sequence Diagram 中 Checkpoint 与外部返回是不同事件；
- [ ] Flowchart 中 WAITING、FAILED 和 fail-closed 分支没有混用；
- [ ] 所有源码依据都指向 GitHub `v0.5.1`。

### 6.2 动画验收

- [x] 共 5 个精修动画，编号为 A01-A05，已嵌入正文与本规格；
- [ ] 每个动画都有固定布局、清晰时间轴和最终定格结论；
- [ ] 不通过移动 Actor 位置来表达状态变化；
- [ ] 不使用颜色作为唯一状态编码；
- [ ] Checkpoint 提交前后有明确、可见的视觉分界；
- [ ] `WAITING` 不使用失败态的红色主视觉；
- [ ] 外部 Effect 不使用“回滚消失”动画；
- [ ] Attempt replacement 不产生 `ABABC` 一类错误拼接；
- [ ] Lease 过期不表现为旧进程或请求被强制终止；
- [ ] 每个动画可以导出静态关键帧，用于无视频环境的文章版本。
