Skip to content

Chat 方言的静默终帧导致会话自动结束:中继侧空终帧护栏 #80

Closed

Description

@GGGBB

现象(dogfood 实测)

本机链路 codex 客户端 → codex-layer → mlx-serve 上,会话反复出现「无人安排的自动结束」:
task_started 后 30–60 秒即 task_complete,且最后一条 agent 消息正文为空。

对近三天 68 份会话 rollout 的逐条核对(判据:一轮内零工具调用且最后 assistant 消息
无可见正文)钉死了形态:模型只产出 reasoning 就结束——finalize 后的 completed
终帧里没有任何工具调用项、没有非空可见正文(一条实测 output 1623 token 全部是
reasoning)。这不是 max_tokens 截断:这些收尾的 output 量在 150~8000 之间,从未落在
4096/16384 这类整额上限上;finish_reason 是 stop 而非 length。

协议层的投影:Responses 客户端把「completed 且无调用项」判成本轮无动作、立即交回
控制权。于是每一帧这样的「静默终帧」= 一次用户视角的会话中断。中继侧日志同期
全部是 stream_completed——流是「成功地」静默的,丢包/超时/子 agent 都不是原因。

修复方案:Chat 方言的空终帧护栏

在转换层 finalize 处判定「completed 终帧是否静默」(纯函数 silent_final_frame),
命中则扣下 completed + [DONE],由流泵向同一上游重发一轮,把重试藏进同一条
客户端事件流:

  • 第一次重试同体重采样(请求体逐字节不变,前缀缓存全命中,纯赌温度);
    第二次带推动文案(往 input 尾追加一条用户消息,显式要求「可见正文或工具调用」)。
  • 重试轮条目续排同一个 response(保留 created 与 output_index 计数),客户端拿到的
    始终是单一、连续的 Responses 流。
  • 上限 2 次(EMPTY_FRAME_MAX_RETRIES);重发失败或额度用尽则把扣下的终帧原样放行
    ——绝不造成「既无 completed 也无 failed」的静默断流,这是本护栏的红线。
  • 边界:finish_reason=length 的 incomplete 终态不算静默(合法截断,客户端自己看得见);
    failed 收口不归护栏管;除 reasoning/message 外的任何条目形态都算有效输出不重发;
    压缩轮与原生 Responses 透传不挂护栏。
  • 护栏开关只在「Chat 方言 + 非压缩轮 + 客户端请求可解析 + 重发通道可用」时打开,
    其余路径行为逐字节不变(有专项测试钉住)。

诊断面新增两个事件:codex_layer.empty_frame_retry(含 attempt/nudged)与
codex_layer.empty_frame_exhausted(含 attempts,重发失败时带 reason)。护栏生效
后可按事件密度量化上游模型的静默帧率。

已知代价(明示)

  1. 重试轮的 usage 覆盖被丢弃的静默轮:交付账本只算最终交付物,静默轮另记诊断事件,
    两套账不混。
  2. 第二次重试的推动请求会在上游造成一次尾部增量与(若钉定基线在意)一条 divergence
    记录——只在第一次同体重采样也静默后才会发生,频次远低于原故障本身。
  3. 非流式 Chat 回包本期不挂护栏:codex 客户端恒走流式,非流式形态留待有真实需求再立项。

Activity

  1. added
    type/bug正确性缺陷或行为回归
    P1高优先级:语义漂移或重要能力缺口
    on Oct 15, 2026
  2. added a commit that references this issue on Oct 15, 2026
    89a6cca
  3. GGGBB commented on Oct 15, 2026

    @GGGBB
    OwnerAuthor

    修复已随 PR #81 并入 main(89a6cca)。验收对应:

    • 判据本体:empty_frame_guard_tests::silent_verdict_covers_reasoning_only_and_empty_but_not_real_output(reasoning-only/空帧=静默;正文/工具/未知项/length=非静默)。
    • 推动项形态:nudge_appends_to_input_array_without_touching_original(端到端过 Responses→Chat 转换,落成最后一条 user 消息)与 nudge_gives_up_when_input_is_not_an_array。
    • 无回归红线:guard_off_keeps_legacy_behavior_byte_for_byte;非静默帧不误扣 guard_never_defers_non_silent_frames。
    • 同流续排:retry_attempt_continues_the_same_response_stream(created 不重复、两轮条目同占独立 output_index)。
    • 绝不静默断流:guard_deferred_frame_and_release_restores_it、exhausted_guard_releases_the_original_frame_untouched。
    • 诊断面:codex_layer.empty_frame_retry / codex_layer.empty_frame_exhausted 已登记 events::layer::ALL。

    CI 两跑全绿(cargo test / smoke / gates / aftercheck)。本机部署与真机观察结果另行记档。

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1高优先级:语义漂移或重要能力缺口type/bug正确性缺陷或行为回归

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions