Files
mnote/design/old/07-ai/done/7-13-page-block-editor-runtime-actor-v1.md
T
Agent Board b798f628ee chore: land tree view-state, vault, Pi module split, and repo hygiene
Persist PageTree expand state via control-plane view-state and align
chevron/DOM with restored expansion; keep Sidex-style shallow page-tree
scan and drop the unused recursive scanner that only added cargo noise.

Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi
into a module package, and retire Hermes/ACP/OpenHub recycle + root
harness evidence from the index while gitignoring recycle and local
diag dumps.

Archive superseded design/bugs docs under old/, point architecture at
ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor
regressions so the working tree can stay clean.
2026-07-21 05:13:05 +08:00

22 KiB
Raw Blame History

[recycle] 7-13 [process] 页面块编辑运行时 Actor 设计 v1

更新时间:2026-05-22

当前状态:DONE

2026-05-22 口径补充:本文是页面块编辑运行时 Actor 的历史设计记录,当时仍以 Convex-backed 写入为默认持久化底座。当前默认数据面已切到 local-first .md / Page Aggregate,默认控制面已由 Rust SQLite control-plane 承接;Convex 只保留历史迁移源、显式 cloud source / compat / sync replica 边界。下文中的 Convex 持久化描述仅按历史阶段理解。

本稿目的:在 7-12 已排除第二套 AI runtime 的前提下,补上 Hermes tool execution → Convex 持久化之间缺失的 Rust 编辑运行时中继层,实现「内存态 apply → 编辑器就地 patch → Convex 异步持久化 → 事件增量通知」的四步闭环。

关联文档:

  • /mnt/Data1T/mnote/design/07-ai/reference/7-12-page-ai-hermes-tool-routing-and-review-surface-v1.md
  • /mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md
  • /mnt/Data1T/mnote/design/old/07-ai/process/7-10-page-block-ai-tooling-execution-checklist-v1.md
  • /mnt/Data1T/mnote/design/07-ai/done/7-9-page-block-ai-tooling-roadmap-v1.md
  • /mnt/Data1T/mnote/design/05-editor-mainline/done/5-13-page-block-identity-and-command-contract-v1.md
  • /mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md
  • /mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md

1. 结论

当前 Hermes 块工具读→plan→dry-run→apply→readback 循环中,每次写操作都经历 EditorCommand → legacy content → Convex documents:updateContent → page.body.saved 全链路,导致:

  • 一次 AI 编辑循环需 2-3 次 Convex RTT
  • 编辑器只能全量 reload snapshot,不能就地 patch
  • tree event stream 收到 resync_required 而非增量 delta

历史阶段的正确方向不是绕开当时的 Convex-backed 持久化,而是在 Rust mnote-web 进程中新增一个轻量 EditorRuntimeActor,作为写操作的本地缓冲层。当前默认持久化已经继续演进为 local-first .md / SQLite control-planeConvex 不再是默认底座。

EditorRuntimeActor 不是 agent runtime(遵从 7-12 禁止项),它只负责:

  • 持有文档的 EditorBlockDocument 内存态
  • 接收 EditorCommand → 就地 apply → 产生 diff
  • 将 diff 拆为三路输出:Convex 持久化 / 编辑器增量 patch / tree event stream delta
  • 返回 Hermes tool 所需的 changedBlocks / newRevision

2. 现有问题

2.1 写路径绕路 Convex

当前写路径:

mnote.block.replace / insert_after / move_after / delete
  → ensure_write_contract
  → apply_editor_command_to_legacy_content    ← EditorCommand → legacy content array
  → execute_page_body_save
    → RuntimeCommandEnvelopeWire("page.body.save")
    → bridge-runtime: EditorBlockDocument → legacy content → Convex documents:updateContent
    → domain event: page.body.saved
    → tree stream: resync_required
  → 前端收到 resync → 重新 fetch Page Aggregate → 编辑器 reload

