Skip to content

Commit 632ba60

Browse files
authored
feat(desktop): run a chat's desktop tools in the background executor (#8668)
* feat(desktop): run a chat's desktop tools in the background executor The Sim desktop app now picks up and runs the desktop tool calls of turns bound to it from Electron main, so work keeps going when the user leaves the chat, switches workspace, or reloads the window. It speaks the device protocol from the previous PRs and stays dormant until Sim enables it for the user. - Identity: a stable install id in userData, registered with the app's own session on sign-in and session change, retired on sign-out (which also stops every action and clears the outbox). A 409 mints a new id. - Pickup: an SSE doorbell plus the reconciling inbox read on connect, on every ring, on wake and network return, and on Sim's reconcile timer. - Execution: each call is claimed as soon as it is offered, queued per chat and surface, and run in that chat's own browser scope, terminals, or granted folders, from Sim's record of the call. Leases are renewed from claim to acknowledgement. Covers browser_*, terminal, read_local_file and user-local read/grep/glob; import_local_files is answered as not run for now. - Outbox: an encrypted journal (claiming, claimed, started, result) written before each step. A restart reports a call that never started as not started, one that did as outcome unknown, and a finished one with its result; nothing runs twice. Results are retried with backoff until Sim answers. - Stop: inbox cancels and refused lease renewals stop the action. Terminal runs get a real cancel: Ctrl-C, then SIGTERM and SIGKILL to the foreground process group. - Approvals: a notification for a background chat waiting on the user, opening the chat at its approval card and closing once decided; it never names the command. - preload exposes desktopExecutor.getDevice() so the composer binds turns. The browser, terminal and local-filesystem result projections move from the web app into @sim/desktop-bridge so the chat view and the executor build identical results; the renderer behaviour is unchanged. * fix(desktop): make the executor's journal, sign-out and terminal stops hold under races - A journal transition that cannot be made durable now fails: an unrecorded claim is not made, and an action whose start could not be recorded is not run and is reported as not run. The journal keeps result data within what a restart can read back. - Sign-out fences registrations in flight, and a rotated install id never reuses the previous id's client. - A stopped call's result carries no content, only the acknowledgement. - Terminal stops: a Stop before the command starts runs nothing; escalation signals only the stopped command's own process group, rechecked before each signal; a stopped tmux run ends its wait at once. - A chat's next terminal operation waits for one that outlived its deadline. - The doorbell bounds its handshake and reconnects at once on wake. - Approval notifications name neither the chat nor the command. - A user-local glob that stopped early says so. * fix(desktop): stop every terminal operation, answer only from granted folders, and bound the outbox in bytes - Stop reaches `input`, `kill` and pane `close`, not just `run` and `handoff`. A Stop that lands while the session resolves means none of them starts. A batch of keys or lines stops between keystrokes and says some input may already have arrived. - A terminal call stopped while it waited for the chat's previous operation never reaches the terminal. - Sign-out stops running actions before it waits on a reconcile in flight. - The outbox budgets result data in UTF-8 bytes, so a journal of non-ASCII results still reads back after a restart. - A local read, grep, glob, list or stat answers only while its folder is still granted when it finishes. This covers the chat view's path as well. - Grep says when its results are truncated, as glob does. - A doorbell stream that keeps closing as soon as it opens backs off instead of reconnecting every second. - The not-started results follow #8666's wording: what happened, that nothing ran, and what the model should do instead of retrying. * test(mothership): a desktop tool's executor resolves only once the tool has settled * fix(desktop): hold refused results until the device registers again, and stay awake through recovery - A result Sim refuses because it no longer recognizes the device is parked rather than retried every 30 s. The journal keeps it, and it is sent once the device registers again. The service is told once. - A dormant device's 15-minute registration recheck is no longer pushed out by every refused request. - A 408 is retried like any other timeout. - Results a previous app run left behind keep the executor busy until Sim has them, so the machine does not sleep with them undelivered. - A Sim that answers registration with another protocol version gets no new turns bound to this device. - Selecting an already granted folder again while a read runs keeps the read's answer. Only a revoked or different grant discards it. * fix(desktop): stop the agent's terminal commands at sign-out, by their own process groups Commands the agent started could outlive the session that started them. Closing a shell only hangs up its foreground, so a command that ignores SIGHUP kept running, and a tmux run outlives the Sim terminal entirely. - Sign-out and an account change now stop every command the agent started before the shells are torn down. A plain shell's command is stopped by its own process group, the same Ctrl-C, SIGTERM, SIGKILL escalation Stop uses, and each tmux run window still going gets Ctrl-C, then is closed. - A command the user started is never touched: only a command a `run` started counts as the agent's. - The agent's command stays the agent's until it really exits, even after an interactive command detaches from its tool call. Stop and sign-out can still end it then. * fix(desktop): a signed-out executor goes idle once and never speaks for its successor dispose() now reports idle once. A recovered delivery that settles after sign-out no longer reports through the shared busy callback, where it could release the sleep blocker the next session's executor was holding. * fix(desktop): stop agent tmux runs from the moment they start, and when Terminal is switched off - A tmux run is tracked as soon as its window exists, not once its wait ends. Sign-out now reaches a command the chat view started that is still inside its wait window. Reaping skips runs whose call is still reading their files. - Switching Terminal off stops the agent's commands before the shells go, as sign-out does. - The runner's deadline is built on the shared interruptible sleep. - A hostname longer than Sim's 128-character limit is shortened, so registration does not fail on it. * fix(desktop): keep sign-out, Terminal off and run files from racing each other - Sign-out waits for a restart's journal walk before clearing the journal, and a walk or delivery that starts after sign-out sends nothing, so none of the previous session's results outlive it. - An unrecognized device's stopped loops stay stopped: the reconcile timer no longer re-arms itself after them. - Switching Terminal off disposes the shells only if it is still off once the agent's commands have stopped. - A tmux run whose terminal closes mid-wait keeps its output files until its call has read them. - A terminal operation that outlives its deadline is reported as outcome unknown and not to be retried, as a browser action is. * fix(desktop): say why local files are out of reach, and drop non-null assertions in tests When the app has no usable account storage (signing out, switching account, storage unavailable), a local file call now says so. It no longer tells the model a setting is off and to ask the user to switch it on. * test(desktop): drive the doorbell backoff test with fake timers * fix(desktop): stop only the run's own tmux pane, stay dormant while the executor is off - A tmux run's pane is tagged with a per-run @sim-run-id option. Stops and closes check that tag first and act on the run's own pane only, never the active pane or the whole window. A pane the user split beside it, or an id a restarted tmux server handed to one of the user's panes, is never touched. A run whose pane is gone is released. - With the executor off for this user, the app no longer opens the inbox or the doorbell. It rechecks registration on the existing triggers and every 15 minutes. Against a Sim without the executor routes (404 or 405) it stays dormant until the next launch, session change or toggle. - An inbox read that Sim answers after sign-out raises no approval notification and claims nothing. - A result this device cannot encode is reported once as outcome unknown, instead of being retried forever as if the network had failed. - Only a missing install-id file counts as no id; any other read failure is retried rather than minting a new id over the old one. - A generic terminal failure reports its code to the model again. - The doorbell, notifier and service tests observe state through typed fakes. * fix(desktop): a result that keeps failing to reach Sim stops keeping the machine awake Delivery still retries it, but after ten minutes of failures it no longer counts as work the machine must stay awake for. * fix(desktop): refuse untaggable tmux runs, keep install ids durable, never reject a reconcile - A tmux run whose pane cannot be tagged is closed at once and reported, so nothing the agent starts is ever beyond a later stop. - A pane tmux cannot be asked about is neither stopped nor given up on; only a confirmed absence or a mismatched tag releases a run. - A new install id is used only once it is saved. Registration retries instead of running under an id the next launch would not find. - A Sim without the executor routes is rechecked every 15 minutes, logged once, so an upgrade reaches an open app. - An inbox read never rejects: a failing notifier is logged, and the read settles. - A long completion message is shortened without splitting a character. * fix(desktop): a tmux run starts only once its pane is tagged, and sign-out reaches runs of closed tabs - A run's command now waits for its pane to be tagged as the run's before it starts. Untagged, it never starts and its pane closes by itself, so no pane is ever closed by an id a restarted tmux server might have handed to the user. A short command can no longer finish before the tag exists. - The run script is a file instead of a bash -c string. tmux hands its command to sh -c, which expanded the counter and PIPESTATUS meant for bash first, so tmux runs reported no exit code. - A run whose Sim terminal closed while it kept going in tmux is still stopped at sign-out and when Terminal is switched off. - Retiring an install id either replaces it or removes it from disk, so a retired id is never picked up again. A 409 whose new id cannot be saved backs off instead of retrying at once. - A failing approval notification no longer keeps the inbox read from claiming its calls. * test(desktop): a throwing notifier still claims, and sign-out reaches a closed tab's tmux run * fix(desktop): gate a run past tagging's timeout, forget gone orphans, re-register when back online * test(desktop): drop a test that only checked which tmux calls were made
1 parent f4855cd commit 632ba60

52 files changed

Lines changed: 6457 additions & 591 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/desktop-e2e.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ jobs:
8484
- name: Run Playwright _electron smoke suite
8585
working-directory: apps/desktop
8686
run: bunx playwright test
87+
env:
88+
BACKGROUND_EXECUTOR_REPORT_PATH: test-results/background-executor-report.json
8789

8890
- name: Upload test results
8991
if: failure()

0 commit comments

Comments
 (0)