Skip to content

原生方言空终帧护栏:把守门挪到真正有流量的透传支路 - #84

Open
GGGBB wants to merge 1 commit into
mainfrom
agent/83-native-empty-frame-guard
Open

GGGBB wants to merge 1 commit into
mainfrom
agent/83-native-empty-frame-guard

Conversation

@GGGBB

@GGGBB GGGBB commented Oct 16, 2026

Copy link
Copy Markdown
Owner

关联 issue

Closes #83

变更摘要

本机 dogfood 全程是 Responses 原生方言,而 v0.3.4 的空终帧护栏只住在 Chat→Responses 转换层,对这条支路上游直接回的 response.completed 零覆盖:模型把 output 预算全花在 reasoning 上、终帧没有可见正文也没有调用项时,客户端判「本轮无动作」立即交回控制权,用户侧就是反复出现的「会话自动结束」。

本 PR 把守门挪到真正有流量的支路:新增 NativeResponsesGuard,在事件边界上缓冲上游字节,用与 Chat 方言同一条判据(native_response_is_silent)判静默。判成静默就扣下终帧连同其后已到字节,同上游重发(第一次同体重采样、第二次带推动文案,上限沿用 EMPTY_FRAME_MAX_RETRIES);重发轮的条目事件按位移改写、终帧合并、响应身份回填为首轮 id,客户端全程只看到单条 created 与单个 completed。

同时钉死审查定档的三条判据问题:refusal 部件与非空字符串 content 算有效输出(合规拒绝不再被误判重发);被丢弃轮的 reasoning/total token 在扣帧时入账,empty_frame_retry / empty_frame_exhausted 带 droppedUsage,stream_completed 带 attempts,白烧的预算与真实上游轮次都看得见;推动文案跟随会话语言、去掉元叙述、尾部带识别标记并在注入前剥除历史同类项,轮次标记豁免推动项(重发轮仍钉在真实用户指令上,与非重试轮逐字节同形)。

行为影响面

  • 兼容口径(必须写明):护栏开启后,原生透传由「chunk 原样」变为「事件边界对齐下发」。SSE 事件本就以空行为完整边界,客户端解析器按事件消费,不依赖 TCP chunk 切分——这是本层既有「事件完整性」假设的显式化,不是新语义。
  • 重发轮的重复身份事件(created / in_progress)不再出门;被丢轮的 [DONE](若上游发)随该轮一并丢弃,终态由终帧事件表达。
  • 客户端可见变化只在「上游本轮静默」这一形态上:原本立即拿到静默 completed 并结束回合,现在同一客户端轮内多最多两次上游重发,拿到带正文的合并终帧。非静默流逐字节不变。
  • 压缩轮(含降级轮)不开护栏;上游自报 failed / incomplete / error 的流不拦不改;护栏开关不开时行为逐字节不变(有红线测试钉住)。
  • Chat 方言侧的改动限于审查项:判据补 refusal 与字符串 content、诊断字段 droppedUsage / attempts、推动文案双语化与历史剥除、轮次标记豁免。轮次标记豁免只在请求以推动项收尾时生效,普通请求路径逐字节不变。
  • 巨帧(>2 MiB 单事件)或迟迟等不到事件边界的字节:整段原样直通并永久失能本轮护栏,补帧收口机器照常。

测试证据

  • cargo test --all-targets:lib 212 + 集成 220,0 失败(基线 lib 202,测试只增不减)。
  • 新增 native_empty_frame_guard_tests 单元对拍 10 条:判据全形态(reasoning-only / 空 output / 空白正文 / 可见正文 / 字符串 content / 工具调用项 / refusal / 非 completed 终态)、护栏关闭逐字节不变、静默扣帧与逐字节放行、非静默不误伤、重发轮合并与位移与 id 回填、连续静默扣合并帧并累加 usage、上游失败事件不改写、超限字节退化为直通、推动文案双语选择与历史剥除、轮次标记豁免。
  • 新增 tests/native_empty_frame_guard.rs 端到端 2 条(真实二进制 + 假原生上游 + 临时端口 + 临时配置):首轮静默重发一次后客户端拿到单条 created 与单个合并终帧(首轮条目垫底、本轮正文接位、id 回填首轮、output_index 位移、无 failed),第一次重试同体;连续静默打满额度仍出门一个合并 completed(绝不静默断流),第二次重发体带推动项且推动项不含轮次标记、不进客户端历史;诊断账 droppedUsage 与 attempts 实测对得上。
  • python3 scripts/smoke/pinning.py:PASS(4/4);python3 scripts/smoke/streaming.py:ALL PASS。
  • 端到端均为集成测试形态(临时端口 / 临时配置 / 假上游),未触碰本机常驻 com.codex-layer.proxy 与 com.user.mlx-ssm-env。真机 MLX 往返按部署计划在合并后进行。
  • 夹具说明:tests/responses_dialect_stabilizer.rs 与 tests/tools_cache_persistence.rs 的 mock 终帧原本不带 output 条目,正是护栏的触发形态,已补可见正文并在注释点明理由——这两处测的是稳定层可见性与 tools 回填,不应顺带点燃护栏。