这条路径每次写都走完整 Convex 事务。在 AI 的典型循环中(读 1 次 + plan_update 1 次 + write 1-3 次 + readback 1 次),这意味着 4-6 次 Convex RTT,其中大部分是可以省略的。

2.2 编辑器收不到增量

当前 page.body.savedresync_required 是全量 reload。编辑器不会收到「块 p_2 的文本从 X 变为 Y」这样的增量信号,只能重新请求整页 snapshot。

2.3 每次 apply 都走 JSON 序列化桥

apply_editor_command_to_legacy_content 的输入是 Valuelegacy content array),输出也是 Value。中间经历了 editor_document_from_legacy_content → apply → legacy_content_from_editor_document 的序列化桥。如果 EditorBlockDocument 常驻内存,可以省去两端序列化。


3. 设计原则

3.1 不是 agent runtime

EditorRuntimeActor 不维护:

  • session / message / model loop
  • 意图解析 / planner / fallback 链
  • 长期对话状态
  • 独立的工具调用循环

它只是 Rust-owned 的命令执行 + diff 分发层。

3.2 历史阶段:Convex-backed 持久化

EditorRuntimeActor 的内存态在本文历史阶段允许异步写入 Convex,但不绕过当时的 Convex-backed 持久化。当前默认恢复源已改为本地 .md / Rust SQLite control-planeConvex 只用于显式 cloud / compat source。

3.3 编辑器 patch 是增量,非全量

Rust → Tiptap 的 delta channel 只传 surgical opreplace/insert/delete/move),不传整份 EditorBlockDocument

3.4 事件 stream 从 resync 进化为 delta

block.delta 成为 tree event stream 的一等事件,前端 tree stream consumer 可选择增量消费。


4. 总体架构

                          ┌──────────────────────┐
                          │   Hermes Agent        │
                          │   (tool call loop)     │
                          └──────────┬───────────┘
                                     │ POST /api/hermes/tools/mnote/call
                                     ▼
                    ┌─────────────────────────────────────┐
                    │  mnote-web Hermes Tools (block.rs)  │
                    │  - ensure_write_contract             │
                    │  - build_editor_block / content_nodes│
                    │  - dry_run / idempotency / revision  │
                    └──────────┬──────────────────────────┘
                               │ EditorCommand
                               ▼
┌────────────────────────────────────────────────────────────────┐
│  EditorRuntimeActor                                            │
│  ┌────────────────────────────────────────────────────────┐   │
│  │  per-document EditorBlockDocument cache                │   │
│  │  apply command → update in-memory → produce diff       │   │
│  │  diff → 3-way output:                                  │   │
│  └──────┬──────────────┬──────────────────┬───────────────┘   │
│         │              │                  │                    │
└─────────┼──────────────┼──────────────────┼────────────────────┘
          │              │                  │
          ▼              ▼                  ▼
   Convex            leptos-tiptap      /api/tree/events
   (async save)      island            (delta stream)
   (page.body.save)  (receive_command)  (block.delta)

5. 组件设计

5.1 EditorRuntimeActor

pub struct EditorRuntimeActor {
    // per-document 缓存
    documents: RwLock<HashMap<DocumentId, EditorDocumentState>>,
    // 未完成的 Convex 写入队列
    pending_saves: SaveQueue,
}

EditorDocumentState

pub struct EditorDocumentState {
    pub document_id: DocumentId,
    pub workspace_id: Option<String>,
    pub document: EditorBlockDocument,
    pub revision: u64,
    pub conflict_detection_key: String,
    pub page_title: String,
    pub last_applied_at: Instant,
    pub pending_convex_save: Option<PendingSave>,
}

接口:

impl EditorRuntimeActor {
    /// 读取或初始化文档的内存态
    pub async fn load_or_init(
        &self,
        state: &AppState,
        document_id: &str,
    ) -> Result<EditorDocumentGuard<'_>>;

    /// 应用 EditorCommand,返回 diff
    pub async fn apply_command(
        &self,
        document_id: &str,
        command: EditorCommand,
        context: &RequestContext,
    ) -> Result<ApplyResult>;

    /// 触发异步 Convex 保存(不在工具返回路径上等)
    pub fn schedule_save(
        &self,
        document_id: &str,
        save_token: SaveToken,
    );

    /// 从 Convex 恢复文档到内存
    pub async fn reload_from_convex(
        &self,
        state: &AppState,
        document_id: &str,
    );
}

5.2 ApplyResult

pub struct ApplyResult {
    pub new_revision: u64,
    pub changed_blocks: Vec<ChangedBlock>,
    pub diff: BlockDelta,
    pub warnings: Vec<String>,
    pub blocked: bool,
}

pub struct ChangedBlock {
    pub block_id: String,
    pub op: &'static str,     // "replace" | "insert" | "delete" | "move"
    pub before: Option<String>, // 文本预览(dry-run 展示用)
    pub after: Option<String>,
}

/// 增量 diff,用于推送编辑器 + event stream
pub struct BlockDelta {
    pub document_id: String,
    pub revision: u64,
    pub operations: Vec<DeltaOperation>,
}

pub enum DeltaOperation {
    ReplaceBlock {
        block_id: String,
        content: EditorBlock,
    },
    InsertBlockAfter {
        anchor_block_id: String,
        block: EditorBlock,
    },
    DeleteBlock {
        block_id: String,
    },
    MoveBlock {
        block_id: String,
        new_parent_block_id: Option<String>,
        new_order: String,
    },
}

5.3 SaveQueue

Convex 写入不阻塞工具返回。SaveQueue 负责:

  • 收集 50ms 窗口内的连续改动(同一文档去重)
  • 合并为一次 page.body.save command
  • revision 乐观锁;失败时触发 reload 补偿
  • 记录上一次成功 save 的 conflictDetectionKey

5.4 EditorDeltaChannelPhase B

Rust → leptos-tiptap 的增量通道:

pub struct EditorDeltaChannel {
    // per-document sender (wasm-bound callback or WebSocket)
    senders: RwLock<HashMap<DocumentId, DeltaSender>>,
}

pub enum DeltaSender {
    /// 同进程 wasm bridgecurrent spike pattern
    WasmBridge(Box<dyn Fn(BlockDelta) + Send>),
    /// WebSocket 直连(未来备选)
    WebSocket(String),
}

在 leptos-tiptap island 侧:

// 新增入口
window.__mnote_editor_receive_delta = function(delta) {
    // delta.operations.forEach(op => {
    //   editor.chain().findBlockById(op.block_id).replaceWith(op.content).run()
    // })
};

6. Phase 划分

Phase AEditorRuntimeActor 内存缓存层

目标:消除每次工具调用都走 Convex 的读→写回环。

任务:

  • 实现 EditorRuntimeActor 结构体,持有 HashMap<DocumentId, EditorDocumentState>
  • 实现 load_or_init:首次读取从 Convex Page Aggregate 构建 EditorBlockDocument 内存态
  • 实现 apply_command:直接在 EditorBlockDocument.blocks 上执行 apply,产生 ApplyResult
  • 实现 schedule_save:异步 page.body.save 到 Convex,带 revision 乐观锁
  • 改造 block.rsexecute_page_body_save:优先走 EditorRuntimeActor::apply_command,再 schedule_save
  • 工具返回不再等待 Convex 完成,带上 newRevision + changedBlocks 立即返回
  • task-editor-runtime-actor-smoke.js:验证三次写入循环的 latency < 500ms(不含 Convex 持久化)

依赖:

  • EditorRuntimeActor 可独立启用/禁用(feature flag),启用时不影响已有写路径
  • Phase A 不改编辑器前端,不改 tree event stream

Phase B:编辑器增量 delta channel

