2026-07-21 05:13:05 +08:00
|
|
|
|
# [recycle] 7-58 Page AI Reasonix / Hermes 官方会话与响应对齐 v1
|
2026-06-10 07:09:04 +08:00
|
|
|
|
|
|
|
|
|
|
> 创建时间:2026-06-09
|
|
|
|
|
|
>
|
|
|
|
|
|
> 当前状态:`PROCESS`
|
|
|
|
|
|
>
|
|
|
|
|
|
> Owner:07-ai / Page AI / Reasonix ACP / Hermes ACP / mnote-web runtime
|
|
|
|
|
|
>
|
|
|
|
|
|
> 参考代码:
|
|
|
|
|
|
> - Reasonix 官方 `main-v2/desktop`:https://github.com/esengine/DeepSeek-Reasonix/tree/main-v2/desktop
|
|
|
|
|
|
> - Hermes 官方仓库:`NousResearch/hermes-agent`:https://github.com/NousResearch/hermes-agent
|
|
|
|
|
|
> - 本地参考副本:`reference-code/DeepSeek-Reasonix-main`
|
|
|
|
|
|
> - MNote 当前链路:`rust/crates/mnote-web/src/routes/hermes_client.rs`、`rust/crates/mnote-web/src/acp_session_manager.rs`、`rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js`、`rust/crates/mnote-web/src/ssr/styles.rs`
|
|
|
|
|
|
>
|
|
|
|
|
|
> 上位依据:
|
|
|
|
|
|
> - `design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md`
|
|
|
|
|
|
> - `design/07-ai/done/7-40-page-ai-context-envelope-and-run-receipt-v1.md`
|
|
|
|
|
|
> - `design/10-review/reference/13-hermes-vscode-plugin-gap-review-v1.md`
|
|
|
|
|
|
> - `design/10-review/done/10-current-mnote-ai-runtime-review-v1.md`
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 第一结论
|
|
|
|
|
|
|
|
|
|
|
|
Page AI 里 Reasonix 不能继续按“每个 run 都可 `session/load` 恢复旧 ACP session”的模式设计。
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix 官方 `main-v2/desktop` 的 ACP 模型是:
|
|
|
|
|
|
|
|
|
|
|
|
- `initialize.agentCapabilities.loadSession=false`。
|
|
|
|
|
|
- `session/new` 创建一个内存 `Session`,其中持有 `CacheFirstLoop`、toolset、MCP clients、Eventizer 和 aborter。
|
|
|
|
|
|
- `session/prompt` 必须命中同一个内存 `Session`,然后调用 `session.loop.step(text)` 继续对话。
|
|
|
|
|
|
- 上下文主要在 Reasonix 本地 loop / session 对象里延续,而不是靠 MNote 每轮重发完整聊天历史。
|
|
|
|
|
|
|
|
|
|
|
|
因此 MNote 对 Reasonix 的正确集成不是“保存 acpSessionId,下轮再 load”,而是:**同一个 MNote Page AI session 在同一个 Rust Web 进程生命周期内必须复用同一个活的 Reasonix ACP session manager;如果底层 Reasonix session 已丢失,必须显式进入 cold resume/fallback,不得假装仍是原生 Reasonix 上下文。**
|
|
|
|
|
|
|
|
|
|
|
|
这也解释当前现象:用户第二轮只回“可以”时,MNote payload 没有带历史 messages;它本来就依赖 Reasonix 本地 session 记住上一轮。但实际后端每轮都 fallback 出新的 Reasonix ACP session,Reasonix 当然只能把“可以”当新会话开头。
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 当前实测问题
|
|
|
|
|
|
|
|
|
|
|
|
2026-06-09 现场证据:同一个 MNote session `mnote_current_page-ai-mq6q24d5` 连续三轮 run,实际 Reasonix ACP session 发生了三次变化:
|
|
|
|
|
|
|
|
|
|
|
|
| 轮次 | 用户输入 | 前端发送的 acpSessionId | 后端实际 session.info.updated acpSessionId |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| 1 | 看看你现在能不能看到有3个mcp | 空 | `19dbc6c6-b61e-45d1-8a28-52d0e79a02e3` |
|
|
|
|
|
|
| 2 | 可以 | `19dbc6c6-b61e-45d1-8a28-52d0e79a02e3` | `daa898ff-e985-49da-bbb9-ba3072f130c7` |
|
|
|
|
|
|
| 3 | 帮我检查一下 LightRAG 服务的启用状态或者帮它启动起来 | `daa898ff-e985-49da-bbb9-ba3072f130c7` | `ed358871-5176-4f3c-869c-91578cefa2b3` |
|
|
|
|
|
|
|
|
|
|
|
|
并且第二轮 run payload 中没有 `messages` 字段,只有当前 `message="可以"`、`pageContext`、`contextRefs`、`acpSessionId` 等。这证明当前 Page AI 并没有 MNote 侧 transcript 注入兜底。
|
|
|
|
|
|
|
|
|
|
|
|
同一轮后端还出现大量:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
WARN ACP SSE lagged: N events dropped
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
这说明 ACP event 转发路径有背压丢事件。当前事件路径会在 `broadcast` 接收后先持久化 SQLite / local journal,再写入浏览器 SSE;当 Reasonix 高频输出 `thought.delta` / `message.delta` 时,256 buffer 很容易落后。终止事件或状态同步被拖慢/丢失后,前端 assistant bubble 的 `streaming=true` 不能及时清掉,CSS 就继续显示 `AI · 输出中`。
|
|
|
|
|
|
|
|
|
|
|
|
当前 SQLite `ai_runtime_runs.status` 仍停在 `acp_pending`,但同 run 的 `ai_runtime_events` 已有 `run.completed`。这会继续污染 active-run 判断、恢复逻辑和前端状态。
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Reasonix 官方模式摘录
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 ACP 能力
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix 官方 `src/cli/commands/acp.ts` 在 `initialize` 中返回:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
agentCapabilities: {
|
|
|
|
|
|
loadSession: false,
|
|
|
|
|
|
promptCapabilities: { image: false, audio: false, embeddedContext: true },
|
|
|
|
|
|
mcpCapabilities: { http: false, sse: false },
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
含义:MNote 不能把 `session/load` 当成 Reasonix 的正常恢复入口。对于 Reasonix,`acpSessionId` 只在当前 ACP 进程内有效。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 session/new
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix `session/new` 做的事不是只分配一个 id,而是完整构造一个 session:
|
|
|
|
|
|
|
|
|
|
|
|
- resolve workspace root。
|
|
|
|
|
|
- build code toolset。
|
|
|
|
|
|
- load MCP servers,并把 MCP tools bridge 进 tool registry。
|
|
|
|
|
|
- 构造 `ImmutablePrefix`。
|
|
|
|
|
|
- 创建 `CacheFirstLoop`。
|
|
|
|
|
|
- 创建 `Eventizer`。
|
|
|
|
|
|
- 保存到内存 `sessions` map。
|
|
|
|
|
|
|
|
|
|
|
|
这就是 Reasonix 的上下文承载点。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 session/prompt
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix `session/prompt` 通过 `params.sessionId` 找内存 session,然后:
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
for await (const ev of session.loop.step(text)) {
|
|
|
|
|
|
for (const kev of session.eventizer.consume(ev, session.ctx)) {
|
|
|
|
|
|
dispatchKernelEvent(server, session.id, kev);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
因此多轮对话是否连续,关键是 MNote 是否一直把后续 prompt 发给同一个 live Reasonix session。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.4 Desktop UI 终止语义
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix 官方 desktop controller 在 turn 结束时发送类似 `$turn_complete` 的终止事件,并在 finally 中清理 running 状态、刷新 sessions/balance。MNote Page AI 当前只靠 `run.completed` SSE 和前端 `streaming=false` 清理 UI,这条链路在 SSE lag/drop 下不够稳。
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 目标架构
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 双层 session 模型
|
|
|
|
|
|
|
|
|
|
|
|
MNote 必须区分两层 session:
|
|
|
|
|
|
|
|
|
|
|
|
| 层 | 作用 | 是否持久 | 对 Reasonix 的含义 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| MNote Page AI session | UI 历史、SQLite/local JSONL、用户可见会话、run audit | 持久 | 可用于恢复 UI 和构造 fallback context |
|
|
|
|
|
|
| Reasonix ACP session | Reasonix 内存 `CacheFirstLoop`、MCP toolset、模型上下文 | 进程内 live | 不能假设可 load,丢失后就是 cold |
|
|
|
|
|
|
|
|
|
|
|
|
新增/收口一个运行时绑定概念:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
PageAiAcpLiveBinding
|
|
|
|
|
|
mnoteSessionId
|
|
|
|
|
|
acpRuntime = reasonix
|
|
|
|
|
|
acpSessionId
|
|
|
|
|
|
workspaceRoot / cwd
|
|
|
|
|
|
rootUri / allowedRootsHash
|
|
|
|
|
|
profile / model
|
|
|
|
|
|
managerHandle
|
|
|
|
|
|
status: idle | running | queued | cold | closed
|
|
|
|
|
|
lastRunId
|
|
|
|
|
|
lastEventSeq
|
|
|
|
|
|
createdAt / lastPromptAt
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`managerHandle` 只在 Rust Web 进程内存中存在;SQLite 只保存 `acpSessionId` 和 metadata,用于 UI 展示、审计和检测 stale,而不是承诺可恢复 Reasonix 原生上下文。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 Reasonix run 启动规则
|
|
|
|
|
|
|
|
|
|
|
|
对于 `acpRuntime=reasonix`:
|
|
|
|
|
|
|
|
|
|
|
|
1. 先按 `mnoteSessionId + acpRuntime` 查 live binding。
|
|
|
|
|
|
2. 若 binding 存在且 status 非 closed,直接复用同一个 `AcpSessionManager` 和 `acpSessionId` 调 `session/prompt`。
|
|
|
|
|
|
3. 若 binding 不存在:
|
|
|
|
|
|
- 不调用或不依赖 `session/load`。
|
|
|
|
|
|
- 调 `session/new` 创建新 Reasonix session。
|
|
|
|
|
|
- 将新 `acpSessionId` 写回 MNote session metadata。
|
|
|
|
|
|
- 若该 MNote session 已有历史 transcript,标记为 `cold_resume`,走 fallback context 注入。
|
|
|
|
|
|
4. 当前 run 完成后,不关闭 manager,不从 live binding 中移除;只把 binding 状态改回 idle。
|
|
|
|
|
|
5. 用户显式关闭会话、切换 workspace/root/model/profile、登出、进程退出或资源压力回收时,才关闭 binding。
|
|
|
|
|
|
|
|
|
|
|
|
这和当前 `run_prompt_with_mnote_context(...).await` 后直接 `mgr_clone.close().await`、`ACP_ACTIVE_RUNS.remove(...)` 的模式相反。对于 Reasonix,turn 完成不等于 Reasonix session 生命周期结束。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.3 Hermes 与 Reasonix 分歧
|
|
|
|
|
|
|
|
|
|
|
|
不要把 Hermes VSCode 的 `session/load` 结论机械套到 Reasonix:
|
|
|
|
|
|
|
|
|
|
|
|
- Hermes adapter 如果声明支持 `loadSession`,MNote 应继续优先 `session/load`。
|
|
|
|
|
|
- Reasonix 官方明确 `loadSession=false`,MNote 应使用 live binding + cold resume fallback。
|
|
|
|
|
|
- UI 上要能显示当前会话是否是 `native-live`、`cold-resumed`、`new-session`,避免用户误以为 Reasonix 原生上下文仍在。
|
|
|
|
|
|
|
|
|
|
|
|
### 4.4 Hermes Desktop / Web UI 对照
|
|
|
|
|
|
|
|
|
|
|
|
Hermes 参考实现反而证明:Hermes 与 Reasonix 的上下文工程必须分开设计,只能共享外层 MNote envelope / audit / UI projection。
|
|
|
|
|
|
|
|
|
|
|
|
Hermes 官方公开文档与本地参考包需要分层理解:
|
|
|
|
|
|
|
|
|
|
|
|
- ACP adapter 的 `session/load` / `session/new` / `session/prompt` 属于 adapter 会话合同;host 不能只把旧 `sessionId` 写回内存后直接 prompt。
|
|
|
|
|
|
- 官方 user guide 明确提醒:ACP session 列表、load、resume、fork 的范围以当前运行的 ACP server process 为准。也就是说,MNote 不能把 Hermes `session/load` 永久等同为跨进程必然成功。
|
|
|
|
|
|
- Hermes VSCode 插件的 `SessionManager.ensureSession()` 说明了 host 侧正确动作:已有 `storedSessionId` 时必须先调用 ACP `session/load`,让 adapter 注册会话;`session/load` 返回 `null` 或失败后才能 `session/new`。插件注释还指出,跳过 `session/load` 会形成 phantom session。
|
|
|
|
|
|
- Hermes Web / dashboard 路径体现的是 server / DB 级会话资产:`state.db` 中有 sessions、messages、conversation chain、search/detail/export 这类持久查询能力。这是 MNote 做会话管理和 history dashboard 的参考,但不能直接推导为 ACP adapter 任意时刻都能无损 replay。
|
|
|
|
|
|
- 本地历史包 `hermes-web-ui-0.5.18` 的 Socket.IO server 体现了 Web/API chat 路径:`resumeSession()` 先找 live `sessionMap`,找不到再从 DB `loadSessionStateFromDb()` 读取 messages、compression snapshot 和 token 统计。这条路径不是 ACP `session/load`,应放在 Track W。
|
|
|
|
|
|
|
|
|
|
|
|
因此 Hermes 的正确集成不是“无条件相信 session id 可恢复”,而是:**按 runtime capability 探测 -> 有 stored `acpSessionId` 就先 `session/load` -> load 成功才认为底层 adapter 连续 -> load 失败进入 Hermes cold/new;若 adapter replay 了 history,MNote 必须消费、去重、排序。**
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix 官方 desktop/ACP 与这两者都不同:Reasonix ACP 声明 `loadSession=false`,上下文在当前进程内的 `CacheFirstLoop` 上。MNote 如果每轮关闭 `AcpSessionManager`,就等于每轮丢掉 Reasonix 原生上下文。
|
|
|
|
|
|
|
|
|
|
|
|
因此 MNote 后续应冻结三套 runtime policy:
|
|
|
|
|
|
|
|
|
|
|
|
| Runtime | 上下文主承载 | 恢复策略 | MNote 责任 |
|
|
|
|
|
|
| --- | --- | --- | --- |
|
|
|
|
|
|
| Hermes ACP | adapter session;若 adapter 支持并成功 `session/load`,才视为底层连续 | stored ACP session id -> `session/load` -> 按 load 结果和 replay evidence 决定连续性 -> `session/prompt` | 保存/加载 acpSessionId;消费可能出现的 replay;处理 load 失败 fallback |
|
|
|
|
|
|
| Hermes Web/API Chat | Hermes server transcript + DB state | DB messages / compression snapshot -> resume socket/run | 管理 messages、summary、run status、queue |
|
|
|
|
|
|
| Reasonix ACP | live `CacheFirstLoop` in ACP process | live binding 复用;丢失后 cold resume packet | 维持 live binding,不假装 session/load 可用 |
|
|
|
|
|
|
|
|
|
|
|
|
共享部分只包括:
|
|
|
|
|
|
|
|
|
|
|
|
- MNote Page AI session / run id / trace id。
|
|
|
|
|
|
- `agentRunEnvelope`、target package、allowed roots、capability policy。
|
|
|
|
|
|
- SQLite/local JSONL audit。
|
|
|
|
|
|
- 前端 projection:assistant final、thought、tool、plan、permission、citation、run status。
|
|
|
|
|
|
|
|
|
|
|
|
不共享的部分是:底层 agent session 生命周期、上下文恢复机制、是否重发 transcript、是否允许 `session/load`。
|
|
|
|
|
|
|
|
|
|
|
|
## 5. Context 工程设计
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 native-live 路径
|
|
|
|
|
|
|
|
|
|
|
|
当 Reasonix live binding 存在时:
|
|
|
|
|
|
|
|
|
|
|
|
- MNote 每轮 prompt 只发送当前用户输入、当前页面/selection/target package、allowed roots、MNote capability envelope。
|
|
|
|
|
|
- 不重发完整 transcript,避免破坏 Reasonix 自己的 prefix/cache/memory 设计。
|
|
|
|
|
|
- MNote 仍持久化 user/assistant/tool events,用于 UI 历史、审计、fallback 和 reload。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.2 cold resume fallback
|
|
|
|
|
|
|
|
|
|
|
|
当 MNote Page AI session 有历史,但 Reasonix live binding 丢失时:
|
|
|
|
|
|
|
|
|
|
|
|
- 创建新的 Reasonix ACP session。
|
|
|
|
|
|
- 将 MNote 最近 N 轮 transcript、上一次 summary、当前 page context、allowed roots 组成一个明确的 `MNote cold resume context packet`。
|
|
|
|
|
|
- 作为 embedded/context prompt 的前置内容发给 Reasonix。
|
|
|
|
|
|
- 前端显示轻量状态:`Reasonix 原生会话已重建,已用 MNote 历史摘要恢复上下文`。
|
|
|
|
|
|
- 该状态进入 run audit,不能静默伪装成原生连续会话。
|
|
|
|
|
|
|
|
|
|
|
|
fallback packet 必须控制大小:
|
|
|
|
|
|
|
|
|
|
|
|
- 优先 summary。
|
|
|
|
|
|
- 再补最近 3-5 轮 user/assistant final。
|
|
|
|
|
|
- tool raw output 只放 tool name、result summary、changed files、citations,不放大段 stdout。
|
|
|
|
|
|
- thought/reasoning 默认不进入 fallback。
|
|
|
|
|
|
|
|
|
|
|
|
### 5.3 用户短回复消歧
|
|
|
|
|
|
|
|
|
|
|
|
对“可以”“继续”“按刚才说的”“同意”这类短回复:
|
|
|
|
|
|
|
|
|
|
|
|
- native-live:完全交给 Reasonix 原生上下文。
|
|
|
|
|
|
- cold-resumed:fallback packet 必须包含上一轮 user intent 和 assistant proposal。
|
|
|
|
|
|
- new-session:前端或后端应提示“当前 Reasonix 原生上下文不可用,短回复缺少指代对象”,不要让模型编造。
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Response / UI 状态设计
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 事件分类
|
|
|
|
|
|
|
|
|
|
|
|
Reasonix event 到 MNote UI 的分类应对齐官方 desktop 的 projection 思路:
|
|
|
|
|
|
|
|
|
|
|
|
| Reasonix / ACP 事件 | MNote 展示 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| message/model delta | 当前 assistant streaming bubble |
|
|
|
|
|
|
| model final / run completed | 关闭 streaming bubble,写入 final |
|
|
|
|
|
|
| thought delta | 折叠的“思考过程”,不混入 final answer |
|
|
|
|
|
|
| tool intent/result | tool card,带 status、locations、citations、audit id |
|
|
|
|
|
|
| plan/todo | plan/status panel,不当作 assistant 正文 |
|
|
|
|
|
|
| turn complete | 解除 busy / 输出中 / active run snapshot |
|
|
|
|
|
|
|
|
|
|
|
|
### 6.2 终止事件不可丢
|
|
|
|
|
|
|
|
|
|
|
|
`run.completed` / `run.failed` / `run.aborted` / synthetic `turn.complete` 属于不可丢事件。
|
|
|
|
|
|
|
|
|
|
|
|
即使中间 `thought.delta` 或 `message.delta` 被限流/合并,终止事件也必须送达浏览器并持久化。前端 `输出中` 只能由 active run status 和 streaming flag 共同决定:
|
|
|
|
|
|
|
|
|
|
|
|
- 收到 terminal event:强制 `streaming=false`。
|
|
|
|
|
|
- SSE 断开但后端 run 已 terminal:`/active-run` 或 `/runs/{id}/events` reconciliation 必须合成 terminal event。
|
|
|
|
|
|
- SQLite run status terminal 时,active-run 不得再返回该 run。
|
|
|
|
|
|
|
|
|
|
|
|
### 6.3 状态持久化
|
|
|
|
|
|
|
|
|
|
|
|
当前 `ai_runtime_events` 已记录 `run.completed`,但 `ai_runtime_runs.status` 仍停在 `acp_pending`,这是 UI 卡 “输出中” 和 active-run 误判的根因之一。
|
|
|
|
|
|
|
|
|
|
|
|
后续必须保证:
|
|
|
|
|
|
|
|
|
|
|
|
- `register_runtime_from_create_run_response` 初始化 run 为 `running` 或 `queued`,不要长期停在 `acp_pending`。
|
|
|
|
|
|
- `persist_acp_runtime_event(run.completed)` 同步更新 SQLite `ai_runtime_runs.status=completed`。
|
|
|
|
|
|
- `persist_acp_runtime_event(run.failed)` 同步更新 `failed`。
|
|
|
|
|
|
- `persist_acp_runtime_event(run.aborted)` 同步更新 `aborted`。
|
|
|
|
|
|
- 内存 registry 与 SQLite status 不一致时,以 terminal event journal 为 reconciliation truth,补写 run status。
|
|
|
|
|
|
|
|
|
|
|
|
## 7. SSE / 背压设计
|
|
|
|
|
|
|
|
|
|
|
|
当前 `broadcast` consumer 同时承担“持久化事件”和“转发浏览器 SSE”,导致高频 Reasonix 输出时 `ACP SSE lagged`。
|
|
|
|
|
|
|
|
|
|
|
|
目标拆分:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Reasonix ACP event
|
|
|
|
|
|
-> in-memory run event bus
|
|
|
|
|
|
-> browser SSE fanout, low latency, terminal priority
|
|
|
|
|
|
-> persistence worker, async/batched, can lag but不能阻塞 SSE
|
|
|
|
|
|
-> run status reconciler, terminal immediate
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
规则:
|
|
|
|
|
|
|
|
|
|
|
|
- `thought.delta` 可合并、降采样或只持久化部分摘要。
|
|
|
|
|
|
- `message.delta` 可按时间片或 token chunk 合并,保证顺序。
|
|
|
|
|
|
- tool events、permission events、session.info、plan、terminal events 不降采样。
|
|
|
|
|
|
- terminal event 使用独立 high-priority path;即使普通 delta buffer 满,也不能 drop。
|
|
|
|
|
|
- persistence worker 失败只产生 audit warning,不应阻塞浏览器终止状态。
|
|
|
|
|
|
|
|
|
|
|
|
## 8. 后续执行总图
|
|
|
|
|
|
|
|
|
|
|
|
本设计后续不应拆成“先随手修 Reasonix,再顺手修 Hermes”。正确顺序是先冻结共享合同,再分别推进 Reasonix 和 Hermes 的 runtime policy,最后统一 UI projection 与 smoke。
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
Phase 0: Runtime policy / event store / status reconciliation 共享底座
|
|
|
|
|
|
-> Track R: Reasonix ACP live binding + cold resume
|
|
|
|
|
|
-> Track H: Hermes ACP session/load + replay 消费
|
|
|
|
|
|
-> Track W: Hermes Web/API chat transcript resume 兼容
|
|
|
|
|
|
-> Track U: Page AI UI / history / debug / smoke 矩阵
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 8.1 共享底座:Runtime policy
|
|
|
|
|
|
|
|
|
|
|
|
先在 MNote 侧抽象出明确的 runtime policy,而不是在 `hermes_client.rs` 里靠 `if acpRuntime == "reasonix"` 散落判断。
|
|
|
|
|
|
|
|
|
|
|
|
建议合同:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
AgentRuntimePolicy
|
|
|
|
|
|
runtime: hermes | reasonix | chat_only | api_chat
|
|
|
|
|
|
contextCarrier:
|
|
|
|
|
|
- adapter_loadable_session
|
|
|
|
|
|
- adapter_live_loop
|
|
|
|
|
|
- server_transcript
|
|
|
|
|
|
supportsSessionLoad: bool
|
|
|
|
|
|
supportsHistoryReplay: bool
|
|
|
|
|
|
supportsForkListResume: bool
|
|
|
|
|
|
promptContinuity:
|
|
|
|
|
|
- native_live_required
|
|
|
|
|
|
- load_then_prompt
|
|
|
|
|
|
- transcript_rebuild
|
|
|
|
|
|
terminalSemantics:
|
|
|
|
|
|
- run.completed
|
|
|
|
|
|
- run.failed
|
|
|
|
|
|
- run.aborted
|
|
|
|
|
|
- synthetic.turn_complete
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
MNote Page AI 每次 run 必须先解析 policy,再决定:
|
|
|
|
|
|
|
|
|
|
|
|
- 是否允许 `session/load`。
|
|
|
|
|
|
- 是否应复用 live manager。
|
|
|
|
|
|
- 是否需要从 SQLite/local JSONL 构造 cold resume packet。
|
|
|
|
|
|
- 是否要接收 adapter replay history。
|
|
|
|
|
|
- terminal event 如何更新 SQLite run status 和前端 active-run snapshot。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.2 共享底座:Session 身份
|
|
|
|
|
|
|
|
|
|
|
|
统一命名并避免当前概念混用:
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 含义 | 生命周期 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `mnoteSessionId` | MNote Page AI 会话 id,UI/history/audit 主键 | 持久 |
|
|
|
|
|
|
| `mnoteRunId` | MNote 一次 run id,events/status/audit 主键 | 持久 |
|
|
|
|
|
|
| `acpSessionId` | adapter 返回的底层 session id | runtime 相关 |
|
|
|
|
|
|
| `runtimeBindingId` | MNote 内存 runtime binding id | 进程内 |
|
|
|
|
|
|
| `providerConversationId` | ChatOnly 网页 provider 远端会话 id | provider 相关 |
|
|
|
|
|
|
|
|
|
|
|
|
SQLite `ai_runtime_runs` 必须保存这些字段的快照,但只有 `mnoteSessionId` / `mnoteRunId` 是 MNote 自己的长期真相。`acpSessionId` 对 Hermes 是可 load 引用,对 Reasonix 只是 live binding 标识,不代表可恢复。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.3 共享底座:Event store / terminal reconciliation
|
|
|
|
|
|
|
|
|
|
|
|
先修这个底座,否则 Reasonix 和 Hermes 都会继续被 `输出中`、active-run stale、历史恢复误判影响。
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] browser SSE 转发与 persistence 解耦:浏览器低延迟 fanout,不在同一 consumer 里同步写 SQLite。
|
|
|
|
|
|
- [ ] terminal event 高优先级:`run.completed` / `run.failed` / `run.aborted` / synthetic `turn.complete` 不可被 delta 洪峰挤掉。
|
|
|
|
|
|
- [ ] `persist_acp_runtime_event` 或独立 reconciler 在 terminal event 到达时同步更新 `ai_runtime_runs.status`。
|
|
|
|
|
|
- [ ] `/api/page-ai/sessions/{id}/active-run` 不得返回已有 terminal event 的 run。
|
|
|
|
|
|
- [ ] `/api/page-ai/runs/{id}/events` 能在 run status terminal 但缺 terminal event 时合成 reconciliation event。
|
|
|
|
|
|
- [ ] 前端收到 terminal / reconciliation terminal 后强制清理 `streaming=true`、`pageAiBusy`、active run snapshot。
|
|
|
|
|
|
|
|
|
|
|
|
### 8.4 共享底座:Response projection
|
|
|
|
|
|
|
|
|
|
|
|
所有 runtime 的可见输出投影统一,但事件来源不统一:
|
|
|
|
|
|
|
|
|
|
|
|
- assistant final:只来自 message/model delta + final output。
|
|
|
|
|
|
- thought:只进折叠思考过程,默认不展开,不混入 final。
|
|
|
|
|
|
- tool:独立 tool card,保留 status、toolCallId、locations、citations、auditId。
|
|
|
|
|
|
- plan/todo:独立 plan/status panel,不当作 assistant 正文。
|
|
|
|
|
|
- permission:独立权限卡,用户决策必须回写 adapter。
|
|
|
|
|
|
- citation:优先 tool 返回的 `citationMarkdown` / `citationUrl`,不让模型手拼。
|
|
|
|
|
|
- busy/输出中:只由 active run + streaming bubble 共同决定,terminal 后立即清除。
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Track R:Reasonix ACP
|
|
|
|
|
|
|
|
|
|
|
|
### R0:官方模式冻结
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] fake Reasonix ACP fixture:`initialize.loadSession=false`、`session/new` 创建内存 loop、`session/prompt` 必须命中同一 session。
|
|
|
|
|
|
- [ ] 在测试中证明:如果 MNote 每轮新建 session,短回复“可以”无法关联上一轮;如果复用 live binding,可以关联。
|
|
|
|
|
|
- [ ] 在 runtime payload / SQLite detail 中暴露 `reasonixSessionMode: native_live | cold_resumed | new_session`。
|
|
|
|
|
|
|
|
|
|
|
|
### R1:Live binding pool
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] `AcpRuntimeManager` / `hermes_client` 增加按 `mnoteSessionId + acpRuntime` 复用的 Reasonix live binding pool。
|
|
|
|
|
|
- [ ] Reasonix run 完成后不关闭 manager,不移除 binding,只从 running 改 idle。
|
|
|
|
|
|
- [ ] 同一 binding 内 run 串行执行;第二条输入进入 binding queue,不另开 session。
|
|
|
|
|
|
- [ ] Stop/abort 只取消当前 prompt,不默认销毁 binding。
|
|
|
|
|
|
- [ ] 切 workspace/root/model/profile/allowedRootsHash 时关闭旧 binding,并把下一轮标记为 cold/new。
|
|
|
|
|
|
|
|
|
|
|
|
### R2:Cold resume fallback
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 从 SQLite/local JSONL 构造 bounded transcript summary。
|
|
|
|
|
|
- [ ] cold resume 首轮 prompt 注入 `MNote cold resume context packet`。
|
|
|
|
|
|
- [ ] packet 只包含 summary、最近 3-5 轮 user/assistant final、tool summary、changedFiles、citations,不包含 thought 大段文本。
|
|
|
|
|
|
- [ ] UI 显示 cold resume 状态,不误导为 Reasonix 原生会话。
|
|
|
|
|
|
- [ ] `new_session` 且用户只发短回复时提示缺上下文,不让模型编造。
|
|
|
|
|
|
|
|
|
|
|
|
### R3:Reasonix 工具与 MCP 能力
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 确认 `mnoteCapabilities` / runtime manifest 进入 Reasonix 模型可见 prompt,而不只是 Rust payload。
|
|
|
|
|
|
- [ ] 继续保持 Reasonix 原生文件读写为 local-first 普通 Markdown 主路径;MNote 不扩写一套本地文件编辑工具。
|
|
|
|
|
|
- [ ] MCP 状态作为 Reasonix 工具可见事实进入 tool/status card,而不是让模型只凭自然语言猜测。
|
|
|
|
|
|
- [ ] LightRAG / MNote bridge MCP 失败时返回结构化 tool failed,不污染 assistant final。
|
|
|
|
|
|
|
|
|
|
|
|
## 10. Track H:Hermes ACP
|
|
|
|
|
|
|
|
|
|
|
|
### H0:官方 load 合同冻结
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] fake Hermes ACP fixture:`initialize.loadSession=true`,支持 `session/load`;另准备一个会 replay history `session/update` 的变体。
|
|
|
|
|
|
- [ ] MNote 启动 Hermes run 时读取 stored `acpSessionId`,先 `session/load`,再 `session/prompt`。
|
|
|
|
|
|
- [ ] `session/load` 成功时,MNote 认为 adapter 当前已注册该 session;如果 adapter replay 出 user/assistant/thought/tool/plan,前端/后端必须消费并去重,不能覆盖乱序。
|
|
|
|
|
|
- [ ] `session/load` 返回 `null` 或失败时,标记 `hermesSessionMode=cold_resumed | new_session`,不假装原生连续。
|
|
|
|
|
|
|
|
|
|
|
|
### H1:Hermes session store 对齐
|
|
|
|
|
|
|
|
|
|
|
|
Hermes Web / dashboard 和部分本地服务有 `state.db` 级 session store。MNote 不应复制 Hermes DB,也不能把 Web DB 语义直接套到 ACP adapter;但要保存映射与审计:
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] SQLite `ai_runtime_runs/session metadata` 保存 `acpSessionId`、Hermes profile、cwd/rootUri、lastLoadedAt、lastReplaySeq。
|
|
|
|
|
|
- [ ] MNote session detail 能展示底层 Hermes session id 和 load/replay 状态。
|
|
|
|
|
|
- [ ] 删除 MNote Page AI session 时,默认只删除 MNote history/audit;是否调用 Hermes delete/remove 需要单独确认,不默认删除 Hermes 外部 session store。
|
|
|
|
|
|
- [ ] 恢复历史会话时,区分“只恢复 MNote UI 历史”和“恢复 Hermes 底层 ACP session”。
|
|
|
|
|
|
|
|
|
|
|
|
### H2:Hermes replay 消费
|
|
|
|
|
|
|
|
|
|
|
|
部分 Hermes adapter 或 Web/API resume 路径可能 replay 历史。MNote 当前已有自己的 UI history,因此只要收到 replay,就必须做去重和排序:
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] replay event 带 `source=adapter_replay`,不能当作新的当前 run 输出。
|
|
|
|
|
|
- [ ] replay user/assistant/tool 与 MNote 已有消息按 content/run/time 去重。
|
|
|
|
|
|
- [ ] replay tool start/completed 能恢复 tool card 状态,但不触发新的写入刷新。
|
|
|
|
|
|
- [ ] replay plan/todo 恢复 plan panel,但不混入 assistant final。
|
|
|
|
|
|
- [ ] replay 完成后再允许当前 prompt streaming bubble 开始。
|
|
|
|
|
|
|
|
|
|
|
|
### H3:Hermes 功能面
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] 支持 Hermes ACP `session/list` / `session/resume` / `session/fork` 的能力探测,先展示为 debug/高级入口,不默认替代 MNote session list。
|
|
|
|
|
|
- [ ] 权限请求沿 Hermes ACP decision loop 回传,不做前端假 allow。
|
|
|
|
|
|
- [ ] tool `locations[]` 保留并能打开 MNote resource tab。
|
|
|
|
|
|
- [ ] image prompt capability 只在 MNote target/attachment 明确支持时开启,不把图片 silently 变成文本。
|
|
|
|
|
|
|
|
|
|
|
|
## 11. Track W:Hermes Web/API Chat
|
|
|
|
|
|
|
|
|
|
|
|
Hermes 官方 Web UI 是 dashboard,不是 MNote Page AI 的直接聊天实现。但它提供两个有用原则:session 是可查询/搜索/导出的持久资产;server 端负责 auth、session token 和 API 管理。
|
|
|
|
|
|
|
|
|
|
|
|
### W0:ChatOnly / API Chat 边界
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] ChatOnly / API Chat 继续走 provider conversation 或 MNote server transcript,不复用 ACP session/load 设计。
|
|
|
|
|
|
- [ ] providerConversationId 只属于网页 provider,不与 `acpSessionId` 混用。
|
|
|
|
|
|
- [ ] API Chat 不写网页 provider conversation binding。
|
|
|
|
|
|
- [ ] MNote UI history 和 provider 远端历史删除语义分开。
|
|
|
|
|
|
|
|
|
|
|
|
### W1:Session dashboard 能力
|
|
|
|
|
|
|
|
|
|
|
|
参考 Hermes Web UI 的 sessions dashboard,MNote Page AI 可补以下管理能力:
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] session list 展示 runtime、profile、message count、tool count、active/cold/new 状态。
|
|
|
|
|
|
- [ ] session detail 展示 user/assistant/tool/plan/citation 的投影,而不是 raw event dump。
|
|
|
|
|
|
- [ ] session search 支持按用户问题、assistant final、tool name、citation file 搜索。
|
|
|
|
|
|
- [ ] export session JSON/Markdown 包含 MNote session id、runs、events、runtime binding summary。
|
|
|
|
|
|
- [ ] rename/delete/bulk delete 明确只作用 MNote session,外部 provider/Hermes DB 删除需单独入口。
|
|
|
|
|
|
|
|
|
|
|
|
## 12. Track U:UI 与浏览器验证
|
|
|
|
|
|
|
|
|
|
|
|
### U0:可见状态
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] Page AI header/debug surface 显示 runtime、profile、MNote session id、acpSessionId、sessionMode、active run status。
|
|
|
|
|
|
- [ ] `native_live`、`cold_resumed`、`new_session` 用小状态标签表示,不占用 assistant 正文。
|
|
|
|
|
|
- [ ] `输出中` 只在 run active 且 assistant bubble streaming 时显示;terminal 后必须消失。
|
|
|
|
|
|
|
|
|
|
|
|
### U1:Smoke 脚本
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] `task558-page-ai-reasonix-live-session-context-smoke.js`:两轮 Reasonix,同一 acpSessionId,第二轮“可以”能引用上一轮。
|
|
|
|
|
|
- [ ] `task559-page-ai-terminal-status-reconciliation-smoke.js`:高频 thought/message delta 后 terminal 不丢,SQLite status terminal,UI 无输出中。
|
|
|
|
|
|
- [ ] `task560-page-ai-hermes-load-replay-smoke.js`:Hermes stored acpSessionId -> session/load;覆盖无 replay 与有 replay 两种变体 -> 当前 prompt。
|
|
|
|
|
|
- [ ] `task561-page-ai-session-dashboard-smoke.js`:session list/detail/search/export/rename/delete 的最小可见验证。
|
|
|
|
|
|
|
|
|
|
|
|
### U2:回归矩阵
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] Reasonix live binding 不破坏 Hermes `session/load`。
|
|
|
|
|
|
- [ ] Hermes replay 不污染 Reasonix live transcript。
|
|
|
|
|
|
- [ ] ChatOnly provider conversation binding 不受 ACP binding pool 影响。
|
|
|
|
|
|
- [ ] local-folder allowed roots / readonly guard / agent audit 对所有 runtime 生效。
|
|
|
|
|
|
- [ ] tool write receipt 仍通过 watcher/event bus 刷新当前文档,不重复全量 sidebar refresh。
|
|
|
|
|
|
|
|
|
|
|
|
## 13. 建议执行顺序
|
|
|
|
|
|
|
|
|
|
|
|
### 13.1 第一批:先修共性底座
|
|
|
|
|
|
|
|
|
|
|
|
1. Event store / terminal reconciliation。
|
|
|
|
|
|
2. Runtime policy 表和 session 身份字段梳理。
|
|
|
|
|
|
3. 前端 `输出中` / active-run 清理。
|
|
|
|
|
|
|
|
|
|
|
|
这批完成后,Reasonix 和 Hermes 后续调试不会继续被 stale status 干扰。
|
|
|
|
|
|
|
|
|
|
|
|
### 13.2 第二批:Reasonix 原生连续会话
|
|
|
|
|
|
|
|
|
|
|
|
1. Reasonix fake ACP fixture。
|
|
|
|
|
|
2. Reasonix live binding pool。
|
|
|
|
|
|
3. Reasonix 两轮“可以” browser smoke。
|
|
|
|
|
|
4. Cold resume fallback。
|
|
|
|
|
|
|
|
|
|
|
|
这批完成后,解决当前用户可见的上下文断裂。
|
|
|
|
|
|
|
|
|
|
|
|
### 13.3 第三批:Hermes 官方 load/replay
|
|
|
|
|
|
|
|
|
|
|
|
1. Hermes fake ACP fixture。
|
|
|
|
|
|
2. `session/load` replay 消费与去重。
|
|
|
|
|
|
3. Hermes session metadata/debug。
|
|
|
|
|
|
4. Hermes load/replay browser smoke。
|
|
|
|
|
|
|
|
|
|
|
|
这批完成后,Hermes 与 Reasonix 的上下文机制真正分开。
|
|
|
|
|
|
|
|
|
|
|
|
### 13.4 第四批:会话管理与其它功能
|
|
|
|
|
|
|
|
|
|
|
|
1. Page AI session dashboard:list/detail/search/export/rename/delete。
|
|
|
|
|
|
2. Tool locations/citations/open action 统一。
|
|
|
|
|
|
3. permission decision loop 复核。
|
|
|
|
|
|
4. model/profile/skills/MCP 状态面板对齐。
|
|
|
|
|
|
|
|
|
|
|
|
## 14. 验收矩阵
|
|
|
|
|
|
|
|
|
|
|
|
### 14.1 单元 / 集成测试
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] fake Reasonix ACP:`loadSession=false`,同一 MNote session 连续两轮只调用一次 `session/new`,第二轮仍用同一 `acpSessionId`。
|
|
|
|
|
|
- [ ] fake Reasonix ACP:第二轮输入“可以”,adapter 可读到上一轮 loop 历史,回答能引用上一轮提议。
|
|
|
|
|
|
- [ ] fake Hermes ACP:`loadSession=true`,`session/load` 成功后可 prompt;replay 变体中 MNote 能消费且去重。
|
|
|
|
|
|
- [ ] manager 丢失后 cold resume:后端创建新 ACP session,并注入 bounded MNote context packet。
|
|
|
|
|
|
- [ ] SQLite status reconciliation:有 `run.completed` event 后,`ai_runtime_runs.status` 必为 `completed`。
|
|
|
|
|
|
- [ ] 高频 delta:3000 条 `thought.delta` + terminal,不丢 terminal,前端最终无 `data-page-ai-streaming="true"`。
|
|
|
|
|
|
|
|
|
|
|
|
### 14.2 浏览器 smoke
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] Reasonix:打开 Page AI,发送“我下一条只说可以,你要根据这条消息继续检查 LightRAG 服务”。
|
|
|
|
|
|
- [ ] Reasonix:第二轮发送“可以”。
|
|
|
|
|
|
- [ ] Reasonix:捕获 `/api/hermes/client/runs` 和 `session.info.updated`,两轮实际 `acpSessionId` 相同。
|
|
|
|
|
|
- [ ] Reasonix:第二轮可见回答能理解“可以”指上一轮提议,不要求用户重新说明任务。
|
|
|
|
|
|
- [ ] Hermes:恢复已有 MNote Page AI session 后,先调用 `session/load`;若收到 replay,去重后再开始当前 prompt。
|
|
|
|
|
|
- [ ] 通用:回答完成后页面不显示 `AI · 输出中`。
|
|
|
|
|
|
- [ ] 通用:SQLite `ai_runtime_runs` 对所有 terminal run 都进入 terminal status。
|
|
|
|
|
|
|
|
|
|
|
|
### 14.3 回归边界
|
|
|
|
|
|
|
|
|
|
|
|
- [ ] Hermes runtime 仍可走 `session/load`,不被 Reasonix special case 破坏。
|
|
|
|
|
|
- [ ] Reasonix runtime 不调用或不依赖 `session/load`。
|
|
|
|
|
|
- [ ] ChatOnly provider conversation binding 不受 Reasonix live binding / Hermes load replay 影响。
|
|
|
|
|
|
- [ ] local-folder allowed roots / readonly guard / agent audit 仍按现有 Page AI envelope 生效。
|
|
|
|
|
|
- [ ] Stop/abort 能取消当前 prompt,不误删可复用 session;需要销毁 session 时必须有显式 close/reset 语义。
|
|
|
|
|
|
|
|
|
|
|
|
## 15. 不做事项
|
|
|
|
|
|
|
|
|
|
|
|
- 不让 MNote 为 Reasonix 重写一套长期 agent memory。
|
|
|
|
|
|
- 不把完整 transcript 每轮强塞给 native-live Reasonix,避免破坏 Reasonix 官方 prefix/cache 设计。
|
|
|
|
|
|
- 不把 `mnote.doc.*` 工具扩成 local-first 普通 Markdown 主路径。
|
|
|
|
|
|
- 不把 Reasonix 官方 desktop 的 React/Wails UI 结构照搬到 MNote;只对齐 session、event、busy/turn-complete 和 response projection 语义。
|
|
|
|
|
|
- 不继续把 `session/load` 失败当成普通 fallback 成功;对 Reasonix 这是上下文断裂事件,必须可见、可审计。
|
|
|
|
|
|
|
|
|
|
|
|
## 16. 当前建议优先级
|
|
|
|
|
|
|
|
|
|
|
|
优先做 `13.1 第一批:共性底座`,再做 `13.2 Reasonix 原生连续会话`。
|
|
|
|
|
|
|
|
|
|
|
|
原因:
|
|
|
|
|
|
|
|
|
|
|
|
- 共性底座直接修复 `ACP SSE lagged`、SQLite status stale、active-run stale 和 `输出中` 卡住;不先修它,Reasonix/Hermes 的会话验证会继续被假状态干扰。
|
|
|
|
|
|
- Reasonix live binding 直接修复“回复可以不理解上一轮”的根因。
|
|
|
|
|
|
- Hermes `session/load` / replay 应在底座稳定后推进,否则 replay 历史和当前 streaming 输出容易继续混在同一条 UI/event 路径里。
|
|
|
|
|
|
- cold resume 是必要兜底,但不能替代 native-live;如果先做 transcript 注入,容易把 Reasonix 官方 session 模型继续绕坏。
|
|
|
|
|
|
|
|
|
|
|
|
## 17. 文件级实施映射
|
|
|
|
|
|
|
|
|
|
|
|
本节用于把上面的架构拆到真实 owner,避免后续实现继续在单个 route 中临时判断。
|
|
|
|
|
|
|
|
|
|
|
|
### 17.1 后端 owner
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 当前职责 | 后续改动边界 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/routes/hermes_client.rs` | Page AI run/create/stream、ACP active run、SQLite runtime event 持久化、active-run API | 第一批只改 terminal reconciliation、active-run 过滤、runtime policy 分派入口;不要把 Reasonix/Hermes 细节继续散落到所有分支 |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/acp_session_manager.rs` | ACP `session/new`、`session/load`、`session/prompt`、event mapping | 增加 capability-aware `ensure_session_with_policy` 或等价接口;Reasonix policy 下不得尝试 `session/load` |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/acp_runtime.rs` | runtime config、process switch、active client 生命周期 | 当前只有一个 active runtime process;若支持多 live binding,需要明确是否仍共享同一 process,还是按 runtime/profile/workspace 拆 manager pool |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/acp_bridge.rs` | ACP event -> SSE event projection | 补 terminal priority、thought/message delta 限流策略、tool/permission/plan 分类,避免 UI 和持久化各自猜事件语义 |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/routes/mod.rs` | Page AI route 注册 | 只在新增 debug/session dashboard API 时扩展,不放业务逻辑 |
|
|
|
|
|
|
|
|
|
|
|
|
### 17.2 前端 owner
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 当前职责 | 后续改动边界 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| `rust/crates/mnote-web/browser/sidebar-page-ai-runtime.js` | Page AI 输入、run streaming、event 消费、active run snapshot | terminal event 后强制清理 streaming/busy;显示 sessionMode/debug;不要把 thought/tool/plan 拼进 final |
|
|
|
|
|
|
| `rust/crates/mnote-web/browser/sidebar-page-ai-session-runtime.js` | session list/history/active-run 恢复 | active-run API 返回 stale 时要忽略 terminal run;后续接 session dashboard/detail/search/export |
|
|
|
|
|
|
| `rust/crates/mnote-web/browser/sidebar-page-ai-render-runtime.js` | 消息和 tool card 渲染 | `native_live/cold_resumed/new_session` 用状态标签,不占 assistant 正文;输出中只从 streaming attr 得出 |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/ssr/styles.rs` | `AI · 输出中` 等可见样式 | 只保留样式,不承担状态判断;状态必须来自 JS runtime 和后端 terminal truth |
|
|
|
|
|
|
|
|
|
|
|
|
### 17.3 测试 / fixture owner
|
|
|
|
|
|
|
|
|
|
|
|
| 文件或建议文件 | 目的 |
|
|
|
|
|
|
| --- | --- |
|
|
|
|
|
|
| `rust/crates/mnote-web/src/acp_session_manager.rs` tests | fake Reasonix/Hermes ACP 的 `session/load` / `session/new` / `session/prompt` 单元合同 |
|
|
|
|
|
|
| `scripts/task558-page-ai-reasonix-live-session-context-smoke.js` | 浏览器两轮 Reasonix,上下文连续、同一底层 session、短回复可解析 |
|
|
|
|
|
|
| `scripts/task559-page-ai-terminal-status-reconciliation-smoke.js` | 高频 delta 后 terminal 不丢、SQLite terminal、UI 无输出中 |
|
|
|
|
|
|
| `scripts/task560-page-ai-hermes-load-replay-smoke.js` | Hermes stored acpSessionId -> load;覆盖无 replay / 有 replay 两种变体 -> 当前 prompt |
|
|
|
|
|
|
| `scripts/task561-page-ai-session-dashboard-smoke.js` | session list/detail/search/export/rename/delete 可见闭环 |
|
|
|
|
|
|
|
|
|
|
|
|
## 18. Runtime 状态机与不变量
|
|
|
|
|
|
|
|
|
|
|
|
### 18.1 Run 状态机
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
created
|
|
|
|
|
|
-> queued
|
|
|
|
|
|
-> running
|
|
|
|
|
|
-> tool_calling
|
|
|
|
|
|
-> running
|
|
|
|
|
|
-> completed | failed | aborted
|
|
|
|
|
|
|
|
|
|
|
|
terminal = completed | failed | aborted
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不变量:
|
|
|
|
|
|
|
|
|
|
|
|
- `ai_runtime_events` 中出现 terminal event 后,`ai_runtime_runs.status` 必须在同一事务或 reconciliation pass 中进入 terminal。
|
|
|
|
|
|
- `/active-run` 只能返回非 terminal run;如果 SQLite status 非 terminal 但 event journal 已 terminal,API 必须先补状态再返回空。
|
|
|
|
|
|
- 前端收到 terminal 后必须清除对应 assistant bubble 的 `streaming=true`,即使最后一段 message delta 没到。
|
|
|
|
|
|
- `acp_pending` 只能是创建到 SSE 建立前的短暂状态,不得作为完成后残留状态。
|
|
|
|
|
|
|
|
|
|
|
|
### 18.2 Reasonix binding 状态机
|
|
|
|
|
|
|
|
|
|
|
|
## 19. 完成记录(2026-06-10)
|
|
|
|
|
|
|
|
|
|
|
|
本稿已按 `13.1` 到 `13.4` 的顺序完成可执行闭环,代码与验证证据如下:
|
|
|
|
|
|
|
|
|
|
|
|
- 第一批共性底座:已实现 runtime policy、session 身份字段、terminal reconciliation、active-run 终态过滤、`/runs/{id}/events` synthetic terminal、前端 `streaming` / `输出中` 强制清理。验证:`page_ai_session_active_run*`、`acp_runtime_event_persistence_reconciles_terminal_status`、`task559-page-ai-terminal-status-reconciliation-smoke.js`。
|
|
|
|
|
|
- 第二批 Reasonix:已实现 `loadSession=false` fake fixture、live binding pool、Reasonix run 后不关闭 manager、同一 MNote session 二轮复用同一 `acpSessionId`、cold resume packet。验证:`test_reasonix_live_session_keeps_short_reply_context`、`task558-page-ai-reasonix-live-session-context-smoke.js`、`page-ai-browser-verify` Reasonix clean reply。
|
|
|
|
|
|
- 第三批 Hermes:已保留 Hermes `session/load` 路径,显式 runtime config 不再被 `MNOTE_WEB_HERMES_BIN` 覆盖;已捕获 `session/load` 期间 adapter replay,标记 `source=adapter_replay` 并在当前 streaming bubble 中去重/忽略。验证:`test_ensure_session_loads_stored`、`test_hermes_load_replay_events_before_prompt`、`task560-page-ai-hermes-load-replay-smoke.js` 覆盖 no replay / replay。
|
|
|
|
|
|
- 第四批 session dashboard 与状态面板:既有 history UI 的 list/detail/search/rename/delete 已复核;本轮新增 export API 与 UI 操作,修复 history action 点击遮挡;runtime/profile/session/acpSessionId/status 继续由 Page AI header/debug surface 和 session row meta 展示。验证:`hermes_client_acp_session_export_uses_sqlite_store`、`task561-page-ai-session-dashboard-smoke.js`。
|
|
|
|
|
|
- ChatOnly / API Chat 边界:ACP binding pool 与 Hermes load/replay 未复用 provider conversation binding。验证:`cargo test -p mnote-web api_chat`、`cargo test -p mnote-web chatonly_doubao_session_delete`。
|
|
|
|
|
|
|
|
|
|
|
|
逐项归档说明:
|
|
|
|
|
|
|
|
|
|
|
|
- `session/list` / `session/resume` / `session/fork` 的 Hermes ACP 高级入口保持为 debug/future 能力探测,不替代 MNote session list;本轮完成的是 stored `acpSessionId -> session/load -> prompt` 的官方必需路径。
|
|
|
|
|
|
- 外部 Hermes DB / provider 远端删除未自动执行;删除 MNote Page AI session 仍只默认删除/标记 MNote history 与 provider binding,外部 store 删除需要单独确认。
|
|
|
|
|
|
- permission decision loop、tool locations/citations、allowed roots、readonly guard、tool write receipt 属于既有 Page AI envelope/tool projection 路径,本轮只复核不改写;后续若专项扩展需另开设计,不继续堆入 7-58。
|
|
|
|
|
|
- 流式 apply / review session、Reasonix 长期 memory、每轮完整 transcript 注入明确不做,保持 `7-18` local-first agent file editing 主线。
|
|
|
|
|
|
|
|
|
|
|
|
状态:`DONE`。本文件可移入 `design/07-ai/done/`。
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
missing
|
|
|
|
|
|
-> new_session
|
|
|
|
|
|
-> native_live.idle
|
|
|
|
|
|
-> native_live.running
|
|
|
|
|
|
-> native_live.idle
|
|
|
|
|
|
-> cold_required | closed
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不变量:
|
|
|
|
|
|
|
|
|
|
|
|
- 同一 `mnoteSessionId + runtime + profile + workspaceKey + allowedRootsHash` 同时最多一个 running prompt。
|
|
|
|
|
|
- prompt 完成只释放 run,不销毁 Reasonix ACP session manager。
|
|
|
|
|
|
- abort 只取消当前 prompt;reset/close 才销毁 binding。
|
|
|
|
|
|
- workspace/root/model/profile/allowedRootsHash 变化必须关闭旧 binding,并把下一轮标记为 `cold_resumed` 或 `new_session`。
|
|
|
|
|
|
|
|
|
|
|
|
### 18.3 Hermes ACP 状态机
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
missing
|
|
|
|
|
|
-> has_stored_acp_session_id
|
|
|
|
|
|
-> load_attempted
|
|
|
|
|
|
-> adapter_loaded | load_failed
|
|
|
|
|
|
-> prompt_running
|
|
|
|
|
|
-> completed | failed | aborted
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不变量:
|
|
|
|
|
|
|
|
|
|
|
|
- 有 stored `acpSessionId` 时必须先 `session/load`,不能跳过 load 直接 prompt。
|
|
|
|
|
|
- `session/load` 成功只表示 adapter 当前承认该 session;是否 replay history 由事件流证据确认。
|
|
|
|
|
|
- replay event 必须标记 `source=adapter_replay` 或等价来源,不能当作当前 run 的新输出。
|
|
|
|
|
|
- load 失败不是普通成功路径,应进入 `hermesSessionMode=cold_resumed | new_session` 并写入 audit。
|
|
|
|
|
|
|
|
|
|
|
|
### 18.4 Hermes Web/API Chat 状态机
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
providerConversationId | serverSessionId
|
|
|
|
|
|
-> load server transcript / compression snapshot
|
|
|
|
|
|
-> append current user message
|
|
|
|
|
|
-> stream provider response
|
|
|
|
|
|
-> flush messages/status
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
不变量:
|
|
|
|
|
|
|
|
|
|
|
|
- Web/API Chat 不复用 ACP `session/load`。
|
|
|
|
|
|
- providerConversationId 不得写入 `acpSessionId` 字段。
|
|
|
|
|
|
- 删除 MNote UI session 不默认删除 provider 或 Hermes 官方 DB 中的会话资产。
|
|
|
|
|
|
|
|
|
|
|
|
## 19. 风险登记
|
|
|
|
|
|
|
|
|
|
|
|
| 风险 | 影响 | 控制方式 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 把 Reasonix 做成 transcript replay | 破坏 Reasonix `CacheFirstLoop` / prefix / cache 语义,短期看似恢复,长期上下文和成本异常 | native-live 优先;cold resume 必须显式标记且 bounded |
|
|
|
|
|
|
| 把 Hermes `session/load` 写成必然成功 | 进程重启、adapter 清理或 profile 变化后产生 phantom continuity | capability + load result + replay evidence 三重判断 |
|
|
|
|
|
|
| terminal event 与 delta 共用可丢 buffer | UI 卡 `输出中`、active-run stale、历史恢复错判 | terminal high-priority path + status reconciler |
|
|
|
|
|
|
| replay history 与 MNote history 重复 | 用户看到重复消息、tool card 重复执行感 | replay source 标记、content/run/time 去重、replay 完成后再开当前 bubble |
|
|
|
|
|
|
| 多 runtime 共用一个 active process manager | 切 profile/runtime 时杀掉别的 live session | 明确 process pool 设计;Reasonix binding 与 Hermes load 不共用生命周期假设 |
|
|
|
|
|
|
| 删除语义混淆 | 删除 MNote session 时误删 Hermes DB 或 provider 远端历史 | UI 和 API 分开 MNote-only delete 与 external delete |
|
|
|
|
|
|
| 代码中已有未完成 scaffold | 后续实现误以为 live binding 已完成,或产生重复 static / 不可编译 | 第一批实现前先跑 `cargo test -p mnote-web`,清点当前未提交改动归属,不回滚用户改动 |
|
|
|
|
|
|
|
|
|
|
|
|
## 20. 第一批落地切片
|
|
|
|
|
|
|
|
|
|
|
|
第一批只处理共享底座,不碰 Reasonix live binding 的完整生命周期,也不做 Hermes replay UI。目标是让 run 状态可信。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.1 后端切片 A:terminal reconciliation
|
|
|
|
|
|
|
|
|
|
|
|
- 输入:任意 runtime 的 `run.completed` / `run.failed` / `run.aborted` event。
|
|
|
|
|
|
- 输出:`ai_runtime_runs.status` 同步进入 terminal,`status_reason` 写入 terminal event 名称或 error code。
|
|
|
|
|
|
- 改动点:`persist_acp_runtime_event`、`update_runtime_by_run_id`、`/active-run` 查询路径。
|
|
|
|
|
|
- 验收:构造 `ai_runtime_events` 已 terminal 但 `ai_runtime_runs.status=acp_pending` 的 fixture,active-run 返回空并补写 status。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.2 后端切片 B:SSE terminal priority
|
|
|
|
|
|
|
|
|
|
|
|
- 输入:高频 `thought.delta` / `message.delta` + terminal。
|
|
|
|
|
|
- 输出:普通 delta 可合并或丢摘要,terminal 必达浏览器和 event store。
|
|
|
|
|
|
- 改动点:ACP event fanout/persistence worker;如果短期不能拆 worker,至少让 terminal 事件绕过可 lag 的普通 receiver。
|
|
|
|
|
|
- 验收:3000 条 delta 后仍收到 terminal,日志不再出现 terminal 丢失导致的 stale run。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.3 前端切片 C:输出中清理
|
|
|
|
|
|
|
|
|
|
|
|
- 输入:terminal SSE、reconciliation terminal、active-run 空结果。
|
|
|
|
|
|
- 输出:对应 assistant bubble `streaming=false`,document root 移除 active-run snapshot/busy 标记。
|
|
|
|
|
|
- 改动点:`sidebar-page-ai-runtime.js`、`sidebar-page-ai-session-runtime.js`。
|
|
|
|
|
|
- 验收:回答完成后 DOM 不存在 `data-page-ai-streaming="true"`,`.wolai-page-ai-message-role::after` 不再显示 `AI · 输出中`。
|
|
|
|
|
|
|
|
|
|
|
|
### 20.4 文档归档条件
|
|
|
|
|
|
|
|
|
|
|
|
本设计留在 `process/`,直到至少完成:
|
|
|
|
|
|
|
|
|
|
|
|
- 第一批共性底座 smoke 通过。
|
|
|
|
|
|
- Reasonix 两轮短回复 smoke 通过。
|
|
|
|
|
|
- Hermes load 与 replay 变体最小 smoke 通过。
|
|
|
|
|
|
- Page AI session dashboard 的范围已拆成新设计或在本设计中完成验收。
|
|
|
|
|
|
|
|
|
|
|
|
满足后移动到 `design/07-ai/done/`;若后续另起 v2 覆盖本稿,则本稿移动到 `design/old/process/` 并标记 `[recycle]`。
|