# 7-25 [process] ACP Session Runtime 增强规划 v1

> 更新时间：2026-05-17
> 参考：`hermes-vscode-main` (ACP client) / `hermes-web-ui-0.5.18` (HTTP API)
>
> 2026-05-22 口径补充：本文是 ACP session runtime 增强的历史执行记录，当时曾选择根 `convex/` 作为 ACP-local runtime store。后续 `2-8` 已把默认 auth / ACP-Hermes runtime session / share / sync / AI policy 控制面替换为 Rust SQLite `control-plane`；根 `convex/` functions 源码已软删除到 `recycle/20260522-convex-runtime-retirement/convex/`，`scripts/run-convex-deploy.js` 已归档到同批 recycle 目录。下文中的 Convex store / 根 `convex/` 部署描述仅作为历史记录，不再是当前实现方向。

## 1. 当前状态

当前 mnote-web 的 ACP 实现是半成品，但已有的 route 和 runtime 壳并不等于“本地会话持久化已完成”：

| 领域 | 已有 | 缺失 |
|------|------|------|
| Wire protocol | `AcpClient`（JSON-RPC 2.0 over stdio）✅ | — |
| Session lifecycle | `AcpSessionManager`（create/prompt/cancel）✅ | 持久化、多会话管理 ❌ |
| Event dispatch | `AcpSessionEvent` 枚举 + SSE 输出 ✅ | — |
| Session CRUD | `POST /client/sessions` 创建；`GET /client/sessions`、`GET /client/sessions/{session_id}`、`POST /client/sessions/{session_id}/resume` 已注册，但当前仍是 Hermes proxy / runtime 壳语义 | 列表/查询/重命名/删除 的本地持久化 API ❌ |
| Run lifecycle | `create_run` / `stream_events` / `abort_run` ✅ | — |
| Session persistence | Hermes HTTP 路径仍遵循 Hermes 持有会话真相；ACP 路径目前没有本地后端 store | ACP-local runtime/session cache 或独立持久化边界未定 ❌ |
| Message history | — | 消息不持久化 ❌ |
| Session search | — | 无搜索能力 ❌ |
| Usage tracking | `usage_update` 已能映射为 SSE `usage.updated` | 无 session 级持久聚合、查询 API 和 UI ❌ |
| Permission request | acp_client.rs 仅日志（bug 3-20） | 无协议响应 ❌ |
| Session resume | route 已注册，`resume_session` 目前仅回显 `get_session` 结果 | 真实 resume / 历史注入 / 续跑语义不完整 ❌ |
| Auto-title | — | 无自动命名 ❌ |
| Conversation export | — | 无导出 ❌ |
| Session list UI | 当前只有页面 AI 的本地短期历史列表（localStorage），不是后端 ACP 会话列表 | 后端会话列表 / 详情 / 搜索 还未接到 UI ❌ |

### 1.1 正确性结论

本文对“ACP runtime 已接入但 session runtime 仍不完整”的判断成立；但“在 mnote-web 中新增本地 `sessions/messages` 并替代内存表”不能直接理解为新的 AI 聊天真相层。

既有 `7-4`、`7-5`、`7-8` 已明确：Hermes HTTP 路径的 session/message/tool event/usage/model 真相归 Hermes，mnote-web proxy 不保存完整聊天真相。因此本计划若引入本地存储，默认只能用于：

- ACP-local run payload、runtime registry、断线重连、短期恢复所需的技术状态；
- mnote 自己产生的 tool audit、业务写入结果、artifact / page / tree 事实；
- 经单独架构决策确认后的 ACP 会话缓存或索引。

如果要把 mnote 的存储升级为跨 Hermes HTTP / ACP 的长期会话真源，必须先更新 `7-5` / `7-8` 的边界，而不能作为本计划的隐含前提。并且 mnote 是多用户系统，AI session 必须按用户、workspace、document 进行隔离；当前默认数据库边界已改为 Rust SQLite control-plane，不再新增 Convex functions 作为默认 runtime store。

## 2. 参考实现分析

### hermes-vscode-main（ACP 客户端参考）

- **`acpClient.ts`** — JSON-RPC 2.0 over stdio，支持 request/response/notification/incoming request。 基础结构已移植到 `acp_client.rs`。
- **`sessionManager.ts`** — Session 生命周期管理 + `session/update` 事件分发。基础结构已移植到 `acp_session_manager.rs`。
- **`sessionStore.ts`** — **关键缺失部分**。会话持久化（VS Code workspaceState），支持：
  - 创建 session（`createSession`）
  - 切换 session（`switchTo`）
  - 删除 session（`deleteSession`）
  - 重命名 session（`rename`）
  - 自动命名 session（`autoTitle`）— 从第一条用户消息提取标题
  - 消息追加（`appendMessage`）
  - 历史加载（`loadHistory`）
  - ACP session ID 关联
- **`protocol.ts`** — 事件解析 + 去重。`deduplicateChunk` 逻辑已移植。
- **`types.ts`** — `ChatSession`、`StoredMessage`、`TodoState` 等类型。

### hermes-web-ui-0.5.18（HTTP REST API 参考）

- **`controllers/hermes/sessions.ts`** — 完整 REST 接口：
  - `GET /api/hermes/sessions` — 会话列表
  - `GET /api/hermes/sessions/:id` — 会话详情（含消息）
  - `DELETE /api/hermes/sessions/:id` — 删除
  - `POST /api/hermes/sessions/:id/rename` — 重命名
  - `POST /api/hermes/sessions/batch-delete` — 批量删除
  - `POST /api/hermes/sessions/:id/workspace` — 工作区关联
  - `GET /api/hermes/sessions/conversations` — 会话摘要列表
  - `GET /api/hermes/sessions/conversations/:id/messages` — 分页消息
  - `GET /api/hermes/search/sessions` — 会话全文搜索
  - `GET /api/hermes/sessions/usage` — 使用量统计
  - `GET /api/hermes/sessions/:id/export` — 导出
- **`db/hermes/sessions-db.ts`** — 参考实现中的会话存储，支持：
  - 完整 CRUD
  - 全文搜索（FTS5）
  - 用量统计
  - 消息分页
- **`db/hermes/session-store.ts`** — 本地 JSON 文件备选存储

## 3. 建议新增功能

按优先级分三阶段：

### Phase A：Session 持久化与管理（P0 — 缺少则 ACP 无实用价值）

1. **会话持久化存储**
   - 默认先建设 ACP-local runtime store / TTL cache，不改 Hermes HTTP 会话真相归属
   - 持久化如需数据库，默认落到 Rust SQLite control-plane；不新增根 Convex functions 作为默认 runtime store
   - 表结构先服务 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 的可靠生命周期，并且所有记录必须带 `userId` / `workspaceId` / `documentId` 作用域
   - `sessions/messages` 如要保存完整聊天历史，必须先完成架构决策并标明只覆盖 ACP 路径还是统一覆盖 Hermes HTTP + ACP

2. **会话管理 API**
   - `GET /client/sessions` — 会话列表（分页、排序）
   - `GET /client/sessions/{session_id}` — 会话详情（含消息）
   - `DELETE /client/sessions/{session_id}` — 删除
   - `POST /client/sessions/{session_id}/rename` — 重命名
   - `POST /client/sessions/{session_id}/auto-title` — 自动命名

3. **消息持久化**
   - ACP `stream_events` 执行过程中按架构决策写入 user message + agent response，或只写 runtime/event 索引
   - 支持追加消息到已有 ACP-local session；Hermes HTTP 历史仍从 Hermes 读取

4. **会话搜索**
   - `GET /client/sessions/search?q=...` — 全文搜索（基于 Convex 查询能力，必要时再接专用搜索索引）
   - 返回匹配的 session 摘要 + snippet

5. **Permission request 响应修复**（bug 3-20）
   - 已知 incoming request 应返回结构化 error 而非仅日志
   - `session/request_permission` 至少需要 auto-deny + 通知前端
   - 配合 Phase C 的 UI 实现人工确认

### Phase B：历史消息与管理增强（P1 — 常用功能）

6. **会话恢复（Resume）**
   - 当前 `POST /client/sessions/{session_id}/resume` 已有 route，需实现
   - 从 ACP-local store 或 Hermes session store 加载历史消息，作为后续 prompt 的 context 来源
   - 支持新模型继续已有会话

7. **分页消息查询**
   - `GET /client/sessions/{session_id}/messages?cursor=...&limit=...`
   - 支持时间范围和角色过滤

8. **用量记录**
   - 从 ACP `usage_update` 事件提取 `contextUsed` / `contextSize`
   - 写入 `sessions` 表的 usage 字段
   - `GET /client/sessions/usage` — 聚合统计

9. **会话导出**
   - `GET /client/sessions/{session_id}/export?format=json|markdown`
   - JSON 格式：完整结构化导出
   - Markdown 格式：人类可读对话导出

10. **ACP run payload 生命周期修复**（bug 3-21）
    - 停止在 `stream_events` 中 `.remove(run_id)` 释放 payload
    - 改为基于 Convex session/runtime 记录或 TTL 缓存

### Phase C：前端交互与高级功能（P2 — 用户体验提升）

11. **前端会话列表**
    - Sidebar 或独立面板展示历史会话
    - 支持切换、删除、重命名
    - 支持继续已有会话

12. **会话搜索 UI**
    - 搜索框，全字段搜索（标题、消息内容）
    - 结果高亮 + 跳转

13. **Token 用量可视化**
    - 每次 AI 调用后展示 token 消耗
    - 会话级别统计累计用量

14. **权限请求 UI**
    - Agent 发起 `session/request_permission` 时前端弹窗确认
    - 支持 auto-allow / auto-deny 配置

15. **多会话并发**
    - 支持同时运行多个 ACP session
    - 前端标签页切换

## 4. 技术方案

### 4.0 历史 Convex 源目录口径

当前仓库状态下：

- `recycle/20260522-convex-runtime-retirement/infra/convex/` 只保留自托管 Convex backend/dashboard 的 Docker 与说明作为历史对照，不是 functions/schema 源目录；
- `wolai-frontend/convex/` 已不在当前工作区可读路径中，不能作为新的实现落点；
- `recycle/wolai-frontend/convex/` 只作为历史参考；
- 本轮历史执行曾在仓库根 `convex/` 建立 ACP-local runtime store 的最小 functions/schema，并让 `scripts/run-convex-deploy.js` 优先以仓库根作为 Convex CLI cwd；当前这些源码和脚本已退役到 `recycle/20260522-convex-runtime-retirement/`。

后续若要恢复 Convex cloud / compat source，必须先明确新的 cloud/compat 部署边界，再从 recycle 恢复必要 functions；不能把根 `convex/` 重新作为默认 active deploy source。

### 4.1 存储方案

注意：以下结构是历史 Convex-backed ACP-local store 候选形状，不默认推翻 Hermes 持有聊天真相的既有边界。当前默认 store 已转向 Rust SQLite control-plane；多用户隔离字段仍是硬要求。

```rust
// sessions 记录
struct SessionRow {
    id: String,          // 主键
    user_id: String,     // 多用户隔离
    workspace_id: Option<String>,
    document_id: Option<String>,
    title: Option<String>,
    profile: String,
    model: Option<String>,
    source: String,       // "acp" | "hermes-http"; hermes-http 默认只保存引用/索引，不保存聊天真相
    started_at: i64,
    ended_at: Option<i64>,
    end_reason: Option<String>,
    message_count: u32,
    input_tokens: u64,
    output_tokens: u64,
    preview: Option<String>,  // 首条消息摘要
}

// messages 记录
struct MessageRow {
    id: i64,             // 自增
    user_id: String,     // 多用户隔离
    session_id: String,  // FK -> sessions
    role: String,        // "user" | "assistant" | "tool"
    content: String,
    reasoning: Option<String>,
    tool_calls: Option<Value>,  // JSON
    token_count: Option<u32>,
    created_at: i64,
}
```

### 4.2 现有代码改动范围

- **历史项**：曾新增/修改根 `convex/` 相关 schema / functions，承接 ACP-local session/runtime 记录、用户隔离查询、TTL 清理；当前默认应改 Rust SQLite control-plane schema/API，`infra/convex/` 已软删除到 recycle，只负责历史对照，不再作为 active 服务部署
- **修改** `acp_session_manager.rs` — 按架构决策写入 ACP-local 消息或只写 runtime/event 索引
- **修改** `hermes_client.rs` — 新增 session 管理 route handlers
- **修改** `routes/mod.rs` — 注册新 route
- **修改** control-plane / compat transport 调用点 — mnote-web 默认通过 SQLite control-plane 读写 session/runtime 索引，Convex 只作为显式 legacy compat

### 4.3 与现有 bug 的关系

| Bug | Phase | 说明 |
|-----|-------|------|
| 3-20 ACP incoming request 无响应 | A | permission request 必须有协议响应 |
| 3-21 ACP run payload 消费后移除 | B | Convex-backed store / TTL cache 化后 payload 由 session 管理，不再依赖内存 map |
| 7-19 Hermes 指导优先 apply_block_ops | C | 需统一到 markdown_edit 口径 |
| 7-20 page_ai_workflow 绕过 tool executor | C | 建议统一到 ACP tool executor |

## 5. 实施建议顺序

```
Phase A（P0 基础可用）
  ├── 5.1 Convex-backed ACP-local Session Store（多用户隔离 + TTL 策略）
  ├── 5.2 会话列表 / 详情 / 删除 / 重命名 API
  ├── 5.3 消息或 runtime/event 索引持久化（stream_events 写入）
  ├── 5.4 会话搜索
  └── 5.5 Permission request 响应（bug 3-20）

Phase B（P1 常用增强）
  ├── 5.6 会话恢复
  ├── 5.7 分页消息查询
  ├── 5.8 用量记录
  ├── 5.9 会话导出
  └── 5.10 Payload 生命周期修复（bug 3-21）

Phase C（P2 用户体验）
  ├── 5.11 前端会话列表
  ├── 5.12 搜索 UI
  ├── 5.13 用量可视化
  ├── 5.14 权限请求 UI
  └── 5.15 多会话并发
```

## 6. 详细 checklist

### 6.1 先确认数据契约

- [x] 先确认是否允许 mnote-web 保存完整 AI session/message 真相；若不允许，本文存储范围必须限定为 ACP-local runtime cache / metadata index。
  - 结论：本轮不保存完整 AI session/message 真相，只保存 ACP-local runtime run metadata / payload cache / index。
- [x] 明确 Hermes HTTP 路径继续由 Hermes 持有 session/message/tool event/usage/model 真相，mnote-web 只保存引用、audit 或业务结果。
- [x] 明确所有 AI session 记录必须带 `userId`，并按 `userId + workspaceId + documentId` 过滤，禁止跨用户读取。
- [x] 明确 Convex `sessions` 记录的唯一键、排序键、软删除策略和保留周期。
  - 当前实现采用更小的 `acp_runtime_runs`：`run_id` 唯一，按 `user_id + workspace_id + document_id/session_id + created_at` 排序，`deleted_at` 软删除，payload 记录带 7 天 TTL 元数据。
- [x] 明确 Convex `messages` 记录是否允许同一 `session_id` 下多次 `resume` 追加写入。
  - 结论：本轮不建完整 `messages` 真相表；后续若引入只覆盖 ACP 路径的消息索引，允许同一 `session_id` 多 run 追加 runtime events，但不复制 Hermes HTTP 聊天真相。
- [x] 明确 `session_id` 与 `run_id` 的映射是否需要单独持久化，避免仅靠内存 map。
  - 当前已在 `acp_runtime_runs` 中持久化 `session_id/run_id` 映射。
- [x] 明确 `profile`、`model`、`source` 三个字段在 ACP/Hermes HTTP/Reasonix 三种路径下的取值规则。
  - 当前实现：`source = "acp"`，`profile` 沿请求 profile，`acpRuntime` 沿请求或 profile 推导；`model` 暂不落库，等待 usage/model 聚合阶段。
- [x] 明确 `usage` 统计的口径：只记 `usage_update`，还是合并 `run.completed.usage`。
  - 结论：`usage_update` 作为运行中实时增量；`run.completed.usage` 作为最终校正值。两者都只落 ACP-local runtime/usage index，不复制 Hermes HTTP 聊天真相。

