fix(cli): query daemon for status and pairing offer over RPC
Closes #1081
This commit is contained in:
@@ -11,7 +11,7 @@ The namespace reads left to right:
|
|||||||
|
|
||||||
- Domain: `checkout`
|
- Domain: `checkout`
|
||||||
- Provider or subsystem: `github`
|
- Provider or subsystem: `github`
|
||||||
- Operation: `set_auto_merge`
|
- Operation: `set_auto_merge`; this segment is a verb, not a noun. If you would name an RPC `noun.request`, name it `get_noun.request` instead.
|
||||||
- Direction: `request` or `response`
|
- Direction: `request` or `response`
|
||||||
|
|
||||||
Use dots, not slashes. Dots are protocol namespaces; slashes imply paths or transport routing.
|
Use dots, not slashes. Dots are protocol namespaces; slashes imply paths or transport routing.
|
||||||
|
|||||||
@@ -392,9 +392,15 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
|
|||||||
const env: NodeJS.ProcessEnv = {
|
const env: NodeJS.ProcessEnv = {
|
||||||
...envWithHome(options.home),
|
...envWithHome(options.home),
|
||||||
// Status should reflect local persisted config + pid file, not inherited daemon env overrides.
|
// Status should reflect local persisted config + pid file, not inherited daemon env overrides.
|
||||||
|
// This is CLI-side defensive scrubbing; the daemon RPC is authoritative when available.
|
||||||
PASEO_LISTEN: undefined,
|
PASEO_LISTEN: undefined,
|
||||||
PASEO_HOSTNAMES: undefined,
|
PASEO_HOSTNAMES: undefined,
|
||||||
PASEO_ALLOWED_HOSTS: undefined,
|
PASEO_ALLOWED_HOSTS: undefined,
|
||||||
|
PASEO_RELAY_ENABLED: undefined,
|
||||||
|
PASEO_RELAY_ENDPOINT: undefined,
|
||||||
|
PASEO_RELAY_PUBLIC_ENDPOINT: undefined,
|
||||||
|
PASEO_RELAY_USE_TLS: undefined,
|
||||||
|
PASEO_RELAY_PUBLIC_USE_TLS: undefined,
|
||||||
};
|
};
|
||||||
const home = resolvePaseoHome(env);
|
const home = resolvePaseoHome(env);
|
||||||
const config = loadConfig(home, { env });
|
const config = loadConfig(home, { env });
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
import { generateLocalPairingOffer, loadConfig, resolvePaseoHome } from "@getpaseo/server";
|
||||||
|
import { tryConnectToDaemon } from "../../utils/client.js";
|
||||||
|
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
|
||||||
import { addJsonOption } from "../../utils/command-options.js";
|
import { addJsonOption } from "../../utils/command-options.js";
|
||||||
|
|
||||||
interface PairOptions {
|
interface PairOptions {
|
||||||
@@ -22,6 +24,35 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const paseoHome = resolvePaseoHome();
|
const paseoHome = resolvePaseoHome();
|
||||||
|
const state = resolveLocalDaemonState({ home: paseoHome });
|
||||||
|
const host = resolveTcpHostFromListen(state.listen);
|
||||||
|
|
||||||
|
// Try to get the pairing offer from the running daemon first.
|
||||||
|
if (host) {
|
||||||
|
const client = await tryConnectToDaemon({ host, timeout: 1500 });
|
||||||
|
if (client) {
|
||||||
|
const supportsDaemonStatusRpc =
|
||||||
|
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
|
||||||
|
if (supportsDaemonStatusRpc) {
|
||||||
|
try {
|
||||||
|
const offer = await client.getDaemonPairingOffer();
|
||||||
|
await client.close().catch(() => {});
|
||||||
|
outputPairingResult(
|
||||||
|
{ relayEnabled: offer.relayEnabled, url: offer.url, qr: offer.qr ?? null },
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// COMPAT(daemon-rpc-rollout): fall back to CLI-side pairing generation while
|
||||||
|
// old daemons lack daemonStatusRpc. Remove once the daemon floor is past
|
||||||
|
// v0.1.76; pairing should come from daemon.get_pairing_offer.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await client.close().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to local pairing offer generation.
|
||||||
const config = loadConfig(paseoHome);
|
const config = loadConfig(paseoHome);
|
||||||
const pairing = await generateLocalPairingOffer({
|
const pairing = await generateLocalPairingOffer({
|
||||||
paseoHome,
|
paseoHome,
|
||||||
@@ -34,6 +65,13 @@ export async function runPairCommand(options: PairOptions): Promise<void> {
|
|||||||
includeQr: true,
|
includeQr: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
outputPairingResult(pairing, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function outputPairingResult(
|
||||||
|
pairing: { relayEnabled: boolean; url: string | null; qr: string | null },
|
||||||
|
options: PairOptions,
|
||||||
|
): void {
|
||||||
if (!pairing.relayEnabled || !pairing.url) {
|
if (!pairing.relayEnabled || !pairing.url) {
|
||||||
console.error(chalk.red("Relay pairing is disabled for this daemon config."));
|
console.error(chalk.red("Relay pairing is disabled for this daemon config."));
|
||||||
console.error(chalk.yellow("Enable relay and run this command again."));
|
console.error(chalk.yellow("Enable relay and run this command again."));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ interface ProviderBinaryStatus {
|
|||||||
label: string;
|
label: string;
|
||||||
path: string | null;
|
path: string | null;
|
||||||
version: string | null;
|
version: string | null;
|
||||||
|
source?: "daemon" | "local";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DaemonStatus {
|
interface DaemonStatus {
|
||||||
@@ -99,7 +100,7 @@ function createStatusSchema(status: DaemonStatus): OutputSchema<StatusRow> {
|
|||||||
return "red";
|
return "red";
|
||||||
}
|
}
|
||||||
if (item.key.startsWith(" ")) {
|
if (item.key.startsWith(" ")) {
|
||||||
if (item.value === "not found") return "red";
|
if (item.value === "not found" || item.value === "not found (daemon)") return "red";
|
||||||
if (item.value.endsWith("(--version failed)")) return "yellow";
|
if (item.value.endsWith("(--version failed)")) return "yellow";
|
||||||
return "green";
|
return "green";
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,13 @@ function toStatusRows(status: DaemonStatus): StatusRow[] {
|
|||||||
rows.push({ key: "", value: "" });
|
rows.push({ key: "", value: "" });
|
||||||
rows.push({ key: "Providers", value: "" });
|
rows.push({ key: "Providers", value: "" });
|
||||||
for (const provider of status.providers) {
|
for (const provider of status.providers) {
|
||||||
if (!provider.path) {
|
if (provider.source === "daemon") {
|
||||||
|
if (!provider.path) {
|
||||||
|
rows.push({ key: ` ${provider.label}`, value: "not found (daemon)" });
|
||||||
|
} else {
|
||||||
|
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (daemon)` });
|
||||||
|
}
|
||||||
|
} else if (!provider.path) {
|
||||||
rows.push({ key: ` ${provider.label}`, value: "not found" });
|
rows.push({ key: ` ${provider.label}`, value: "not found" });
|
||||||
} else if (!provider.version) {
|
} else if (!provider.version) {
|
||||||
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` });
|
rows.push({ key: ` ${provider.label}`, value: `${provider.path} (--version failed)` });
|
||||||
@@ -210,6 +217,7 @@ interface DaemonProbeResult {
|
|||||||
runningAgents?: number;
|
runningAgents?: number;
|
||||||
idleAgents?: number;
|
idleAgents?: number;
|
||||||
daemonNodeOverride?: string;
|
daemonNodeOverride?: string;
|
||||||
|
daemonProviders?: ProviderBinaryStatus[];
|
||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,12 +239,32 @@ async function probeDaemonOverWebsocket(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
|
const daemonVersion = client.getLastServerInfoMessage()?.version ?? null;
|
||||||
|
const supportsDaemonStatusRpc =
|
||||||
|
client.getLastServerInfoMessage()?.features?.daemonStatusRpc === true;
|
||||||
try {
|
try {
|
||||||
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
|
const agentsPayload = await client.fetchAgents({ filter: { includeArchived: true } });
|
||||||
const agents = agentsPayload.entries.map((entry) => entry.agent);
|
const agents = agentsPayload.entries.map((entry) => entry.agent);
|
||||||
const runningAgents = agents.filter((a) => a.status === "running").length;
|
const runningAgents = agents.filter((a) => a.status === "running").length;
|
||||||
const idleAgents = agents.filter((a) => a.status === "idle").length;
|
const idleAgents = agents.filter((a) => a.status === "idle").length;
|
||||||
|
|
||||||
|
let daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||||
|
if (supportsDaemonStatusRpc) {
|
||||||
|
try {
|
||||||
|
const statusPayload = await client.getDaemonStatus();
|
||||||
|
const labelMap = new Map(PROVIDER_BINARIES.map((p) => [p.binary, p.label]));
|
||||||
|
daemonProviders = statusPayload.providers.map((p) => ({
|
||||||
|
label: labelMap.get(p.provider) ?? p.provider,
|
||||||
|
path: p.available ? "available" : null,
|
||||||
|
version: p.available ? null : (p.error ?? null),
|
||||||
|
source: "daemon" as const,
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
// COMPAT(daemon-rpc-rollout): fall back to CLI-side provider resolution while
|
||||||
|
// old daemons lack daemonStatusRpc. Remove once the daemon floor is past
|
||||||
|
// v0.1.76; status should come from daemon.get_status.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!state.running) {
|
if (!state.running) {
|
||||||
return {
|
return {
|
||||||
connectedDaemon: "reachable",
|
connectedDaemon: "reachable",
|
||||||
@@ -244,6 +272,7 @@ async function probeDaemonOverWebsocket(args: {
|
|||||||
runningAgents,
|
runningAgents,
|
||||||
idleAgents,
|
idleAgents,
|
||||||
daemonNodeOverride: "unknown (API reachable, PID unresolved)",
|
daemonNodeOverride: "unknown (API reachable, PID unresolved)",
|
||||||
|
daemonProviders,
|
||||||
note: state.pidInfo
|
note: state.pidInfo
|
||||||
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
|
? `Connected daemon is reachable at ${host} even though local daemon PID ${state.pidInfo.pid} is stale`
|
||||||
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
|
: `Connected daemon is reachable at ${host} but no local daemon PID file was found`,
|
||||||
@@ -255,6 +284,7 @@ async function probeDaemonOverWebsocket(args: {
|
|||||||
daemonVersion,
|
daemonVersion,
|
||||||
runningAgents,
|
runningAgents,
|
||||||
idleAgents,
|
idleAgents,
|
||||||
|
daemonProviders,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
@@ -278,6 +308,7 @@ interface ProbeMergeState {
|
|||||||
daemonVersion: string | null;
|
daemonVersion: string | null;
|
||||||
runningAgents: number | null;
|
runningAgents: number | null;
|
||||||
idleAgents: number | null;
|
idleAgents: number | null;
|
||||||
|
daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||||
note: string | undefined;
|
note: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,6 +321,7 @@ function applyProbeToStatus(input: ProbeMergeState): Omit<ProbeMergeState, "prob
|
|||||||
daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
|
daemonVersion: probe.daemonVersion !== undefined ? probe.daemonVersion : input.daemonVersion,
|
||||||
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
|
runningAgents: probe.runningAgents !== undefined ? probe.runningAgents : input.runningAgents,
|
||||||
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
|
idleAgents: probe.idleAgents !== undefined ? probe.idleAgents : input.idleAgents,
|
||||||
|
daemonProviders: probe.daemonProviders ?? input.daemonProviders,
|
||||||
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
note: probe.note ? appendNote(input.note, probe.note) : input.note,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -338,6 +370,7 @@ export async function runStatusCommand(
|
|||||||
let runningAgents: number | null = null;
|
let runningAgents: number | null = null;
|
||||||
let idleAgents: number | null = null;
|
let idleAgents: number | null = null;
|
||||||
let daemonVersion: string | null = null;
|
let daemonVersion: string | null = null;
|
||||||
|
let daemonProviders: ProviderBinaryStatus[] | undefined;
|
||||||
let note: string | undefined;
|
let note: string | undefined;
|
||||||
|
|
||||||
if (!state.running && state.stalePidFile && state.pidInfo) {
|
if (!state.running && state.stalePidFile && state.pidInfo) {
|
||||||
@@ -347,17 +380,26 @@ export async function runStatusCommand(
|
|||||||
|
|
||||||
if (host) {
|
if (host) {
|
||||||
const probe = await probeDaemonOverWebsocket({ host, state });
|
const probe = await probeDaemonOverWebsocket({ host, state });
|
||||||
({ connectedDaemon, localDaemon, daemonNode, daemonVersion, runningAgents, idleAgents, note } =
|
({
|
||||||
applyProbeToStatus({
|
connectedDaemon,
|
||||||
probe,
|
localDaemon,
|
||||||
connectedDaemon,
|
daemonNode,
|
||||||
localDaemon,
|
daemonVersion,
|
||||||
daemonNode,
|
runningAgents,
|
||||||
daemonVersion,
|
idleAgents,
|
||||||
runningAgents,
|
daemonProviders,
|
||||||
idleAgents,
|
note,
|
||||||
note,
|
} = applyProbeToStatus({
|
||||||
}));
|
probe,
|
||||||
|
connectedDaemon,
|
||||||
|
localDaemon,
|
||||||
|
daemonNode,
|
||||||
|
daemonVersion,
|
||||||
|
runningAgents,
|
||||||
|
idleAgents,
|
||||||
|
daemonProviders,
|
||||||
|
note,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
|
note = appendNote(note, "Daemon is configured for unix socket listen; API probe skipped");
|
||||||
}
|
}
|
||||||
@@ -370,7 +412,7 @@ export async function runStatusCommand(
|
|||||||
note = appendNote(note, serverIdResult.error);
|
note = appendNote(note, serverIdResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = await checkProviderBinaries();
|
const providers = daemonProviders ?? (await checkProviderBinaries());
|
||||||
|
|
||||||
const daemonStatus: DaemonStatus = {
|
const daemonStatus: DaemonStatus = {
|
||||||
serverId,
|
serverId,
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ import type {
|
|||||||
GetProvidersSnapshotResponseMessage,
|
GetProvidersSnapshotResponseMessage,
|
||||||
RefreshProvidersSnapshotResponseMessage,
|
RefreshProvidersSnapshotResponseMessage,
|
||||||
ProviderDiagnosticResponseMessage,
|
ProviderDiagnosticResponseMessage,
|
||||||
|
DaemonGetStatusResponse,
|
||||||
|
DaemonGetPairingOfferResponse,
|
||||||
ListTerminalsResponse,
|
ListTerminalsResponse,
|
||||||
CreateTerminalResponse,
|
CreateTerminalResponse,
|
||||||
SubscribeTerminalResponse,
|
SubscribeTerminalResponse,
|
||||||
@@ -317,6 +319,8 @@ type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
|
|||||||
type GetProvidersSnapshotPayload = GetProvidersSnapshotResponseMessage["payload"];
|
type GetProvidersSnapshotPayload = GetProvidersSnapshotResponseMessage["payload"];
|
||||||
type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage["payload"];
|
type RefreshProvidersSnapshotPayload = RefreshProvidersSnapshotResponseMessage["payload"];
|
||||||
type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"];
|
type ProviderDiagnosticPayload = ProviderDiagnosticResponseMessage["payload"];
|
||||||
|
type DaemonStatusPayload = DaemonGetStatusResponse["payload"];
|
||||||
|
type DaemonPairingOfferPayload = DaemonGetPairingOfferResponse["payload"];
|
||||||
type ReadProjectConfigPayload = Extract<
|
type ReadProjectConfigPayload = Extract<
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
{ type: "read_project_config_response" }
|
{ 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(
|
async patchDaemonConfig(
|
||||||
config: MutableDaemonConfigPatch,
|
config: MutableDaemonConfigPatch,
|
||||||
requestId?: string,
|
requestId?: string,
|
||||||
|
|||||||
@@ -936,6 +936,16 @@ export async function createPaseoDaemon(
|
|||||||
workspaceGitService,
|
workspaceGitService,
|
||||||
github,
|
github,
|
||||||
config.pushNotificationSender,
|
config.pushNotificationSender,
|
||||||
|
{
|
||||||
|
listen: formatListenTarget(boundListenTarget ?? listenTarget),
|
||||||
|
relay: {
|
||||||
|
enabled: relayEnabled,
|
||||||
|
endpoint: relayEndpoint,
|
||||||
|
publicEndpoint: relayPublicEndpoint,
|
||||||
|
useTls: relayUseTls,
|
||||||
|
publicUseTls: relayPublicUseTls,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (relayEnabled) {
|
if (relayEnabled) {
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ import type { TurnDetectionProvider } from "./speech/turn-detection-provider.js"
|
|||||||
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
|
import { maybePersistTtsDebugAudio } from "./agent/tts-debug.js";
|
||||||
import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
|
import { isPaseoDictationDebugEnabled } from "./agent/recordings-debug.js";
|
||||||
import { listAvailableEditorTargets, openInEditorTarget } from "./editor-targets.js";
|
import { listAvailableEditorTargets, openInEditorTarget } from "./editor-targets.js";
|
||||||
|
import { getPidLockInfo } from "./pid-lock.js";
|
||||||
|
import { generateLocalPairingOffer } from "./pairing-offer.js";
|
||||||
import {
|
import {
|
||||||
DictationStreamManager,
|
DictationStreamManager,
|
||||||
type DictationStreamOutboundMessage,
|
type DictationStreamOutboundMessage,
|
||||||
@@ -595,6 +597,18 @@ export interface SessionOptions {
|
|||||||
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
|
agentProviderRuntimeSettings?: AgentProviderRuntimeSettingsMap;
|
||||||
providerOverrides?: Record<string, ProviderOverride>;
|
providerOverrides?: Record<string, ProviderOverride>;
|
||||||
isDev?: boolean;
|
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 =
|
export type SessionLifecycleIntent =
|
||||||
@@ -805,6 +819,9 @@ export class Session {
|
|||||||
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
private readonly agentProviderRuntimeSettings: AgentProviderRuntimeSettingsMap | undefined;
|
||||||
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
|
private readonly providerOverrides: Record<string, ProviderOverride> | undefined;
|
||||||
private readonly isDev: boolean;
|
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 voiceModeAgentId: string | null = null;
|
||||||
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
private voiceModeBaseConfig: VoiceModeBaseConfig | null = null;
|
||||||
|
|
||||||
@@ -850,6 +867,9 @@ export class Session {
|
|||||||
agentProviderRuntimeSettings,
|
agentProviderRuntimeSettings,
|
||||||
providerOverrides,
|
providerOverrides,
|
||||||
isDev,
|
isDev,
|
||||||
|
serverId,
|
||||||
|
daemonVersion,
|
||||||
|
daemonRuntimeConfig,
|
||||||
} = options;
|
} = options;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.appVersion = appVersion ?? null;
|
this.appVersion = appVersion ?? null;
|
||||||
@@ -901,6 +921,9 @@ export class Session {
|
|||||||
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
this.agentProviderRuntimeSettings = agentProviderRuntimeSettings;
|
||||||
this.providerOverrides = providerOverrides;
|
this.providerOverrides = providerOverrides;
|
||||||
this.isDev = isDev === true;
|
this.isDev = isDev === true;
|
||||||
|
this.serverId = serverId;
|
||||||
|
this.daemonVersion = daemonVersion;
|
||||||
|
this.daemonRuntimeConfig = daemonRuntimeConfig;
|
||||||
this.abortController = new AbortController();
|
this.abortController = new AbortController();
|
||||||
this.workspaceDirectory = new WorkspaceDirectory({
|
this.workspaceDirectory = new WorkspaceDirectory({
|
||||||
logger: this.sessionLogger,
|
logger: this.sessionLogger,
|
||||||
@@ -1864,6 +1887,10 @@ export class Session {
|
|||||||
payload: { requestId: msg.requestId, config: this.daemonConfigStore.get() },
|
payload: { requestId: msg.requestId, config: this.daemonConfigStore.get() },
|
||||||
});
|
});
|
||||||
return undefined;
|
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":
|
case "set_daemon_config_request":
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "set_daemon_config_response",
|
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(
|
private async handleListAvailableProvidersRequest(
|
||||||
msg: Extract<SessionInboundMessage, { type: "list_available_providers_request" }>,
|
msg: Extract<SessionInboundMessage, { type: "list_available_providers_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|||||||
@@ -332,6 +332,18 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
private readonly externalSessionsByKey: Map<string, SessionConnection> = new Map();
|
private readonly externalSessionsByKey: Map<string, SessionConnection> = new Map();
|
||||||
private readonly serverId: string;
|
private readonly serverId: string;
|
||||||
private readonly daemonVersion: 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 agentManager: AgentManager;
|
||||||
private readonly agentStorage: AgentStorage;
|
private readonly agentStorage: AgentStorage;
|
||||||
private readonly projectRegistry: ProjectRegistry;
|
private readonly projectRegistry: ProjectRegistry;
|
||||||
@@ -416,6 +428,16 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
workspaceGitService?: WorkspaceGitService,
|
workspaceGitService?: WorkspaceGitService,
|
||||||
github?: GitHubService,
|
github?: GitHubService,
|
||||||
pushNotificationSender?: PushNotificationSender,
|
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.logger = logger.child({ module: "websocket-server" });
|
||||||
this.serverId = serverId;
|
this.serverId = serverId;
|
||||||
@@ -423,6 +445,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
throw new MissingDaemonVersionError();
|
throw new MissingDaemonVersionError();
|
||||||
}
|
}
|
||||||
this.daemonVersion = daemonVersion.trim();
|
this.daemonVersion = daemonVersion.trim();
|
||||||
|
this.daemonRuntimeConfig = daemonRuntimeConfig;
|
||||||
this.agentManager = agentManager;
|
this.agentManager = agentManager;
|
||||||
this.agentStorage = agentStorage;
|
this.agentStorage = agentStorage;
|
||||||
this.projectRegistry = projectRegistry ?? createNoopProjectRegistry();
|
this.projectRegistry = projectRegistry ?? createNoopProjectRegistry();
|
||||||
@@ -921,6 +944,9 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
agentProviderRuntimeSettings: this.agentProviderRuntimeSettings,
|
||||||
providerOverrides: this.providerOverrides,
|
providerOverrides: this.providerOverrides,
|
||||||
isDev: this.isDev,
|
isDev: this.isDev,
|
||||||
|
serverId: this.serverId,
|
||||||
|
daemonVersion: this.daemonVersion,
|
||||||
|
daemonRuntimeConfig: this.daemonRuntimeConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
connection = {
|
connection = {
|
||||||
@@ -1053,6 +1079,8 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
providersSnapshot: true,
|
providersSnapshot: true,
|
||||||
// COMPAT(checkoutGithubSetAutoMerge): added in v0.1.75, remove gate after 2026-11-13.
|
// COMPAT(checkoutGithubSetAutoMerge): added in v0.1.75, remove gate after 2026-11-13.
|
||||||
checkoutGithubSetAutoMerge: true,
|
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(),
|
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({
|
export const GetDaemonConfigRequestMessageSchema = z.object({
|
||||||
type: z.literal("get_daemon_config_request"),
|
type: z.literal("get_daemon_config_request"),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
@@ -1764,6 +1774,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
SetVoiceModeMessageSchema,
|
SetVoiceModeMessageSchema,
|
||||||
SendAgentMessageRequestSchema,
|
SendAgentMessageRequestSchema,
|
||||||
WaitForFinishRequestSchema,
|
WaitForFinishRequestSchema,
|
||||||
|
DaemonGetStatusRequestSchema,
|
||||||
|
DaemonGetPairingOfferRequestSchema,
|
||||||
GetDaemonConfigRequestMessageSchema,
|
GetDaemonConfigRequestMessageSchema,
|
||||||
SetDaemonConfigRequestMessageSchema,
|
SetDaemonConfigRequestMessageSchema,
|
||||||
ReadProjectConfigRequestMessageSchema,
|
ReadProjectConfigRequestMessageSchema,
|
||||||
@@ -2026,6 +2038,8 @@ export const ServerInfoStatusPayloadSchema = z
|
|||||||
.object({
|
.object({
|
||||||
providersSnapshot: z.boolean().optional(),
|
providersSnapshot: z.boolean().optional(),
|
||||||
checkoutGithubSetAutoMerge: 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(),
|
.optional(),
|
||||||
})
|
})
|
||||||
@@ -2586,6 +2600,50 @@ export const GetDaemonConfigResponseMessageSchema = z.object({
|
|||||||
.passthrough(),
|
.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({
|
export const SetDaemonConfigResponseMessageSchema = z.object({
|
||||||
type: z.literal("set_daemon_config_response"),
|
type: z.literal("set_daemon_config_response"),
|
||||||
payload: z
|
payload: z
|
||||||
@@ -3481,6 +3539,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
ClearAgentAttentionResponseMessageSchema,
|
ClearAgentAttentionResponseMessageSchema,
|
||||||
SendAgentMessageResponseMessageSchema,
|
SendAgentMessageResponseMessageSchema,
|
||||||
SetVoiceModeResponseMessageSchema,
|
SetVoiceModeResponseMessageSchema,
|
||||||
|
DaemonGetStatusResponseSchema,
|
||||||
|
DaemonGetPairingOfferResponseSchema,
|
||||||
GetDaemonConfigResponseMessageSchema,
|
GetDaemonConfigResponseMessageSchema,
|
||||||
SetDaemonConfigResponseMessageSchema,
|
SetDaemonConfigResponseMessageSchema,
|
||||||
ReadProjectConfigResponseMessageSchema,
|
ReadProjectConfigResponseMessageSchema,
|
||||||
@@ -3643,6 +3703,8 @@ export type ListProviderFeaturesResponseMessage = z.infer<
|
|||||||
typeof ListProviderFeaturesResponseMessageSchema
|
typeof ListProviderFeaturesResponseMessageSchema
|
||||||
>;
|
>;
|
||||||
export type ListAvailableProvidersResponse = z.infer<typeof ListAvailableProvidersResponseSchema>;
|
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<
|
export type GetProvidersSnapshotResponseMessage = z.infer<
|
||||||
typeof GetProvidersSnapshotResponseMessageSchema
|
typeof GetProvidersSnapshotResponseMessageSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
Reference in New Issue
Block a user