## 背景:#81 护栏审查定档(P0~P2) 对 `89a6cca`(v0.3.4 空终帧护栏)的审查结论:转换层实现本身是好的,但它守的是 Chat→Responses 转换 finalize 这道门,而本机 dogfood 全程走 Responses 原生方言 (`protocol = "responses"`,MLX 直接回 Responses SSE)。实测两个故障窗口与常驻 v0.3.4 至今的全部 `stream_open` 均为 `Responses`、`empty_frame_*` 事件 0 次—— **当前机器上的自动结束,v0.3.4 一点没管**。 审查逐条定档: - **P0** 护栏需落到 Responses 原生透传支路:上游直接回 `response.completed` 且 output 只有 reasoning(或空)时同样构成静默终帧,透传路径必须能扣帧、重发、并让 客户端拿到单一连续的流。 - **P1** 重试轮缺 `stream_open` 侧可观测性:监看看不出一次客户端轮实际打了多次上游。 - **P1** `refusal` 会被误判成静默并重发——拿推动文案顶回一次合规拒绝,可能顶穿 安全边界。`refusal` 必须算有效输出。 - **P1** 推动文案以 user 消息注入,与轮次标记/回显有打架风险:措辞必须显式禁止 复述,且不进客户端历史(这条 #81 已对)。 - **P2** 推动文案语言写死中文,会把非中文会话拽出中文;且「会话被自动结束」是 元叙述,不该进模型上下文。语言应跟随会话。 - **P2** 被丢弃静默轮的 usage 在诊断里彻底消失——恰恰是最该盯的数(预算全烧在 reasoning 就是根因本身)。 ## 方案 1. 新增原生支路的事件感知守护器(`NativeResponsesGuard`): - 透传的唯一前提是「这一帧可分类」:无法解析或超预算(2MB)的事件一律原样透传, 宁漏判不误伤;守卫开启后透传由「chunk 原样」变「事件边界对齐」,字节语义不变。 - 首次尝试:静默 completed(判据与 Chat 方言同一条纯函数)连同其后尾字节 (含 `[DONE]`)整体扣下,交装配层决策;非静默/不可分类即刻原样透传。 - 重试轮:`response.created`/`in_progress` 丢弃(客户端已见一轮响应头); 带 `output_index` 的事件与 `item` 内嵌索引整体位移;终帧改写为 「历史轮已出门条目 + 本轮条目」的合并形态,`response.id` 回填为首轮 id, 保证整条流单一、连续、id 一致。 - 重发窗口与推动文案复用既有机器(同体重采样 → 推动文案),推动文案注入前 先剥本层此前注入的同类项,防跨轮累积。 - 红线不变:额度用尽或重发失败把扣下的帧**原样放行**,绝不静默断流。 2. `silent_final_frame` 补 `refusal`:message 项里的 refusal 部件与字符串形态的 非空 content 都算有效输出,不重发。 3. 推动文案双语(跟随会话最近语句语言,CJK 判据),删「会话被自动结束」元叙述, 加「不要提及或复述本条提示」。 4. 可观测性:`empty_frame_retry` 事件带被丢弃轮的 usage(`droppedUsage`); 终态 `stream_completed` 事件带 `attempts`(本轮实际打了几次上游),监看可配对。 ## 兼容口径声明(明示) 原生方言的流式透传自本单起不再是「任意字节流原样转发」:守卫开启后,转发以事件 边界对齐,超预算帧原样透传但顺序保证不拆帧。这是实现原生护栏的必要代价,写进 代码注释与本单,不留暗账。
Activity