You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while smoke-testing the v2.3.0 milestone, verifying #1998 (PR #2047).
The problem
runWithCommandAuthRecoveryrethrows anything that isn't an AuthRecoveryRequiredError (clients/web/src/App.tsx:2063):
}catch(err){if(errinstanceofAuthRecoveryRequiredError){/* …recover… */}throwerr;// <- everything else propagates to the caller}
It is called as void runWithCommandAuthRecovery(...) at 18 sites in App.tsx. Exactly one of them attaches a rejection handler — onRefreshTasks (:3679), which .catch()es and shows a notification. The other 17 have no handler, so any non-auth failure of the wrapped operation becomes an unhandled rejection in the browser.
Reproduced
Against a stdio server whose tools/list rejects, with the browser listening on unhandledrejection:
Chromium also surfaces these as a pageerror. Prompts and resources have the identical wiring.
So this is not a #1998 leftover: #1998 fixed the connect-time float inside the paged states, and the remaining floats are in the user-initiated handlers, in both pagination modes. That wiring predates it — onLoadMore* came in with #1721 (b3b04c6, 2026-07-19) and is untouched by #2047.
Why it matters
Nothing is visibly broken: in every case above the list panel already renders "Couldn't load tools" with the real message and a Retry, so the user sees the failure. The cost is elsewhere:
Two gates that should catch this can't.void is the sanctioned suppression for @typescript-eslint/no-floating-promises, so Enable @typescript-eslint/no-floating-promises so an unhandled rejection can't fail the gate again #1959 does not flag these. And smoke:web:browserhard-fails on exactly this signal (unhandled rejection / pageerror) — it just never connects to a server, so it never reaches the code. Any future smoke that drives a failing list turns red on a bug nobody introduced.
This is a debugging tool. A spurious uncaught ProtocolError in the console is worse here than in an ordinary app: someone inspecting their own server sees an error the Inspector produced, not one their server did, and has to rule it out.
Suggested shape
Give every void runWithCommandAuthRecovery(...) a rejection handler. Two classes, and the file already contains an example of each:
The failure is already surfaced in the panel (the list refresh / load-more paths — the paged and managed stores both record the error). A terminal .catch(() => {}) is enough, with a comment saying the error is displayed elsewhere — the same reasoning PagedToolsState uses for its connect-time void this.loadPage(undefined).catch(() => {}).
Nothing else records it. Follow onRefreshTasks (:3679) and show a notification.
Worth auditing all 18 sites rather than only the three verified above, since the same void shape is used for several other commands.
A cheaper structural option, if it fits: have the handlers await through a small wrapper that owns the reporting, so a new call site cannot reintroduce the gap by omission.
Acceptance
With a server whose list calls reject, neither Retry nor Load next page produces an unhandledrejection or pageerror, in either pagination mode.
The existing error UI (alert + Retry, page 1 retained on a page-2 failure) is unchanged.
No void runWithCommandAuthRecovery(...) in App.tsx is left without a rejection handler.
Notes
Pre-existing; not a regression from anything in v2.3.0. Filed against v2.4.0 rather than v2.3.0, which is due 2026-08-19 with only the release bump (#2013) outstanding.
Found while smoke-testing the v2.3.0 milestone, verifying #1998 (PR #2047).
The problem
runWithCommandAuthRecoveryrethrows anything that isn't anAuthRecoveryRequiredError(clients/web/src/App.tsx:2063):It is called as
void runWithCommandAuthRecovery(...)at 18 sites inApp.tsx. Exactly one of them attaches a rejection handler —onRefreshTasks(:3679), which.catch()es and shows a notification. The other 17 have no handler, so any non-auth failure of the wrapped operation becomes an unhandled rejection in the browser.Reproduced
Against a stdio server whose
tools/listrejects, with the browser listening onunhandledrejection:PagedToolsStateconnect-time loadonRefreshTools→:3505unhandledrejection: ProtocolError: …onRefreshTools→:3510unhandledrejection: ProtocolError: …onLoadMoreTools→:3635unhandledrejection: ProtocolError: …Chromium also surfaces these as a
pageerror. Prompts and resources have the identical wiring.So this is not a #1998 leftover: #1998 fixed the connect-time float inside the paged states, and the remaining floats are in the user-initiated handlers, in both pagination modes. That wiring predates it —
onLoadMore*came in with #1721 (b3b04c6, 2026-07-19) and is untouched by #2047.Why it matters
Nothing is visibly broken: in every case above the list panel already renders "Couldn't load tools" with the real message and a Retry, so the user sees the failure. The cost is elsewhere:
voidis the sanctioned suppression for@typescript-eslint/no-floating-promises, so Enable @typescript-eslint/no-floating-promises so an unhandled rejection can't fail the gate again #1959 does not flag these. Andsmoke:web:browserhard-fails on exactly this signal (unhandled rejection /pageerror) — it just never connects to a server, so it never reaches the code. Any future smoke that drives a failing list turns red on a bug nobody introduced.ProtocolErrorin the console is worse here than in an ordinary app: someone inspecting their own server sees an error the Inspector produced, not one their server did, and has to rule it out.Suggested shape
Give every
void runWithCommandAuthRecovery(...)a rejection handler. Two classes, and the file already contains an example of each:.catch(() => {})is enough, with a comment saying the error is displayed elsewhere — the same reasoningPagedToolsStateuses for its connect-timevoid this.loadPage(undefined).catch(() => {}).onRefreshTasks(:3679) and show a notification.Worth auditing all 18 sites rather than only the three verified above, since the same
voidshape is used for several other commands.A cheaper structural option, if it fits: have the handlers
awaitthrough a small wrapper that owns the reporting, so a new call site cannot reintroduce the gap by omission.Acceptance
unhandledrejectionorpageerror, in either pagination mode.void runWithCommandAuthRecovery(...)inApp.tsxis left without a rejection handler.Notes
Pre-existing; not a regression from anything in v2.3.0. Filed against v2.4.0 rather than v2.3.0, which is due 2026-08-19 with only the release bump (#2013) outstanding.