fix(stream): 识别空流/截断流,不再伪造成功 - #146
Merged
Merged
Conversation
…aking success
parseEventStream returns nil on a clean EOF, so a stream that died mid-answer
was indistinguishable from a finished one: no error, OnComplete fired, and the
handlers closed the turn with stop_reason: end_turn. Clients treated a half
answer as done and stopped; non-stream callers returned truncated text as 200.
classifyStreamIntegrity judges a transport-clean stream from the signals the
previous commit made reliable: a non-empty stopReason, or a delivered tool
call, means complete. Content or reasoning without either means truncated.
This is stricter than Kiro IDE on purpose. The IDE's truncation predicate ends
with (contentChars > 0 || !reasoningSeen), so a reasoning-only turn with no
stopReason is complete to the IDE and is not retried. The IDE is its own
client and knows another turn follows; a proxy does not, and shipping thinking
with no answer is exactly the reported symptom. The comment records the
divergence rather than claiming to mirror the IDE.
There is deliberately no separate empty-response error: errEmptyKiroStream
already fires one layer down from the same three signals, so an all-zero
stream never reaches the classifier with a nil error.
runKiroWithIntegrityRetry wires all six chat paths (Claude / OpenAI /
Responses x stream / non-stream) and removes six near-identical copies of the
retry block. Retries reuse the caller's callback; both CallKiroAPIContext and
parseEventStreamTracked copy the struct before wrapping a field, so a retry
cannot double-wrap it, and per-attempt state is cleared by reset instead.
Retry safety follows the boundary the author already established rather than
overturning it:
- Not flushed yet -> retry the same account twice, then rotate. Truncation
never reaches the endpoint fallback (CallKiroAPIContext returns nil for
it), so the only multiplier is account rotation: 9 upstream requests worst
case versus 6 at a single retry. The budget pays off on the three
non-stream paths, which never flush early.
- Already flushed -> propagate the integrity error and emit that path's own
failure event (SSE error / response.failed). Never forge an end_turn.
Integrity errors are soft failures: callers rotate the account but skip
handleAccountFailure, so an upstream blip cannot mark a healthy account
unhealthy or drain the pool. Cancellation is checked first in every branch and
is never reported as an integrity failure.
Quorinex
force-pushed
the
fix/stream-integrity-retry
branch
from
July 29, 2026 13:05
e46f3c1 to
851bd20
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
摘要
依赖 #145。 本 PR 补的是「读出来的流完不完整」这一层,建立在 #145 修好的
OnStopReason信号上。parseEventStream在干净 EOF 时返回nil,所以中途断掉的流和正常结束的流对调用方完全一样:没有 error、OnComplete照常触发、handler 用stop_reason: end_turn收尾。客户端把半截答案当成已完成并停止;非流式路径把截断文本当正常 200 返回。这是线上「思考完整但该进入下一步时就停了」最直接的对症修复。
改动
classifyStreamIntegrity:有 stopReason 或有工具调用 → 完整;有内容或仅 reasoning 但无终止信号 → 截断。runKiroWithIntegrityRetry,六条路径(Claude / OpenAI / Responses × stream / non-stream)统一接入。error/response.failed),绝不伪造end_turn。handleAccountFailure,上游偶发不会把账号标记为不健康。与官方 IDE 的关系(一手源码)
官方 Kiro IDE 解包源码:
C43 = contentChars===0 && toolCallCount===0 && !reasoningSeen && stopReason===undefined—— 与本 PR 一致。P45末尾是(contentChars > 0 || !reasoningSeen),所以「仅 reasoning 无正文」在 IDE 里判完整、不重试。errEmptyKiroStream已在更低层覆盖(触发信号完全相同),再写一层是死代码。重试次数
同账号 2 次。IDE 的
Dt3 = 1是单凭据客户端的上限;本代理有账号池,截断时CallKiroAPIContext返回nil,不走 endpoint 回退,唯一乘数是账号轮换。最坏 3 账号 × 3 次 = 9 个请求,比预算 1 只多 3 个。收益主要落在三条非流式路径上(永不提前 flush,能吃满预算)。测试
metadataEventstopReason 帧(真实上游会发,不是放宽断言)。gofmt/go vet/go build/go test ./... -count=1全绿。依赖
请先合并 #145。#145 合并后,本 PR 的 Files 会自动只显示本 PR 的 diff。