From d84a67cd240e785291873c19686765c1d1f60c22 Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 3 Oct 2026 13:51:41 +0000 Subject: [PATCH] Treat a cancelled SSE stream as aborted Why: the race detector can deliver message_end when the context is already cancelled, because select picks either ready case. Evidence: GOWORK=off go test -count=50 -race ./runtime/llm/ passed, including TestParseSSECancel. Impact: a cancelled parse ends with the aborted event. Refs: D1 Co-authored-by: Cursor --- runtime/llm/sse.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/runtime/llm/sse.go b/runtime/llm/sse.go index d7e8230..0bd3f32 100644 --- a/runtime/llm/sse.go +++ b/runtime/llm/sse.go @@ -151,6 +151,12 @@ func emitParsed(ctx context.Context, r io.Reader, out chan<- Event) { emitError(out, content.String(), true, err.Error()) return } + // 取消必须压过正常结束。select 在两个分支都就绪时随机挑选, + // 竞态下会把 message_end 发给已经取消的调用方。 + if ctx.Err() != nil { + emitAbort(out, content.String(), ctx.Err()) + return + } msg := Message{Role: RoleAssistant, Content: content.String(), StopReason: mapFinish(finished, len(order) > 0)} for _, idx := range order { msg.ToolCalls = append(msg.ToolCalls, *calls[idx])