M19 复审抓到的真缺口:internal/services/gateway/proactive.go 的 SendProactive -> persistProactiveToMain 这条 Web 活跃客户端主动推送路由 完全绕过 turnreply.NormalizeParagraphs —— 同一条主动消息走 IM 是规整过的、 走 Web 实时推还是原样(半包 <p> 在客户端按「存在完整 <p> 就不再按空行回退」 的契约下整条塌成一坨)。 - 接线:SendProactive 在落库之前先 msg.Content = turnreply.NormalizeParagraphs(msg.Content), 落库与 proactive_message 帧取同一个字符串 ⇒ 两端逐字相同;不在 gateway 另写实现。 - 新增 proactive_normalize_test.go:修前红(web 原样 / im 已规整),修后断言 ① 落库是规整后文本 ② 推帧 == 落库行 ③ 两条路由同源同文本 ④ <br>/<action> 语义不动。 - 既有两条断言按新口径更新(纯散文现在也被包成一个 <p>,与其它三处一致)。
修主人报的硬故障「后台思考的主动推送一个都没有推出来」(根因两条,缺一不可): - 后端 gateway/proactive.go 只广播 proactive_message 帧、**从不写会话历史** ⇒ 刷新补不回来 - 前端**零消费点** ⇒ 帧被静默丢弃(既无气泡也无通知) 后端:新增 proactive_persist.go(**先落库、再发帧**;web/assistant/mode=chat;未接线时空操作且不谎称已落库) 载荷新增 id/createdAt(落库行权威 id);PushResult.Detail 三态如实分开报告「投递到几个连接」与「是否落库」 前端:handlers/proactive.ts(气泡**恒落** + 按落库 id 去重 + **非前台才补通知** + 拒绝授权/畸形载荷可见降级) + lib/notify/proactive.ts(纯逻辑 isBackground/decideProactiveNotice/有界去重表) + handlers/task.ts(另三个被丢的帧 task_done/automation_result/subagent_result) + ws/resync.ts + useWsResync.ts(**重连重跑本页引导读取**,首次连接不跑) 双份核查(带三层证据):现状不双份 —— 路由分叉「一次 Push 至多一个 sink 成功」,且 IM 路由本来就把主动消息镜像回主会话恰好一次 契约 v0.72;变异实测:只发帧不落库 / 写成 user 角色 / 去重失效 / 前台也弹 / IM 成功再打活跃 ⇒ 全部打红