fix: five correctness bugs (model-wipe, infinite retry loop, type gap) - #260
Closed
dylanneve1 wants to merge 1 commit into
Closed
dylanneve1 wants to merge 1 commit into
dylanneve1 wants to merge 1 commit into
Conversation
1. fallback_model path wipes modelByBackend (CRITICAL)
getChatSettings(chatId).model is undefined after migrateLegacyModelField
runs, so the finally-block restore called setChatModel(chatId, undefined),
permanently deleting every per-backend model override the user had set.
Fix: pass the fallback model via params.model instead of mutating chat
settings at all. Applied to handle-retry.ts (shared helper), the inline
copy in openai-agents/handler.ts, and codex maybeFallbackForChatGptMismatch.
2. openai-agents flow-violation infinite retry loop
detectFlowViolation was called without retryCount or maxRetries. With the
new retryCount-based API, retried:true maps to retryCount=1 which is always
< FLOW_VIOLATION_MAX_RETRIES=3, so shouldRetry is permanently true and the
handler recurses indefinitely on persistent violators. Fix: pass
retryCount:(_retried?1:0) and maxRetries:1 to cap at a single retry (the
documented single-pass behaviour). Also adds the missing toolCalls field so
tool-call-only violations are detected.
3. openai-agents flow-violation log uses wrong format string
The log line hardcoded "trailing prose (N chars)" even when the violation
was caused by tool calls with no terminator. Fix: use violation.reason.
4. RemoteAssistantInfo missing id field / untyped dedup cast
The listSessionMessages dedup cast (message as ...).info as {id?:string}
was untyped and fragile. Fix: add id?:string to RemoteAssistantInfo and
use the typed form (message as {info?:RemoteAssistantInfo}).info?.id.
5. events.ts scope filter uses truthiness instead of !== undefined
if (evtSessionID && ...) treats sessionID="" as "no scope tag", allowing
a session.turn.close with a blank sessionID to stop any session's loop.
Fix: use evtSessionID !== undefined for the guard.
https://claude.ai/code/session_01JpQ53zUQ5RpZq16mEH1Tgj
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
Deep code review surfaced five bugs, three of them confirmed critical through independent verification.
1. CRITICAL β
fallback_modelpath permanently wipesmodelByBackend(3 files)Files:
src/backend/shared/handle-retry.ts,src/backend/openai-agents/handler.ts,src/backend/codex/handler.tsAfter
migrateLegacyModelFieldruns at startup,getChatSettings(chatId).modelisundefined(the legacy field is deleted). Thefallback_modelretry path saved this asoriginalModel, then thefinallyblock calledsetChatModel(chatId, undefined). Perchat-settings.tsline 346β354, passingundefinedtosetChatModelis the "reset everything" semantic: it deletes bothmodelByBackendandmodel, permanently wiping every per-backend model the user had selected.Fix: Pass the fallback model via
params.modelinstead of mutating chat settings. The swap is now purely in-memory β nosetChatModelcalls, notry/finally, and user preferences survive the retry untouched.2. CRITICAL β
openai-agentsflow-violation check causes infinite retry loopFile:
src/backend/openai-agents/handler.tsdetectFlowViolationwas called withretried: _retried(boolean) but withoutretryCountormaxRetries. With the new retryCount-based API inflow-violation.ts,retried: truemaps toretryCount = inputs.retryCount ?? 1. Since1 < FLOW_VIOLATION_MAX_RETRIES (3)is always true,shouldRetrynever becomes false. On a model that persistently violates the flow contract, the handler recurses indefinitely.Fix: Explicitly pass
retryCount: (_retried ? 1 : 0)andmaxRetries: 1, preserving the single-pass behaviour documented in the handler's own comment. Also adds the missingtoolCalls: streamState.toolCallsparameter so tool-call-only violations (model calls tools but no terminator) are detected correctly.3. MEDIUM β
openai-agentsflow-violation log always says "trailing prose"File:
src/backend/openai-agents/handler.tsThe log line hardcoded
"trailing prose (N chars)"even when the violation was caused by tool calls without a terminator. Now usesviolation.reason, which is already correctly phrased for both cases byflow-violation.ts.4. MEDIUM β
RemoteAssistantInfomissingidfield; dedup cast was untypedFile:
src/backend/remote-server/session-helpers.tslistSessionMessagesdeduplicates messages byinfo.id, butRemoteAssistantInfodidn't declare anidfield. The working dedup relied on an ad-hoc double-cast(message as Record<string, unknown>)?.info as { id?: string }that TypeScript couldn't validate. A future refactor could silently break the cast without any type error.Fix: Add
id?: stringtoRemoteAssistantInfoand simplify the dedup cast to the typed(message as { info?: RemoteAssistantInfo })?.info?.id.5. PLAUSIBLE β
events.tsscope filter uses truthiness instead of!== undefinedFile:
src/backend/remote-server/events.tsif (evtSessionID && evtSessionID !== ctx.sessionId)treatssessionID: ""(empty string) as "no session scope", allowing asession.turn.closewith a blanksessionIDto prematurely stop any session's SSE loop. The defensive fix uses!== undefinedso only a truly absentsessionIDis treated as unscoped.Test plan
npm testpasses (2997 tests, 0 failures) β verified locallyshared-handle-retry.test.tsβ updated two test cases to assert the new correct behaviour (fallback model injected viaparams.model, chat settings untouched)getChatSettingsafter afallback_modelretry βmodelByBackendshould be unchangedhttps://claude.ai/code/session_01JpQ53zUQ5RpZq16mEH1Tgj
Generated by Claude Code