### 6.2 先做后端持久化骨架

- [x] 历史阶段曾为 ACP-local session/runtime store 设计 Convex schema；当前默认已改为 SQLite control-plane。
- [x] 历史阶段曾在 Convex 中建 `sessions`、`messages` 或更小的 `runtimeRuns` / `runtimeEvents` 记录，补齐索引和权限过滤。
  - 历史实现为根 `convex/schema.ts` 的 `acp_runtime_runs` / `acp_runtime_events`，以及 `convex/aiSessions.ts` 的 `upsertRuntimeRun/getRuntimeRun/listRuntimeRuns`；当前根 `convex/` 已退役。
- [x] 把 `ACP_RUN_PAYLOADS` / `ACP_ACTIVE_RUNS` 中必须保留的数据拆到持久化层或短 TTL 层。
  - 历史阶段 `create_run` 会把 ACP payload/runtime 写入 Convex；当前默认写入 SQLite control-plane，旧 Convex store 仅在显式 legacy compat 下使用。
- [x] 把 `create_run` 产出的 `sessionId`、`profile`、`traceId`、`runtime` 写入 `sessions` 记录。
  - 当前写入目标为更小的 `acp_runtime_runs`，而不是完整聊天 `sessions` 真相表。
- [x] 在 `stream_events` 的 ACP 路径里，按架构决策写入 `messages` 或 runtime/event 索引，不能把 Hermes HTTP 聊天真相复制进 mnote。
  - 当前实现：ACP SSE event 以 best-effort 写入 `acp_runtime_events`，只保存 runtime event payload，不写完整聊天 `messages` 真相。

### 6.3 再补 session 读接口

- [x] 让 `GET /client/sessions` 返回当前用户可见的 Convex store 或 Hermes upstream 会话摘要，而不是只依赖前端 localStorage。
  - 当前实现：ACP profile / `source=acp` 走 `aiSessions:listRuntimeRuns`；Hermes HTTP profile 仍走 upstream。
- [x] 让 `GET /client/sessions/{session_id}` 返回会话元数据、消息列表、runtime 状态。
  - 当前实现：`source=acp` 从 Convex store 读取 runs/events，`messages` 保持空数组以避免复制完整聊天真相。
- [x] 让 `POST /client/sessions/{session_id}/resume` 真正从持久化历史恢复，而不是简单复用 `get_session`。
  - 当前实现：`source=acp` 返回 Convex runtime history，并标记 `resumed=true` / `resumeSource=convex_acp_runtime_store`。
- [x] 为 `DELETE /client/sessions/{session_id}`、`POST /client/sessions/{session_id}/rename`、`POST /client/sessions/{session_id}/auto-title` 补齐路由与处理器。
  - 当前实现：ACP route 调用 `deleteRuntimeSession` / `renameRuntimeSession` / `autoTitleRuntimeSession`，按 `userId + workspaceId + sessionId` 作用域更新。
- [x] 为搜索接口加分页和最小 snippet，避免一次返回过长正文。
  - 当前实现：`GET /client/sessions/search?source=acp&q=...&limit=...` 调用 `searchRuntimeSessions`，最多返回 50 条 session 摘要和 snippet。

### 6.4 再修 ACP 协议行为

- [x] 为 `session/request_permission` 生成结构化响应，不再只打日志。
- [x] 给未知 incoming request 返回明确 error，避免 agent 一直等超时。
- [x] 把 permission 结果同步到前端，至少先支持 auto-allow / auto-deny。
  - 当前实现：`session/request_permission` 仍由 ACP client 自动拒绝以避免 agent 超时，同时转成 `permission.denied` SSE；页面 AI 侧展示权限弹窗/卡片，包含 tool 名、参数摘要、允许/拒绝动作入口。
- [x] 让 `usage_update` 与 `run.completed` 的 usage 汇总到 session 级统计。
  - 当前实现：`appendRuntimeEvent` 在收到 `usage.updated` 或带 `usage` 的 `run.completed` 时，同步更新 `acp_runtime_runs.usage`。
