Skip to content

feat: 带图发送延迟优化、MCP 工具统计归并与会话关闭的子进程回收 - #96

Merged
Jaxton07 merged 3 commits into
mainfrom
feat/image-send-latency-mcp-summary
Oct 8, 2026
Merged

Jaxton07 merged 3 commits into
mainfrom
feat/image-send-latency-mcp-summary

Conversation

@Jaxton07

@Jaxton07 Jaxton07 commented Oct 8, 2026

Copy link
Copy Markdown
Owner

本 PR = 本地积压的 3 个提交(内容与本地 main 逐字节一致,git diff main HEAD 为空)。

1. fix(session) 关会话时先发 session_shutdown,修 MCP 子进程泄漏

SDK 的 AgentSession.dispose() 只 invalidate() 扩展 runner、不发 session_shutdown,而官方内置扩展里只有 mcp 靠这个事件释放资源 → 关掉的会话其 MCP stdio server(uvx/node 子树)永久常驻,只有整个 app 退出才清。修法与官方 AgentSessionRuntime.dispose() 对齐(走公开 API 等价复现 emitSessionShutdown():extensionRunner 是 public getter、ExtensionRunner 从包根导出),调用点 = PiBackend.disposeSession + SessionRegistry.disposeAll。

2. feat(chat) MCP 工具调用按 server 归并成一段统计

MCP 工具原先落到 other 类目 → 按工具名各自成段,一个 server 有几十个工具时状态行会被撑爆(blender › execute_blender_code ×1 · blender › look ×1 · blender › get_scene_info ×1)。现在新增 mcp 类目,按 server 归并成一段 blender ×3;展开区明细与圆点行不变。统计文案在桌面与 lan-web 两处同步;MCP 段不走 displayName(server 名可能与内置工具同名而被翻译串味)。

3. feat(chat) 带图发送即时回显 + 附件预缩放,修「发送后气泡要等 ~1s」

成因:SDK 1.0.4 的 _normalizePromptImages() 在构造用户消息之前 await 每个附件的 processImage()(Photon WASM 每图各起一个 worker,Lanczos3 缩放 + PNG/JPEG 试编码取更小),全屏 Retina 截图实测 758–1089ms/张(同一条消息 2 张 ≈1.7s);渲染端那时已清空输入框的图片托盘、又还没有乐观用户消息,于是那段窗口里对话区什么都没有。

修法:

  • 乐观用户气泡:发送瞬间本地插一条同位置气泡,权威 message_start(user) 到达时原位替换(而非追加),prompt 抛错则撤回。只在空闲发送时插(运行中发送走队列、QueueBar 已在显示;斜杠命令可能不产生用户消息)。纯 UI 态,不入历史/trace/磁盘。
  • 附件预缩放:粘贴/选图时就按 SDK 同规格(2000×2000 / 4.5MB base64 / jpeg q80,镜像 image-resize-core.js 的 DEFAULT_OPTIONS;模型目录 1125 条 inputLimits.images.resize 全是这组)缩到限内 → SDK 只走「已在限内」快路径;在限内则原图原样(连 base64 都惰性读),失败一律退回原图。预缩放把「粘图→可发」拉到几百 ms,所以发送前会 await 在途预处理,避免「粘图立刻回车」丢图。

实测(dev,2880×1800 / 5.42MB PNG):

指标 修复前 修复后
点发送 → 气泡出现在对话区 ~1064ms 14ms
主进程 prompt → 用户消息事件 758–1089ms 75–93ms
附件预处理(2880×1800 / 5.42MB) 在发送路径上阻塞气泡 移到粘贴时:194ms(热)/413ms(冷),产物 2000×1250 JPEG 0.17MB

会话 jsonl 里不再出现 SDK 的 [Image: original …] 提示(= 确认 SDK 真没再缩一遍);同一文本在气泡里只出现一次(认领未产生重复)。

验证

  • 本地:npm run lint / typecheck / test(765 项)/ build 全绿;dev 真实会话端到端实测(上表)。
  • CI:lint + typecheck + test + build + electron-builder --dir 打包冒烟。
  • 已知取舍与踩坑全量复盘写在 docs/PITFALLS.md(含「streaming 中发送的队列路径不归一化附件」这条 SDK 侧遗留问题)。

SDK 的 `AgentSession.dispose()` 只 `invalidate()` 扩展 runner、**不发 `session_shutdown`**
(pi 1.0.4 `core/agent-session.js:988`),而官方内置扩展里**只有 mcp** 靠这个事件释放资源
(整包 bundle 里 `pi.on("session_shutdown", …)` 只出现一处)。于是关掉的会话其 MCP stdio
server 永久常驻:`uvx`/`node` 那棵子树照旧活着,还占着 server 的连接,只有整个 app 退出才清。
实测:连开带关 3 个会话 → 留下 3 个 `uvx`,且宿主日志显示会话已 `closed` 而子进程仍在。

修法与官方 `AgentSessionRuntime.dispose()` 对齐,但它内部用的 `emitSessionShutdownEvent`
没从包根导出(exports map 只开 `.`/`./rpc-entry`/`./client`/`./experimental/plugin`),
所以走公开 API 等价复现:新增 `session/shutdown.ts` 的 `emitSessionShutdown()`
(`session.extensionRunner` 是 public getter,`ExtensionRunner` 从包根导出)。
调用点两处:`PiBackend.disposeSession`(closeSession/deleteSession,`dispose()` 前 await)、
`SessionRegistry.disposeAll`(`before-quit` 的同步收尾,只能 fire-and-forget)。

两处防御:

- `emitSessionShutdown` 把同步异常转成 rejected promise。它对调用方的契约必须是
  「要么返回 Promise、要么 reject」——`session.extensionRunner` 缺失时访问 `.emit`
  会**同步抛** TypeError,而调用方的 `.catch()` 对同步异常无效,会连带跳过
  `session.dispose()`,正好复现本修复要防的「漏掉清理」。
- `disposeSession` 用 `.catch(log.warn)` 兜住,派发失败不阻断后续 registry/gate/traces 清理。

验证:

- 单测 `test/session-shutdown.test.ts`:顺序正确 / emit 失败不阻断 dispose / disposeAll 也发
- `test/channel-watch-extension.test.ts` 补「显式 cleanup + session_shutdown 双路径幂等」——
  这是本次引入的新组合,既有用例只覆盖单路径
- dev 端到端 + **对照实验**(临时移除修复):无修复时关会话后子进程存活,恢复后回收
- 顺带修 `test/mcp-reload-guard.test.ts` 的 agentDir 隔离:它原本直接读写开发者真实的
  `~/.pi/agent/mcp.json`,断言一旦失败用例后半段的 remove 不执行,测试数据会残留

文档:`docs/PITFALLS.md` 记条目(含「用宿主日志对齐子进程启动时刻」「用 `ps eww` 看
`__CFBundleIdentifier`/`PI_SESSION_ID` 判子进程归属」两个诊断手法);`docs/INDEX.md` 补
`session/shutdown.ts`。
- shared/transcript/meta-summary: 新增 mcp 类目,categoryOf 认 mcp__<server>__<tool>;
  summarizeCategories 按 server 归并(key = mcp:<server>,新增 server 字段),
  前缀不成形的仍归 other
- 统计行文案:桌面 meta-summary-label 与 lan-web MetaGroup 各加 mcp 分支,
  server 名原样呈现(不走 displayName,避免与内置工具同名被翻译串味)
- 明细不丢:展开折叠组仍是逐个工具卡;圆点行不变(一次调用一粒)
SDK 1.0.4 的 `AgentSession._normalizePromptImages()` 在构造用户消息**之前** `await` 每个附件的
`processImage()`(Photon WASM 每图各起一个 worker,Lanczos3 缩放 + PNG/JPEG 试编码取更小),
全屏 Retina 截图实测 758–1089ms/张;渲染端那时已清空输入框的图片托盘、又还没有乐观用户消息,
于是那段窗口里对话区什么都没有(观感 = 发出去了却半天不出现)。两处修:

- **乐观用户气泡**(shared `transcript/pending-user.ts` + reducer 认领分支):发送瞬间本地插一条
  同位置气泡,权威 `message_start(user)` 到达时**原位替换**(而非追加),prompt 抛错则撤回。
  只在空闲发送时插(运行中发送走队列、QueueBar 已在显示排队行;斜杠命令可能不产生用户消息)。
  纯 UI 态:不入历史、不入 trace、不写盘。
- **附件预缩放**(renderer `lib/image-attachment.ts` + 纯函数 `image-fit.ts`):粘贴/选图时就按
  SDK 同规格(2000×2000 / 4.5MB base64 / jpeg q80,镜像 `image-resize-core.js` 的 DEFAULT_OPTIONS)
  缩到限内,SDK 于是只走「已在限内」快路径;在限内则原图原样(连 base64 都惰性读),失败一律退回原图。
  预缩放把「粘图→可发」拉到几百 ms,所以 Composer 发送前会 await 在途预处理
  (`preparingRef` 收集 + `latestSendRef` 调最新一份 handleSend),避免「粘图立刻回车」丢图。

实测(dev,2880×1800 / 5.42MB PNG):托盘缩略图 194–413ms 出现 2000×1250 JPEG 0.17MB;
点发送 → 气泡 14ms(修复前同类截图 ~1064ms);主进程侧 prompt→用户消息事件 75–93ms;
会话 jsonl 里不再出现 SDK 的 `[Image: original …]` 提示(= SDK 真没再缩一遍)。

顺手:pending 期间不显示「撤回」(后端还没这条消息)。

测试:`lib/image-fit.test.ts`(规格纯函数)+ `stores/transcript.test.ts`(乐观插入/认领/撤回/
位置与「夹了 system 通知行」的边界)。docs/INDEX.md、docs/PITFALLS.md 同步更新。
@Jaxton07
Jaxton07 merged commit 2277978 into main Oct 8, 2026
1 check passed
@Jaxton07
Jaxton07 deleted the feat/image-send-latency-mcp-summary branch October 8, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant