diff --git a/assistant/src/config/defaults.ts b/assistant/src/config/defaults.ts index 888c0b9bb24..ae9d2c01e41 100644 --- a/assistant/src/config/defaults.ts +++ b/assistant/src/config/defaults.ts @@ -254,6 +254,8 @@ export const DEFAULT_CONFIG: AssistantConfig = { qaRecording: { defaultRetentionDays: 7, cleanupIntervalMs: 6 * 60 * 60 * 1000, // 6 hours + captureScope: 'display' as const, + includeAudio: false, }, sms: { enabled: false, diff --git a/assistant/src/config/schema.ts b/assistant/src/config/schema.ts index 2577049f729..3da2576f005 100644 --- a/assistant/src/config/schema.ts +++ b/assistant/src/config/schema.ts @@ -1066,6 +1066,12 @@ export const QaRecordingConfigSchema = z.object({ .positive('qaRecording.cleanupIntervalMs must be a positive integer') .max(2_147_483_647, 'qaRecording.cleanupIntervalMs must be at most 2147483647 (setInterval-safe limit)') .default(6 * 60 * 60 * 1000), + captureScope: z + .enum(['window', 'display'], { error: 'qaRecording.captureScope must be "window" or "display"' }) + .default('display'), + includeAudio: z + .boolean({ error: 'qaRecording.includeAudio must be a boolean' }) + .default(false), }); export const SmsConfigSchema = z.object({ @@ -1383,6 +1389,8 @@ export const AssistantConfigSchema = z.object({ qaRecording: QaRecordingConfigSchema.default({ defaultRetentionDays: 7, cleanupIntervalMs: 6 * 60 * 60 * 1000, + captureScope: 'display' as const, + includeAudio: false, }), sms: SmsConfigSchema.default({ enabled: false, diff --git a/assistant/src/daemon/handlers/misc.ts b/assistant/src/daemon/handlers/misc.ts index f987bb2f984..02d5e83f1fd 100644 --- a/assistant/src/daemon/handlers/misc.ts +++ b/assistant/src/daemon/handlers/misc.ts @@ -97,6 +97,8 @@ export async function handleTaskSubmit( qaMode: true, reportToSessionId: msg.conversationId, retentionDays: config.qaRecording.defaultRetentionDays, + captureScope: config.qaRecording.captureScope, + includeAudio: config.qaRecording.includeAudio, } : {}), }); } else { diff --git a/assistant/src/daemon/handlers/shared.ts b/assistant/src/daemon/handlers/shared.ts index 044664f741d..9a461b16358 100644 --- a/assistant/src/daemon/handlers/shared.ts +++ b/assistant/src/daemon/handlers/shared.ts @@ -260,6 +260,8 @@ export function wireEscalationHandler( ...(isQa ? { qaMode: true, retentionDays: config.qaRecording.defaultRetentionDays, + captureScope: config.qaRecording.captureScope, + includeAudio: config.qaRecording.includeAudio, } : {}), }); diff --git a/assistant/src/daemon/ipc-contract.ts b/assistant/src/daemon/ipc-contract.ts index 73fc3372764..7b9f189a677 100644 --- a/assistant/src/daemon/ipc-contract.ts +++ b/assistant/src/daemon/ipc-contract.ts @@ -1553,6 +1553,10 @@ export interface TaskRouted { reportToSessionId?: string; /** Recording retention in days (from daemon config). */ retentionDays?: number; + /** Capture scope for QA recording (from daemon config). */ + captureScope?: 'window' | 'display'; + /** Whether to include audio in QA recording (from daemon config). */ + includeAudio?: boolean; } export interface RideShotgunResult { diff --git a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift index 22b3e99f268..ab4cba8a86d 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift @@ -57,7 +57,9 @@ extension AppDelegate { screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, qaMode: routed.qaMode ?? false, - retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7 + retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, + captureScope: routed.captureScope ?? "display", + includeAudio: routed.includeAudio ?? false ) // Don't bind relatedViewModel for escalated sessions — the active view model // may be unrelated if the user switched threads. Tool calls for escalated @@ -205,7 +207,9 @@ extension AppDelegate { screenRecorder: (routed.qaMode == true) ? ScreenRecorder() : nil, reportToSessionId: routed.reportToSessionId, qaMode: routed.qaMode ?? false, - retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7 + retentionDays: routed.retentionDays.flatMap { Int($0) } ?? 7, + captureScope: routed.captureScope ?? "display", + includeAudio: routed.includeAudio ?? false ) // Don't bind relatedViewModel — sessions started via startSession() don't // originate from a chat thread, so there's no ChatViewModel to extract diff --git a/clients/macos/vellum-assistant/ComputerUse/Session.swift b/clients/macos/vellum-assistant/ComputerUse/Session.swift index 22e2fda09d7..08c126bc51c 100644 --- a/clients/macos/vellum-assistant/ComputerUse/Session.swift +++ b/clients/macos/vellum-assistant/ComputerUse/Session.swift @@ -42,6 +42,10 @@ final class ComputerUseSession: ObservableObject { let qaMode: Bool /// Recording retention in days (from daemon config, default 7). let retentionDays: Int + /// Capture scope for QA recording (from daemon config, default "display"). + let captureScope: String + /// Whether to include audio in QA recording (from daemon config, default false). + let includeAudio: Bool /// Weak reference to the chat view model for extracting tool calls for notifications. weak var relatedViewModel: ChatViewModel? @@ -88,7 +92,9 @@ final class ComputerUseSession: ObservableObject { screenRecorder: ScreenRecording? = nil, reportToSessionId: String? = nil, qaMode: Bool = false, - retentionDays: Int = 7 + retentionDays: Int = 7, + captureScope: String = "display", + includeAudio: Bool = false ) { self.id = sessionId ?? UUID().uuidString self.task = task @@ -107,6 +113,8 @@ final class ComputerUseSession: ObservableObject { self.reportToSessionId = reportToSessionId self.qaMode = qaMode self.retentionDays = retentionDays + self.captureScope = captureScope + self.includeAudio = includeAudio self.verifier = ActionVerifier(maxSteps: maxSteps) self.logger = SessionLogger(task: task, attachments: attachments) } @@ -135,7 +143,7 @@ final class ComputerUseSession: ObservableObject { // Start screen recording in QA mode if qaMode, let recorder = screenRecorder { do { - try await recorder.startRecording(windowID: nil, displayID: nil, includeAudio: false) + try await recorder.startRecording(windowID: nil, displayID: nil, includeAudio: self.includeAudio) log.info("QA mode: screen recording started for session \(self.id)") } catch { log.error("QA mode: failed to start screen recording: \(error.localizedDescription)") diff --git a/clients/shared/IPC/Generated/IPCContractGenerated.swift b/clients/shared/IPC/Generated/IPCContractGenerated.swift index adbe84f8604..1b2427dbac6 100644 --- a/clients/shared/IPC/Generated/IPCContractGenerated.swift +++ b/clients/shared/IPC/Generated/IPCContractGenerated.swift @@ -1746,6 +1746,10 @@ public struct IPCTaskRouted: Codable, Sendable { public let reportToSessionId: String? /// Recording retention in days (from daemon config). public let retentionDays: Double? + /// Capture scope for QA recording (from daemon config). + public let captureScope: String? + /// Whether to include audio in QA recording (from daemon config). + public let includeAudio: Bool? } /// Server push — broadcast when a task run creates a conversation, so the client can show it as a chat thread.