Replies: 2 comments
|
更新——已在真实 dsh 宿主(TUI)上验证 上面的过渡插件已在真实
仓库还带完整自动化套件:单测(匹配表、注入随机数的退避计算、配置校验、真实 cordis context 上的决策链)+ agent-loop e2e(真实 English versionUpdate — validated on a real dsh host (TUI) The workaround plugin above is now verified end-to-end on a live
The repo also carries the automated suite: unit tests (matcher table, backoff math with injected randomness, config validation, and the decision chain on a real cordis context with a real session store) plus an agent-loop e2e that drives the real |
|
这帖已有两条可用产物:上游窄修复和真实 rc.8 TUI 验证过的过渡插件 |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
问题现象
在 OpenAI 兼容网关(如 OpenCode Zen)后面,turn 会直接硬失败,错误串恒定:
网关把自身上游连接的瞬时失败作为流的
finish_reason上报;立即重试几乎总能成功(同类报告见 anomalyco/opencode#41236)。dsh 把该失败归类为不可重试,于是整个 turn 硬失败而不是重试——一次真实会话中,subagent 通道连续 8 次都挂在这个串上。根因
两个 adapter 把线上
finish_reason翻译成错误码,都漏掉了 network 变体:finish_reason映射为stopReason: 'error',errorMessage 为Provider finish_reason: network_error(@earendil-works/pi-aiapi/openai-completions.js的mapStopReason)。随后classifyPiAiError(packages/llm/llm-pi-ai/src/stream.ts)用/\b(?:network|connection|socket|fetch)\b/匹配——但\b在k与_之间不成立(下划线是 word 字符),network_error落到PI_AI_ERROR,而它不在DEFAULT_RETRYABLE_CODES里。llm-retry放行,turn 失败。mapFinishReason(packages/llm/llm-deepseek/src/translate.ts)把未知 reason 大写化为失败码:network_error→NETWORK_ERROR,同样不在DEFAULT_RETRYABLE_CODES。参照
opencode 在 2026-08-21 修复了同样的网关行为:
network error扩为network[-_\s]error,覆盖连字符/下划线变体。rawFinishReason === "network_error"时按失败处理,进入既有重试管线(测试断言第 2 次尝试成功)。修复(已备好,已测试)
由于仓库当前不接受外部 PR,修复已在 fork 分支上完成并充分测试:
https://github.com/fan56/deepseek-harness/tree/fix/network-error-retryable
llm-pi-ai:classifyPiAiError在既有 alternation 之前增加显式的/network[-_ ]error/i分支,三种拼写都归TRANSPORT。既有匹配(AUTH/quota/429/4xx/5xx/timeout/流截断/ECONN*/terminated/premature close)全部不动。llm-deepseek:mapFinishReason把network_error/network-error映射为{kind:'error', failure:{message:'model stopped: <reason>', code:'TRANSPORT'}}。TRANSPORT已在DEFAULT_RETRYABLE_CODES中,无需改动 retry policy。测试:
llm-pi-ai/tests/convert.spec.ts扩展了 transport wording 表;llm-deepseek/tests/translate.spec.ts增加mapFinishReason用例。vitest run packages/llm/llm-pi-ai packages/llm/llm-deepseek→ 608 全过;pnpm run typecheck→ 干净。如果团队愿意合入,我可以随时 rebase/调整。已发布版本的过渡插件
给已发布版本用户的插件(在
agent/request-errorwaterfall 末端重试这类失败,仅当所有下游 listener 都弃权时才行动):@aiwayds/dsh-llm-net-retry@0.1.0(dsh plugin --profile tui add @aiwayds/dsh-llm-net-retry)它的 e2e 用真实 agent loop +
llm-pi-aiopenai-completionsadapter 打一个脚本化网关(前两次请求回finish_reason: network_error),断言第三次完成 turn。基础修复合入后该插件自然无害:它只在整个 waterfall 弃权时行动,且绝不触碰 llm-retry 自身的计数。已在真实 dsh 0.1.0-rc.8 TUI 宿主上验证——见下方更新评论。效果
修复后,网关
network_error在两条 adapter 路径上都归为TRANSPORT,落入默认可重试码,llm-retry以标准有界指数退避自动重试(5 次,500 ms→10 s)。既有llm/retry事件消费者(TUI)无需任何改动即可展示重试。English version
Problem
Behind OpenAI-compatible gateways (e.g. OpenCode Zen), a turn fails outright with:
The gateway reports its own transient upstream connection failure as the stream's
finish_reason; retrying immediately usually succeeds (same class of reports as anomalyco/opencode#41236). dsh classifies this failure as non-retryable, so the whole turn hard-fails instead of retrying — in one real session the subagent channel hit this string 8 times in a row.Root cause
Two adapters turn the wire
finish_reasoninto an error code, and both miss the network variant:finish_reasontostopReason: 'error'witherrorMessage: 'Provider finish_reason: network_error'(@earendil-works/pi-aiapi/openai-completions.js,mapStopReason).classifyPiAiError(packages/llm/llm-pi-ai/src/stream.ts) then tests/\b(?:network|connection|socket|fetch)\b/— but\bdoes not hold betweenkand_(underscore is a word character), sonetwork_errorfalls through toPI_AI_ERROR, whichDEFAULT_RETRYABLE_CODESdoes not include.llm-retrypasses the failure through and the turn fails.mapFinishReason(packages/llm/llm-deepseek/src/translate.ts) uppercases unknown reasons into the failure code:network_error→NETWORK_ERROR, also not inDEFAULT_RETRYABLE_CODES.Reference
opencode hit and fixed the same gateway behavior on 2026-08-21:
network errortonetwork[-_\s]error, covering the hyphen/underscore variants.rawFinishReason === "network_error"so the existing retry pipeline handles it (test asserts the 2nd attempt succeeds).Fix (prepared, tested)
Since the repository does not accept external PRs at the moment, the fix is prepared and fully tested on a fork branch:
https://github.com/fan56/deepseek-harness/tree/fix/network-error-retryable
llm-pi-ai:classifyPiAiErrorgains an explicit/network[-_ ]error/iarm ahead of the existing alternation, classifying all three spellings asTRANSPORT. The existing matches (AUTH/quota/429/4xx/5xx/timeout/stream-truncation/ECONN*/terminated/premature close) are untouched.llm-deepseek:mapFinishReasonmapsnetwork_error/network-errorto{kind:'error', failure:{message:'model stopped: <reason>', code:'TRANSPORT'}}.TRANSPORTis already inDEFAULT_RETRYABLE_CODES, so no retry-policy change is needed.Tests: transport-wording table extended in
llm-pi-ai/tests/convert.spec.ts;mapFinishReasoncases inllm-deepseek/tests/translate.spec.ts.vitest run packages/llm/llm-pi-ai packages/llm/llm-deepseek→ 608 passed;pnpm run typecheck→ clean. Happy to rebase/adjust if the team wants to land it.Workaround plugin for released versions
A plugin for users on released versions (retries these failures at the end of the
agent/request-errorwaterfall, only when every downstream listener declined):https://github.com/fan56/dsh-llm-net-retry — also on npm as
@aiwayds/dsh-llm-net-retry@0.1.0(dsh plugin --profile tui add @aiwayds/dsh-llm-net-retry).Its e2e drives the real agent loop +
llm-pi-aiopenai-completionsadapter against a scripted gateway failing the first two requests withfinish_reason: network_errorand asserts the turn completes on the third. The plugin stays harmless once the base fix lands: it only acts when the whole waterfall declined and never touches llm-retry's own counting. Now also validated on a real dsh 0.1.0-rc.8 TUI host — see the update comment below.Result
With the fix, a gateway
network_errorclassifies asTRANSPORTon both adapter paths, falls inside the default retryable codes, andllm-retryretries it with the standard bounded exponential backoff (5 attempts, 500 ms→10 s). Existingllm/retryevent consumers (TUI) surface the retries with no changes.All reactions