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
A Chat open in the desktop app and in a web tab at the same time could lose a browser or terminal tool's real result: the agent got an error instead
Every client tailing a chat starts each client-executed call it sees. A web tab has no desktop bridge, so for a desktop tool it fails at once and posts an error to /api/copilot/confirm. The confirm route takes a pre-claim error while the call is still pending, so the web tab's error usually lands before the desktop claims the call (the desktop restores its browser scope first). The desktop's claim then 404s, and its retried reports 404 too
Fix: the stream tool-event handler starts a desktop-only tool (agent browser, terminal, local files) only in the desktop app. Any other client leaves the call pending for the desktop, which claims it atomically through /api/desktop/tool/authorize as before
isDesktopExecutedToolCall now names that set in client-executed-tools.ts, and isClientExecutedToolCall is built from it, so the gate and the client-executed set can't drift apart
Workflow tools are unchanged: they still start in any client, and their claim settles who runs them
Design notes
Rejected: a server-side owner check on confirm. Native claims record a constant owner (desktop-browser/desktop-terminal), not which client holds them, and the desktop renderer's pre-claim errors (stale event, closed session, replay-guard limits) need the pre-claim path. Telling the web tab and the desktop apart would mean trusting a client header, or adding a per-client claim token across Electron main, the renderer and the server, which old desktop builds don't send
Rejected: changing the 404 on a late confirm for an already-finished native call to a 200 or 409. With this fix that 404 no longer happens in this scenario. A native confirm carries no executor identity, so a 200 would tell a client its result was accepted when it was thrown away
Behavior changes
Web only (no desktop app): no change. The server offers browser, terminal and local-file tools only when the request reports desktop capabilities, so a web-only turn never gets these calls
Desktop and a web tab open together: the desktop's result reaches the agent. The web tab shows the call as running until the result streams in
Desktop app (any shell version): no change. The renderer is served by Sim, and every shell injects the bridge the gate checks
If the desktop app quits for good while a web tab stays open, and the agent then issues a desktop tool, the call is no longer failed right away with a misleading "no active browser session" error from the web tab. It now ends on the worker's deferred-call timeout, the same as when no web tab is open. A desktop that exits mid-action still reports through its page-exit path, as before
Not covered here: two desktop windows on the same chat both try the call. The losing window's rejected claim is still reported as an error. This predates this PR and is unchanged by it. Fixing it needs a per-window claim identity
Type of Change
Bug fix
Testing
New lib/mothership/tools/client/desktop-tool-executor.integration.ts runs against real Postgres and Redis. A web tab runs the production stream handler and client executors. The desktop claims through the real authorize route and reports through the real confirm route. The server-side waiter shows what the agent receives. Covers browser_find and terminal
Red on origin/staging: the desktop's claim gets 404 and the agent receives the web tab's error
Green with the fix: the claim returns 200, the confirm returns 200, the agent receives success, and the row is completed
handle-tool-event.test.ts: the existing desktop routing tests now set the desktop bridge explicitly
Reverting the gate turns the integration test red again
bun run lint, bun run type-check (apps/sim), bun run check:audits, docs-manifest:check, block-registry check, root bun run test
Checklist
Code follows project style guidelines
Self-reviewed my changes
Tests added/updated and passing (new tests pass the test-audit authoring gate)
[Medium risk] Refactors which tool calls run on desktop versus web clients.
The PR appears safe to merge, though the omitted web-tab routing coverage is worth restoring.
Summary
The PR prevents web tabs tailing a chat from executing desktop-only tool calls, leaving those calls for the desktop app to claim and complete.
Separates desktop-executed tools from workflow tools in the client-executed classification.
Adds a database- and Redis-backed regression test for browser and terminal results.
Removes the prior mock-call test, leaving workflow and local-file web-tab routing without equivalent coverage.
Diagram
sequenceDiagram
participant Web as Web tab
participant Desktop as Desktop app
participant Server as Tool-call server
participant Agent as Agent waiter
Server-->>Web: Desktop-only call frame
Server-->>Desktop: Desktop-only call frame
Web->>Web: Leave call pending
Desktop->>Server: Claim call
Server-->>Desktop: Authorized
Desktop->>Server: Confirm native result
Server-->>Agent: Deliver result
The deleted test was the only one checking that workflows still start in a web tab while local-file calls stay pending. The remaining unit tests all use a desktop bridge, and the new integration test covers only browser and terminal calls. This leaves both omitted paths open to regressions without a failing test. Please restore coverage through observable outcomes rather than mock-call assertions.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Re the outside-diff note on web-tab routing coverage: leaving this as is, deliberately.
Local files: they take the same branch as browser and terminal. The gate is isDesktopApp() || !isDesktopExecutedToolCall(name, args), and isClientExecutedToolCall is now built from isDesktopExecutedToolCall, so the set of calls a client starts and the set it leaves to the desktop can't drift apart. The integration suite exercises that branch end to end for browser and terminal. Adding import_local_files needs a DOM window in the web-tab realm, because the native-file executor registers a pagehide listener. In this Node-environment suite, a global window would also flip the server modules that read it to tell server from browser (env resolution). I tried it, and the harness that keeps the two realms apart was more machinery than the coverage is worth.
Workflow tools: they are outside isDesktopExecutedToolCall by construction, so the gate never applies to them. If a regression ever stopped a web tab from starting one, the server's workflow pickup race (raceWorkflowToolClientPickup) would still run it after the grace period. The removed unit test was the only cheap guard, and it asserted mock calls, which the repo's testing rules prohibit.
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
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
/api/copilot/confirm. The confirm route takes a pre-claim error while the call is still pending, so the web tab's error usually lands before the desktop claims the call (the desktop restores its browser scope first). The desktop's claim then 404s, and its retried reports 404 too/api/desktop/tool/authorizeas beforeisDesktopExecutedToolCallnow names that set inclient-executed-tools.ts, andisClientExecutedToolCallis built from it, so the gate and the client-executed set can't drift apartDesign notes
desktop-browser/desktop-terminal), not which client holds them, and the desktop renderer's pre-claim errors (stale event, closed session, replay-guard limits) need the pre-claim path. Telling the web tab and the desktop apart would mean trusting a client header, or adding a per-client claim token across Electron main, the renderer and the server, which old desktop builds don't sendBehavior changes
Type of Change
Testing
lib/mothership/tools/client/desktop-tool-executor.integration.tsruns against real Postgres and Redis. A web tab runs the production stream handler and client executors. The desktop claims through the real authorize route and reports through the real confirm route. The server-side waiter shows what the agent receives. Coversbrowser_findandterminalorigin/staging: the desktop's claim gets 404 and the agent receives the web tab's errorcompletedhandle-tool-event.test.ts: the existing desktop routing tests now set the desktop bridge explicitlybun run lint,bun run type-check(apps/sim),bun run check:audits,docs-manifest:check, block-registry check, rootbun run testChecklist
test-auditauthoring gate)