改动可比性与回归风险

触及 protocol_proxy.rs(新增守护器与判据修正)与 main.rs(两条支路的接线)。不改行为的搬运与本 PR 的行为演进分开成一次提交不可行——本 PR 全部改动都服务于同一行为演进,无搬运混入。回归风险集中在三点,各有测试钉住:护栏关闭时的逐字节一致性、扣帧后任何收口路径的放行完整性、观察者收口证据随轮次重开(否则重发轮半途断流会被误判成已完成)。重发把最坏情况的上游轮次抬到 3 次 prefill,代价由 EMPTY_FRAME_MAX_RETRIES 封顶,且真实轮次经 attempts 可观测。

自查清单

  • 已关联 issue,type/* 与 P1/P2 标签已打
  • commit author 使用角色名(codex-layer-stabilizer)
  • cargo test 全绿,测试只增不减
  • 两条冒烟脚本全绿
  • 端到端自测仅用临时 Label + 临时端口 + 临时配置,未触碰本机常驻 com.codex-layer.proxy 与 com.user.mlx-ssm-env
  • 无真实密钥入库,配置示例用占位符
  • 未做全仓 cargo fmt,不改行为的改动与行为改动分开提交

@GGGBB GGGBB added type/bug 正确性缺陷或行为回归 P1 高优先级:语义漂移或重要能力缺口 breaking-change 涉及对外可感知契约(配置项、端口、字段形态)的变更 labels Oct 16, 2026
@GGGBB

GGGBB commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

会话内作者指示留痕(逐字)

用户在会话内对上一轮审查结论的处置指示原文:

直接上手

审查结论中对应用户贴入的判定原文(本 PR 的规格来源):

一句判断:护栏的转换层实现是好的,但它守错了门。真正该补的是 Responses 原生透传支路的静默终帧护栏(MLX 直接回 response.completed 且 output 只有 reasoning 或空),外加把 P1 三条判据钉死。

本 PR 覆盖:原生透传支路护栏(P0)+ refusal 误判 / 重发可观测 / 推动文案 user 注入风险(P1×3)+ 文案语言与元叙述、丢弃轮 usage 可见性(P2×2)。

按流程本 PR 不自合:CI 转绿后交回维护者,等待维护者在 PR 上留下批准口径再走 rebase 合并。

本机 dogfood 全程走 Responses 原生方言,护栏此前只住在 Chat 转换层,
对这条支路的静默终帧零覆盖。现在 NativeResponsesGuard 在事件边界上
判静默、扣终帧、同上游重发,重发轮条目按位移续排、终帧合并、身份回填,
客户端始终只看到单条 created 与单个 completed。

判据与红线:
- 静默判定与 Chat 方言共用 native_response_is_silent 本体;refusal 与
  非空字符串 content 算有效输出,合规拒绝不再被误判重发;
- 扣帧后后到字节全部并入暂存区,任何收口路径原样放行,绝不静默断流;
- 上游自报 failed/incomplete/error 不拦不改;观察者永吃原始字节,
  重开一轮时收口证据同步重开,防止「重发轮半途断流」被误判成已完成;
- 巨帧或等不到边界的字节整段直通并永久失能,护栏永不无界缓冲。

可观测与文案:
- 被丢轮的 reasoning/total token 在扣帧时入账,retry/exhausted 事件带
  droppedUsage;stream_completed 带 attempts,监看能看见真实上游轮次;
- 决策日志先于重发落盘;重发轮补记 stream_open 的口径改为 attempts
  内嵌在配对事件里,不制造无终态的孤儿 open;
- 推动文案跟随会话语言(CJK 判中/英),去掉「会话被自动结束」的元叙述,
  尾部带识别标记并在注入前剥除历史同类项,禁止复述本条提示;
- 轮次标记豁免推动项:重发轮仍钉在真实用户指令上(与非重试轮逐字节
  同形),剔除推动项后非 user 收尾则维持不钉。

兼容口径:护栏开启后原生透传由「chunk 原样」变为「事件边界对齐下发」。
SSE 事件本以空行为完整边界,客户端按事件消费,不依赖 TCP chunk 切分。

夹具说明:两处原生 mock 的终帧原本没有 output 条目,正是护栏的触发形态,
已补可见正文并在注释里点明理由(这些测试测稳定层与 tools 回填,不应
顺带点燃护栏)。

测试:cargo test --all-targets 全绿(lib 212 + 集成 220,0 失败);
新增原生护栏单元对拍 10 条与端到端 2 条(单创建帧/合并终帧/id 回填/
位移/额度用尽仍出门/推动项只进重发体);pinning 与 streaming 冒烟 ALL PASS。
@GGGBB
GGGBB force-pushed the agent/83-native-empty-frame-guard branch from f43ec2c to b455d79 Compare October 16, 2026 02:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change 涉及对外可感知契约(配置项、端口、字段形态)的变更 P1 高优先级:语义漂移或重要能力缺口 type/bug 正确性缺陷或行为回归

Projects

None yet

Development

Successfully merging this pull request may close these issues.

原生 Responses 支路的空终帧护栏 + 审查修正(refusal/语言/可观测性/usage 记账)

2 participants