Skip to content

plugin pipe: degenerate-turn one-shot retry is dead on the Gemini wire — gate eligibility + per-lane answer-accounting asymmetries (checklist) #2769

Closed

Description

@ranxianglei

来源: #2764 (fix #2756) 分析退化轮门控 (answer-text gate) 时发现

Context

PR #2764 redefines the plugin pipes' one-shot degenerate-turn retry (#732/#821) gate as "did the host receive ANSWER text?" (an answerProse accumulator alongside byte-exact visibleTextChars). While reviewing it, three unverified facets of this SAME mechanism family surfaced (per-wire gate eligibility + answer accounting, one failure domain), managed here as one checklist.

Checklist

1. The gate is unreachable on the Gemini wire (the retry machinery is dead there; pre-existing)

2. Google-lane answer-accounting asymmetries (must close BEFORE activating item 1)

  • Fast path: processGoogle's fast path incremented visibleTextChars without feeding answerProse — fixed in fix(#2756): degenerate-turn gate asks for ANSWER text, not visible bytes #2764 review commit 392c7a1 plus a regression test. Left unfixed, the moment the gate reaches this lane EVERY healthy thought+prose turn would read as zero-answer and fire a spurious one-shot retry (double-billed turn, duplicated continuation).
  • Fold site (processGoogle, ~line 2782): unlike the openai/anthropic lanes it never consumes droppedTagInFrame. Today the consequence is classification-only (post-drop released bytes count into answerProse instead of releasedMarkupChars; net gate outcome for affected turns matches master because sawStrippedEcho covers the same turns), but the lane should reach parity in the same change that activates item 1.

3. OpenAI-fold slice-subtraction corner (#2764 new code, latent)

At the openai fold site, when droppedTagInFrame && tailLen > 0 && the folded tail is non-orphan && |tail| < |released|, answerProse = answerProse.slice(0, answerProse.length - released.length) removes bytes from the TAIL instead of from released (keeps a prefix of the released markup, drops real tail prose). Reachability needs an exotic combination (e.g. an absorb-instructed tcHold flush co-existing with a dropped tag in the same terminal frame); TagEchoFilter.flush() CAN return non-orphan prose, so the corner is reachable in principle. Fix: pin flush()'s return semantics with a targeted filter test first, then replace the subtraction with explicit branch accounting (exclude released when dropped-tag, include the tail when non-orphan).

Suggested path

  1. Wire item 1: add the google turnTerminal branch AND accept "STOP" (case-normalize or extend the set), keeping the clean-stop semantics identical to the other wires.
  2. In the SAME change, complete item 2 parity (google fold-site droppedTagInFrame consumption).
  3. Prove both directions with tests: a gemini degenerate turn (thinking-only or whitespace-only content) retries exactly once; a healthy gemini turn (thought part + real prose) never retries and logs no refusal.
  4. Item 3 separately, after the filter-semantics pin.

Activity

  1. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 收到。已核实:PR #2764 仍 open(head 392c7a1e5,base master),item 2 的 fast-path 修复已在该分支(392c7a1)。由于 item 1/2 依赖 #2764 的 answerProse 机制且要求「同一变更」完成,我将在 2026-10-11_degenerate-gate 分支上以 additive commits 继续(沿用 392c7a1 的先例),item 3 作为独立 commit 跟进。开始审代码,完成后按八问体例回复并更新 PR body(Closes #2769)。

  2. added 2 commits that reference this issue on Oct 11, 2026
  3. ranxianglei commented on Oct 11, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    [bot] 🏷 #2769 三个 checklist 项全部关闭,外加接线时发现的一个小修。以 additive commit 形式推在 PR #2764 的分支上(2026-10-11_degenerate-gate,392c7a1e5..9656dc4f7,与 review commit 392c7a1e5 同模式),PR 描述已更新(行为变更披露 + Closes #2769)。全量回归交给 CI。

    1. 是什么问题:退化轮一次重试(#732/#821/#2303)在 Gemini wire 上完全不存在——门控根本够不到;同一机制族还有两处记账不对称(google lane 不消费 dropped-tag、openai lane 用 slice 减法做 tag 内部记账),不先堵死就会在门控接通时造成误重试/漏重试。

    2. 什么原因导致的:三层——① 门控调用点最初只写了 anthropic/openai 两个分支,google 分支缺失(机制先天缺位,非 #2764 引入);② Gemini 的终止原因是大写 STOP,与 CLEAN_TURN_REASONS 的小写集合不匹配(只加分支仍会全拒);③ 两处 fold 站点的 tag 内部记账各写各的:google 从不消费 droppedTagInFrame;openai 用 answerProse.slice(0, len - released.length) 从字符串末尾减——但 flag 跨 stream 全局而 released/tail 是 per-stream,且 folded tail 在 released 之后还可能带非孤儿 prose,减法会砍掉真实尾部 prose、留下 markup 前缀。

    3. 影响范围与严重程度:

    4. 怎么修复的:

    • 项 1:turnTerminal 调用点补 google 分支(镜像 openai 形状);大小写归一化放在该分支内完成(原始原因保留给日志/诊断),没有扩展共享 clean-stop 集合、也没有全局 lowercase——其他 wire 字节不变。
    • 顺手发现的小修(commit/PR 里单独标注):retry 接管时现在同时清掉 lastChunkMeta["finishReason"]——google 的 syntheticTail 会把该字段重复进合成 chunk,不清的话重试中途的 flush 会把带着第一次尝试终止标记的 chunk 递给客户端。只有 google 往这个 meta 里放 finishReason(openai 的 meta 是 id/created/model,不受影响)。
    • 项 2:google fold 站点改为消费 droppedTagInFrame,与 openai/anthropic 对齐(dropped-tag 下的 released 字节计入 markup 残量而非 answer)。
    • 项 3:先用定向 filter 测试钉住 TagEchoFilter.flush() 语义(push 可返回非空 release 并触发 onDrop,其后的 flush 可返回非孤儿 prose),再把 slice 减法换成显式分支记账(dropped-tag 时排除 released;tail 按其自身孤儿分类计数)。

    5. 是否有回归问题:逐项过过既有行为——其他 wire(anthropic/openai;responses pipe 本就不在此机制面内)的门控可达性、clean-stop 判定、字节精确路径均不变;google 健康轮(thought+真 prose)永不重试且不发 refusal 日志(新测试钉住);MAX_TOKENS/SAFETY 等非干净停止照旧拒绝并记录 refusal 原因(新测试钉住);visibleTextChars/stats/#673 warn 不变。负向对照实测:stash 掉修复后新增测试全部失败(含项 3 corner 回归),恢复后全绿。

    6. 是否破坏缓存:不涉及——未触碰会话状态、快照、ref 游标或任何失效策略;重试复用既有的 refetch/接管机制。

    7. 是否改了设计方向:没有。沿用 #2764 确立的方向("host 有没有收到 ANSWER 文本?"),只是把同一门控语义延伸到第三条 wire,并把三处记账收敛到同一纪律。

    8. 是否引入了新的配置:无。

    行为变更披露(旧→新):退化轮一次重试现在会在 Gemini wire 上触发(旧:该 wire 上机制死亡,退化轮静默结束;新:恰好重试一次,clean-stop 语义与其他 wire 一致——仅 stop/end_turn/stop_sequence 合格,大写 STOP 经 google 分支归一化)。这正是本 issue 项 1 要求的激活,已在 PR 描述显著位置标注。

    判定:三项全部关闭,证据齐(负向对照 + 定向套件 107 绿 + typecheck 干净),等 CI 全量门禁后由人合并。PR: #2764

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions