feat: add remote daemon self-update from client (#1513)

* feat: add remote daemon self-update from client

Adds a daemon.update.request RPC that lets the client trigger a remote
daemon update over WebSocket. The daemon runs npm update -g @getpaseo/cli,
reports progress via daemon_update_progress status events, then restarts
through the supervisor to load the new code.

New feature flag server_info.features.daemonSelfUpdate gates the UI.
The UpdateDaemonCard appears on the host settings page when the daemon
supports self-update and the app/daemon versions differ.

* fix: clean up progress subscription on unmount, use typed phase cast

Address Greptile review:
- P1: store unsubscribe ref and clean up on component unmount to prevent
  listener leak during in-flight update
- P2: use DaemonUpdateProgressStatusPayload type instead of { phase?: string }

* fix: use npm install@latest and global concurrency guard

Address Greptile review:
- npm update -g can silently exit 0 without resolving major-version
  gaps; switch to npm install -g @getpaseo/cli@latest
- Move daemonUpdateInProgress from per-session instance to module-level
  variable so concurrent clients share the same guard

* feat(daemon): harden remote self-update

Probe the global npm CLI install before updating, keep request handling in a daemon self-update session controller, and emit scoped daemon.update.progress messages.

* test(daemon): make self-update paths platform-stable

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Davy
2026-06-26 16:13:29 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 9ad49fca92
commit 40265782d4
26 changed files with 1536 additions and 8 deletions
+20
View File
@@ -402,3 +402,23 @@ describe("paseo worktree archive request compatibility", () => {
expect(parsed.scope).toBe("workspace");
});
});
describe("daemon update messages", () => {
test("daemon update progress is a scoped outbound message", () => {
const parsed = SessionOutboundMessageSchema.parse({
type: "daemon.update.progress",
payload: {
requestId: "update-1",
phase: "installing",
},
});
expect(parsed).toEqual({
type: "daemon.update.progress",
payload: {
requestId: "update-1",
phase: "installing",
},
});
});
});
+33
View File
@@ -1252,6 +1252,11 @@ export const ShutdownServerRequestMessageSchema = z.object({
requestId: z.string(),
});
export const DaemonUpdateRequestMessageSchema = z.object({
type: z.literal("daemon.update.request"),
requestId: z.string(),
});
export const AgentTimelineCursorSchema = z.object({
epoch: z.string(),
seq: z.number().int().nonnegative(),
@@ -2053,6 +2058,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
CancelAgentRequestMessageSchema,
ShutdownServerRequestMessageSchema,
RestartServerRequestMessageSchema,
DaemonUpdateRequestMessageSchema,
FetchAgentTimelineRequestMessageSchema,
SetAgentModeRequestMessageSchema,
SetAgentModelRequestMessageSchema,
@@ -2327,6 +2333,8 @@ export const ServerInfoStatusPayloadSchema = z
agentDetach: z.boolean().optional(),
// COMPAT(daemonDiagnostics): added in v0.1.100, remove gate after 2026-12-25 once daemon floor >= v0.1.100.
daemonDiagnostics: z.boolean().optional(),
// COMPAT(daemonSelfUpdate): added in v0.1.93, remove gate after 2026-12-13.
daemonSelfUpdate: z.boolean().optional(),
})
.optional(),
})
@@ -4093,6 +4101,29 @@ export const TerminalAttentionRequiredSchema = z.object({
}),
});
export const DaemonUpdateResponseSchema = z.object({
type: z.literal("daemon.update.response"),
payload: z.object({
requestId: z.string(),
success: z.boolean(),
error: z.string().nullable(),
previousVersion: z.string().nullable(),
newVersion: z.string().nullable(),
}),
});
export type DaemonUpdateResponse = z.infer<typeof DaemonUpdateResponseSchema>;
export const DaemonUpdateProgressMessageSchema = z.object({
type: z.literal("daemon.update.progress"),
payload: z.object({
requestId: z.string(),
phase: z.enum(["starting", "downloading", "installing", "complete"]),
}),
});
export type DaemonUpdateProgressMessage = z.infer<typeof DaemonUpdateProgressMessageSchema>;
export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
ActivityLogMessageSchema,
AssistantChunkMessageSchema,
@@ -4228,6 +4259,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
LoopInspectResponseSchema,
LoopLogsResponseSchema,
LoopStopResponseSchema,
DaemonUpdateProgressMessageSchema,
DaemonUpdateResponseSchema,
]);
export type SessionOutboundMessage = z.infer<typeof SessionOutboundMessageSchema>;