fix(cli): query daemon for status and pairing offer over RPC

Closes #1081
This commit is contained in:
Zexin Yuan
2026-05-19 03:15:52 +00:00
committed by GitHub
parent 8ff63a6d71
commit 383b380d8a
9 changed files with 334 additions and 15 deletions
@@ -60,6 +60,8 @@ import type {
GetProvidersSnapshotResponseMessage,
RefreshProvidersSnapshotResponseMessage,
ProviderDiagnosticResponseMessage,
DaemonGetStatusResponse,
DaemonGetPairingOfferResponse,
ListTerminalsResponse,
CreateTerminalResponse,
SubscribeTerminalResponse,
@@ -317,6 +319,8 @@ type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
type GetProvidersSnapshotPayload = GetProvidersSnapshotResponseMessage["payload"];
type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage["payload"];
type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"];
type DaemonStatusPayload = DaemonGetStatusResponse["payload"];
type DaemonPairingOfferPayload = DaemonGetPairingOfferResponse["payload"];
type ReadProjectConfigPayload = Extract<
SessionOutboundMessage,
{ type: "read_project_config_response" }
@@ -3298,6 +3302,28 @@ export class DaemonClient {
});
}
async getDaemonStatus(requestId?: string): Promise<DaemonStatusPayload> {
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "daemon.get_status.request",
},
responseType: "daemon.get_status.response",
timeout: 10000,
});
}
async getDaemonPairingOffer(requestId?: string): Promise<DaemonPairingOfferPayload> {
return this.sendCorrelatedSessionRequest({
requestId,
message: {
type: "daemon.get_pairing_offer.request",
},
responseType: "daemon.get_pairing_offer.response",
timeout: 10000,
});
}
async patchDaemonConfig(
config: MutableDaemonConfigPatch,
requestId?: string,
+10
View File
@@ -936,6 +936,16 @@ export async function createPaseoDaemon(
workspaceGitService,
github,
config.pushNotificationSender,
{
listen: formatListenTarget(boundListenTarget ?? listenTarget),
relay: {
enabled: relayEnabled,
endpoint: relayEndpoint,
publicEndpoint: relayPublicEndpoint,
useTls: relayUseTls,
publicUseTls: relayPublicUseTls,
},
},
);
if (relayEnabled) {
+107
View File
@@ -47,6 +47,8 @@ import type { TurnDetectionProvider } from "./speech/turn-detection-provider.js"
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
import { listAvailableEditorTargets, openInEditorTarget } from "./editor-targets.js";
import { getPidLockInfo } from "./pid-lock.js";
import { generateLocalPairingOffer } from "./pairing-offer.js";
import {
DictationStreamManager,
type DictationStreamOutboundMessage,
@@ -595,6 +597,18 @@ export interface SessionOptions {
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
providerOverrides?: Record<string, ProviderOverride>;
isDev?: boolean;
serverId?: string;
daemonVersion?: string;
daemonRuntimeConfig?: {
listen: string | null;
relay: {
enabled: boolean;
endpoint: string;
publicEndpoint: string;
useTls: boolean;
publicUseTls: boolean;
} | null;
};
}
export type SessionLifecycleIntent =
@@ -805,6 +819,9 @@ export class Session {
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
private readonly isDev: boolean;
private readonly serverId: string | undefined;
private readonly daemonVersion: string | undefined;
private readonly daemonRuntimeConfig: SessionOptions["daemonRuntimeConfig"];
private voiceModeAgentId: string | null = null;
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
@@ -850,6 +867,9 @@ export class Session {
agentProviderRuntimeSettings,
providerOverrides,
isDev,
serverId,
daemonVersion,
daemonRuntimeConfig,
} = options;
this.clientId = clientId;
this.appVersion = appVersion ?? null;
@@ -901,6 +921,9 @@ export class Session {
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
this.providerOverrides = providerOverrides;
this.isDev = isDev === true;
this.serverId = serverId;
this.daemonVersion = daemonVersion;
this.daemonRuntimeConfig = daemonRuntimeConfig;
this.abortController = new AbortController();
this.workspaceDirectory = new WorkspaceDirectory({
logger: this.sessionLogger,
@@ -1864,6 +1887,10 @@ export class Session {
payload: { requestId: msg.requestId, config: this.daemonConfigStore.get() },
});
return undefined;
case "daemon.get_status.request":
return this.handleDaemonGetStatusRequest(msg);
case "daemon.get_pairing_offer.request":
return this.handleDaemonGetPairingOfferRequest(msg);
case "set_daemon_config_request":
this.emit({
type: "set_daemon_config_response",
@@ -3813,6 +3840,86 @@ export class Session {
}
}
private async handleDaemonGetStatusRequest(
msg: Extract<SessionInboundMessage, { type: "daemon.get_status.request" }>,
): Promise<void> {
try {
const pidInfo = await getPidLockInfo(this.paseoHome);
const providers = (await this.agentManager.listProviderAvailability()).map((p) => ({
provider: p.provider,
available: p.available,
error: p.error ?? null,
}));
this.emit({
type: "daemon.get_status.response",
payload: {
requestId: msg.requestId,
serverId: this.serverId ?? "",
version: this.daemonVersion ?? null,
pid: process.pid,
nodePath: process.execPath,
startedAt: pidInfo?.startedAt ?? null,
listen: this.daemonRuntimeConfig?.listen ?? null,
relay: this.daemonRuntimeConfig?.relay ?? null,
providers,
},
});
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to handle daemon status request");
this.emit({
type: "daemon.get_status.response",
payload: {
requestId: msg.requestId,
serverId: this.serverId ?? "",
version: this.daemonVersion ?? null,
pid: process.pid,
nodePath: process.execPath,
startedAt: null,
listen: null,
relay: null,
providers: [],
},
});
}
}
private async handleDaemonGetPairingOfferRequest(
msg: Extract<SessionInboundMessage, { type: "daemon.get_pairing_offer.request" }>,
): Promise<void> {
try {
const relay = this.daemonRuntimeConfig?.relay;
const pairing = await generateLocalPairingOffer({
paseoHome: this.paseoHome,
relayEnabled: relay?.enabled ?? true,
relayEndpoint: relay?.endpoint,
relayPublicEndpoint: relay?.publicEndpoint,
relayUseTls: relay?.useTls,
relayPublicUseTls: relay?.publicUseTls,
includeQr: true,
logger: this.sessionLogger,
});
this.emit({
type: "daemon.get_pairing_offer.response",
payload: {
requestId: msg.requestId,
url: pairing.url ?? "",
qr: pairing.qr ?? null,
relayEnabled: pairing.relayEnabled,
},
});
} catch (error) {
this.sessionLogger.error({ err: error }, "Failed to handle daemon pairing offer request");
this.emit({
type: "rpc_error",
payload: {
requestId: msg.requestId,
requestType: "daemon.get_pairing_offer.request",
error: error instanceof Error ? error.message : String(error),
},
});
}
}
private async handleListAvailableProvidersRequest(
msg: Extract<SessionInboundMessage, { type: "list_available_providers_request" }>,
): Promise<void> {
@@ -332,6 +332,18 @@ export class VoiceAssistantWebSocketServer {
private readonly externalSessionsByKey: Map<string, SessionConnection> = new Map();
private readonly serverId: string;
private readonly daemonVersion: string;
private readonly daemonRuntimeConfig:
| {
listen: string | null;
relay: {
enabled: boolean;
endpoint: string;
publicEndpoint: string;
useTls: boolean;
publicUseTls: boolean;
};
}
| undefined;
private readonly agentManager: AgentManager;
private readonly agentStorage: AgentStorage;
private readonly projectRegistry: ProjectRegistry;
@@ -416,6 +428,16 @@ export class VoiceAssistantWebSocketServer {
workspaceGitService?: WorkspaceGitService,
github?: GitHubService,
pushNotificationSender?: PushNotificationSender,
daemonRuntimeConfig?: {
listen: string | null;
relay: {
enabled: boolean;
endpoint: string;
publicEndpoint: string;
useTls: boolean;
publicUseTls: boolean;
};
},
) {
this.logger = logger.child({ module: "websocket-server" });
this.serverId = serverId;
@@ -423,6 +445,7 @@ export class VoiceAssistantWebSocketServer {
throw new MissingDaemonVersionError();
}
this.daemonVersion = daemonVersion.trim();
this.daemonRuntimeConfig = daemonRuntimeConfig;
this.agentManager = agentManager;
this.agentStorage = agentStorage;
this.projectRegistry = projectRegistry ?? createNoopProjectRegistry();
@@ -921,6 +944,9 @@ export class VoiceAssistantWebSocketServer {
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
providerOverrides: this.providerOverrides,
isDev: this.isDev,
serverId: this.serverId,
daemonVersion: this.daemonVersion,
daemonRuntimeConfig: this.daemonRuntimeConfig,
});
connection = {
@@ -1053,6 +1079,8 @@ export class VoiceAssistantWebSocketServer {
providersSnapshot: true,
// COMPAT(checkoutGithubSetAutoMerge): added in v0.1.75, remove gate after 2026-11-13.
checkoutGithubSetAutoMerge: true,
// COMPAT(daemonStatusRpc): added in v0.1.76, remove gate after 2026-11-18.
daemonStatusRpc: true,
},
};
}
+62
View File
@@ -979,6 +979,16 @@ export const WaitForFinishRequestSchema = z.object({
timeoutMs: z.number().int().positive().optional(),
});
export const DaemonGetStatusRequestSchema = z.object({
type: z.literal("daemon.get_status.request"),
requestId: z.string(),
});
export const DaemonGetPairingOfferRequestSchema = z.object({
type: z.literal("daemon.get_pairing_offer.request"),
requestId: z.string(),
});
export const GetDaemonConfigRequestMessageSchema = z.object({
type: z.literal("get_daemon_config_request"),
requestId: z.string(),
@@ -1764,6 +1774,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
SetVoiceModeMessageSchema,
SendAgentMessageRequestSchema,
WaitForFinishRequestSchema,
DaemonGetStatusRequestSchema,
DaemonGetPairingOfferRequestSchema,
GetDaemonConfigRequestMessageSchema,
SetDaemonConfigRequestMessageSchema,
ReadProjectConfigRequestMessageSchema,
@@ -2026,6 +2038,8 @@ export const ServerInfoStatusPayloadSchema = z
.object({
providersSnapshot: z.boolean().optional(),
checkoutGithubSetAutoMerge: z.boolean().optional(),
// COMPAT(daemonStatusRpc): added in v0.1.76, remove gate after 2026-11-18.
daemonStatusRpc: z.boolean().optional(),
})
.optional(),
})
@@ -2586,6 +2600,50 @@ export const GetDaemonConfigResponseMessageSchema = z.object({
.passthrough(),
});
export const DaemonGetStatusResponseSchema = z.object({
type: z.literal("daemon.get_status.response"),
payload: z
.object({
requestId: z.string(),
serverId: z.string(),
version: z.string().nullable().optional(),
pid: z.number(),
nodePath: z.string(),
startedAt: z.string().nullable().optional(),
listen: z.string().nullable(),
relay: z
.object({
enabled: z.boolean(),
endpoint: z.string(),
publicEndpoint: z.string(),
useTls: z.boolean(),
publicUseTls: z.boolean(),
})
.nullable()
.optional(),
providers: z.array(
z.object({
provider: z.string(),
available: z.boolean(),
error: z.string().nullable().optional(),
}),
),
})
.passthrough(),
});
export const DaemonGetPairingOfferResponseSchema = z.object({
type: z.literal("daemon.get_pairing_offer.response"),
payload: z
.object({
requestId: z.string(),
url: z.string(),
qr: z.string().nullable().optional(),
relayEnabled: z.boolean(),
})
.passthrough(),
});
export const SetDaemonConfigResponseMessageSchema = z.object({
type: z.literal("set_daemon_config_response"),
payload: z
@@ -3481,6 +3539,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
ClearAgentAttentionResponseMessageSchema,
SendAgentMessageResponseMessageSchema,
SetVoiceModeResponseMessageSchema,
DaemonGetStatusResponseSchema,
DaemonGetPairingOfferResponseSchema,
GetDaemonConfigResponseMessageSchema,
SetDaemonConfigResponseMessageSchema,
ReadProjectConfigResponseMessageSchema,
@@ -3643,6 +3703,8 @@ export type ListProviderFeaturesResponseMessage = z.infer<
typeof ListProviderFeaturesResponseMessageSchema
>;
export type ListAvailableProvidersResponse = z.infer<typeof ListAvailableProvidersResponseSchema>;
export type DaemonGetStatusResponse = z.infer<typeof DaemonGetStatusResponseSchema>;
export type DaemonGetPairingOfferResponse = z.infer<typeof DaemonGetPairingOfferResponseSchema>;
export type GetProvidersSnapshotResponseMessage = z.infer<
typeof GetProvidersSnapshotResponseMessageSchema
>;