Symptom
tests/integration/uitest/diagnoseloop / TestDiagnoseLoop_NoToolRegression:
scrollback shape = [block.Message block.Message block.Message]; want [block.Message block.Message]
First seen on PR #96 CI (chore branch = main + 1 TSV line); rerun green.
Repro
go test ./tests/integration/uitest/diagnoseloop/ -run TestDiagnoseLoop_NoToolRegression -race -count=100
~1-2% failure rate under parallel load (t.Parallel ×100 amplifies scheduling contention).
Bisect — PRE-EXISTING, not spec-2.3
| commit |
100× -race failures |
3af7ac0 (pre spec-2.3) |
2 |
0cf1005+ (main, post spec-2.3) |
1 |
spec-2.3's loop changes are confined to the FinishToolUse arm + per-turn Tools assembly (nil filter = identity); the failing path is the no-tool FinishStop chain — untouched.
Preliminary RCA (spec-1.21.1 R-fix H-1 neighborhood)
llmapp/model.go streamDoneMsg handler recovers residual = previewNodes + stream.Drain() to preserve partial text on cancel paths. On the NORMAL path the comment asserts both are empty — but there is a window: loopStartCmd goroutine teardown runs ts.Close() (flushes the final partial line into the TokenStream's pending nodes) before close(ctrl). If the Model's SealedText handler performed its discard-drain BEFORE the Close-flush landed, the flushed final line survives in the closed stream and streamDoneMsg's stream.Drain() re-commits the already-sealed text → duplicate assistant block.Message.
Candidate fixes (for the owning spec/hotfix, NOT verified):
- streamDoneMsg: skip residual commit when
finishHandled == true (normal-finish path needs no recovery — recovery exists for the lost-EventFinish cancel path only)
- or: dedupe residual against the last sealed segment text
Scope
Pre-existing 1.21.1-adjacent race; candidate for a spec-1.21.2 hotfix branch (hotfix matrix: 不动 spec 设计, 三路 code review). Not blocking spec-2.3 (FROZEN @ v0.53.0-stage2.53).
Spec: spec-1.21.1-stream-ordering-fix.md (neighborhood)
Symptom
tests/integration/uitest/diagnoseloop/TestDiagnoseLoop_NoToolRegression:First seen on PR #96 CI (chore branch = main + 1 TSV line); rerun green.
Repro
go test ./tests/integration/uitest/diagnoseloop/ -run TestDiagnoseLoop_NoToolRegression -race -count=100~1-2% failure rate under parallel load (t.Parallel ×100 amplifies scheduling contention).
Bisect — PRE-EXISTING, not spec-2.3
3af7ac0(pre spec-2.3)0cf1005+ (main, post spec-2.3)spec-2.3's loop changes are confined to the FinishToolUse arm + per-turn Tools assembly (nil filter = identity); the failing path is the no-tool FinishStop chain — untouched.
Preliminary RCA (spec-1.21.1 R-fix H-1 neighborhood)
llmapp/model.gostreamDoneMsghandler recoversresidual = previewNodes + stream.Drain()to preserve partial text on cancel paths. On the NORMAL path the comment asserts both are empty — but there is a window:loopStartCmdgoroutine teardown runsts.Close()(flushes the final partial line into the TokenStream's pending nodes) beforeclose(ctrl). If the Model's SealedText handler performed its discard-drain BEFORE the Close-flush landed, the flushed final line survives in the closed stream andstreamDoneMsg'sstream.Drain()re-commits the already-sealed text → duplicate assistantblock.Message.Candidate fixes (for the owning spec/hotfix, NOT verified):
finishHandled == true(normal-finish path needs no recovery — recovery exists for the lost-EventFinish cancel path only)Scope
Pre-existing 1.21.1-adjacent race; candidate for a spec-1.21.2 hotfix branch (hotfix matrix: 不动 spec 设计, 三路 code review). Not blocking spec-2.3 (FROZEN @ v0.53.0-stage2.53).
Spec: spec-1.21.1-stream-ordering-fix.md (neighborhood)