fix: cancel-then-send hang, turn contamination, abort storm, and thought-level root cause - #48
Merged
Merged
Conversation
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.
Summary
Fixes the long-standing cancel-then-send hang and a family of bugs uncovered while fixing it, plus the thought-level dropdown regression. 8 commits, all verified against the live backend with an end-to-end probe.
Root cause of the original hang (cancel → send)
The backend holds a prompt lock that stays taken for 18–41s after
session/stop(streaming-connection cleanup). The old code guessed readiness from bridge-local signals instead:preemptInFlightTurnspun waiting for the old turn'spendingTurnsdeletion — blocked the new prompt for the whole backend finalisation window.session/sendwas one-shot: a busy reject (code 1308) immediately threw, so the new message "disappeared".Fix: the backend's prompt lock is the single readiness signal.
session/sendnow retries every 500ms (30s cap) while busy; preempt returns immediately; the magic window is gone. Send (command channel) and event ingestion (reader loop) are fully decoupled.Turn-boundary contamination (found in review)
Removing the old silent-drain let a cancelled turn A's events leak into the next turn B's listener queue. Fixed with a turn-attribution gate (drop pre-
turn.startedevents, only when this send preempted another) plus per-message replay dedup. Review hardening (af335fe):translate()— the translator'sturnDone/turnFailedflags are sticky; translating residue made turn B exit at its ownturn.startedwith zero output.preemptInFlightTurnnow cancels all matching turns (mirrorscancel()); the first-match break could leave a live turn running and fail the third prompt after 30s of busy retries.fetchLastReplyregisters its message id so the completion-diff replay can't double-display the same text.Abort storm (event-loop starvation)
requestWithTimeoutleaked a 100ms cancel-pollsetIntervalafterPromise.racesettled — N orphan timers per turn warned 10×/s each and starved the loop. Now every racer registers a disposer; the race winner tears them all down. Cancelled turns also drain+decline server requests inline instead of racing the cancel-poll against the editor.Thought level showed enabled/disabled instead of max/high/low
Two sibling bugs dropped the per-model
reasoningdefinition (variants → protocollevels):buildProviderRegistrycompressed models to bare{modelId}and pushed aftersession/create— new sessions fell back to the apiFormat's 2-state default. Now the registry carries full model definitions (label/contextWindow/maxOutputTokens/reasoning) and is pushed before create and before resume.buildRuntimeModel(resume overlay +/modelswitch) had the same compression — a resumed session silently reset to 2-state. Both paths now sharebuildModelElement./thoughthint updated tomax|high|low(GLM-5.3 variants).Also: default model constants (
GLM-5.3) extracted tooptions.ts; stale preempt-wait comments corrected.Verification
builtin:bigmodel-coding-plan/GLM-5.3withthoughtLevel = [high, low, max].Relationship to recently merged PRs
Rebased on top of #43 / #46 / #47 (all merged). Division of labour:
reasoningdefinitions, sosession/readreturned the apiFormat's 2-state default (enabled/disabled) no matter what the client advertised. Fixed by carrying full model definitions (label / contextWindow / maxOutputTokens / reasoning levels) inworkspace/updateProviderRegistry(pushed before create/resume) and in theruntimeModeloverlay (resume +/modelswitch), via a sharedbuildModelElement.Both halves are needed: without this branch, #47 alone still showed enabled/disabled on live sessions.
Conflict resolution during rebase deliberately took #47's side for every pending-display piece (CONFIG_META fallback, bugfixes/dispatch assertions) and kept only the backend-side root-cause fixes here.