Skip to content

fix(desktop): quit retirement always times out before the Host can exit #4730

Description

@Astro-Han

What happened

Quitting Desktop fails with "Runtime Host did not exit before retirement". The quit is cancelled and the window comes back, while the Host actually exits a moment later — so a second quit attempt succeeds.

This is not intermittent. Whenever the Host needs its shutdown grace period, the Desktop side is guaranteed to give up first.

Two independent 10s budgets, neither aware of the other:

  • Host: DEFAULT_SHUTDOWN_GRACE_MS = 10_000 (packages/runtime-host/src/server/host-kernel.ts), armed inside #requestDrain() — that is, while host.upgrade.prepare is still being handled.
  • Desktop: waitForProcessExit deadline Date.now() + 10_000 (apps/desktop/src/main/runtime-host-desktop-manager.ts), started only after the prepare response arrives.

The Host timer starts strictly earlier, and after it fires the process still has to run process.exit and be reaped by the OS. Desktop polls process.kill(pid, 0), so it observes termination strictly later than the Host decides on it. Whenever the Host reaches its grace-period fallback, Desktop's deadline has already expired. This holds by construction, not by timing luck.

The Host reaches that fallback when composition.close() cannot settle in time: RootTurnCoordinator.close() stops every active Turn and then awaits disposition.active.done, which has no timeout of its own. One Turn blocked in a backend call is enough.

On failure, prepareToQuit() rejects, app-quit-coordinator sets the phase back to running and calls focusOrCreateWindow() — hence the dialog plus the reappearing window.

How to reproduce

  1. Start a turn that will not stop quickly (a long or stalled model call is enough).
  2. Quit Desktop while that turn is still active.
  3. The failure dialog appears; the window returns. Quitting again succeeds.

Environment

  • Surface: Desktop (quit path) + Runtime Host
  • Reported by a user on Windows; the code path is shared, so this is not platform-specific.
  • Present on current main.

Logs, screenshots, or additional context

Introduced by #3706, which added the synchronous retirement this timeout lives in.

Related architecture: #3709. Its convergence step 7 — "observe writer release rather than infer retirement from stale registration" — would remove the PID polling this bug lives in. Its acceptance criterion "retire, or surface a bounded, actionable failure" is currently satisfied by a failure that is guaranteed rather than exceptional.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions