Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions assistant/src/config/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions assistant/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/daemon/handlers/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/daemon/handlers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ export function wireEscalationHandler(
...(isQa ? {
qaMode: true,
retentionDays: config.qaRecording.defaultRetentionDays,
captureScope: config.qaRecording.captureScope,
includeAudio: config.qaRecording.includeAudio,
} : {}),
});

Expand Down
4 changes: 4 additions & 0 deletions assistant/src/daemon/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 6 additions & 2 deletions clients/macos/vellum-assistant/App/AppDelegate+Sessions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions clients/macos/vellum-assistant/ComputerUse/Session.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply captureScope when starting QA recordings

When qaRecording.captureScope is set to "window", this call still passes windowID: nil and displayID: nil, so ScreenRecorder.startRecording always takes its display fallback path and records the full display. That makes the new captureScope config effectively a no-op for all QA sessions, which will surprise anyone enabling window-only capture and can invalidate QA recording expectations.

Useful? React with 👍 / 👎.

log.info("QA mode: screen recording started for session \(self.id)")
} catch {
log.error("QA mode: failed to start screen recording: \(error.localizedDescription)")
Expand Down
4 changes: 4 additions & 0 deletions clients/shared/IPC/Generated/IPCContractGenerated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading