fix(mothership): fail unclaimed desktop calls fast and stop dropping them silently
Merged
WA
1mo ago+1269+1269+0123456789012345678901234567890123456789-285-285-012345678901234567890123456789
Summary
Builds on #8643 (merged). Only the chat view showing a chat starts that chat's desktop calls, so a call issued while the user is on another chat or page was claimed by nobody. The turn then waited 90 to 160 s for a watchdog that called it "hung".
- One desktop wait (
request/tools/desktop-wait.ts). A desktop call the desktop claims on pickup that is still unclaimed after a 15 s grace is settled as never started, using the inverse of the claim (pending -> failed). Exactly one side wins: a late claim is refused, and a call claimed in time keeps waiting for its result.- The failure is sealed like any client completion, through one shared
settleToolCallFailurethat the watchdog uses too. - A result that lands as the grace runs out is returned, not discarded.
- The model gets
{ error, notStarted: true }: never started, the chat isn't open in the desktop app, safe to retry.
- The failure is sealed like any client completion, through one shared
- Local reads (
read_local_file, user-local VFSread/grep/glob) are covered too, for a desktop that advertiseslocalReadClaims:- the preload exposes it, the renderer sends it with the turn's desktop capabilities, and pre-persist then stores those calls
pending; - Electron main claims each one through authorize (
claim: true), as it already does for imports; later reads of the same call ride on that claim. - Older desktops don't advertise the capability, and a recovered leg doesn't carry it (the strict recovery config is untouched). In both cases those calls keep today's
runningbehaviour, which authorize still accepts.
- the preload exposes it, the renderer sends it with the turn's desktop capabilities, and pre-persist then stores those calls
- The single "hung and was abandoned" result becomes two:
notStarted, for a desktop call nobody claimed;outcomeUnknown+doNotRetry, for one that started and lost its result.
- Terminal
runbudget. The server budget now follows the wait the desktop holds a run for (waitSeconds, up to 120 s), plus headroom; it was a fixed 60 + 30 s.resolveRunWaitMsmoves into@sim/terminal-protocol, shared with the desktop. - A report that says a call never started can only settle an unclaimed call. A stale replay can no longer end a command the desktop is running.
- Renderer:
- a running browser action is cancelled only by the user's Stop. Recovering the stream when the window returns to view, or leaving the chat view, lets it finish and report its own result;
- a terminal call delivered too late is reported as not started instead of being skipped.
- One classifier.
lib/mothership/tools/desktop-tools.tsis the only desktop-tool classifier; the authorize and confirm routes and the pickup path use it. - Typed claims. The Sim execution claim and the desktop claim share one run-admission lock but have their own outcome types, so Sim callers cannot receive the desktop-only
awaiting_permission. - The worker needs no change: it passes tool result data to the model as opaque JSON, and
success: falsefeeds its failure breaker as before.
Type of Change
- Bug fix
Testing
desktop-tool-pickup.integration.ts(real Postgres + Redis, production pre-persist and dispatch, authorize and confirm routes), 7 tests:- an unclaimed browser, terminal or capable-desktop local read fails as not started at about 15 s; on
stagingit hangs for the whole test budget; - a late claim is refused;
- a call claimed in time, including a claimed local read read again past the grace, still completes;
- a stale not-started report can't end a running command;
- an older desktop's local read works as before.
- an unclaimed browser, terminal or capable-desktop local read fails as not started at about 15 s; on
- Unit and DOM tests:
desktop-wait.test.tsforces a result to land inside the grace/abort window;executor.test.tscovers both abandoned shapes and the terminal budget;terminal-tool-execution.test.ts;use-chat.dom.test.tsx: recovery and unmount leave the action running, and Stop cancels it;- the desktop
ipc.test.tsclaim flag.
- Reverted each of the 12 guards on its own and watched its test go red.
bun run lint,bun run type-check,bun run check:audits,bun run test,bun run test:integration,bun run docs-manifest:check, block-registry check: all pass.
Checklist
- Code follows project style guidelines
- Self-reviewed my changes
- Tests added/updated and passing (new tests pass the
test-auditauthoring gate) - No new warnings introduced
- I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)