diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
index 792dc9f2a7..892f34b259 100644
--- a/apps/desktop/electron/main.ts
+++ b/apps/desktop/electron/main.ts
@@ -248,6 +248,7 @@ import {
profileNameFromDeleteRequest,
resolveRouteProfile
} from './profile-delete-routing'
+import { prepareProfileRenameLifecycle, profileRenameFromRequest } from './profile-rename-routing'
import {
buildSidebarSessionSliceParams,
fetchPrimaryProfileSessions,
@@ -265,7 +266,6 @@ import {
} from './remote-liveness'
import { missingRendererAssets } from './renderer-bundle'
import { attachRendererConsoleCapture, formatRendererBoundaryReport } from './renderer-log'
-import { prepareProfileRenameLifecycle, profileRenameFromRequest } from './profile-rename-routing'
import {
buildSessionWindowUrl,
chatWindowWebPreferences,
@@ -13260,6 +13260,7 @@ async function handleHermesApiRequest(request) {
// primary rename the lifecycle has already made `default` the temporary
// primary until the PATCH settles, so the request routes there.
const apiRoute = resolveProfileApiRequest(profile, request.path, profileRouteOptions(profile, request))
+
const routeProfile = profileRename
? profileRename.routeProfile
: resolveRouteProfile(tornDownProfile, apiRoute.backendProfile)
diff --git a/apps/desktop/src/app/profiles/rename-profile-dialog.test.tsx b/apps/desktop/src/app/profiles/rename-profile-dialog.test.tsx
new file mode 100644
index 0000000000..7f4089f790
--- /dev/null
+++ b/apps/desktop/src/app/profiles/rename-profile-dialog.test.tsx
@@ -0,0 +1,59 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { afterEach, expect, it, vi } from 'vitest'
+
+import { renameProfile } from '@/hermes'
+import { retireLocalProfileGateways } from '@/store/gateway'
+
+import { RenameProfileDialog } from './rename-profile-dialog'
+
+// Pins the rename half of the deleted-profile-resurrection class (#88638 fixed
+// the delete half): a retained renderer socket for the OLD profile name must be
+// retired BEFORE the rename PATCH tears down its backend, or the socket's
+// reconnect loop respawns the old-name backend and recreates the directory the
+// rename just moved.
+
+afterEach(() => {
+ cleanup()
+ vi.clearAllMocks()
+})
+
+vi.mock('@/hermes', () => ({
+ renameProfile: vi.fn(async () => ({ name: 'renamed', ok: true, path: '/x' }))
+}))
+
+vi.mock('@/store/gateway', () => ({
+ retireLocalProfileGateways: vi.fn()
+}))
+
+it('retires the old-name local gateways before issuing the rename', async () => {
+ const order: string[] = []
+
+ vi.mocked(retireLocalProfileGateways).mockImplementationOnce(() => {
+ order.push('retire')
+ })
+ vi.mocked(renameProfile).mockImplementationOnce(async () => {
+ order.push('rename')
+
+ return { name: 'renamed', ok: true, path: '/x' }
+ })
+
+ render()
+
+ fireEvent.change(screen.getByLabelText(/new name/i), { target: { value: 'renamed' } })
+ fireEvent.click(screen.getByRole('button', { name: /^rename$/i }))
+
+ await waitFor(() => expect(renameProfile).toHaveBeenCalledWith('selena', 'renamed'))
+ expect(retireLocalProfileGateways).toHaveBeenCalledWith('selena')
+ expect(order).toEqual(['retire', 'rename'])
+})
+
+it('does not retire gateways when validation rejects the submit', async () => {
+ render()
+
+ fireEvent.change(screen.getByLabelText(/new name/i), { target: { value: '' } })
+ fireEvent.click(screen.getByRole('button', { name: /^rename$/i }))
+
+ await waitFor(() => expect(screen.getByText('Name is required.')).toBeTruthy())
+ expect(retireLocalProfileGateways).not.toHaveBeenCalled()
+ expect(renameProfile).not.toHaveBeenCalled()
+})
diff --git a/apps/desktop/src/app/profiles/rename-profile-dialog.tsx b/apps/desktop/src/app/profiles/rename-profile-dialog.tsx
index 7973472260..c6cb6f83e3 100644
--- a/apps/desktop/src/app/profiles/rename-profile-dialog.tsx
+++ b/apps/desktop/src/app/profiles/rename-profile-dialog.tsx
@@ -16,6 +16,7 @@ import { renameProfile } from '@/hermes'
import { useI18n } from '@/i18n'
import { AlertTriangle } from '@/lib/icons'
import { slug } from '@/lib/sanitize'
+import { retireLocalProfileGateways } from '@/store/gateway'
import { isValidProfileName } from './create-profile-dialog'
@@ -72,6 +73,11 @@ export function RenameProfileDialog({
setError(null)
try {
+ // A retained renderer socket for the old name would treat the rename's
+ // backend teardown as a transient drop and redial, resurrecting the
+ // old-name backend whose ensure_hermes_home() recreates the directory
+ // the rename just moved (same class as the delete path, #88638).
+ retireLocalProfileGateways(currentName)
await renameProfile(currentName, trimmed)
await onRenamed?.(trimmed)
setStatus('done')