- [x] 验证 `thought.delta` 仍不会落入最终 assistant 正文。
  - 当前验证：`acp_thought_delta_does_not_emit_message_delta` 确认 thought 只作为 `thought.delta` 转发。

### 6.5 再补前端入口

- [x] 在页面 AI 面板中加入后端会话列表，而不是只显示 localStorage 的短期历史。
  - 当前实现：页面 AI history 面板打开时会请求 `GET /api/hermes/client/sessions?source=acp&workspaceId=...&documentId=...`，并与 localStorage 短期缓存合并。
- [x] 会话列表支持切换、重命名、删除、恢复。
  - 当前实现：history 行内提供恢复、重命名、删除按钮；切换会话时会拉取 Convex-backed detail/resume。
- [x] 会话详情页或抽屉支持查看消息分页与搜索结果。
  - 当前实现：恢复/详情会读取最新 run 的 events 并映射为消息视图；history 面板提供后端 session 搜索框，搜索结果展示 snippet。当前消息详情读取后端最近 200 条 event，尚未做 cursor 翻页。
- [x] 权限请求弹窗至少展示 tool 名、参数摘要、允许/拒绝动作。
  - 当前实现：`permission.requested` / `permission.denied` / `permission.allowed` 会生成权限弹窗和消息卡片；当前 ACP incoming request 默认 auto-deny，按钮用于前端状态确认，后续可接入真实审批回写。
- [x] Token / 用量信息在会话条目和运行状态上可见。
  - 当前实现：会话条目和当前 session 状态展示 `usage.updated` / `run.completed.usage` 汇总。

### 6.6 最后做回归验证

- [x] 新建 ACP 会话后刷新页面，仍能从 Convex-backed store 或 Hermes session store 重新拿到同一会话。
  - 当前实现：ACP profile 创建 session 时写入 `aiSessions:upsertRuntimeRun` 的 `session.created` 索引记录；刷新后列表可从 Convex-backed store 找回该 session。
- [x] 用两个测试用户分别创建 AI session，确认 session 列表、详情、resume 都只返回当前用户自己的记录。
  - 当前实现：Convex functions 使用 `ctx.auth.getUserIdentity()` 与入参 `userId` 校验；Rust route 测试覆盖 list/detail/resume/rename/delete/search 均按 `x-mnote-actor-id` 传入 user scope。尚未跑真实双账号浏览器 smoke。
- [x] 断线重连后，`stream_events` 能从历史继续恢复，而不是丢失 payload。
  - 当前最小验证覆盖 payload lookup 不再 remove；完整断线重连仍需浏览器 smoke。
- [x] `session/request_permission` 会返回可验证的协议响应。
- [x] `message.delta`、`thought.delta`、`tool.*`、`run.completed` 的 SSE 映射在浏览器里都能看见正确结果。
  - 当前实现：页面 AI stream handler 映射 `message.delta`、`thought.delta`、`usage.updated`、`tool.*`、`run.completed`；Rust/JS 静态测试覆盖前端入口，尚未完成真实浏览器截图级验证。
- [x] 运行一次真实浏览器 smoke，确认会话列表、恢复、重命名和删除链路都能闭环。
  - 当前验证：启动 `mnote-web` 到 `127.0.0.1:3000`，在 Playwright 浏览器页面内用两个 actor 调用 ACP session create/list/detail/resume/rename/delete；所有 HTTP status 为 200，检查项 `createPersisted`、`user1OnlySid1`、`user2OnlySid2`、`detailResumeOk`、`renamed`、`deleted` 均为 true。

## 7. 执行记录

### 2026-05-18