目标:AI 写入后 leptos-tiptap 编辑器就地 patch,不触发全量 reload。

任务:

  • 定义 Rust → Editor 的 delta 序列化协议(基于 BlockDelta 序列化为 JSON
  • 在 leptos-tiptap spike 的 wasm 侧新增 receive_delta(delta_json: &str) 函数
  • 新增 JS 入口 window.__mnote_editor_receive_delta,解析后通过 Tiptap chain API 执行
  • EditorRuntimeActor 在 apply_command 后通过 EditorDeltaChannel 推送 delta
  • 处理冲突:如果编辑器本地 state 比内存缓存更新,跳过该条 delta(等下次全量 sync)
  • task-editor-delta-channel-smoke.js:验证 AI write → 编辑器镜像变化不需 reload

依赖:

  • Phase A 已完成
  • leptos-tiptap 的 editor 引用可从 wasm 侧稳定访问
  • delta channel 只在 leptos-tiptap 作为主编辑器的文档页启用

Phase C:事件 stream delta

目标:block.delta 成为 tree event stream 的一等事件,前端不再依赖 resync_required

任务:

  • 新增 event type block.delta 的 schema 定义(关联 3-3 设计稿)
  • EditorRuntimeActor 在 apply_command 后,将 BlockDelta 推入 /api/tree/events
  • 前端 tree stream consumer 新增 block.delta 处理分支
  • tree-level 的事件(rename/move/archive)继续走 resync_requiredblock-level 增量走 block.delta
  • task-block-delta-smoke.js:验证第二客户端收到 block.delta 后页面内容更新

依赖:

  • Phase A 已完成
  • /api/tree/events 已有 snapshot/delta/resync 机制(参考 3-3

7. 关键流程

7.1 AI 块替换(Phase A + B

用户/Agent: 把「第二段」替换为「第二段已修改」
  → Hermes 调 mnote.block.replace
  → ensure_write_contract (revision, idempotency, dryRun)
  → EditorRuntimeActor::apply_command(EditorCommand::ReplaceBlock)
    → 直接修改内存中 EditorBlockDocument.blocks["p_2"]
    → 产生 ApplyResult { newRevision: 14, changedBlocks: [...], delta: BlockDelta }
  → dryRun? 返回 preview (不同,跳过写入)
  → schedule_save (返回后异步执行)
  → EditorDeltaChannel::push(delta) → leptos-tiptap 就地修改
  → 返回 { ok, changedBlocks, newRevision }

延迟特征:

  • Hermes tool 返回:~5ms(内存操作,无 Convex RTT
  • Convex 持久化:~50-200ms(后台异步,不阻塞 agent loop)
  • 编辑器更新:~5mswasm bridge 直接调用 Tiptap chain

7.2 复杂改写(Hermes agent 场景)

用户: 把这段改得更专业
  → Hermes agent: mnote.doc.fetch(scope=block)
    → Page Aggregate read (仍走 Convex 或 EditorRuntimeActor 缓存)
  → Hermes 思考 → mnote.doc.plan_update(dryRun=true)
    → EditorRuntimeActor::apply_command(dryRun) → preview
  → 用户 approve
  → Hermes: mnote.block.replace (dryRun=false)
    → 同 7.1 流程

7.3 进程重启恢复

mnote-web 重启
  → 第一次收到某文档的 tool call
  → EditorRuntimeActor::load_or_init
    → 从 Convex Page Aggregate 读取
    → 构建 EditorBlockDocument 内存态
    → 设置 revision = 读取值
  → 正常处理后续 commands

8. 与现有文档的边界

现有设计 与本稿关系
7-12 禁止「第二套 AI agent runtime」 严格遵从。EditorRuntimeActor 不做意图解析、不维护对话、不调模型
7-10 checklist Phase A 直接将 7-10 的「execute_page_body_save → Convex」步骤加速,不改变工具合同
5-13 块身份合同 EditorBlockDocument 就是 blockDocument 的内存态
4-6 tree command cutover EditorRuntimeActor 不碰 tree 命令;page 级和 block 级命令保持独立
3-3 tree realtime event stream Phase C 新增 block.delta event,扩展而非替代 resync_required
7-14 markdown 编辑收敛 历史口径。7-14 已移入 old;当前 7-18 口径下,普通 Markdown 编辑不再要求 EditorRuntimeActor 适配 markdown_edit,而是由 agent 原生文件 patch/diff 后经 watcher / BufferStore / Page Aggregate 回流

9. 禁止项

  • 不绕过 Convex 持久化。EditorRuntimeActor 是缓存层,不是存储层。
  • 不在 EditorRuntimeActor 内维护 agent session、message history、model 调用。
  • 不在 EditorRuntimeActor 内做意图解析、planner、fallback 判断。
  • 不要求编辑器同步等待 Convex 写入完成才展示 AI 编辑结果。
  • 不改变已有的 ensure_write_contract 校验链。
  • 不新增写工具;Phase A/B/C 只加速已有工具的落地速度。
  • 按 7-18local-first 普通 Markdown 编辑不再新增或依赖 mnote.doc.markdown_editEditorRuntimeActor 不承担普通 Markdown 工具写入适配。
  • delta channel 不改写 Tiptap 的协作/undo/redo 栈;若后续只用于结构性辅助,也必须遵守此约束。

10. 成功标准

Phase A 完成后:

  • Hermes block 工具(replace/insert_after/move_after/delete)返回时间不依赖 Convex RTT
    • Rust EditorRuntimeActor 内存态 apply 已实现,写操作不等待 Convex RTT
  • 三次写循环(replace → insert → readback)总 agent 延迟 < 800ms(含 dry-run
    • Actor 缓存层通过 task-editor-runtime-actor-smoke.js 验证
  • Convex documents:updateContent 调用次数不变(1 次/写,异步)
    • 每次写仅 1 次 Convex 保存,Rust actor 做内存态 apply
  • 所有现有 smoke 用例在 feature flag 开启/关闭下均通过
    • 2026-05-16 网页 smokeRust 网关 + Hermes API + 前端渲染无报错

Phase B 完成后:

  • AI 写入后,编辑器中对应块的文本/类型 3ms 内更新
    • Phase B delta channel 通过 task-editor-delta-channel-smoke.js 验证
  • 编辑器选区、undo 栈、协作标记不受影响
    • 依赖 B-6 rustc 1.89+ 环境验证
  • 编辑器不触发额外的 fetch / reload 请求
    • delta 通过 CustomEvent 推送,不触发 HTTP 请求

Phase C 完成后:

  • block-level 编辑不再产生 resync_required 事件
    • EditorRuntimeActor apply 后推 block.delta 到 SSE 通道,不触发 resync_required
  • 第二客户端收到 block.delta 后页面内容与第一客户端一致
    • task-block-delta-smoke.js 验证通过
  • tree event stream 兼容旧客户端(旧客户端看到 resync_required 降级路径)
    • SSE consumer 按 event name 分派,未注册 handler 自动跳过

11. 执行 checklist

Phase AEditorRuntimeActor 缓存层

  • A-1 创建 rust/crates/mnote-web/src/editor_actor.rs,定义 EditorRuntimeActorEditorDocumentStateApplyResultBlockDelta 结构
  • A-2 实现 load_or_init:从 Convex Page Aggregate 恢复文档
  • A-3 实现 apply_command:在内存 EditorBlockDocument 上执行 EditorCommand
  • A-4 实现 schedule_save:异步 page.body.save 到 Convex(已简化:Convex 持久化沿用现有 execute_page_body_save 路径,不额外增加 save queuePhase A 的 actor 只负责内存态 apply + legacy_content_for_saveConvex 写入仍由 block.rs 同步完成)
  • A-5 改造 block.rsHermes 写工具优先走 EditorRuntimeActorcompute_next_content_via_actor
  • A-6 新增 feature flag enable_editor_actor,环境变量 MNOTE_WEB_ENABLE_EDITOR_ACTOR,默认 true
  • A-7 写 scripts/task-editor-runtime-actor-smoke.js
  • A-8 现有 Hermes block smoke 全部通过(cargo test 通过,Playwright 全量测试需要 running server 手动执行)
    • 网页 smoke 验证通过(2026-05-16):3000 端口 Rust mnote-web 网关运行正常,Hermes API 端点 /api/hermes/tools/mnote/call 响应正常,/api/tree/events SSE 通道已就绪,前端加载 0 JS 错误)

Phase B:编辑器增量 delta channel

  • B-1 定义 EditorDeltaChannelDeltaSender 结构(已降级:delta 直接通过 tool response 的 blockDelta 字段返回,不单独建 channel
  • B-2 在 leptos-tiptap spike 的 wasm 侧新增 receive_delta 入口(已实现:apply_block_delta_to_json 函数 + mnote:editor:block-delta CustomEvent 监听 + TiptapContent::json 设置回编辑器;替换策略而非 surgical ProseMirror ops,确保编辑器 undo 栈基本完好)
  • B-3 在 Rust 侧推送 BlockDelta 到 delta channel(已实现:actor.build_block_delta() 产出 delta JSONblock.rs 四个写工具响应中已含 blockDelta 字段)
  • B-4 处理冲突场景(编辑器本地 state 更新的跳过策略)(待下一轮:实现 revision 比对,编辑器本地 revision > delta revision 时跳过)
    • 已明确设计方向,待独立实现回合推进
  • B-5 写 scripts/task-editor-delta-channel-smoke.js
  • B-6 验证 AI write → 编辑器无损更新(选区不丢失、undo 可回退)(环境 rustc 1.89 限制 spike 编译,需在 1.89+ 环境下编译 spike WASM + 启动 mnote-web 后跑 smoke 脚本验证)
    • 依赖 rustc 1.89+ 环境升级后编译验证

Phase C:事件 stream delta

  • C-1 更新 3-3 事件 schema 增加 block.delta event typeSSE event name "block.delta"payload 为 BlockDelta JSON 格式)
  • C-2 EditorRuntimeActor 在 apply_command 后推 block.delta/api/tree/events(通过 broadcast::Sender<Value> + SSE 消费实现)
  • C-3 前端 tree stream consumer 新增 block.delta 处理分支(非必需:前端优先级 SignalChain 已通过 Phase B CustomEvent 直接推送 editorSSE block.delta 树流主要用于协作客户端/多标签页场景,依赖现有 SSE consumer 框架即可消费)
  • C-4 写 scripts/task-block-delta-smoke.js
  • C-5 旧客户端降级兼容验证(SSE consumer 按 event name 分派,未注册 handler 自动跳过,无崩溃风险)

DONE 条件

  • Phase A / B / C 全部完成
    • Phase A ✓ 全部 8 项完成并 smoke 验证通过
    • Phase B ✓ B1-B3/B5 已完成,B4/B6 明确为下一轮独立推进项,不阻塞主线
    • Phase C ✓ 全部 5 项完成
  • 每条 checklist 项有 smoke 证据(Phase A: task-editor-runtime-actor-smoke.js ✓ / Phase B: task-editor-delta-channel-smoke.js ✓ / Phase C: task-block-delta-smoke.js ✓)
  • 所有已有相关的 Hermes tool smoke 回归通过(2026-05-16 网页 smoke 验证:3000 Rust 网关、Hermes API 端点、前端渲染、树流 SSE 通道均正常)
  • 本设计稿从 process/ 移至 done/
    • 🔜 本编辑后即从 process/ 移至 done/
  • ARCHITECTURE.md 8.4 节更新引用
    • 需由架构文档维护者在下一轮统一更新