fix(stream): report upstream interruption instead of faking success - #1
Merged
lzhs1995 merged 2 commits intoJul 26, 2026
Conversation
上游断流时,流式与缓冲两条路径都走正常收尾(generate_final_events / finish_and_get_all_events),客户端因此收到 `message_delta` (stop_reason=end_turn) + `message_stop`——一次失败被伪装成一次完整成功的 回合。Claude Code CLI 据此认为本轮已完成:不重试、继续往下走,于是半截的 工具调用参数被丢弃、多步任务在中途"正常"结束。这是 CLI 侧观察到的 「工具调用不严格执行」的一个真因。 改为在断流时只关闭已打开的内容块,并下发 Anthropic `overloaded_error` 事件,客户端 SDK / CLI 对该类型走重试路径。断流收尾时不做任何残留 flush (thinking / invoke 嗅探 / XML 过滤器 / 工具 JSON 累积器),那些残留本身 就是不完整数据,flush 出去只会把半截内容伪装成有效内容。 同时置位 message_delta_sent / message_ended 幂等位,使断流之后任何路径 再调 generate_final_events 都无法补出正常收尾(防回归)。 trace 侧行为不变:仍记 interrupted + STREAM_INTERRUPTED,用量仍记 error。 新增 6 个测试锁定契约:流式 / 缓冲各自「必须报错且无正常收尾」与「事后 不可回退成伪造成功」、半截工具 JSON 不得作为 tool_use 发出,以及反向对照 「正常收尾不受影响,仍发 message_delta + message_stop」。 CI:把 stream.rs 纳入候选分支的 rustfmt 闸门,并把本分支加入触发列表。
There was a problem hiding this comment.
Pull request overview
This PR changes the Anthropic SSE streaming implementation so that an upstream transport interruption is surfaced to clients as an explicit Anthropic error event (retryable overloaded_error) instead of emitting a normal successful completion (message_delta with stop_reason: end_turn + message_stop). This prevents clients (notably Claude Code CLI) from treating truncated responses as completed turns and silently continuing without retrying.
Changes:
- Add an explicit “interrupted” termination path that closes open content blocks, marks completion idempotency flags, and emits a terminal
errorevent (nomessage_delta/message_stop). - Wire the new interruption path into both streaming and buffered SSE handlers for
Some(Err(_))upstream read failures. - Extend the document-candidate workflow trigger and rustfmt gate list to include the touched stream file.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/anthropic/stream.rs |
Introduces interrupted termination event generation and adds regression tests ensuring interruptions don’t fake success. |
src/anthropic/handlers.rs |
Routes upstream read errors to the new interrupted termination path in both streaming and buffered modes. |
.github/workflows/kiro-document-candidate.yml |
Updates branch trigger list and adds stream.rs to the rustfmt gate file list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+2399
to
+2412
| pub fn generate_interrupted_events(&mut self, message: &str) -> Vec<SseEvent> { | ||
| let mut events = self.state_manager.generate_interrupted_events(); | ||
| events.push(SseEvent::new( | ||
| "error", | ||
| json!({ | ||
| "type": "error", | ||
| "error": { | ||
| "type": "overloaded_error", | ||
| "message": message | ||
| } | ||
| }), | ||
| )); | ||
| events | ||
| } |
conversationId 已经从 metadata.user_id 的 session UUID 派生,但 agentContinuationId 仍是每请求 Uuid::new_v4()。上游因此把同一个客户端会话 的每一轮当成一条全新的 agent 任务线,多步任务(工具调用链、长任务续写)在 上游侧没有连续性。 改为用同一个会话锚点派生 continuation(domain 前缀 + SHA-256,保持与 conversationId 一一对应但互不相等)。拿不到锚点时仍退回随机值,保持旧行为 ——宁可丢连续性,不可乱绑任务线。 Tests: 585 passed (新增 5 个派生级 + 3 个端到端)
lzhs1995
merged commit Jul 26, 2026
4226290
into
feat/opus5-upstream-driven-models-20260725
2 checks passed
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.
Problem
When the upstream stream breaks mid-response, both SSE paths called the normal
completion routine:
create_sse_stream,Some(Err(e))) ->ctx.generate_final_events()create_buffered_sse_stream,Some(Err(e))) ->ctx.finish_and_get_all_events()Both emit
message_deltawithstop_reason: end_turnplusmessage_stop. The tracewas already recorded as
interrupted, but the client saw a fully successful turn.Consequence for Claude Code CLI: a truncated response is treated as a finished turn,
so the client does not retry. Half-written tool arguments are silently dropped and
multi-step tasks "complete" in the middle.
Fix
New interruption path that closes any open content blocks and emits an Anthropic
errorevent withtype: "overloaded_error"(a retryable type), and neveremits
message_delta/message_stop:SseStateManager::generate_interrupted_events()- closes open blocks, sets themessage_delta_sent/message_endedidempotency flags so no later path canappend a normal completion.
StreamContext::generate_interrupted_events(message)- the above plus theerrorevent. Deliberately performs no flush of thinking / invoke-sniff / XML-filter
residue and no tool-JSON accumulator finish: that residue is incomplete data, and
flushing it would dress up half content as valid content.
BufferedStreamContext::interrupt_and_get_all_events(message)- emits the bufferedevents (useful for diagnosis) with the same
errortail and the same usagecorrection on
message_startas the success path.Erronly ever comes from a transport failure; normal completion isNone(EOF),which still goes through the unchanged success path.
Tests
6 new tests in
src/anthropic/stream.rs:interrupted_stream_emits_error_and_no_normal_completionoverloaded_error, it is last, open blocks closed, nomessage_delta/message_stopinterrupted_stream_cannot_regress_to_fake_successgenerate_final_events()cannot append a normal completioninterrupted_stream_does_not_flush_truncated_tool_jsontool_useblockbuffered_interrupted_stream_emits_error_and_no_normal_completionbuffered_interrupted_stream_cannot_regress_to_fake_successnormal_completion_still_emits_message_stopend_turn+message_stop, no error)Full suite:
cargo test --locked --all-targets --no-default-features-> 577 passed, 0 failed.rustfmt --edition 2024 --checkclean on the CI gate file list.CI
src/anthropic/stream.rsadded to the rustfmt gate (it is now a candidate file).upstream baseline and none are in the two files touched here.
Attribution
Behaviour reference: the
overloaded_error-on-interruption approach indmcyfelix/kiro2cc-proxy@50bd8d0. No code copied.Not in scope
stopReason/metadataEventparsing (Quorinex/Kiro-Go#141) was dropped: rawEventStream captures against our own upstream endpoint show zero
stopReasonfields and no
metadataEventframes, so there is nothing to parse.