Skip to content

fix(engine): don't send the first message before the real default loads - #103

Closed
Its-My-Work wants to merge 1 commit into
Lexus2016:mainfrom
Its-My-Work:fix/engine-default-race
Closed

Its-My-Work wants to merge 1 commit into
Lexus2016:mainfrom
Its-My-Work:fix/engine-default-race

Conversation

@Its-My-Work

@Its-My-Work Its-My-Work commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

_globalDefaultEngine starts hardcoded 'api' and only becomes the actual configured default once checkVersion()'s /api/version fetch resolves and calls resolveEngineForView(). A message sent before that fetch lands — in practice, the very first message on a freshly loaded page — went out with curEngine still on that placeholder 'api'.

The server bakes whatever engine arrives into the session's run_engine permanently (server.js ~line 11914), so this silently and irreversibly routed that chat's billing through the API key instead of the user's actually-configured Subscription default, for the life of the chat.

Fix

Capture checkVersion()'s promise as _versionCheckDone and await it at the top of send() — a no-op once already resolved, so this costs nothing beyond the first message of a session.

Verified

  • Confirmed against a real deployment: brand-new sessions were landing in the DB with run_engine='api' explicitly set even though the configured default was 'subscription' — never NULL (which would mean "inherit the default").
  • After the fix, the first message correctly waits for the real default before it's sent.

Test plan

  • Set the default engine to Subscription, hard-refresh the page, immediately send a message as the very first thing in a new session — confirm it resolves to the Subscription engine, not API.

🤖 Generated with Claude Code

https://claude.ai/code/session_011E2bJQ2sL9LgEWosjvydTR

_globalDefaultEngine starts hardcoded 'api' and only becomes the actual
configured default once checkVersion()'s /api/version fetch resolves and
calls resolveEngineForView(). A message sent before that fetch landed —
in practice, the very first message on a freshly loaded page — went out
with curEngine still on that placeholder 'api'. The server bakes
whatever `engine` arrives into the session's run_engine permanently
(server.js ~11914), so this silently and irreversibly routed that
chat's billing through the API key instead of the user's actually
configured Subscription default, for the life of the chat.

Capture checkVersion()'s promise as _versionCheckDone and await it at
the top of send() — a no-op once already resolved, so this costs
nothing beyond the first message of a session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011E2bJQ2sL9LgEWosjvydTR
@Its-My-Work

Copy link
Copy Markdown
Contributor Author

Closing this one — a different, more concrete bug (mobile engine-chip highlight sync, #105) turned out to be the actual cause of the reported symptom. Superseding with #105.

Lexus2016 added a commit that referenced this pull request Oct 7, 2026
…the first send, sync the mobile chips (#127)

Three ways the UI and the chat disagree about the engine. #103 and #105 proposed two of them and
were withdrawn because they did not cure the reported 402; verifying them in a browser found the
cause they both missed.

- checkVersion() called loadTerminalCapability() before it stored the default engine, and that helper
  lives in the last script block. A reply landing before the parser reached it threw a ReferenceError
  into checkVersion's own catch{}, skipping the configured engine, the chat defaults and loadBots().
  The default is now stored right after applyTmuxCapability(), ahead of every call that can throw.
- send() read curEngine on its 'api' placeholder before /api/version had landed. It now awaits
  engineDefaultReady(): bounded by VERSION_WAIT_MS, skipped for a user who already clicked an engine
  chip (_engineUserPicked), and tied to the local version reply only (the GitHub release lookup moved
  to _checkLatestRelease(), not awaited).
- syncMobSheetFromDesktop() had no engine branch, so opening the mobile sheet blanked both engine chips.

The wait's two extra rules came from an independent review (codex, agy) of the first version; both
defects were reproduced before they were fixed. test/render/engine-default.test.mjs: 18 tests, each
watched failing first. CLAUDE.md test counts corrected (101 = 20 + 81).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant