fix(cli): query daemon for status and pairing offer over RPC
Closes #1081
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
Reference in New Issue
Block a user