归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
24 KiB
7-25 [process] ACP Session Runtime 增强规划 v1
更新时间:2026-05-17 参考:
hermes-vscode-main(ACP client) /hermes-web-ui-0.5.18(HTTP API)
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 进行隔离;需要数据库时使用当前 Convex 底座,不新增 SQLite。
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 关联
- 创建 session(
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 无实用价值)
-
会话持久化存储
- 默认先建设 ACP-local runtime store / TTL cache,不改 Hermes HTTP 会话真相归属
- 持久化如需数据库,必须落到 Convex;不在 mnote-web 内新增 SQLite
- 表结构先服务
ACP_RUN_PAYLOADS/ACP_ACTIVE_RUNS的可靠生命周期,并且所有记录必须带userId/workspaceId/documentId作用域 sessions/messages如要保存完整聊天历史,必须先完成架构决策并标明只覆盖 ACP 路径还是统一覆盖 Hermes HTTP + ACP
-
会话管理 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— 自动命名
-
消息持久化
- ACP
stream_events执行过程中按架构决策写入 user message + agent response,或只写 runtime/event 索引 - 支持追加消息到已有 ACP-local session;Hermes HTTP 历史仍从 Hermes 读取
- ACP
-
会话搜索
GET /client/sessions/search?q=...— 全文搜索(基于 Convex 查询能力,必要时再接专用搜索索引)- 返回匹配的 session 摘要 + snippet
-
Permission request 响应修复(bug 3-20)
- 已知 incoming request 应返回结构化 error 而非仅日志
session/request_permission至少需要 auto-deny + 通知前端- 配合 Phase C 的 UI 实现人工确认
Phase B:历史消息与管理增强(P1 — 常用功能)
-
会话恢复(Resume)
- 当前
POST /client/sessions/{session_id}/resume已有 route,需实现 - 从 ACP-local store 或 Hermes session store 加载历史消息,作为后续 prompt 的 context 来源
- 支持新模型继续已有会话
- 当前
-
分页消息查询
GET /client/sessions/{session_id}/messages?cursor=...&limit=...- 支持时间范围和角色过滤
-
用量记录
- 从 ACP
usage_update事件提取contextUsed/contextSize - 写入
sessions表的 usage 字段 GET /client/sessions/usage— 聚合统计
- 从 ACP
-
会话导出
GET /client/sessions/{session_id}/export?format=json|markdown- JSON 格式:完整结构化导出
- Markdown 格式:人类可读对话导出
-
ACP run payload 生命周期修复(bug 3-21)
- 停止在
stream_events中.remove(run_id)释放 payload - 改为基于 Convex session/runtime 记录或 TTL 缓存
- 停止在
Phase C:前端交互与高级功能(P2 — 用户体验提升)
-
前端会话列表
- Sidebar 或独立面板展示历史会话
- 支持切换、删除、重命名
- 支持继续已有会话
-
会话搜索 UI
- 搜索框,全字段搜索(标题、消息内容)
- 结果高亮 + 跳转
-
Token 用量可视化
- 每次 AI 调用后展示 token 消耗
- 会话级别统计累计用量
-
权限请求 UI
- Agent 发起
session/request_permission时前端弹窗确认 - 支持 auto-allow / auto-deny 配置
- Agent 发起
-
多会话并发
- 支持同时运行多个 ACP session
- 前端标签页切换
4. 技术方案
4.0 当前 Convex 源目录口径
当前仓库状态下:
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。
后续若要恢复完整业务 Convex functions,需要把历史 recycle/wolai-frontend/convex/ 中仍需要的 documents/workspaces/media 等 functions 正式迁移到根 convex/,不能继续依赖已删除的 wolai-frontend/ 路径。
4.1 存储方案
注意:以下结构是 Convex-backed ACP-local store 候选形状,不默认推翻 Hermes 持有聊天真相的既有边界。mnote-web 不新增 SQLite;多用户隔离字段是硬要求。
// 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 清理;infra/convex/只负责自托管服务部署 - 修改
acp_session_manager.rs— 按架构决策写入 ACP-local 消息或只写 runtime/event 索引 - 修改
hermes_client.rs— 新增 session 管理 route handlers - 修改
routes/mod.rs— 注册新 route - 修改 Convex bridge / transport 调用点 — mnote-web 通过已有 Convex 底座读写 session/runtime 索引
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 先确认数据契约
- 先确认是否允许 mnote-web 保存完整 AI session/message 真相;若不允许,本文存储范围必须限定为 ACP-local runtime cache / metadata index。
- 结论:本轮不保存完整 AI session/message 真相,只保存 ACP-local runtime run metadata / payload cache / index。
- 明确 Hermes HTTP 路径继续由 Hermes 持有 session/message/tool event/usage/model 真相,mnote-web 只保存引用、audit 或业务结果。
- 明确所有 AI session 记录必须带
userId,并按userId + workspaceId + documentId过滤,禁止跨用户读取。 - 明确 Convex
sessions记录的唯一键、排序键、软删除策略和保留周期。- 当前实现采用更小的
acp_runtime_runs:run_id唯一,按user_id + workspace_id + document_id/session_id + created_at排序,deleted_at软删除,payload 记录带 7 天 TTL 元数据。
- 当前实现采用更小的
- 明确 Convex
messages记录是否允许同一session_id下多次resume追加写入。- 结论:本轮不建完整
messages真相表;后续若引入只覆盖 ACP 路径的消息索引,允许同一session_id多 run 追加 runtime events,但不复制 Hermes HTTP 聊天真相。
- 结论:本轮不建完整
- 明确
session_id与run_id的映射是否需要单独持久化,避免仅靠内存 map。- 当前已在
acp_runtime_runs中持久化session_id/run_id映射。
- 当前已在
- 明确
profile、model、source三个字段在 ACP/Hermes HTTP/Reasonix 三种路径下的取值规则。- 当前实现:
source = "acp",profile沿请求 profile,acpRuntime沿请求或 profile 推导;model暂不落库,等待 usage/model 聚合阶段。
- 当前实现:
- 明确
usage统计的口径:只记usage_update,还是合并run.completed.usage。- 结论:
usage_update作为运行中实时增量;run.completed.usage作为最终校正值。两者都只落 ACP-local runtime/usage index,不复制 Hermes HTTP 聊天真相。
- 结论:
6.2 先做后端持久化骨架
- 为 ACP-local session/runtime store 设计 Convex schema,不新增 SQLite。
- 在 Convex 中建
sessions、messages或更小的runtimeRuns/runtimeEvents记录,补齐索引和权限过滤。- 当前实现为根
convex/schema.ts的acp_runtime_runs/acp_runtime_events,以及convex/aiSessions.ts的upsertRuntimeRun/getRuntimeRun/listRuntimeRuns。
- 当前实现为根
- 把
ACP_RUN_PAYLOADS/ACP_ACTIVE_RUNS中必须保留的数据拆到持久化层或短 TTL 层。- 当前
create_run会把 ACP payload/runtime 写入 Convex;内存 map 仍作为热路径保留,stream_events不再消费删除 payload。
- 当前
- 把
create_run产出的sessionId、profile、traceId、runtime写入sessions记录。- 当前写入目标为更小的
acp_runtime_runs,而不是完整聊天sessions真相表。
- 当前写入目标为更小的
- 在
stream_events的 ACP 路径里,按架构决策写入messages或 runtime/event 索引,不能把 Hermes HTTP 聊天真相复制进 mnote。- 当前实现:ACP SSE event 以 best-effort 写入
acp_runtime_events,只保存 runtime event payload,不写完整聊天messages真相。
- 当前实现:ACP SSE event 以 best-effort 写入
6.3 再补 session 读接口
- 让
GET /client/sessions返回当前用户可见的 Convex store 或 Hermes upstream 会话摘要,而不是只依赖前端 localStorage。- 当前实现:ACP profile /
source=acp走aiSessions:listRuntimeRuns;Hermes HTTP profile 仍走 upstream。
- 当前实现:ACP profile /
- 让
GET /client/sessions/{session_id}返回会话元数据、消息列表、runtime 状态。- 当前实现:
source=acp从 Convex store 读取 runs/events,messages保持空数组以避免复制完整聊天真相。
- 当前实现:
- 让
POST /client/sessions/{session_id}/resume真正从持久化历史恢复,而不是简单复用get_session。- 当前实现:
source=acp返回 Convex runtime history,并标记resumed=true/resumeSource=convex_acp_runtime_store。
- 当前实现:
- 为
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作用域更新。
- 当前实现:ACP route 调用
- 为搜索接口加分页和最小 snippet,避免一次返回过长正文。
- 当前实现:
GET /client/sessions/search?source=acp&q=...&limit=...调用searchRuntimeSessions,最多返回 50 条 session 摘要和 snippet。
- 当前实现:
6.4 再修 ACP 协议行为
- 为
session/request_permission生成结构化响应,不再只打日志。 - 给未知 incoming request 返回明确 error,避免 agent 一直等超时。
- 把 permission 结果同步到前端,至少先支持 auto-allow / auto-deny。
- 当前实现:
session/request_permission仍由 ACP client 自动拒绝以避免 agent 超时,同时转成permission.deniedSSE;页面 AI 侧展示权限弹窗/卡片,包含 tool 名、参数摘要、允许/拒绝动作入口。
- 当前实现:
- 让
usage_update与run.completed的 usage 汇总到 session 级统计。- 当前实现:
appendRuntimeEvent在收到usage.updated或带usage的run.completed时,同步更新acp_runtime_runs.usage。
- 当前实现:
- 验证
thought.delta仍不会落入最终 assistant 正文。- 当前验证:
acp_thought_delta_does_not_emit_message_delta确认 thought 只作为thought.delta转发。
- 当前验证:
6.5 再补前端入口
- 在页面 AI 面板中加入后端会话列表,而不是只显示 localStorage 的短期历史。
- 当前实现:页面 AI history 面板打开时会请求
GET /api/hermes/client/sessions?source=acp&workspaceId=...&documentId=...,并与 localStorage 短期缓存合并。
- 当前实现:页面 AI history 面板打开时会请求
- 会话列表支持切换、重命名、删除、恢复。
- 当前实现:history 行内提供恢复、重命名、删除按钮;切换会话时会拉取 Convex-backed detail/resume。
- 会话详情页或抽屉支持查看消息分页与搜索结果。
- 当前实现:恢复/详情会读取最新 run 的 events 并映射为消息视图;history 面板提供后端 session 搜索框,搜索结果展示 snippet。当前消息详情读取后端最近 200 条 event,尚未做 cursor 翻页。
- 权限请求弹窗至少展示 tool 名、参数摘要、允许/拒绝动作。
- 当前实现:
permission.requested/permission.denied/permission.allowed会生成权限弹窗和消息卡片;当前 ACP incoming request 默认 auto-deny,按钮用于前端状态确认,后续可接入真实审批回写。
- 当前实现:
- Token / 用量信息在会话条目和运行状态上可见。
- 当前实现:会话条目和当前 session 状态展示
usage.updated/run.completed.usage汇总。
- 当前实现:会话条目和当前 session 状态展示
6.6 最后做回归验证
- 新建 ACP 会话后刷新页面,仍能从 Convex-backed store 或 Hermes session store 重新拿到同一会话。
- 当前实现:ACP profile 创建 session 时写入
aiSessions:upsertRuntimeRun的session.created索引记录;刷新后列表可从 Convex-backed store 找回该 session。
- 当前实现:ACP profile 创建 session 时写入
- 用两个测试用户分别创建 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。
- 当前实现:Convex functions 使用
- 断线重连后,
stream_events能从历史继续恢复,而不是丢失 payload。- 当前最小验证覆盖 payload lookup 不再 remove;完整断线重连仍需浏览器 smoke。
session/request_permission会返回可验证的协议响应。message.delta、thought.delta、tool.*、run.completed的 SSE 映射在浏览器里都能看见正确结果。- 当前实现:页面 AI stream handler 映射
message.delta、thought.delta、usage.updated、tool.*、run.completed;Rust/JS 静态测试覆盖前端入口,尚未完成真实浏览器截图级验证。
- 当前实现:页面 AI stream handler 映射
- 运行一次真实浏览器 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表和索引。 - 新增根
convex/aiSessions.ts:upsertRuntimeRun、getRuntimeRun、listRuntimeRuns,按userId校验 identity 作用域。 - 修改
scripts/run-convex-deploy.js:优先以仓库根convex/作为 Convex deploy cwd,避免继续指向已不存在的wolai-frontend/。 - 后续修正:根
convex/当前只包含 ACP session runtime store,不能直接覆盖完整本地 Convex 部署;scripts/run-convex-deploy.js已改为只有根convex/具备完整 schema 时才用根目录,否则继续使用现有完整 Convex 源。为保持本机3210部署完整,已将aiSessions.ts与acp_runtime_*schema 同步到现有完整 Convex 源并重新部署。 - 修改
rust/crates/mnote-web/src/routes/hermes_client.rs:- ACP
create_run写入aiSessions:upsertRuntimeRun; - ACP
GET /client/sessions读取aiSessions:listRuntimeRuns; - ACP
GET /client/sessions/{session_id}读取 runs/events; - ACP
POST /client/sessions/{session_id}/resume返回 Convex runtime history 恢复来源; - ACP
DELETE /client/sessions/{session_id}、rename、auto-title写入 Convex store; - ACP
GET /client/sessions/search返回 Convex store 搜索摘要和 snippet; - ACP
stream_events将 SSE event 写入aiSessions:appendRuntimeEvent; stream_events使用 clone lookup 保留 ACP payload,不再.remove(run_id)。
- ACP
- 验证:
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:通过。node scripts/run-convex-deploy.js:部署入口已避免用根最小convex/覆盖完整本地 Convex schema;aiSessions.ts同步补到完整 Convex 源并部署到http://127.0.0.1:3210,最终脚本验证为No indexes are deleted by this push。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:通过。- 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 目录。
- 根