Repository navigation
Commit 632ba60
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 made1 parent f4855cd commit 632ba60
52 files changed
Lines changed: 6457 additions & 591 deletions
File tree
- .github/workflows
- apps
- desktop
- e2e
- src
- main
- browser-agent
- desktop-executor
- terminal
- preload
- test
- sim/lib
- browser-agent
- mothership/tools/client
- packages
- desktop-bridge
- src
- terminal-protocol/src
- scripts
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
| 88 | + | |
87 | 89 | | |
88 | 90 | | |
89 | 91 | | |
| |||
0 commit comments