fix(mothership): fail unclaimed desktop calls fast and stop dropping them silently

Merged
WA
1mo ago
+1269-285

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 settleToolCallFailure that 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.
  • Local reads (read_local_file, user-local VFS read/grep/glob) are covered too, for a desktop that advertises localReadClaims:
    • 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 running behaviour, which authorize still accepts.
  • 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 run budget. 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. resolveRunWaitMs moves 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.ts is 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: false feeds 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 staging it 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.
  • Unit and DOM tests:
    • desktop-wait.test.ts forces a result to land inside the grace/abort window;
    • executor.test.ts covers 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.ts claim 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-audit authoring gate)
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)