Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import {
AlertDialog,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@superset/ui/alert-dialog";
import { Button } from "@superset/ui/button";
import { toast } from "@superset/ui/sonner";
import {
WorkspaceClientProvider,
workspaceTrpc,
} from "@superset/workspace-client";
import { useEffect, useState } from "react";
import {
getHostServiceHeaders,
getHostServiceWsToken,
} from "renderer/lib/host-service-auth";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";

const STATUS_REFETCH_MS = 5_000;
const DISMISSED_FAILURE_STORAGE_KEY_PREFIX =
"superset.daemon-auto-update-failure.dismissed.";

function getDismissedFailureId(storageKey: string): string | null {
try {
return window.localStorage.getItem(storageKey);
} catch {
return null;
}
}

function saveDismissedFailureId(storageKey: string, failureId: string): void {
try {
window.localStorage.setItem(storageKey, failureId);
} catch {
// Best effort; in-memory state still suppresses this failure for the session.
}
}

export function DaemonAutoUpdateFailureDialog() {
const { activeHostUrl, activeOrganizationId } = useLocalHostService();
if (!activeHostUrl) return null;
const dismissedFailureStorageKey = `${DISMISSED_FAILURE_STORAGE_KEY_PREFIX}${activeOrganizationId ?? activeHostUrl}`;
return (
<WorkspaceClientProvider
cacheKey="daemon-auto-update-failure"
key={activeHostUrl}
hostUrl={activeHostUrl}
headers={() => getHostServiceHeaders(activeHostUrl)}
wsToken={() => getHostServiceWsToken(activeHostUrl)}
>
<DaemonAutoUpdateFailureDialogInner
dismissedFailureStorageKey={dismissedFailureStorageKey}
/>
</WorkspaceClientProvider>
);
}

function DaemonAutoUpdateFailureDialogInner({
dismissedFailureStorageKey,
}: {
dismissedFailureStorageKey: string;
}) {
const [activeFailureId, setActiveFailureId] = useState<string | null>(null);
const [dismissedFailureId, setDismissedFailureId] = useState<string | null>(
() => getDismissedFailureId(dismissedFailureStorageKey),
);

const updateStatusQuery =
workspaceTrpc.terminal.daemon.getUpdateStatus.useQuery(undefined, {
refetchInterval: STATUS_REFETCH_MS,
refetchOnWindowFocus: true,
});
const closeDialog = () => {
if (activeFailureId) {
setDismissedFailureId(activeFailureId);
saveDismissedFailureId(dismissedFailureStorageKey, activeFailureId);
}
setActiveFailureId(null);
};
useEffect(() => {
setDismissedFailureId(getDismissedFailureId(dismissedFailureStorageKey));
}, [dismissedFailureStorageKey]);

const sessionsQuery = workspaceTrpc.terminal.daemon.listSessions.useQuery(
undefined,
{
enabled: activeFailureId !== null,
refetchInterval: activeFailureId !== null ? STATUS_REFETCH_MS : false,
refetchOnWindowFocus: true,
},
);
Comment on lines +87 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Session count shown can be stale

sessionsQuery is only set up with refetchOnWindowFocus: true and no refetchInterval. While the dialog is open, the alive-session count will not update unless the user switches windows. If sessions are created or destroyed while the dialog is on screen, the displayed count may be inaccurate and could influence the user's "Force update" decision.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/components/DaemonAutoUpdateFailureDialog/DaemonAutoUpdateFailureDialog.tsx
Line: 51-57

Comment:
**Session count shown can be stale**

`sessionsQuery` is only set up with `refetchOnWindowFocus: true` and no `refetchInterval`. While the dialog is open, the alive-session count will not update unless the user switches windows. If sessions are created or destroyed while the dialog is on screen, the displayed count may be inaccurate and could influence the user's "Force update" decision.

How can I resolve this? If you propose a fix, please make it concise.

const restartDaemon = workspaceTrpc.terminal.daemon.restart.useMutation({
onSuccess: () => {
closeDialog();
toast.success("Daemon restarted", {
description: "All sessions were closed and a fresh daemon is running.",
});
void updateStatusQuery.refetch();
},
onError: (error) => {
toast.error("Failed to restart daemon", { description: error.message });
},
});

const failure = updateStatusQuery.data?.autoUpdateFailure ?? null;
useEffect(() => {
if (!failure) {
setActiveFailureId(null);
return;
}
if (failure.id === dismissedFailureId) return;
setActiveFailureId(failure.id);
}, [failure, dismissedFailureId]);

const sessions = sessionsQuery.data ?? null;
const aliveCount =
sessions === null
? null
: sessions.filter((session) => session.alive).length;

return (
<AlertDialog
open={activeFailureId !== null && !!failure}
onOpenChange={(open) => {
if (!open && !restartDaemon.isPending) closeDialog();
}}
>
<AlertDialogContent className="max-w-[520px] gap-0 p-0">
<AlertDialogHeader className="px-4 pt-4 pb-2">
<AlertDialogTitle className="font-medium">
Daemon update needs confirmation
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-1.5 text-muted-foreground">
<span className="block">
Superset tried to update the terminal daemon without closing
sessions, but the handoff did not finish. Reason:
</span>
<span className="block cursor-text select-text rounded bg-muted/40 px-2 py-1.5 font-mono text-[11px] text-foreground">
{failure?.reason ?? ""}
</span>
<span className="block">
Force update will close every terminal session
{aliveCount && aliveCount > 0 ? ` (${aliveCount} running)` : ""}{" "}
and start a fresh daemon.
</span>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-row justify-end gap-2 px-4 pb-4 pt-2">
<Button
variant="ghost"
size="sm"
disabled={restartDaemon.isPending}
onClick={closeDialog}
>
Keep current daemon
</Button>
<Button
variant="default"
size="sm"
disabled={restartDaemon.isPending}
onClick={() => {
restartDaemon.mutate();
}}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
Force update
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
Comment on lines +166 to +174
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Dialog permanently dismissed after a failed "Force update"

closeDialog() runs before restartDaemon.mutate(), which immediately stores the current activeFailureId into dismissedFailureId. If the mutation fails, the onError toast fires but the useEffect guard failure.id === dismissedFailureId prevents the dialog from ever reopening — the user sees only the transient error toast and has no way to retry the forced update without restarting the app.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/routes/_authenticated/components/DaemonAutoUpdateFailureDialog/DaemonAutoUpdateFailureDialog.tsx
Line: 128-137

Comment:
**Dialog permanently dismissed after a failed "Force update"**

`closeDialog()` runs before `restartDaemon.mutate()`, which immediately stores the current `activeFailureId` into `dismissedFailureId`. If the mutation fails, the `onError` toast fires but the `useEffect` guard `failure.id === dismissedFailureId` prevents the dialog from ever reopening — the user sees only the transient error toast and has no way to retry the forced update without restarting the app.

How can I resolve this? If you propose a fix, please make it concise.

);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { DaemonAutoUpdateFailureDialog } from "./DaemonAutoUpdateFailureDialog";
2 changes: 2 additions & 0 deletions apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { dragDropManager } from "renderer/lib/dnd";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { showWorkspaceAutoNameWarningToast } from "renderer/lib/workspaces/showWorkspaceAutoNameWarningToast";
import { InitGitDialog } from "renderer/react-query/projects/InitGitDialog";
import { DaemonAutoUpdateFailureDialog } from "renderer/routes/_authenticated/components/DaemonAutoUpdateFailureDialog";
import { DashboardNewWorkspaceModal } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal";
import { V1ImportModal } from "renderer/routes/_authenticated/components/V1ImportModal";
import { WorkspaceInitEffects } from "renderer/screens/main/components/WorkspaceInitEffects";
Expand Down Expand Up @@ -210,6 +211,7 @@ function AuthenticatedLayout() {
<AgentHooks />
<FileMenuListener />
<V2NotificationController />
<DaemonAutoUpdateFailureDialog />
<Outlet />
<V1ImportModal />
<WorkspaceInitEffects />
Expand Down
20 changes: 10 additions & 10 deletions packages/host-service/DAEMON_SUPERVISION.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ read the running daemon's `daemonVersion`, compares against
`updatePending: true` on the instance — the renderer surfaces a
"restart to update" affordance. Manual updates try fd-handoff first and
only force-restart after the user confirms. Automatic adoption updates
also try fd-handoff first, then force-restart on failure because there is
no foreground UI to ask for the destructive fallback.
also try fd-handoff first, but they never force-restart in the background;
on failure, the predecessor keeps running and `updatePending` remains
visible for an explicit user action. The failure reason is exposed through
`getUpdateStatus().autoUpdateFailure` so the desktop can show a global
force-update dialog without the supervisor taking the destructive path itself.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Probe failure ≠ stale: a transient socket issue produces
`runningVersion: "unknown", updatePending: false` rather than a
Expand Down Expand Up @@ -137,11 +140,7 @@ The supervisor emits structured `console.log` lines with
`pty_daemon_spawn`, `pty_daemon_adopt`, `pty_daemon_user_restart`,
`pty_daemon_update_pending`, `pty_daemon_update`,
`pty_daemon_auto_update_attempt`, `pty_daemon_auto_update_ok`,
`pty_daemon_auto_update_failed`,
`pty_daemon_auto_update_force_restart`,
`pty_daemon_auto_update_force_restart_ok`,
`pty_daemon_auto_update_force_restart_failed`,
`pty_daemon_auto_update_force_restart_skipped`, `pty_daemon_crash`,
`pty_daemon_auto_update_failed`, `pty_daemon_crash`,
`pty_daemon_circuit_open`, `pty_daemon_spawn_failed`. No PostHog plumbing
on host-service yet — promote to real telemetry when the path is needed.

Expand Down Expand Up @@ -218,9 +217,10 @@ supervisor's `restoreOnFailure()` path leaves the
predecessor's instance record intact — the user's shells keep serving on
the original daemon process. Auto-update on adopt (`kickoffAutoUpdate`)
relies on this contract: a transient failure must never disrupt sessions.
Before the destructive auto-update fallback runs, the supervisor re-checks
that the same stale daemon is still current and still pending so a late
failure from an obsolete attempt cannot restart a fresh daemon.
The old destructive auto-update fallback has been removed. Background
auto-updates leave the predecessor running and surface the failure through
`updatePending` plus `getUpdateStatus().autoUpdateFailure`; any destructive
restart is an explicit user action through the desktop confirmation flow.

Mode signal goes through argv (`--handoff`), not env: bundlers
(Bun, esbuild via electron-vite) statically inline `process.env.X`
Expand Down
Loading
Loading