Skip to content

feat(desktop): guard quits and restarts with host activity checks - #362

Merged
incognitojam merged 6 commits into
mainfrom
styal/host-activity-shutdown
Sep 18, 2026
Merged

incognitojam merged 6 commits into
mainfrom
styal/host-activity-shutdown

Conversation

@incognitojam

@incognitojam incognitojam commented Sep 17, 2026 •

Copy link
Copy Markdown
Owner

Note

Desktop quits and restarts check activity across their owned environments. Idle hosts proceed immediately; running work or failed checks use the existing in-app confirmation dialog. Settings and sidebar update actions were exercised in a packaged macOS app with a controlled installer handoff.

“Restart and install” previously asked for confirmation every time, while ordinary quits could interrupt hosted work without checking it. The desktop now checks activity first: idle hosts restart immediately, and work that would be interrupted is described in the existing styled dialog.

The check covers every owned backend, including WSL and work started from other devices. It combines provider sessions, thread projections, and fresh terminal process inspection with a three-second budget per backend. Active turns, approval/input waits, background work, running commands, and failed checks require confirmation; idle shells do not. Failed terminal inspections are counted separately, so “could not be checked” appears only when inspection actually fails.

One shared guard handles Quit, relaunch, and explicit update installation. Update installation keeps the title Install update <version> and restart styal? and uses counts such as “1 terminal session will be interrupted. Make sure you're ready before continuing.” Cancel preserves running work and downloaded updates and restores preferences when a network or WSL settings restart is declined. Disabling WSL-only uses one settings call, preventing cancellation from triggering a second restart request.

The confirmation host remains mounted on authentication and recovery screens. If macOS has no open window, a required confirmation reopens it. The renderer acknowledges only when the dialog becomes active, not while queued behind another confirmation. If it cannot present the dialog within five seconds, the request expires and a subsequent explicit Quit can bypass confirmation while still running normal shutdown and cleanup. Update installation and settings restarts cannot consume that override. A displayed dialog or renderer recovery clears it, and expired requests cannot authorize shutdown through late acknowledgments or responses. Displayed dialogs have no response deadline. Windows/Linux window closes enter the guard before destroying the window; closing the last macOS window continues to leave the host running.

Desktop bootstrap credentials remain available for the owning server's lifetime, including first use after days of uptime and renewal after bearer expiry. Pairing links and bearer sessions retain their normal expiry. This is a point-in-time check for manual shutdown; automatic restart remains disabled. Independent remote servers, service updates, process termination signals, and updater-controlled quits retain their existing behavior.

In-app confirmation

The existing unconditional update prompt is shown on the left. The right shows the activity-aware prompt with a running terminal command. An idle host skips the dialog.

Before With running work
Existing unconditional update confirmation In-app confirmation for running terminal work

Validation

  • 88 focused tests passed across seven suites covering shutdown confirmation, the activity guard, lifecycle cleanup, hold-to-quit, updates, and the renderer coordinator/root. Regression coverage includes queued versus displayed acknowledgment, presentation expiry, no deadline after display, stale IDs, renderer failure/recovery, removal of expired queued dialogs, and preventing update/settings restarts from consuming the Quit override.
  • Desktop/web typechecks, targeted lint/formatting, and fork ledger validation passed. Server checks cover idle/running/failed terminal inspection, finite commands, thread activity, authentication after long uptime, bearer renewal, and ordinary pairing-link expiry.
  • Packaged macOS Settings: with a running synthetic command, Install showed the styled dialog. Cancel preserved the command and left the update ready for retry.
  • Packaged macOS sidebar: the update button showed the same styled dialog. Confirm stopped the backend and reached the installer handoff; the test app relaunched.
  • Packaged macOS idle update: Install closed the app and reached the handoff without displaying a dialog; the test app relaunched.
  • Packaged macOS Quit: Cancel preserved the command. Closing the last window retained the host; requesting Quit with no windows reopened the app and displayed the styled confirmation.
  • Packaged macOS recovery: an injected authentication failure showed the recovery screen; Quit still displayed the styled dialog for a running command, and Cancel preserved it. With the renderer replaced by a blank page, presentation expired without quitting. Restoring the renderer made the next Quit require confirmation again. Leaving it unavailable allowed a subsequent Quit to close the app through normal backend cleanup; the app and backend exited.
  • Windows and Ubuntu 26.04 WSL activity detection and preference rollback were exercised with isolated packaged builds: idle/running shell counts, dual and WSL-only backends, canceled disable without a second restart request, accepted settings changes, and app/backend cleanup.