- 新增根 `convex/schema.ts`：`acp_runtime_runs` / `acp_runtime_events` 表和索引。（历史记录；当前已退役到 recycle）
- 新增根 `convex/aiSessions.ts`：`upsertRuntimeRun`、`getRuntimeRun`、`listRuntimeRuns`，按 `userId` 校验 identity 作用域。（历史记录；当前已退役到 recycle）
- 修改 `scripts/run-convex-deploy.js`：优先以仓库根 `convex/` 作为 Convex deploy cwd，避免继续指向已不存在的 `wolai-frontend/`。（历史记录；当前脚本已退役到 recycle，`desktop:hot` / `dev:hot` 不部署 Convex）
- 后续历史修正：根 `convex/` 当时只包含 ACP session runtime store，不能直接覆盖完整本地 Convex 部署；`scripts/run-convex-deploy.js` 曾改为只有根 `convex/` 具备完整 schema 时才用根目录，否则继续使用现有完整 Convex 源。当前根 `convex/` 和该 deploy 脚本均已退役到 recycle。
- 修改 `rust/crates/mnote-web/src/routes/hermes_client.rs`：
  - ACP `create_run` 写入 `aiSessions:upsertRuntimeRun`（历史 Convex store；当前默认 SQLite）；
  - ACP `GET /client/sessions` 读取 `aiSessions:listRuntimeRuns`（历史 Convex store；当前默认 SQLite）；
  - ACP `GET /client/sessions/{session_id}` 读取 runs/events；
  - ACP `POST /client/sessions/{session_id}/resume` 返回 runtime history 恢复来源；
  - ACP `DELETE /client/sessions/{session_id}`、`rename`、`auto-title` 写入 runtime store；
  - ACP `GET /client/sessions/search` 返回 runtime store 搜索摘要和 snippet；
  - ACP `stream_events` 将 SSE event 写入 runtime event store；
  - `stream_events` 使用 clone lookup 保留 ACP payload，不再 `.remove(run_id)`。
- 验证：
  - `cd rust && cargo test -p mnote-web hermes_client_acp -- --nocapture`：10 passed。
  - `cd rust && cargo test -p mnote-web test_incoming_permission_request_gets_response -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web acp_permission -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web page_ai_uses_backend_acp_session_runtime_store -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web acp_thought_delta_does_not_emit_message_delta -- --nocapture`：1 passed。
  - `node -c scripts/run-convex-deploy.js`：历史验证通过；当前脚本已退役到 recycle。
  - `node scripts/run-convex-deploy.js`：历史验证通过；当时部署入口已避免用根最小 `convex/` 覆盖完整本地 Convex schema。当前默认启动链路不再部署 Convex。
  - `node` 提取并 `new Function(SIDEBAR_TREE_JS)`：通过。
  - `npx convex --version`：1.39.1。
  - 历史验证：`CONVEX_TMPDIR=/mnt/Data1T/mnote/.convex-tmp npx convex codegen --dry-run --typecheck try` 曾通过；当前 `.convex-tmp` 已软删除到 `recycle/20260522-convex-runtime-retirement/local-state/.convex-tmp`，不再作为 active codegen 临时目录。
  - Playwright browser smoke：`127.0.0.1:3000` 页面内 fetch 创建两个用户的 ACP session，并验证 list/detail/resume/rename/delete 与用户隔离，全部通过；测试数据已调用 delete 清理。
  - 回归收口：`tree_command_purge_uses_tree_command_protocol` 中 purge 请求未携带排序，返回 `sortOrder: null` 符合当前 route 语义，已同步修正测试断言。
  - 回归收口：`hermes_client` / `hermes_tools` / `page_ai_workflow` 测试统一使用 crate 级 Hermes 环境锁，避免完整并发测试时互相修改 `HERMES_HOME` / `MNOTE_WEB_HERMES_*`。
  - `cd rust && cargo test -p mnote-web tree_command_purge_uses_tree_command_protocol -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web hermes_tools_call_rejects_profile_disabled_tool -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web hermes_client_profile_skill_and_memory_routes_use_local_bff_without_upstream -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web block_edit_workflow_respects_disabled_markdown_edit_tool -- --nocapture`：1 passed。
  - `cd rust && cargo test -p mnote-web -- --nocapture`：336 passed / 0 failed；main tests 0 passed / 0 failed；doc tests 4 ignored。
- 依赖记录：
  - 根 `package.json` / lockfile 增加 `convex@1.39.1`，用于根 `convex/` codegen/typecheck。
  - `npm install --save-dev convex@1.39.1` 曾因既有 `node_modules` 布局报 `ENOTDIR`，未保留该失败命令产生的临时 symlink；随后用 pnpm 指定仓库 store 安装，并恢复被 pnpm 移入 `.ignored` 的既有 Playwright / electron-builder 目录。
