You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
来源: #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.
查重:不重复。 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:
✅ 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).
✅ 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.
✅ Plugin responses pipe: gate at src/plugin.ts:4309 requires status === "completed"; a length cut lands as incomplete → blocked.
✅ 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.
A (status quo): zero code change, zero extra spend — but it cements the lane divergence from point 4. The reported population keeps seeing raw drafts until they manually continue.
B (any convicted dump): widens the gate for ALL convicted dumps including mixed turns (draft + real answer + tool call). Those turns carry visible work the host may act on — retrying discards it and can stack two drafts. Largest billing blast radius. Not recommended.
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.
Activity
ranxianglei commentedon Nov 9, 2026
(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:
isDraftDumppredicate is what option C builds on;compresstool, so it writes an<analysis>/<summary>draft as prose — and the draft-tail recovery nudge then steers it somewhere else #4764 / [PR] fix(openai-lane): name the compress tool in the draft-tail retry nudge #4785 — nudge wording never names the compress tool, so retries can produce prose drafts (retry-outcome quality, gate eligibility untouched);degenerateRetry, default OFF) + neutral nudge wording for the degenerate-retry family (billing posture; sequencing interaction with this decision, see point 6);No open [PR] line targets this thread (verified against the mirror list).
标签: 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:
isDraftDumpexists 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 hasendsWithDraftClose(closed-tail form).CLEAN_TURN_REASONS = {"stop","end_turn","stop_sequence"}(src/plugin.ts:2529);retryEmptyTurnbails at src/plugin.ts:2794 onreason === undefined || !CLEAN_TURN_REASONS.has(reason)→length/max_tokenscuts are convicted but never retried.status === "completed"; a length cut lands asincomplete→ blocked.{failed, incomplete, error}plus requiressawDone && !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 inisDraftDump, proxy-mode openai/anthropic length-cut whole-turn dumps will already be retried —length/max_tokensare 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.degenerateRetriedbudget is shared per request across all degenerate paths (src/plugin.ts:2757), so C adds at most ONE extra upstream call per request.degenerateRetryswitch (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:
isDraftDump∧ no tool call ∧ the draft IS the entire visible text — which is exactly the existingdraftTailpredicate shape; the only missing piece is reason acceptance. AcceptCLEAN_TURN_REASONS ∪ isMaxOutputReason(helper already exists at src/degenerate-turn.ts:29-32, wire vocabulary from [Bug][DSH] 长会话切换自定义模型后输出预算恢复到 249 token,连续 thinking-only / max-tokens(#1840 零上限来源场景) #2904) in the draft-dump branch of both plugin pipes (chat: finish reason; responses: also acceptincomplete). Empty-turn branch keeps its clean-only whitelist; core lane needs no change for openai/anthropic (already passes), responses/google stay excluded. Worst case: model re-dumps on retry → two stacked drafts + the existing Plugin passthrough leaves the client an empty turn when the model echoes a render tag #870 in-band error (plugin) or pass-through (core) — same failure class as today's clean-stop 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 retry, not a new one. Per decision request: plugin degenerate-turn retry failure mode — passthrough toggle design (#4913) #4919's production dataset (~9/13 continuation retries succeed), expect most affected turns to recover.八问(结构化结论)
<analysis>…</analysis>+<summary>…)时被上游长度上限截断的回合——检测能定罪,但重试门只认干净结束,所以永远不会重试;用户看到裸草稿、回合不收敛。length/max_tokens(responses 线是incomplete),不在名单内。层次上:门的原始注释担心「截断的重试只会再截断一次」——对空回合成立,对「整轮即草稿」不成立:那里重试是继续写草稿,不是重放截断。另有一处纠正(见上第 4 点):core 车道是黑名单而非白名单,[PR] fix(#5175): strip drifted open-tag names and mid-turn draft envelopes #5178 合并后 proxy 模式的 openai/anthropic 长度截断整轮草稿已经在被自动重试。degenerateRetried);其余路径逐字不动。retryEmptyTurn中,把草稿定罪分支的合格结束从CLEAN_TURN_REASONS扩为∪ isMaxOutputReason(现成 helper,src/degenerate-turn.ts:29-32;responses 线同分支接受incomplete);空回合分支保持干净白名单不变;core 车道 openai/anthropic 已放行无需改动,responses/google 保持排除。不改任何既有判决,只补一个入口。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.