The macOS update-button checks used the real renderer, IPC, activity endpoint, and backend shutdown. Update readiness was supplied through updater events, and the OS installer handoff was replaced with a recorder and app relaunch. The update checks produced no application errors; the recovery checks deliberately injected authentication and renderer failures. Test apps, backends, and the local update feed were stopped.

Verification limits

  • Actual installer transitions with the activity guard; the controlled handoff does not validate package installation or signing.
  • The in-app confirmation transport has been exercised on macOS; Windows/WSL verification covered activity checks and settings restoration before that transport change.

Written by an agent (Codex, GPT-6).

@incognitojam
incognitojam force-pushed the styal/host-activity-shutdown branch from 925531e to f24081b Compare September 18, 2026 19:00
@incognitojam
incognitojam marked this pull request as ready for review September 18, 2026 19:00
@incognitojam

Copy link
Copy Markdown
Owner Author

Manually tested on dev:desktop on macOS: asked an agent to do some work in a new thread, hit Cmd+Q twice and the dialog appeared. Hit cancel, waited for the agent to finish, then Cmd+Q twice again and it did close without the dialog.

@incognitojam
incognitojam merged commit 2569a7d into main Sep 18, 2026
14 checks passed
@incognitojam
incognitojam deleted the styal/host-activity-shutdown branch September 18, 2026 19:54
incognitojam added a commit that referenced this pull request Sep 25, 2026
> [!NOTE]
> The in-app **Update server** action now checks the server for running
work before restarting it, using the same activity check and dialog
wording as desktop quit and restart. Idle servers update without a
dialog.

The **Update server** action restarted a connected server without
checking what was running on it. The only confirmation was a fixed
prompt for desktop-hosted servers, so updating a CLI or service-hosted
server could interrupt an agent turn, a background task such as a Claude
monitor, or a terminal command, including work started from another
device. Desktop quit and restart already check this through the server's
host activity (#362), but that check was limited to backends the desktop
app hosts.

## Behavior

- Before updating, the client reads the server's host activity: active
and waiting threads (including background work), running terminal
commands, and terminals that could not be inspected.
- Idle servers update immediately. Otherwise a confirmation lists what
will be interrupted, for example "Update the Lab server with running
work?" followed by "1 thread will be interrupted." Cancel leaves the
server running.
- With **Continue threads after restarts** on, threads that continuation
will resume are listed as continuing after the restart, and every other
active thread as interrupted. That includes a thread whose turn has
finished but that still has background work such as a monitor, which
continuation does not resume today (upstream `pingdotgg#13400`,
tracked in #454).
- If the check fails or takes longer than five seconds, including on
servers that predate this change, the dialog says activity could not be
checked and still lets the user continue.
- Desktop-hosted servers keep their unconditional relaunch confirmation,
which now also lists running work.

## Implementation

- `listContinuableThreads` in `serverRuntimeStartup.ts` is now the one
place that selects threads for continuation. The self-update marking
step uses it, and the activity check reports how many active threads it
selects as `continuableSessions`. If upstream widens the selection, the
dialog changes with it.
- `server.getHostActivity` is a new WebSocket RPC (orchestration read
scope) that returns the existing `inspectHostActivity` result. It uses
the WebSocket rather than the existing `/api/environment/activity` HTTP
endpoint so it works over relay and tunnel connections as well as direct
ones.
- `describeHostActivity` in `@t3tools/shared/hostActivity` turns one or
more activity results into the dialog lines and the kind of confirmation
needed. The desktop shutdown guard and the web update action both use
it, so their wording stays in sync, and a multi-server update action can
pass several results in and get combined counts. Desktop quit and
restart output is unchanged.
- User docs, the styal differences overview, and the fork feature ledger
describe the new check.

## Screenshots

The same thread, with a Codex turn and a terminal command both running.
Before this change, **Update** restarted the server immediately. It now
asks first.

| Before: Update restarts the server immediately | After: confirmation
lists the running work |
| --- | --- |
| ![Thread with a running Codex turn and the server update
banner](https://github.com/user-attachments/assets/b3e3ad7b-5d47-4a47-a572-3afe178427dc)
| ![Confirmation: 1 thread and 1 terminal session will be
interrupted](https://github.com/user-attachments/assets/bca38db8-7cb3-4005-a102-227678821f61)
|

With **Continue threads after restarts** on, a running turn that
continuation will resume is listed as continuing:

![Confirmation: 1 thread will continue after the
restart](https://github.com/user-attachments/assets/11f0f2b5-8a10-4027-9052-fb1342916a37)

## Validation

Real client: a web dev server on isolated synthetic state, with the
client forced to see the server as behind and supporting in-app updates.
The server update RPCs were stubbed to fail, so confirming did not
install anything. None of that is part of this diff.

- Idle server, Update from Settings → Connections: no dialog; the update
request was sent.
- `sleep 600` running in a thread terminal, Update from the chat banner:
"Update the server with running work? 1 terminal session will be
interrupted." Cancel sent no update request.
- Same state, Update from Settings → Connections: the same dialog.
Confirm sent the update request.
- A real Codex turn running `sleep 120` plus the terminal: "1 thread
will be interrupted." and "1 terminal session will be interrupted."
- A real Codex turn with **Continue threads after restarts** on: "1
thread will continue after the restart." With the setting off, the same
turn: "1 thread will be interrupted."
- Automated tests:
- Server: host activity tests count continuable threads only among
active threads, and the existing continuation-marking tests pass against
the shared selection.
- Server: a WebSocket test calls `server.getHostActivity` against the
app under test with a running provider session and mocked terminal
inspection, and receives those counts.
- Web: component tests cover idle servers updating without a dialog,
Cancel leaving the server alone, Confirm starting the update, and a
failed activity check still asking. Logic tests cover the dialog text
for server and desktop-app updates.
- Shared: tests cover adding up counts across hosts, waiting threads,
continuing versus interrupted threads, uninspected terminals, and failed
or missing host checks.
- Desktop: the existing shutdown-guard suite passes unchanged against
the shared helper.
- Typecheck passes for contracts, shared, client-runtime, server,
desktop, and web.

Not exercised in a real client: a thread waiting for approval, a thread
with only background work, a remote desktop-app update, and a real
install after Confirm. These are covered by the automated tests above,
except the real install, which this change does not touch.

---
Written by an agent (Claude Code, claude-opus-5-5).
incognitojam pushed a commit that referenced this pull request Oct 2, 2026
(cherry picked from commit 3be02ae)

Fork adaptation: The web root keeps the fork's single ConfirmDialogHost, which the fork mounts in AppRoot so desktop quit and restart checks can use it before the router (fork #362), and adds only the snooze dialog host. The thread sidebar guide keeps the fork's notification section before the new snooze section. The lockfile is regenerated from the fork's own, so react-day-picker resolves to 10.0.2 rather than upstream's 10.0.1. The two daylight saving time tests run only where a stubbed TZ takes effect, because Fork CI runs workspace tests with --pool=threads and a worker thread keeps the process time zone.

Upstream-PR: 11800
Fork-Feature: desktop-background-updates
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant