Add mutable daemon config RPC support

This commit is contained in:
Mohamed Boudra
2026-04-11 16:39:09 +07:00
parent 51e96fa303
commit fa552b0faa
7 changed files with 331 additions and 2 deletions

View File

@@ -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;

View 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,
},
},
};
}

View File

@@ -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");

View File

@@ -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,

View File

@@ -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

View File

@@ -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);

View File

@@ -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,