Skip to content

decision request: retry truncated compression-draft turns (length-cut finish reasons) #6019

Open

Description

@ranxianglei

来源: #5175 分析 live report(v0.1.213,OpenCode plugin mode)时发现

Symptom. An assistant turn whose ENTIRE visible text is a compression-draft envelope (<analysis>…</analysis> then <summary>…) was cut off mid-summary by an upstream length limit. The user saw the raw draft and the turn never converged.

State of the fix (PR #5178, open). Detection ALREADY covers this shape: isDraftDump (src/degenerate-turn.ts) returns true whenever the analysis→summary envelope is present without a later </summary> close — i.e. exactly while the draft is unfinished. What blocks ACTION is the retry gate: all three lanes require the turn to have ended cleanly — CLEAN_TURN_REASONS = {stop, end_turn, stop_sequence} (src/plugin.ts) plus the core.ts finish-reason checks — so a length-cut turn is convicted but never retried.

The original #2303 cases retried because upstream cut the final TOOL CALL and still stopped cleanly with "stop". A length cut is a different failure shape and widening the gate has billing/semantics consequences, so this is an owner decision, not a unilateral change.

Options.

  • A — Status quo. Clean finishes only. Cost: truncated full-turn dumps remain visible garbage until the user manually continues. No extra spend.
  • B — Allow length-cuts for any convicted dump. Add length/max_tokens (and possibly undefined) to the draft-dump retry eligibility. Cost: one extra upstream call per affected request; if the model re-dumps instead of continuing, the client sees TWO drafts stacked.
  • C — Allow length-cuts ONLY for whole-turn dumps. Convicted by isDraftDump AND no tool call AND the draft IS the turn (visible text is the whole payload). Reuses the existing one-shot degenerateRetried budget, so at most one extra call per request. Narrowest billing blast radius; covers the reported shape exactly.

Recommendation: C. It restores convergence for the exact convicted population with a hard cap on spend, and leaves mixed turns (draft + real answer + tool call) untouched.

查重: 无(#5175/#5178 修的是检测与剥离;#5349 是漂移率测量;重试门扩展无既有条目)。

Activity

  1. ranxianglei commented on Nov 9, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重:不重复。 Searched open+closed issues/PRs for a retry-gate extension to length-cut reasons — no existing item covers this gap. Related-but-distinct entries checked:

    标签: bug / opencode / scope/wiring / p2 / needs-decision — real convergence defect seen live in OpenCode plugin mode; the gate lives in the client-integration layer (plugin pipes + core loop), same subsystem as #4919; p2 = edge-shape UX breakage with a manual-continue workaround (feel free to re-grade at decision time).

    Verification (live: master @ 406193b, PR #5178 @ c6eaf1f). All claims in the body check out against code, with one correction that sharpens the decision:

    1. ✅ Detection claim holds. isDraftDump exists only in PR [PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 (open; CI as of 09:19Z: 30 checks, 0 failed, 14 pending, mergeable_state blocked): true when the </analysis>→<summary> envelope is present without a later </summary> close — i.e. exactly while the draft is unfinished. Master today only has endsWithDraftClose (closed-tail form).
    2. ✅ Plugin chat pipe: CLEAN_TURN_REASONS = {"stop","end_turn","stop_sequence"} (src/plugin.ts:2529); retryEmptyTurn bails at src/plugin.ts:2794 on reason === undefined || !CLEAN_TURN_REASONS.has(reason) → length/max_tokens cuts are convicted but never retried.
    3. ✅ Plugin responses pipe: gate at src/plugin.ts:4309 requires status === "completed"; a length cut lands as incomplete → blocked.
    4. ⚠️ Correction — the core lane is a blacklist, not a clean-whitelist. The DSH native mode: assistant text ending with </summary> is treated as a completed step, so the turn silently stops (149 samples from session transcripts) #2303 draft-tail gate (src/loop/core.ts:1179-1199) excludes only {failed, incomplete, error} plus requires sawDone && !truncatedDone. The openai adapter emits done with the RAW finishReason and no truncated flag (src/loop/adapter-openai.ts:583, 588-592); anthropic likewise with raw stop_reason. So once [PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 swaps in isDraftDump, proxy-mode openai/anthropic length-cut whole-turn dumps will already be retried — length/max_tokens are not in the exclusion set. Consequence: under option A the three lanes DIVERGE — proxy users get the retry for free while the reported OpenCode plugin-mode population stays broken. Option C would align all lanes.
    5. ✅ Budget claim holds: the one-shot degenerateRetried budget is shared per request across all degenerate paths (src/plugin.ts:2757), so C adds at most ONE extra upstream call per request.
    6. ⚠️ Sequencing note: PR [PR] Fix 400 CONTEXT_WINDOW_EXCEEDED on anthropic-wire sessions bili believes fit #5899 (from [PR] Empty-turn continuation retry: off by default (opt-in switch) + neutral nudge wording #5897, following closed decision record decision request: plugin degenerate-turn retry failure mode — passthrough toggle design (#4913) #4919) proposes making the whole degenerate-retry family OPT-IN behind a new degenerateRetry switch (default OFF, env BILI_DEGENERATE_RETRY=1) with neutral nudge wording. If [PR] Fix 400 CONTEXT_WINDOW_EXCEEDED on anthropic-wire sessions bili believes fit #5899 lands before option C, C's extended gate only fires for opted-in users; if C ships first, [PR] Fix 400 CONTEXT_WINDOW_EXCEEDED on anthropic-wire sessions bili believes fit #5899 will presumably cover it too. Worth deciding together.

    Independent option analysis:

    八问(结构化结论)

    1. 是什么问题:整轮可见文本全是压缩草稿(<analysis>…</analysis>+<summary>…)时被上游长度上限截断的回合——检测能定罪,但重试门只认干净结束,所以永远不会重试;用户看到裸草稿、回合不收敛。
    2. 什么原因导致的:两条 plugin 车道的重试资格门是白名单式「干净结束」,而长度截断的结束原因是 length/max_tokens(responses 线是 incomplete),不在名单内。层次上:门的原始注释担心「截断的重试只会再截断一次」——对空回合成立,对「整轮即草稿」不成立:那里重试是继续写草稿,不是重放截断。另有一处纠正(见上第 4 点):core 车道是黑名单而非白名单,[PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 合并后 proxy 模式的 openai/anthropic 长度截断整轮草稿已经在被自动重试。
    3. 影响范围与严重程度(按选项):
      • A:零代码变更;但 [PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 合并后三车道行为分裂——proxy 用户免费获得重试,报告的 OpenCode plugin 群体继续看到垃圾草稿直到人工续。严重度:特定条件下有问题(整轮草稿+长度截断),可人工续,p2。
      • B:所有被定罪草稿(含混合回合)都允许长度截断重试;最坏每请求多一次全价调用,客户端可能看到两份叠放草稿,且混合回合里已有的真实内容被丢弃重来。
      • C:仅「整轮即草稿 ∧ 无工具调用 ∧ 长度原因」形态多至多一次有预算上限的继续调用(复用一次性 degenerateRetried);其余路径逐字不动。
    4. 怎么修复的(推荐 C):两条 plugin 车道的 retryEmptyTurn 中,把草稿定罪分支的合格结束从 CLEAN_TURN_REASONS 扩为 ∪ isMaxOutputReason(现成 helper,src/degenerate-turn.ts:29-32;responses 线同分支接受 incomplete);空回合分支保持干净白名单不变;core 车道 openai/anthropic 已放行无需改动,responses/google 保持排除。不改任何既有判决,只补一个入口。
    5. 回归风险(按选项):A=无代码风险,但引入车道间不对称(新状态)。B=把空回合/混合回合拉进长度截断重试,扩大 Plugin passthrough leaves the client an empty turn when the model echoes a render tag #870 双退化语义与计费面,回归面最大。C=新增面严格限定在「草稿即整轮 ∧ 长度原因」;既有干净结束行为逐字不变;已识别的最坏情形=模型重试时再次倾泻草稿(两份叠放+Plugin passthrough leaves the client an empty turn when the model echoes a render tag #870 或透传),与 DSH native mode: assistant text ending with </summary> is treated as a completed step, so the turn silently stops (149 samples from session transcripts) #2303 现有失败模式同类而非新增类。
    6. 是否破坏缓存:不涉及——重试走既有 refetch 机制(原请求体+nudge 尾随 user 消息,前缀不变,前缀缓存命中不受影响),与现有 DSH native mode: assistant text ending with </summary> is treated as a completed step, so the turn silently stops (149 samples from session transcripts) #2303 重试完全同路。
    7. 是否改了设计方向:不改架构与边界,只是把既有的「草稿即停滞」判决在长度截断入口补齐。需一并定的排序问题:[PR] Fix 400 CONTEXT_WINDOW_EXCEEDED on anthropic-wire sessions bili believes fit #5899(degenerateRetry 默认关)若在 C 之前合入,C 的效果会被该开关罩住(仅 opted-in 用户生效)。
    8. 是否引入了新的配置:C 本身不引入新配置(复用 isMaxOutputReason 与既有预算);若叠加 [PR] Fix 400 CONTEXT_WINDOW_EXCEEDED on anthropic-wire sessions bili believes fit #5899 则受其 degenerateRetry 开关管辖。

    Awaiting your pick: A / B / C (my recommendation: C). Once decided I'll open the single fix PR for this thread (no [PR] exists for it yet). If you'd rather sequence with #5899 first, say so and I'll hold.

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions