mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Add mutable daemon config RPC support
This commit is contained in:
@@ -70,6 +70,10 @@ import type {
|
|||||||
AgentProvider,
|
AgentProvider,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
} from "../server/agent/agent-sdk-types.js";
|
} from "../server/agent/agent-sdk-types.js";
|
||||||
|
import type {
|
||||||
|
MutableDaemonConfig,
|
||||||
|
MutableDaemonConfigPatch,
|
||||||
|
} from "../shared/messages.js";
|
||||||
import { getAgentProviderDefinition } from "../server/agent/provider-manifest.js";
|
import { getAgentProviderDefinition } from "../server/agent/provider-manifest.js";
|
||||||
import { isRelayClientWebSocketUrl } from "../shared/daemon-endpoints.js";
|
import { isRelayClientWebSocketUrl } from "../shared/daemon-endpoints.js";
|
||||||
import {
|
import {
|
||||||
@@ -516,10 +520,18 @@ type WaitHandle<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RpcWaitResult<T> = { kind: "ok"; value: T } | { kind: "error"; error: DaemonRpcError };
|
type RpcWaitResult<T> = { kind: "ok"; value: T } | { kind: "error"; error: DaemonRpcError };
|
||||||
type CorrelatedResponseMessage = Extract<
|
type GetDaemonConfigResponse = Extract<
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
{ payload: { requestId: string } }
|
{ type: "get_daemon_config_response" }
|
||||||
>;
|
>;
|
||||||
|
type SetDaemonConfigResponse = Extract<
|
||||||
|
SessionOutboundMessage,
|
||||||
|
{ type: "set_daemon_config_response" }
|
||||||
|
>;
|
||||||
|
type CorrelatedResponseMessage =
|
||||||
|
| Extract<SessionOutboundMessage, { payload: { requestId: string } }>
|
||||||
|
| GetDaemonConfigResponse
|
||||||
|
| SetDaemonConfigResponse;
|
||||||
type CorrelatedResponseType = CorrelatedResponseMessage["type"];
|
type CorrelatedResponseType = CorrelatedResponseMessage["type"];
|
||||||
type CorrelatedResponsePayload<TType extends CorrelatedResponseType> = Extract<
|
type CorrelatedResponsePayload<TType extends CorrelatedResponseType> = Extract<
|
||||||
CorrelatedResponseMessage,
|
CorrelatedResponseMessage,
|
||||||
@@ -2706,6 +2718,34 @@ export class DaemonClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDaemonConfig(
|
||||||
|
requestId?: string,
|
||||||
|
): Promise<{ requestId: string; config: MutableDaemonConfig }> {
|
||||||
|
return this.sendCorrelatedSessionRequest({
|
||||||
|
requestId,
|
||||||
|
message: {
|
||||||
|
type: "get_daemon_config_request",
|
||||||
|
},
|
||||||
|
responseType: "get_daemon_config_response",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async patchDaemonConfig(
|
||||||
|
config: MutableDaemonConfigPatch,
|
||||||
|
requestId?: string,
|
||||||
|
): Promise<{ requestId: string; config: MutableDaemonConfig }> {
|
||||||
|
return this.sendCorrelatedSessionRequest({
|
||||||
|
requestId,
|
||||||
|
message: {
|
||||||
|
type: "set_daemon_config_request",
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
responseType: "set_daemon_config_response",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async refreshProvidersSnapshot(options?: {
|
async refreshProvidersSnapshot(options?: {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
requestId?: string;
|
requestId?: string;
|
||||||
|
|||||||
165
packages/server/src/server/daemon-config-store.ts
Normal file
165
packages/server/src/server/daemon-config-store.ts
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import {
|
||||||
|
loadPersistedConfig,
|
||||||
|
savePersistedConfig,
|
||||||
|
type PersistedConfig,
|
||||||
|
} from "./persisted-config.js";
|
||||||
|
import {
|
||||||
|
MutableDaemonConfigSchema,
|
||||||
|
MutableDaemonConfigPatchSchema,
|
||||||
|
} from "../shared/messages.js";
|
||||||
|
|
||||||
|
export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/messages.js";
|
||||||
|
|
||||||
|
type MutableDaemonConfig = import("../shared/messages.js").MutableDaemonConfig;
|
||||||
|
type MutableDaemonConfigPatch = import("../shared/messages.js").MutableDaemonConfigPatch;
|
||||||
|
|
||||||
|
type LoggerLike = {
|
||||||
|
child(bindings: Record<string, unknown>): LoggerLike;
|
||||||
|
info(...args: any[]): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ConfigListener = (config: MutableDaemonConfig) => void;
|
||||||
|
type FieldChangeHandler = (value: unknown) => void;
|
||||||
|
|
||||||
|
function getLogger(logger: LoggerLike | undefined): LoggerLike | undefined {
|
||||||
|
return logger?.child({ module: "daemon-config-store" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deepMerge<T extends Record<string, unknown>>(current: T, patch: Record<string, unknown>): T {
|
||||||
|
const next: Record<string, unknown> = { ...current };
|
||||||
|
|
||||||
|
for (const [key, patchValue] of Object.entries(patch)) {
|
||||||
|
if (patchValue === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const currentValue = next[key];
|
||||||
|
if (isRecord(currentValue) && isRecord(patchValue)) {
|
||||||
|
next[key] = deepMerge(currentValue, patchValue);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
next[key] = patchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValueAtPath(config: MutableDaemonConfig, path: string): unknown {
|
||||||
|
return path
|
||||||
|
.split(".")
|
||||||
|
.reduce<unknown>((value, segment) => (isRecord(value) ? value[segment] : undefined), config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEqualValue(a: unknown, b: unknown): boolean {
|
||||||
|
return JSON.stringify(a) === JSON.stringify(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DaemonConfigStore {
|
||||||
|
private current: MutableDaemonConfig;
|
||||||
|
private readonly paseoHome: string;
|
||||||
|
private readonly logger: LoggerLike | undefined;
|
||||||
|
private readonly changeListeners = new Set<ConfigListener>();
|
||||||
|
private readonly fieldChangeHandlers = new Map<string, Set<FieldChangeHandler>>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
paseoHome: string,
|
||||||
|
initial: MutableDaemonConfig,
|
||||||
|
logger?: LoggerLike,
|
||||||
|
) {
|
||||||
|
this.paseoHome = paseoHome;
|
||||||
|
this.logger = getLogger(logger);
|
||||||
|
this.current = MutableDaemonConfigSchema.parse(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(): MutableDaemonConfig {
|
||||||
|
return this.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public patch(partial: MutableDaemonConfigPatch): MutableDaemonConfig {
|
||||||
|
const parsedPatch = MutableDaemonConfigPatchSchema.parse(partial);
|
||||||
|
const next = MutableDaemonConfigSchema.parse(deepMerge(this.current, parsedPatch));
|
||||||
|
|
||||||
|
const changedFieldPaths = Array.from(this.fieldChangeHandlers.keys()).filter((path) => {
|
||||||
|
return !isEqualValue(getValueAtPath(this.current, path), getValueAtPath(next, path));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changedFieldPaths.length === 0 && isEqualValue(this.current, next)) {
|
||||||
|
return this.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist before updating in-memory state so that if persistence fails,
|
||||||
|
// runtime and disk stay consistent.
|
||||||
|
this.persistConfig(next);
|
||||||
|
this.current = next;
|
||||||
|
|
||||||
|
for (const path of changedFieldPaths) {
|
||||||
|
const handlers = this.fieldChangeHandlers.get(path);
|
||||||
|
if (!handlers) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const value = getValueAtPath(next, path);
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const listener of this.changeListeners) {
|
||||||
|
listener(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public onFieldChange(path: string, handler: FieldChangeHandler): () => void {
|
||||||
|
const handlers = this.fieldChangeHandlers.get(path) ?? new Set<FieldChangeHandler>();
|
||||||
|
handlers.add(handler);
|
||||||
|
this.fieldChangeHandlers.set(path, handlers);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const currentHandlers = this.fieldChangeHandlers.get(path);
|
||||||
|
if (!currentHandlers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentHandlers.delete(handler);
|
||||||
|
if (currentHandlers.size === 0) {
|
||||||
|
this.fieldChangeHandlers.delete(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public onChange(listener: ConfigListener): () => void {
|
||||||
|
this.changeListeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
this.changeListeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private persistConfig(config: MutableDaemonConfig): void {
|
||||||
|
const persisted = loadPersistedConfig(this.paseoHome, this.logger);
|
||||||
|
const nextPersisted = mergeMutableConfigIntoPersistedConfig({
|
||||||
|
persisted,
|
||||||
|
mutable: config,
|
||||||
|
});
|
||||||
|
savePersistedConfig(this.paseoHome, nextPersisted, this.logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeMutableConfigIntoPersistedConfig(params: {
|
||||||
|
persisted: PersistedConfig;
|
||||||
|
mutable: MutableDaemonConfig;
|
||||||
|
}): PersistedConfig {
|
||||||
|
const { persisted, mutable } = params;
|
||||||
|
return {
|
||||||
|
...persisted,
|
||||||
|
daemon: {
|
||||||
|
...persisted.daemon,
|
||||||
|
mcp: {
|
||||||
|
...persisted.daemon?.mcp,
|
||||||
|
injectIntoAgents: mutable.mcp.injectIntoAgents,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -68,6 +68,7 @@ import { experimental_createMCPClient } from "ai";
|
|||||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||||
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
|
import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js";
|
||||||
import { BackgroundGitFetchManager } from "./background-git-fetch-manager.js";
|
import { BackgroundGitFetchManager } from "./background-git-fetch-manager.js";
|
||||||
|
import type { DaemonConfigStore } from "./daemon-config-store.js";
|
||||||
|
|
||||||
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
import { buildProviderRegistry } from "./agent/provider-registry.js";
|
||||||
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
|
||||||
@@ -415,6 +416,7 @@ export type SessionOptions = {
|
|||||||
loopService: LoopService;
|
loopService: LoopService;
|
||||||
checkoutDiffManager: CheckoutDiffManager;
|
checkoutDiffManager: CheckoutDiffManager;
|
||||||
backgroundGitFetchManager: BackgroundGitFetchManager;
|
backgroundGitFetchManager: BackgroundGitFetchManager;
|
||||||
|
daemonConfigStore: DaemonConfigStore;
|
||||||
mcpBaseUrl?: string | null;
|
mcpBaseUrl?: string | null;
|
||||||
stt: Resolvable<SpeechToTextProvider | null>;
|
stt: Resolvable<SpeechToTextProvider | null>;
|
||||||
tts: Resolvable<TextToSpeechProvider | null>;
|
tts: Resolvable<TextToSpeechProvider | null>;
|
||||||
@@ -597,6 +599,7 @@ export class Session {
|
|||||||
private readonly loopService: LoopService;
|
private readonly loopService: LoopService;
|
||||||
private readonly checkoutDiffManager: CheckoutDiffManager;
|
private readonly checkoutDiffManager: CheckoutDiffManager;
|
||||||
private readonly backgroundGitFetchManager: BackgroundGitFetchManager;
|
private readonly backgroundGitFetchManager: BackgroundGitFetchManager;
|
||||||
|
private readonly daemonConfigStore: DaemonConfigStore;
|
||||||
private readonly mcpBaseUrl: string | null;
|
private readonly mcpBaseUrl: string | null;
|
||||||
private readonly downloadTokenStore: DownloadTokenStore;
|
private readonly downloadTokenStore: DownloadTokenStore;
|
||||||
private readonly pushTokenStore: PushTokenStore;
|
private readonly pushTokenStore: PushTokenStore;
|
||||||
@@ -661,6 +664,7 @@ export class Session {
|
|||||||
loopService,
|
loopService,
|
||||||
checkoutDiffManager,
|
checkoutDiffManager,
|
||||||
backgroundGitFetchManager,
|
backgroundGitFetchManager,
|
||||||
|
daemonConfigStore,
|
||||||
mcpBaseUrl,
|
mcpBaseUrl,
|
||||||
stt,
|
stt,
|
||||||
tts,
|
tts,
|
||||||
@@ -689,6 +693,7 @@ export class Session {
|
|||||||
this.loopService = loopService;
|
this.loopService = loopService;
|
||||||
this.checkoutDiffManager = checkoutDiffManager;
|
this.checkoutDiffManager = checkoutDiffManager;
|
||||||
this.backgroundGitFetchManager = backgroundGitFetchManager;
|
this.backgroundGitFetchManager = backgroundGitFetchManager;
|
||||||
|
this.daemonConfigStore = daemonConfigStore;
|
||||||
this.mcpBaseUrl = mcpBaseUrl ?? null;
|
this.mcpBaseUrl = mcpBaseUrl ?? null;
|
||||||
this.terminalManager = terminalManager;
|
this.terminalManager = terminalManager;
|
||||||
this.providerSnapshotManager = providerSnapshotManager ?? null;
|
this.providerSnapshotManager = providerSnapshotManager ?? null;
|
||||||
@@ -1611,6 +1616,26 @@ export class Session {
|
|||||||
await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
|
await this.handleWaitForFinish(msg.agentId, msg.requestId, msg.timeoutMs);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "get_daemon_config_request":
|
||||||
|
this.emit({
|
||||||
|
type: "get_daemon_config_response",
|
||||||
|
payload: {
|
||||||
|
requestId: msg.requestId,
|
||||||
|
config: this.daemonConfigStore.get(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "set_daemon_config_request":
|
||||||
|
this.emit({
|
||||||
|
type: "set_daemon_config_response",
|
||||||
|
payload: {
|
||||||
|
requestId: msg.requestId,
|
||||||
|
config: this.daemonConfigStore.patch(msg.config),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
case "dictation_stream_start":
|
case "dictation_stream_start":
|
||||||
{
|
{
|
||||||
const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
|
const unavailable = this.resolveVoiceFeatureUnavailableContext("dictation");
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
|||||||
getAgent: vi.fn(() => null),
|
getAgent: vi.fn(() => null),
|
||||||
...agentManagerOverrides,
|
...agentManagerOverrides,
|
||||||
};
|
};
|
||||||
|
const daemonConfigStore = {
|
||||||
|
onChange: vi.fn(() => () => {}),
|
||||||
|
};
|
||||||
|
|
||||||
const server = new VoiceAssistantWebSocketServer(
|
const server = new VoiceAssistantWebSocketServer(
|
||||||
{} as any,
|
{} as any,
|
||||||
@@ -74,6 +77,7 @@ function createServer(agentManagerOverrides?: Record<string, unknown>) {
|
|||||||
{} as any,
|
{} as any,
|
||||||
{} as any,
|
{} as any,
|
||||||
"/tmp/paseo-test",
|
"/tmp/paseo-test",
|
||||||
|
daemonConfigStore as any,
|
||||||
null,
|
null,
|
||||||
{ allowedOrigins: new Set() },
|
{ allowedOrigins: new Set() },
|
||||||
undefined,
|
undefined,
|
||||||
|
|||||||
@@ -147,6 +147,9 @@ function createLogger() {
|
|||||||
|
|
||||||
function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | null }) {
|
function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | null }) {
|
||||||
const speechReadiness = options?.speechReadiness ?? null;
|
const speechReadiness = options?.speechReadiness ?? null;
|
||||||
|
const daemonConfigStore = {
|
||||||
|
onChange: vi.fn(() => () => {}),
|
||||||
|
};
|
||||||
return new VoiceAssistantWebSocketServer(
|
return new VoiceAssistantWebSocketServer(
|
||||||
{} as any,
|
{} as any,
|
||||||
createLogger() as any,
|
createLogger() as any,
|
||||||
@@ -165,6 +168,7 @@ function createServer(options?: { speechReadiness?: SpeechReadinessSnapshot | nu
|
|||||||
{} as any,
|
{} as any,
|
||||||
{} as any,
|
{} as any,
|
||||||
"/tmp/paseo-test",
|
"/tmp/paseo-test",
|
||||||
|
daemonConfigStore as any,
|
||||||
null,
|
null,
|
||||||
{ allowedOrigins: new Set() },
|
{ allowedOrigins: new Set() },
|
||||||
speechReadiness
|
speechReadiness
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type { LoopService } from "./loop-service.js";
|
|||||||
import type { ScheduleService } from "./schedule/service.js";
|
import type { ScheduleService } from "./schedule/service.js";
|
||||||
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
|
import type { CheckoutDiffManager, CheckoutDiffMetrics } from "./checkout-diff-manager.js";
|
||||||
import { BackgroundGitFetchManager } from "./background-git-fetch-manager.js";
|
import { BackgroundGitFetchManager } from "./background-git-fetch-manager.js";
|
||||||
|
import type { DaemonConfigStore, MutableDaemonConfig } from "./daemon-config-store.js";
|
||||||
import {
|
import {
|
||||||
type ServerInfoStatusPayload,
|
type ServerInfoStatusPayload,
|
||||||
type WSHelloMessage,
|
type WSHelloMessage,
|
||||||
@@ -239,6 +240,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
private readonly backgroundGitFetchManager: BackgroundGitFetchManager;
|
private readonly backgroundGitFetchManager: BackgroundGitFetchManager;
|
||||||
private readonly downloadTokenStore: DownloadTokenStore;
|
private readonly downloadTokenStore: DownloadTokenStore;
|
||||||
private readonly paseoHome: string;
|
private readonly paseoHome: string;
|
||||||
|
private readonly daemonConfigStore: DaemonConfigStore;
|
||||||
private readonly pushTokenStore: PushTokenStore;
|
private readonly pushTokenStore: PushTokenStore;
|
||||||
private readonly pushService: PushService;
|
private readonly pushService: PushService;
|
||||||
private readonly mcpBaseUrl: string | null;
|
private readonly mcpBaseUrl: string | null;
|
||||||
@@ -276,6 +278,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
private readonly requestLatencies = new Map<string, number[]>();
|
private readonly requestLatencies = new Map<string, number[]>();
|
||||||
private runtimeMetricsInterval: ReturnType<typeof setInterval> | null = null;
|
private runtimeMetricsInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
private unsubscribeSpeechReadiness: (() => void) | null = null;
|
private unsubscribeSpeechReadiness: (() => void) | null = null;
|
||||||
|
private unsubscribeDaemonConfigChange: (() => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
server: HTTPServer,
|
server: HTTPServer,
|
||||||
@@ -285,6 +288,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
agentStorage: AgentStorage,
|
agentStorage: AgentStorage,
|
||||||
downloadTokenStore: DownloadTokenStore,
|
downloadTokenStore: DownloadTokenStore,
|
||||||
paseoHome: string,
|
paseoHome: string,
|
||||||
|
daemonConfigStore: DaemonConfigStore,
|
||||||
mcpBaseUrl: string | null,
|
mcpBaseUrl: string | null,
|
||||||
wsConfig: WebSocketServerConfig,
|
wsConfig: WebSocketServerConfig,
|
||||||
speech?: SpeechService | null,
|
speech?: SpeechService | null,
|
||||||
@@ -333,6 +337,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
});
|
});
|
||||||
this.downloadTokenStore = downloadTokenStore;
|
this.downloadTokenStore = downloadTokenStore;
|
||||||
this.paseoHome = paseoHome;
|
this.paseoHome = paseoHome;
|
||||||
|
this.daemonConfigStore = daemonConfigStore;
|
||||||
this.mcpBaseUrl = mcpBaseUrl;
|
this.mcpBaseUrl = mcpBaseUrl;
|
||||||
this.speech = speech ?? null;
|
this.speech = speech ?? null;
|
||||||
this.terminalManager = terminalManager ?? null;
|
this.terminalManager = terminalManager ?? null;
|
||||||
@@ -352,6 +357,9 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
this.unsubscribeSpeechReadiness = this.speech?.onReadinessChange((snapshot) => {
|
this.unsubscribeSpeechReadiness = this.speech?.onReadinessChange((snapshot) => {
|
||||||
this.publishSpeechReadiness(snapshot);
|
this.publishSpeechReadiness(snapshot);
|
||||||
}) ?? null;
|
}) ?? null;
|
||||||
|
this.unsubscribeDaemonConfigChange = this.daemonConfigStore.onChange((config) => {
|
||||||
|
this.broadcastDaemonConfigChanged(config);
|
||||||
|
});
|
||||||
|
|
||||||
const pushLogger = this.logger.child({ module: "push" });
|
const pushLogger = this.logger.child({ module: "push" });
|
||||||
this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json"));
|
this.pushTokenStore = new PushTokenStore(pushLogger, join(paseoHome, "push-tokens.json"));
|
||||||
@@ -442,6 +450,8 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
this.unsubscribeSpeechReadiness?.();
|
this.unsubscribeSpeechReadiness?.();
|
||||||
this.unsubscribeSpeechReadiness = null;
|
this.unsubscribeSpeechReadiness = null;
|
||||||
|
this.unsubscribeDaemonConfigChange?.();
|
||||||
|
this.unsubscribeDaemonConfigChange = null;
|
||||||
if (this.runtimeMetricsInterval) {
|
if (this.runtimeMetricsInterval) {
|
||||||
clearInterval(this.runtimeMetricsInterval);
|
clearInterval(this.runtimeMetricsInterval);
|
||||||
this.runtimeMetricsInterval = null;
|
this.runtimeMetricsInterval = null;
|
||||||
@@ -637,6 +647,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
scheduleService: this.scheduleService,
|
scheduleService: this.scheduleService,
|
||||||
checkoutDiffManager: this.checkoutDiffManager,
|
checkoutDiffManager: this.checkoutDiffManager,
|
||||||
backgroundGitFetchManager: this.backgroundGitFetchManager,
|
backgroundGitFetchManager: this.backgroundGitFetchManager,
|
||||||
|
daemonConfigStore: this.daemonConfigStore,
|
||||||
mcpBaseUrl: this.mcpBaseUrl,
|
mcpBaseUrl: this.mcpBaseUrl,
|
||||||
stt: () => this.speech?.resolveStt() ?? null,
|
stt: () => this.speech?.resolveStt() ?? null,
|
||||||
tts: () => this.speech?.resolveTts() ?? null,
|
tts: () => this.speech?.resolveTts() ?? null,
|
||||||
@@ -802,10 +813,24 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createDaemonConfigChangedMessage(config: MutableDaemonConfig): WSOutboundMessage {
|
||||||
|
return wrapSessionMessage({
|
||||||
|
type: "status",
|
||||||
|
payload: {
|
||||||
|
status: "daemon_config_changed",
|
||||||
|
config,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private broadcastCapabilitiesUpdate(): void {
|
private broadcastCapabilitiesUpdate(): void {
|
||||||
this.broadcast(this.createServerInfoMessage());
|
this.broadcast(this.createServerInfoMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private broadcastDaemonConfigChanged(config: MutableDaemonConfig): void {
|
||||||
|
this.broadcast(this.createDaemonConfigChangedMessage(config));
|
||||||
|
}
|
||||||
|
|
||||||
private bindSocketHandlers(ws: WebSocketLike): void {
|
private bindSocketHandlers(ws: WebSocketLike): void {
|
||||||
ws.on("message", (data) => {
|
ws.on("message", (data) => {
|
||||||
void this.handleRawMessage(ws, data);
|
void this.handleRawMessage(ws, data);
|
||||||
|
|||||||
@@ -47,6 +47,29 @@ import {
|
|||||||
LoopLogsResponseSchema,
|
LoopLogsResponseSchema,
|
||||||
LoopStopResponseSchema,
|
LoopStopResponseSchema,
|
||||||
} from "../server/loop/rpc-schemas.js";
|
} from "../server/loop/rpc-schemas.js";
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mutable daemon config schemas (shared between server store and client)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const MutableDaemonConfigSchema = z
|
||||||
|
.object({
|
||||||
|
mcp: z
|
||||||
|
.object({
|
||||||
|
injectIntoAgents: z.boolean(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export const MutableDaemonConfigPatchSchema = z
|
||||||
|
.object({
|
||||||
|
mcp: MutableDaemonConfigSchema.shape.mcp.partial().optional(),
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export type MutableDaemonConfig = z.infer<typeof MutableDaemonConfigSchema>;
|
||||||
|
export type MutableDaemonConfigPatch = z.infer<typeof MutableDaemonConfigPatchSchema>;
|
||||||
import type { LiteralUnion } from "./literal-union.js";
|
import type { LiteralUnion } from "./literal-union.js";
|
||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
@@ -725,6 +748,17 @@ export const WaitForFinishRequestSchema = z.object({
|
|||||||
timeoutMs: z.number().int().positive().optional(),
|
timeoutMs: z.number().int().positive().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const GetDaemonConfigRequestMessageSchema = z.object({
|
||||||
|
type: z.literal("get_daemon_config_request"),
|
||||||
|
requestId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SetDaemonConfigRequestMessageSchema = z.object({
|
||||||
|
type: z.literal("set_daemon_config_request"),
|
||||||
|
requestId: z.string(),
|
||||||
|
config: MutableDaemonConfigPatchSchema,
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Dictation Streaming (lossless, resumable)
|
// Dictation Streaming (lossless, resumable)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1397,6 +1431,8 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
SetVoiceModeMessageSchema,
|
SetVoiceModeMessageSchema,
|
||||||
SendAgentMessageRequestSchema,
|
SendAgentMessageRequestSchema,
|
||||||
WaitForFinishRequestSchema,
|
WaitForFinishRequestSchema,
|
||||||
|
GetDaemonConfigRequestMessageSchema,
|
||||||
|
SetDaemonConfigRequestMessageSchema,
|
||||||
DictationStreamStartMessageSchema,
|
DictationStreamStartMessageSchema,
|
||||||
DictationStreamChunkMessageSchema,
|
DictationStreamChunkMessageSchema,
|
||||||
DictationStreamFinishMessageSchema,
|
DictationStreamFinishMessageSchema,
|
||||||
@@ -1732,6 +1768,13 @@ export const ShutdownRequestedStatusPayloadSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const DaemonConfigChangedStatusPayloadSchema = z
|
||||||
|
.object({
|
||||||
|
status: z.literal("daemon_config_changed"),
|
||||||
|
config: MutableDaemonConfigSchema,
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
export const KnownStatusPayloadSchema = z.discriminatedUnion("status", [
|
export const KnownStatusPayloadSchema = z.discriminatedUnion("status", [
|
||||||
AgentCreatedStatusPayloadSchema,
|
AgentCreatedStatusPayloadSchema,
|
||||||
AgentCreateFailedStatusPayloadSchema,
|
AgentCreateFailedStatusPayloadSchema,
|
||||||
@@ -1739,6 +1782,7 @@ export const KnownStatusPayloadSchema = z.discriminatedUnion("status", [
|
|||||||
AgentRefreshedStatusPayloadSchema,
|
AgentRefreshedStatusPayloadSchema,
|
||||||
ShutdownRequestedStatusPayloadSchema,
|
ShutdownRequestedStatusPayloadSchema,
|
||||||
RestartRequestedStatusPayloadSchema,
|
RestartRequestedStatusPayloadSchema,
|
||||||
|
DaemonConfigChangedStatusPayloadSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type KnownStatusPayload = z.infer<typeof KnownStatusPayloadSchema>;
|
export type KnownStatusPayload = z.infer<typeof KnownStatusPayloadSchema>;
|
||||||
@@ -2028,6 +2072,26 @@ export const WaitForFinishResponseMessageSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const GetDaemonConfigResponseMessageSchema = z.object({
|
||||||
|
type: z.literal("get_daemon_config_response"),
|
||||||
|
payload: z
|
||||||
|
.object({
|
||||||
|
requestId: z.string(),
|
||||||
|
config: MutableDaemonConfigSchema,
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SetDaemonConfigResponseMessageSchema = z.object({
|
||||||
|
type: z.literal("set_daemon_config_response"),
|
||||||
|
payload: z
|
||||||
|
.object({
|
||||||
|
requestId: z.string(),
|
||||||
|
config: MutableDaemonConfigSchema,
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
});
|
||||||
|
|
||||||
export const AgentPermissionRequestMessageSchema = z.object({
|
export const AgentPermissionRequestMessageSchema = z.object({
|
||||||
type: z.literal("agent_permission_request"),
|
type: z.literal("agent_permission_request"),
|
||||||
payload: z.object({
|
payload: z.object({
|
||||||
@@ -2658,6 +2722,8 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
FetchAgentTimelineResponseMessageSchema,
|
FetchAgentTimelineResponseMessageSchema,
|
||||||
SendAgentMessageResponseMessageSchema,
|
SendAgentMessageResponseMessageSchema,
|
||||||
SetVoiceModeResponseMessageSchema,
|
SetVoiceModeResponseMessageSchema,
|
||||||
|
GetDaemonConfigResponseMessageSchema,
|
||||||
|
SetDaemonConfigResponseMessageSchema,
|
||||||
SetAgentModeResponseMessageSchema,
|
SetAgentModeResponseMessageSchema,
|
||||||
SetAgentModelResponseMessageSchema,
|
SetAgentModelResponseMessageSchema,
|
||||||
SetAgentThinkingResponseMessageSchema,
|
SetAgentThinkingResponseMessageSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user