From 5d5833416362d8e469bf27a26360e456b2259be5 Mon Sep 17 00:00:00 2001 From: colafornia Date: Fri, 21 Aug 2026 22:42:08 +0800 Subject: [PATCH] fix(desktop): skip idle Host restart prompt --- .../runtime-host-desktop-manager.test.ts | 50 +++++++++++++++++-- .../src/main/runtime-host-desktop-manager.ts | 6 ++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts index da9762da31..12f021edcb 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-desktop-manager.test.ts @@ -603,7 +603,7 @@ test('stops reconnecting when the replacement Host is incompatible', async () => await owner.close(); }); -test('restarts a generation-aware Host through its exact takeover handshake', async () => { +test('restarts an idle generation-aware Host without prompting', async () => { const replacement = candidateHarness(); const starts: DesktopRuntimeHostCandidateStartInput[] = []; const conflict = upgradeRequired(true); @@ -613,7 +613,7 @@ test('restarts a generation-aware Host through its exact takeover handshake', as return starts.length === 1 ? conflict : ready(replacement.candidate); }, upgradePrompts: { - restartable: async () => 'restart', + restartable: async () => assert.fail('idle Host must not prompt before restart'), waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'), }, }); @@ -623,6 +623,46 @@ test('restarts a generation-aware Host through its exact takeover handshake', as await owner.close(); }); +test('prompts before restarting a generation-aware Host with active work', async () => { + const replacement = candidateHarness(); + const conflict = upgradeRequired(true, 1); + let prompts = 0; + const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, { + startCandidate: async (input) => + input.takeoverHostEpoch ? ready(replacement.candidate) : conflict, + upgradePrompts: { + restartable: async () => { + prompts += 1; + return 'restart'; + }, + waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'), + }, + }); + + assert.equal(prompts, 1); + await owner.close(); +}); + +test('prompts before restarting a generation-aware Host with a residency', async () => { + const replacement = candidateHarness(); + const conflict = upgradeRequired(true, 0, [{ label: 'goal', count: 1 }]); + let prompts = 0; + const owner = await startRuntimeHostDesktopManager({} as DesktopRuntimeHostCandidateStartInput, { + startCandidate: async (input) => + input.takeoverHostEpoch ? ready(replacement.candidate) : conflict, + upgradePrompts: { + restartable: async () => { + prompts += 1; + return 'restart'; + }, + waitOnly: async () => assert.fail('restartable conflict used wait-only prompt'), + }, + }); + + assert.equal(prompts, 1); + await owner.close(); +}); + test('waits passively for a Host that cannot be taken over', async () => { const conflict = upgradeRequired(false); let starts = 0; @@ -699,6 +739,8 @@ function incompatibleHost( function upgradeRequired( restartable: boolean, + activeOperations = 0, + residencies: readonly { readonly label: string; readonly count: number }[] = [], ): Extract { const registration = hostRegistration( restartable ? { lifecycleMode: 'ephemeral' } : {}, @@ -723,9 +765,9 @@ function upgradeRequired( replacement: 'blocked_by_residency', activity: { connections: 0, - activeOperations: 0, + activeOperations, processUptimeSeconds: 60, - residencies: [], + residencies, }, }, }; diff --git a/apps/desktop/src/main/runtime-host-desktop-manager.ts b/apps/desktop/src/main/runtime-host-desktop-manager.ts index ed8ee13f7a..999b31a432 100644 --- a/apps/desktop/src/main/runtime-host-desktop-manager.ts +++ b/apps/desktop/src/main/runtime-host-desktop-manager.ts @@ -582,7 +582,11 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager { return result.candidate; } if (result.kind === 'upgrade_required' && result.restartable) { - const decision = await this.#resolveRestartable(result); + const activity = result.handshake?.activity; + const decision = + activity && activity.activeOperations === 0 && activity.residencies.length === 0 + ? 'restart' + : await this.#resolveRestartable(result); if (decision === 'cancel') { throw new RuntimeHostUpgradeCancelledError(); }