Skip to content

[Bug] ask 答复被注入到无关会话:ask-question-handler 在 document 上注册点击委托且 dispose 被丢弃,SPA 切换会话时持续泄漏监听器 #111

Description

@A-BOT-GIT

[Bug] ask 答复被注入到无关会话:ask-question-handler 在 document 上注册点击委托且 dispose 被丢弃,SPA 切换会话时持续泄漏监听器

项目:ygncode/pi-web(Go 版,本机版本 0.0.1-beta.36)
备注:repo 当前 issue 创建受限("Issue creation is restricted"),本文档可通过维护者渠道提交,建议附文中的 JSONL 时间戳证据与 file:line。

现象

在 Web UI 的 A 会话页点击内置 ask(pi_web_ask_user_question)弹窗卡片的选项按钮后,除当前会话正常收到答复外,其他无关会话也被注入了同样的 "问题" = "答复" 格式的用户消息,并且每个无关会话各执行了完整的 agent 轮次(模型被无意义地唤醒数轮)。

注入次数呈现明显的不对称模式:

时间点(UTC) 答复内容 S1 注入次数 S2 注入次数 S3(实际操作页)
19:57~19:58 答复 A 3 3 正常 1
20:03 答复 B 1 0 正常 1
次日 05:32 答复 C 0 0 正常 1

根因

纯前端事件监听器泄漏,导致一次点击触发多个绑定旧会话的处理器发送消息。与服务端、扩展均无关。

泄漏链路(file:line,版本 0.0.1-beta.36)

  1. document 级事件委托web/src/components/session/chat/ask-question-handler.js:66 使用 documentImpl.addEventListener('click', onClick) 把点击处理器注册在 document 上,并在 L67-70 返回 {dispose, onClick}
  2. dispose 被丢弃web/src/components/session/chat/chat-composer-runtime.js:205-208 调用 setupAskQuestionHandlers丢弃了返回值(同文件中 setupSteerQueue L210、setupWorkerStatusPolling L223、contextPopover L246 也同样丢弃返回值);
  3. 无聚合清理runChatComposer(L29-278)本身不返回聚合 dispose;ChatComposer.svelte:56-93 的 onMount 也丢弃 runChatComposer 的返回值,其清理函数只移除了 pi-queue-event 监听;
  4. SPA 切换会话销毁重挂web/src/App.svelte:69-72 使用 {#key sessionId}<SessionPage />{/key},每次切换会话页销毁并重挂整棵组件树——但 document 级监听器不随组件销毁。于是每访问一个会话页,就永久泄漏一个闭包持有该会话 sendChatMessage(旧 sessionId)的 document 级点击监听器;
  5. 错误发送:之后在任何当前页点击 ask 选项按钮,所有泄漏处理器都通过 event.target.closest('.ask-question-option-action')(ask-question-handler.js:35-49)从当前 DOM 读到同一按钮的 question/answer,各自调用 sendChatMessage("问题" = "答复")(L47),经 chat-api.js:1-11 POST /api/chat?id=<旧sessionId> 注入各自的旧会话。

为什么注入次数 = 页面访问次数

一次点击时,当前 tab 内所有泄漏处理器同时入队发送,因此:

  • 答复 A 前 S1/S2 各被访问过 3 次 → 各注入 3 次;
  • 答复 A 与 B 之间发生过一次整页刷新(S1 在 20:02:13.787 有真实手打消息佐证用户刚访问过 S1 页)→ 刷新清空旧泄漏、S1 重新挂载 1 次 → B 时 S1×1、S2×0;
  • 答复 C 是次日全新浏览器会话 → 无泄漏 → 0 注入。

证据

一手证据(会话 JSONL,UTC)

  • 注入消息文本与 ask-question-handler.js:47 模板逐字符一致("…吗?" = "看不到/没有");
  • 三个问题的 pi_web_ask_user_question 工具调用**只存在于 S3(会话 64a27202)**的 JSONL 中(19:56:22 / 20:02:51 / 20:36:30);S1(bb3ff899)无相关 ask 调用(19:53:47 的命中是 bash toolResult 正文文本),S2(01a04e7e)完全没有 → 被点击的卡片只可能在 S3 页面 → S1/S2 的注入只能来自绑定旧会话的泄漏处理器;
  • 串行化证据(排除人工多次点击):S1 注入 Frontend scalable rewrite: Vite-based session frontend + split live/export templates #2 出现在其助手回复后 2ms(19:58:35.954 vs .952)、feat: session info naming and auto-reload scheduling #3 同样(19:58:48.653 vs .650);S2 同构。一次点击产生的 3 个 prompt 同时入队 S1 的 worker,逐个执行完整 agent 轮次(29-51s),轮间毫秒级衔接。S1 的助手甚至回复过「你连续三次都在强调同一件事」。

服务端排除(路由正确,无广播 bug)

  • internal/server/chat.go:29-73 handleChat?id= 定向对应 worker;
  • internal/server/server.go:466-490 SSE broadcast 按 c.sessID 过滤;
  • internal/workers/manager.go:165-184, 310-353 Send/workerFor 按 sessionID 路由。

扩展排除

~/.pi/agent/extensions/ui-sync/ 全部代码只调用 ctx.ui.setStatus/setWidget/notify,无 fetch、无 HTTP、无聊天路由(grep fetch|/api/chat|sendChat 无命中)。

上游检索

GitHub 上无同类报告(关键词 ask answer / cross-session / injected / wrong session,open+closed 两轮检索)。相关度最高的仅 #102「Web UI 不渲染内置 ask_question」(已关闭,不同 bug)。

修复建议

治本(最小改动):让 runChatComposer 收集并返回聚合 dispose(覆盖 setupAskQuestionHandlers / setupSteerQueue / setupWorkerStatusPolling / contextPopover 等全部子控制器),ChatComposer.svelte onMount 的清理函数调用它。

注意:同一批还有 steer-queue 的 window 监听器worker-status 的 setInterval 轮询也在以同样方式泄漏(每访问一次会话页泄漏一个轮询 interval),应一并处理。

加固(可选)

  • 把点击委托从 document 收窄到随组件销毁的容器元素(composer form / messages 区);或
  • onClick 内校验目标卡片属于当前会话(按钮带 data-session-id 且与闭包 sessionId 匹配才发送)。

用户侧规避

点 ask 选项前,若曾在同一浏览器 tab 内切换过其他会话页,先整页刷新当前页(刷新即清空全部泄漏监听器)。重启 pi-web 服务端无效(纯前端 bug)。症状识别:答复后无关会话出现重复的 "问题" = "答复" 消息即中招。

复现要点

  1. 打开 pi-web,依次访问 3 个不同会话页(触发 3 次重挂);
  2. 回到最后一个会话页,触发 ask 弹窗并点击任一选项;
  3. 观察前两个会话:各被注入 1 条 "问题" = "答复" 消息并各自执行 agent 轮次。

环境信息

  • pi-web:Go 版单一实例(pi-web.exe:31415),内嵌前端与 npm 包 @ygncode/pi-web 0.0.1-beta.36 同版本;
  • 浏览器端无 devtools 抓取(泄漏处理器计数由 JSONL 注入计数反推)。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions