Skip to content

fix(engine): apply the configured engine at boot, wait for it before the first send, sync the mobile chips - #127

Merged
Lexus2016 merged 2 commits into
mainfrom
fix/engine-default-race-and-mobile-chip
Oct 7, 2026
Merged

Lexus2016 merged 2 commits into
mainfrom
fix/engine-default-race-and-mobile-chip

Conversation

@Lexus2016

@Lexus2016 Lexus2016 commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Three ways the UI and the chat disagree about the engine. Two were proposed earlier (#103, #105)
and withdrawn by their author because they did not cure the reported
402 api_key allotment exhausted while Subscription is selected. Verifying them in a real browser
found why: neither touched the cause, which is item 1 and is in main.

1. The configured default engine is silently not applied (the cause)

checkVersion() called loadTerminalCapability() before it stored the default engine, and that
helper is declared in the LAST script block. When the /api/version reply lands before the parser has
reached that block, the call throws ReferenceError, which checkVersion's own catch {} swallows.
Everything after it was skipped: the default engine, the editor label, the #58 chat defaults and
loadBots().

The default is now stored right after applyTmuxCapability(), ahead of every call that can throw, and
the late-block call is optional (its other callers load it lazily). Moving the function to the early block
was rejected: it owns _terminalAvailable / _termCapabilityReason, which are lets in the late block.
Same family as isEnginePaneId (test/render/script-scope.test.mjs).

2. The first message can leave on the 'api' placeholder (#103)

curEngine starts as 'api' and only becomes the configured default once /api/version has been
handled. send() now awaits engineDefaultReady() first. Not "for the life of the chat", as #103
claimed: server.js re-persists run_engine on every turn, so the damage is the one turn sent in that
window. The wait has three rules, the last two found by review (below):

  • Bounded (VERSION_WAIT_MS, 3 s) and never throws: a dead /api/version must not freeze the composer.
  • Never applies to a user who already clicked an engine chip (_engineUserPicked, set by
    setOpt/setMobOpt, cleared by resolveEngineForView()); their pick is not a placeholder.
  • Waits on the local version reply only. The GitHub release lookup moved to _checkLatestRelease(), not awaited.

3. The mobile sheet blanks the engine chips (#105)

The API chip ships as class="mob-chip on", and syncMobSheetFromDesktop() compared every chip against
a cur that stayed '' for the engine group, so opening the sheet removed the highlight from both chips.

Independent review

Before merging I had the diff reviewed by two advisors from other providers (codex, agy). They found two
real defects in my first version of item 2, both reproduced first, then fixed with the tests written first:

  • (codex) a chip click made before /api/version answered was overwritten by resolveEngineForView()
    and then sent: pick Subscription, default is API, message leaves on api. A regression my change
    introduced into a window that used to be harmless.
  • (agy) _versionCheckDone was the promise of the whole checkVersion(), which ends by awaiting
    api.github.com; with packets to GitHub dropped, every send() paid the full bound.

Measured (real browser, patched vs clean main, same server config)

scenario main this branch
default subscription, terminal script block late (2.5 s) curEngine: "api" "subscription"
first send while /api/version takes 2.5 s engine: "api" after 8 ms "subscription" after 2467 ms
/api/version never answers — sent after 3008 ms (bounded)
user clicks Subscription, default api, /api/version takes 2.5 s — "subscription" after 7 ms, still selected afterwards
api.github.com unreachable, first send — after 6 ms
mobile sheet, curEngine = "subscription" api:off subscription:off api:off subscription:ON

Tests

test/render/engine-default.test.mjs — 18 tests. 15 were watched failing for the right reason before the change
(including the reviewer's scenario end to end on the real setOpt → checkVersion → send); the other 3 are
guards that pass before and after (other chip groups untouched, a model/mode click is not an engine pick, the
terminal capability is still preloaded once its block exists).
Render suite 111/111; the full npm test chain was run file by file, 81 of 82 commands clean.

The one that was not is test/global-workspace.test.js. Its fixtures are time-dependent (in_progress tasks
reverted to todo while seeding is slow) and it fails the same two assertions on a clean main too
(1 of 3 standalone runs). The limits of that evidence: standalone it passed 5 of 5 on this branch, but inside the
long npm test chain it failed 4 of 5 runs here, and I did not run the chain on main for comparison.
CI (Node 20 and 24) passed on the PR, and on main after the merge. test/auth-boundary.test.js
(revoked socket stayed open) is intermittent the same way (main 1 of 7, branch 3 of 7). Neither reads
index.html or CLAUDE.md; neither is touched here.

Also: CLAUDE.md test counts corrected (101 = 20 + 81; the 89/19/70 were already stale), and a note that
a literal script tag in an index.html comment breaks script-scope.test.mjs's block count (it did, once, while writing this).

Not a release; intended for the next one.

Lexus2016 and others added 2 commits October 7, 2026 18:22
…the first send, sync the mobile chips

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(). Measured on a clean main with
  defaultEngine=subscription and a late xterm.js: curEngine stayed "api". The call is
  now optional; its other callers load the capability lazily.
- send() read curEngine on its 'api' placeholder before /api/version had landed. It now
  awaits engineDefaultReady(), bounded by VERSION_WAIT_MS so a dead /api/version cannot
  freeze the composer. First chat frame: engine "api" after 8 ms -> "subscription" after
  2722 ms.
- syncMobSheetFromDesktop() had no engine branch, so cur stayed '' and the toggle removed
  the shipped `on` from both engine chips.

test/render/engine-default.test.mjs: 10 tests, each watched failing for the right reason
first. CLAUDE.md test counts corrected (101 = 20 + 81; the 89/19/70 were already stale).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ignores the GitHub lookup

The first commit made send() wait for /api/version. An independent review (codex, agy)
found two defects in that wait, both reproduced before this change:

- A click on an engine chip made before /api/version answered was overwritten by
  resolveEngineForView() and the message then left on the overwritten engine: the user
  chose Subscription, the turn ran on API. _engineUserPicked (set by setOpt/setMobOpt,
  cleared by resolveEngineForView) now exempts the pick from both the wait and
  checkVersion()'s resolve.
- _versionCheckDone was the promise of the whole checkVersion(), which ends by awaiting
  api.github.com. With packets to GitHub dropped, every send() paid the full
  VERSION_WAIT_MS. The lookup moved to _checkLatestRelease(), not awaited.

Also from the review: the default engine is stored right after applyTmuxCapability(),
ahead of the notification, terminal and loadBots() calls that can throw.

Measured in a browser: user picks Subscription while /api/version takes 2.5 s -> frame on
"subscription" after 7 ms; GitHub unreachable -> first send after 6 ms.
test/render/engine-default.test.mjs: 18 tests, the new ones watched failing first.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T17:05:48.557380Z c5f0924 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5f0924a0f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread public/index.html
}

async function send() {
await engineDefaultReady(); // a settled promise costs one microtask once the page has been open a moment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the pending send to its originating tab

When /api/version is still pending, this await can last up to three seconds while leaving the composer interactive. If the user presses Send in tab A and then switches to tab B, switchTab() saves A's composer and restores B's; after the await, send() reads the now-current inEl, attachments, activeTabId, session, and project. It will therefore silently abandon A's send or send B's existing draft to B without the user submitting it there. Snapshot or otherwise bind the pending operation to the originating tab before waiting.

Useful? React with 👍 / 👎.

@Lexus2016
Lexus2016 merged commit c5b4c8c into main Oct 7, 2026
3 checks passed
@Lexus2016
Lexus2016 deleted the fix/engine-default-race-and-mobile-chip branch October 7, 2026 17:07
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